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