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.
- checksums.yaml +7 -0
- data/.github/workflows/main.yml +18 -0
- data/.gitignore +14 -0
- data/.mutant.yml +23 -0
- data/.rspec +1 -0
- data/.rubocop.yml +145 -0
- data/CODE_OF_CONDUCT.md +84 -0
- data/Gemfile +12 -0
- data/Gemfile.lock +119 -0
- data/LICENSE.txt +21 -0
- data/README.md +227 -0
- data/Rakefile +12 -0
- data/bin/console +15 -0
- data/bin/mutant +3 -0
- data/bin/setup +8 -0
- data/docs/customer/child.md +23 -0
- data/docs/customer.md +78 -0
- data/docs/device.md +56 -0
- data/docs/order/line.md +27 -0
- data/docs/order/note.md +34 -0
- data/docs/order/task.md +27 -0
- data/docs/order.md +121 -0
- data/docs/queue.md +36 -0
- data/docs/status.md +42 -0
- data/docs/user.md +23 -0
- data/fixably.gemspec +38 -0
- data/lib/fixably/action_policy.rb +94 -0
- data/lib/fixably/actions.rb +148 -0
- data/lib/fixably/active_resource/base.rb +53 -0
- data/lib/fixably/active_resource/paginated_collection.rb +91 -0
- data/lib/fixably/application_resource.rb +49 -0
- data/lib/fixably/argument_parameterisation.rb +77 -0
- data/lib/fixably/authorization.rb +17 -0
- data/lib/fixably/config.rb +39 -0
- data/lib/fixably/create_has_many_record.rb +90 -0
- data/lib/fixably/encoding.rb +38 -0
- data/lib/fixably/load_from_response.rb +116 -0
- data/lib/fixably/logger.rb +33 -0
- data/lib/fixably/resource_lazy_loader.rb +76 -0
- data/lib/fixably/resources/customer.rb +64 -0
- data/lib/fixably/resources/device.rb +27 -0
- data/lib/fixably/resources/order.rb +87 -0
- data/lib/fixably/resources/queue.rb +9 -0
- data/lib/fixably/resources/status.rb +12 -0
- data/lib/fixably/resources/user.rb +15 -0
- data/lib/fixably/version.rb +5 -0
- data/lib/fixably.rb +37 -0
- 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,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
|