fixably 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (48) hide show
  1. checksums.yaml +7 -0
  2. data/.github/workflows/main.yml +18 -0
  3. data/.gitignore +14 -0
  4. data/.mutant.yml +23 -0
  5. data/.rspec +1 -0
  6. data/.rubocop.yml +145 -0
  7. data/CODE_OF_CONDUCT.md +84 -0
  8. data/Gemfile +12 -0
  9. data/Gemfile.lock +119 -0
  10. data/LICENSE.txt +21 -0
  11. data/README.md +227 -0
  12. data/Rakefile +12 -0
  13. data/bin/console +15 -0
  14. data/bin/mutant +3 -0
  15. data/bin/setup +8 -0
  16. data/docs/customer/child.md +23 -0
  17. data/docs/customer.md +78 -0
  18. data/docs/device.md +56 -0
  19. data/docs/order/line.md +27 -0
  20. data/docs/order/note.md +34 -0
  21. data/docs/order/task.md +27 -0
  22. data/docs/order.md +121 -0
  23. data/docs/queue.md +36 -0
  24. data/docs/status.md +42 -0
  25. data/docs/user.md +23 -0
  26. data/fixably.gemspec +38 -0
  27. data/lib/fixably/action_policy.rb +94 -0
  28. data/lib/fixably/actions.rb +148 -0
  29. data/lib/fixably/active_resource/base.rb +53 -0
  30. data/lib/fixably/active_resource/paginated_collection.rb +91 -0
  31. data/lib/fixably/application_resource.rb +49 -0
  32. data/lib/fixably/argument_parameterisation.rb +77 -0
  33. data/lib/fixably/authorization.rb +17 -0
  34. data/lib/fixably/config.rb +39 -0
  35. data/lib/fixably/create_has_many_record.rb +90 -0
  36. data/lib/fixably/encoding.rb +38 -0
  37. data/lib/fixably/load_from_response.rb +116 -0
  38. data/lib/fixably/logger.rb +33 -0
  39. data/lib/fixably/resource_lazy_loader.rb +76 -0
  40. data/lib/fixably/resources/customer.rb +64 -0
  41. data/lib/fixably/resources/device.rb +27 -0
  42. data/lib/fixably/resources/order.rb +87 -0
  43. data/lib/fixably/resources/queue.rb +9 -0
  44. data/lib/fixably/resources/status.rb +12 -0
  45. data/lib/fixably/resources/user.rb +15 -0
  46. data/lib/fixably/version.rb +5 -0
  47. data/lib/fixably.rb +37 -0
  48. metadata +217 -0
