fixably 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
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