@@ -0,0 +1,116 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Fixably
4
+ module LoadFromResponse
5
+ # Fixably returns collections as hashes which confuses Active Resource
6
+ # rubocop:disable Style/OptionalBooleanParameter
7
+ def load(attributes, remove_root = false, persisted = false)
8
+ super(attributes, remove_root, persisted)
9
+ load_nested_paginated_collections
10
+ remove_empty_associations
11
+ self
12
+ end
13
+ # rubocop:enable Style/OptionalBooleanParameter
14
+
15
+ protected
16
+
17
+ def load_attributes_from_response(response)
18
+ resp = response.dup
19
+
20
+ if response_code_allows_body?(resp.code)
21
+ body = self.class.format.decode(resp.body)
22
+ body = decontruct_array_response(body)
23
+ body.deep_transform_keys!(&:underscore)
24
+ resp.body = self.class.format.encode(body)
25
+ end
26
+
27
+ super(resp)
28
+ end
29
+
30
+ private
31
+
32
+ def decontruct_array_response(attributes)
33
+ return attributes if attributes.respond_to?(:to_hash)
34
+
35
+ if attributes.length > 1
36
+ raise(
37
+ ArgumentError,
38
+ "Unable to unpack an array response with more than 1 record"
39
+ )
40
+ end
41
+
42
+ attributes.first
43
+ end
44
+
45
+ def load_nested_paginated_collections
46
+ reflections.each do |name, specs|
47
+ if specs.macro.equal?(:has_many)
48
+ load_has_many(name)
49
+ else
50
+ load_has_one(name)
51
+ end
52
+ end
53
+ end
54
+
55
+ def load_has_many(name)
56
+ collection = attributes[name]
57
+ return unless ActiveResource::PaginatedCollection.paginatable?(collection)
58
+
59
+ resource = reflections.fetch(name).klass
60
+ paginated_collection = resource.
61
+ __send__(:instantiate_collection, collection_attributes(collection))
62
+ paginated_collection.parent_resource = self
63
+ paginated_collection.parent_association = name
64
+ attributes[name] = paginated_collection
65
+ end
66
+
67
+ def collection_attributes(collection)
68
+ collection.attributes.transform_values do |value|
69
+ if value.respond_to?(:map)
70
+ value.map(&:attributes)
71
+ else
72
+ value
73
+ end
74
+ end
75
+ end
76
+
77
+ def load_has_one(name)
78
+ element = attributes[name]
79
+ return unless element.class.name.include?("::Item::")
80
+
81
+ resource = reflections.fetch(name).klass
82
+ attributes[name] = resource.new(element.attributes, true)
83
+ end
84
+
85
+ # Fixably may send back empty records with a href but that causes
86
+ # Active Record to think there is an actual record and removes the ability
87
+ # to perform actions that would either retrieve or create those records
88
+ def remove_empty_associations
89
+ reflections.each do |name, spec|
90
+ next unless attributes.key?(name)
91
+ next unless empty_association?(attributes.fetch(name))
92
+
93
+ attributes.delete(name)
94
+ if instance_variable_defined?(:"@#{name}")
95
+ remove_instance_variable(:"@#{name}")
96
+ end
97
+
98
+ create_empty_collection(name) if spec.macro.equal?(:has_many)
99
+ end
100
+ end
101
+
102
+ def empty_association?(record)
103
+ return false unless record.respond_to?(:attributes)
104
+
105
+ record.attributes.keys.eql?(%w[href])
106
+ end
107
+
108
+ def create_empty_collection(name)
109
+ attributes[name] = self.class.collection_parser.new.tap do |collection|
110
+ collection.resource_class = reflections.fetch(name).klass
111
+ collection.parent_resource = self
112
+ collection.parent_association = name
113
+ end
114
+ end
115
+ end
116
+ end
@@ -0,0 +1,33 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "forwardable"
4
+ require "logger"
5
+
6
+ module Fixably
7
+ module Logger
8
+ extend SingleForwardable
9
+
10
+ def_delegators :logger, *::Logger.instance_methods(false)
11
+
12
+ class << self
13
+ attr_writer :logger
14
+
15
+ def logger
16
+ @logger ||=
17
+ if defined?(Rails.logger)
18
+ Rails.logger
19
+ else
20
+ ruby_logger
21
+ end
22
+ end
23
+
24
+ private
25
+
26
+ def ruby_logger
27
+ log = ::Logger.new($stdout)
28
+ log.level = ::Logger::WARN
29
+ log
30
+ end
31
+ end
32
+ end
33
+ end
@@ -0,0 +1,76 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Fixably
4
+ class ResourceLazyLoader
5
+ attr_reader :model
6
+ attr_reader :associations_to_expand
7
+
8
+ def initialize(model:)
9
+ if !model.respond_to?(:ancestors) ||
10
+ !model.ancestors.include?(ApplicationResource)
11
+ raise(
12
+ ArgumentError,
13
+ "The model is expected to be a class that extend ApplicationResource"
14
+ )
15
+ end
16
+
17
+ @model = model
18
+ @associations_to_expand = Set.new
19
+ end
20
+
21
+ def includes(association)
22
+ unless model.reflections.key?(association)
23
+ raise(
24
+ ArgumentError,
25
+ "#{association} is not a known association of #{model}"
26
+ )
27
+ end
28
+
29
+ associations_to_expand << association
30
+
31
+ self
32
+ end
33
+
34
+ def find(*args)
35
+ id = args.slice(0)
36
+ options = expand_associations(args.slice(1) || {})
37
+ arguments = [options].concat(args[2..] || [])
38
+
39
+ model.find(id, *arguments)
40
+ end
41
+
42
+ def first(*args)
43
+ options = expand_associations(args.slice(0) || {})
44
+ arguments = [options].concat(args[1..] || [])
45
+
46
+ model.first(*arguments)
47
+ end
48
+
49
+ def last(*args)
50
+ options = expand_associations(args.slice(0) || {})
51
+ arguments = [options].concat(args[1..] || [])
52
+
53
+ model.last(*arguments)
54
+ end
55
+
56
+ def all(*args)
57
+ options = expand_associations(args.slice(0) || {})
58
+ arguments = [options].concat(args[1..] || [])
59
+
60
+ model.all(*arguments)
61
+ end
62
+
63
+ def where(clauses = {})
64
+ arguments = expand_associations(clauses)
65
+ model.where(arguments)
66
+ end
67
+
68
+ private
69
+
70
+ def expand_associations(args)
71
+ expand = args[:expand].to_a.to_set
72
+ args[:expand] = expand.merge(associations_to_expand)
73
+ args
74
+ end
75
+ end
76
+ end
@@ -0,0 +1,64 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Fixably
4
+ class Customer < ApplicationResource
5
+ actions %i[create list show update]
6
+
7
+ validate :either_email_or_phone_set
8
+
9
+ schema do
10
+ string :first_name
11
+ string :last_name
12
+ string :company
13
+ string :phone
14
+ string :email
15
+ string :business_id
16
+ string :language
17
+ string :provider
18
+ string :identifier
19
+ end
20
+
21
+ has_one :billing_address, class_name: "fixably/customer/billing_address"
22
+ has_one :shipping_address, class_name: "fixably/customer/shipping_address"
23
+
24
+ has_many :children, class_name: "fixably/customer/child"
25
+
26
+ def either_email_or_phone_set
27
+ return if email.present? || phone.present?
28
+
29
+ errors.add(:base, "Either email or phone must be present")
30
+ end
31
+
32
+ def remove_on_encode = super().concat(%w[tags])
33
+
34
+ class ShippingAddress < ApplicationResource
35
+ schema do
36
+ string :name
37
+ string :address1
38
+ string :address2
39
+ string :address3
40
+ string :zip
41
+ string :city
42
+ string :state
43
+ string :country
44
+ end
45
+ end
46
+
47
+ class BillingAddress < ApplicationResource
48
+ schema do
49
+ string :name
50
+ string :address1
51
+ string :address2
52
+ string :address3
53
+ string :zip
54
+ string :city
55
+ string :state
56
+ string :country
57
+ end
58
+ end
59
+
60
+ class Child < Customer
61
+ actions %i[show]
62
+ end
63
+ end
64
+ end
@@ -0,0 +1,27 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Fixably
4
+ class Device < ApplicationResource
5
+ actions %i[create list show]
6
+
7
+ validates :name, presence: true
8
+ validate :either_serial_number_or_imei_set
9
+
10
+ schema do
11
+ string :serial_number
12
+ string :imei_number1
13
+ string :imei_number2
14
+ string :name
15
+ string :configuration
16
+ string :brand
17
+ string :purchase_country
18
+ date :purchase_date
19
+ end
20
+
21
+ def either_serial_number_or_imei_set
22
+ return if serial_number.present? || imei_number1.present?
23
+
24
+ errors.add(:base, "Either serial number or IMEI must be present")
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,87 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Fixably
4
+ class Order < ApplicationResource
5
+ actions %i[create list show]
6
+
7
+ schema do
8
+ string :internal_location
9
+ boolean :is_draft
10
+ string :reference
11
+ end
12
+
13
+ has_one :contact, class_name: "fixably/order/contact"
14
+ has_one :customer, class_name: "fixably/customer"
15
+ has_one :device, class_name: "fixably/device"
16
+ has_one :handled_by, class_name: "fixably/customer"
17
+ has_one :ordered_by, class_name: "fixably/customer"
18
+ has_one :queue, class_name: "fixably/queue"
19
+ has_one :status, class_name: "fixably/status"
20
+
21
+ has_many :lines, class_name: "fixably/order/line"
22
+ has_many :notes, class_name: "fixably/order/note"
23
+ has_many :tasks, class_name: "fixably/order/task"
24
+
25
+ # TODO
26
+ # has_one :location
27
+ # has_one :store
28
+
29
+ ALLOWED_INTERNAL_LOCATIONS =
30
+ %w[CUSTOMER DEALER_SHOP IN_TRANSIT SERVICE STORE].freeze
31
+
32
+ validates(
33
+ :internal_location,
34
+ inclusion: {
35
+ in: ALLOWED_INTERNAL_LOCATIONS,
36
+ message: "should be one of " + # rubocop:disable Style/StringConcatenation
37
+ ALLOWED_INTERNAL_LOCATIONS.to_sentence(last_word_connector: " or "),
38
+ },
39
+ allow_nil: true
40
+ )
41
+
42
+ class Contact < ApplicationResource
43
+ schema do
44
+ string :full_name
45
+ string :company
46
+ string :phone_number
47
+ string :email_address
48
+ end
49
+ end
50
+
51
+ class Line < ApplicationResource
52
+ actions %i[list show]
53
+ end
54
+
55
+ class Note < ApplicationResource
56
+ actions %i[create list show]
57
+
58
+ ALLOWED_TYPES = %w[DIAGNOSIS INTERNAL ISSUE RESOLUTION].freeze
59
+
60
+ validates(
61
+ :type,
62
+ inclusion: {
63
+ in: ALLOWED_TYPES,
64
+ message: "should be one of " \
65
+ "#{ALLOWED_TYPES.to_sentence(last_word_connector: " or ")}",
66
+ }
67
+ )
68
+
69
+ schema do
70
+ string :title
71
+ string :text
72
+ string :type
73
+ end
74
+ end
75
+
76
+ class Task < ApplicationResource
77
+ actions %i[list show update]
78
+
79
+ # TODO
80
+ # has_one :task
81
+
82
+ def update
83
+ raise "Updating order tasks has not been implemented"
84
+ end
85
+ end
86
+ end
87
+ end
@@ -0,0 +1,9 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Fixably
4
+ class Queue < ApplicationResource
5
+ actions %i[list show]
6
+
7
+ has_many :statuses, class_name: "fixably/status"
8
+ end
9
+ end
@@ -0,0 +1,12 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Fixably
4
+ class Status < ApplicationResource
5
+ actions %i[list show]
6
+
7
+ has_one :custom, class_name: "fixably/status/custom"
8
+ has_one :queue, class_name: "fixably/queue"
9
+
10
+ class Custom < ApplicationResource; end
11
+ end
12
+ end
@@ -0,0 +1,15 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Fixably
4
+ class User < ApplicationResource
5
+ actions :show
6
+
7
+ schema do
8
+ integer :id
9
+ string :first_name
10
+ string :last_name
11
+ string :email
12
+ string :phone
13
+ end
14
+ end
15
+ end