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,53 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module ActiveResource
|
4
|
+
class Base
|
5
|
+
class << self
|
6
|
+
private
|
7
|
+
|
8
|
+
alias original_instantiate_record instantiate_record
|
9
|
+
|
10
|
+
# Fixably uses camel case keys but it's more Ruby-like to use underscores
|
11
|
+
def instantiate_record(record, prefix_options = {})
|
12
|
+
underscored_record = record.deep_transform_keys(&:underscore)
|
13
|
+
original_instantiate_record(underscored_record, prefix_options)
|
14
|
+
end
|
15
|
+
|
16
|
+
alias original_query_string query_string
|
17
|
+
|
18
|
+
# Fixably expects all searches to be sent under a singular query parameter
|
19
|
+
# q=search1,search2,attribute:search3
|
20
|
+
def query_string(options)
|
21
|
+
opts = {}
|
22
|
+
|
23
|
+
non_query_parameters.each do
|
24
|
+
opts[_1] = options.fetch(_1) if options[_1]
|
25
|
+
end
|
26
|
+
|
27
|
+
f = filters(options)
|
28
|
+
opts[:q] = f.join(",") unless f.count.zero?
|
29
|
+
|
30
|
+
original_query_string(opts)
|
31
|
+
end
|
32
|
+
|
33
|
+
def filters(options)
|
34
|
+
options.each_with_object([]) do |(key, value), array|
|
35
|
+
next if non_query_parameters.include?(key)
|
36
|
+
|
37
|
+
array <<
|
38
|
+
if key.equal?(:filter)
|
39
|
+
value
|
40
|
+
else
|
41
|
+
camel_key = key.to_s.camelize(:lower)
|
42
|
+
"#{camel_key}:#{value}"
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
def non_query_parameters = %i[expand limit offset page]
|
48
|
+
end
|
49
|
+
end
|
50
|
+
end
|
51
|
+
|
52
|
+
ActiveResource::Base.collection_parser =
|
53
|
+
Fixably::ActiveResource::PaginatedCollection
|
@@ -0,0 +1,91 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative "../create_has_many_record"
|
4
|
+
|
5
|
+
module Fixably
|
6
|
+
module ActiveResource
|
7
|
+
class PaginatedCollection < ::ActiveResource::Collection
|
8
|
+
class << self
|
9
|
+
def paginatable?(value)
|
10
|
+
collection = attributes(value)
|
11
|
+
return false unless collection.is_a?(Hash)
|
12
|
+
|
13
|
+
interface = %w[limit offset total_items items]
|
14
|
+
(interface - collection.keys).empty?
|
15
|
+
end
|
16
|
+
|
17
|
+
def attributes(collection_wrapper)
|
18
|
+
if collection_wrapper.respond_to?(:attributes)
|
19
|
+
collection_wrapper.attributes
|
20
|
+
else
|
21
|
+
collection_wrapper
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
attr_reader :limit
|
27
|
+
attr_reader :offset
|
28
|
+
attr_reader :total_items
|
29
|
+
|
30
|
+
attr_accessor :parent_resource
|
31
|
+
attr_accessor :parent_association
|
32
|
+
|
33
|
+
def initialize(collection_wrapper = nil)
|
34
|
+
@limit = collection_wrapper&.fetch("limit") || 0
|
35
|
+
@offset = collection_wrapper&.fetch("offset") || 0
|
36
|
+
@total_items = collection_wrapper&.fetch("totalItems") do
|
37
|
+
collection_wrapper.fetch("total_items")
|
38
|
+
end || 0
|
39
|
+
|
40
|
+
collection = collection_wrapper&.fetch("items") || []
|
41
|
+
super(collection)
|
42
|
+
end
|
43
|
+
|
44
|
+
def <<(record)
|
45
|
+
CreateHasManyRecord.(record: record, collection: self)
|
46
|
+
end
|
47
|
+
|
48
|
+
def paginated_each
|
49
|
+
page = self
|
50
|
+
|
51
|
+
loop do
|
52
|
+
page.each { yield(_1) }
|
53
|
+
break unless page.next_page?
|
54
|
+
|
55
|
+
page = page.next_page
|
56
|
+
end
|
57
|
+
end
|
58
|
+
|
59
|
+
def paginated_map
|
60
|
+
[].tap do |records|
|
61
|
+
paginated_each { records << _1 }
|
62
|
+
end
|
63
|
+
end
|
64
|
+
|
65
|
+
def next_page
|
66
|
+
raise StopIteration, "There are no more pages" unless next_page?
|
67
|
+
|
68
|
+
where(limit: limit, offset: offset + limit)
|
69
|
+
end
|
70
|
+
|
71
|
+
def next_page?
|
72
|
+
(limit + offset) < total_items
|
73
|
+
end
|
74
|
+
|
75
|
+
def previous_page
|
76
|
+
raise StopIteration, "There are no more pages" unless previous_page?
|
77
|
+
|
78
|
+
new_offset = offset - limit
|
79
|
+
new_limit = limit
|
80
|
+
new_limit += new_offset if new_offset.negative?
|
81
|
+
new_offset = 0 if new_offset.negative?
|
82
|
+
|
83
|
+
where(limit: new_limit, offset: new_offset)
|
84
|
+
end
|
85
|
+
|
86
|
+
def previous_page?
|
87
|
+
offset.positive?
|
88
|
+
end
|
89
|
+
end
|
90
|
+
end
|
91
|
+
end
|
@@ -0,0 +1,49 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative "actions"
|
4
|
+
require_relative "authorization"
|
5
|
+
require_relative "encoding"
|
6
|
+
require_relative "load_from_response"
|
7
|
+
|
8
|
+
module Fixably
|
9
|
+
class ApplicationResource < ::ActiveResource::Base
|
10
|
+
self.include_format_in_path = false
|
11
|
+
|
12
|
+
include Actions
|
13
|
+
include Encoding
|
14
|
+
include LoadFromResponse
|
15
|
+
extend Authorization
|
16
|
+
|
17
|
+
attr_accessor :parent_association
|
18
|
+
|
19
|
+
class << self
|
20
|
+
def site
|
21
|
+
self.site = site_url unless _site_defined?
|
22
|
+
super()
|
23
|
+
end
|
24
|
+
|
25
|
+
protected
|
26
|
+
|
27
|
+
def site_url
|
28
|
+
subdomain = Fixably.config.require(:subdomain)
|
29
|
+
base_url = "https://#{subdomain}.fixably.com/api/#{api_version}"
|
30
|
+
|
31
|
+
name_parts = name.split("::")
|
32
|
+
return base_url if name_parts.length.equal?(2)
|
33
|
+
|
34
|
+
parent_resource = name_parts.fetch(1).underscore
|
35
|
+
"#{base_url}/#{parent_resource.pluralize}/:#{parent_resource}_id"
|
36
|
+
end
|
37
|
+
|
38
|
+
private
|
39
|
+
|
40
|
+
def api_version = "v3"
|
41
|
+
end
|
42
|
+
|
43
|
+
def initialize(attributes = {}, persisted = false) # rubocop:disable Style/OptionalBooleanParameter
|
44
|
+
super(attributes, persisted)
|
45
|
+
|
46
|
+
self.class.site
|
47
|
+
end
|
48
|
+
end
|
49
|
+
end
|
@@ -0,0 +1,77 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Fixably
|
4
|
+
module ArgumentParameterisation
|
5
|
+
private
|
6
|
+
|
7
|
+
def parametize_arguments(scope, arguments)
|
8
|
+
arguments ||= {}
|
9
|
+
arguments.merge!(arguments.delete(:params)) if arguments.key?(:params)
|
10
|
+
|
11
|
+
associations = expand_associations(scope, arguments)
|
12
|
+
arguments[:expand] = associations if associations
|
13
|
+
|
14
|
+
{ params: arguments }
|
15
|
+
end
|
16
|
+
|
17
|
+
def expand_associations(scope, arguments)
|
18
|
+
return if arguments[:expand].instance_of?(String)
|
19
|
+
|
20
|
+
case scope
|
21
|
+
when :all, :first, :last
|
22
|
+
assoc = associations(arguments)&.join(",")
|
23
|
+
assoc ? "items(#{assoc})" : "items"
|
24
|
+
when :one, nil, Integer, String
|
25
|
+
associations(arguments)&.join(",")
|
26
|
+
else
|
27
|
+
raise ArgumentError, "Unknown scope: #{scope.inspect}"
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
def associations(arguments)
|
32
|
+
arguments[:expand]&.to_set { expand_association(_1) }
|
33
|
+
end
|
34
|
+
|
35
|
+
def expand_association(association)
|
36
|
+
relationship = reflections.fetch(association).macro
|
37
|
+
case relationship
|
38
|
+
when :has_one
|
39
|
+
association
|
40
|
+
when :has_many
|
41
|
+
"#{association}(items)"
|
42
|
+
else
|
43
|
+
raise ArgumentError, "Unknown relationship, #{relationship}"
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
def stringify_array_values(arguments)
|
48
|
+
arguments.tap do |args|
|
49
|
+
args.each do |attribute, value|
|
50
|
+
next unless value.is_a?(Array)
|
51
|
+
|
52
|
+
validate_array_value!(attribute, value)
|
53
|
+
value << nil if value.length.equal?(1)
|
54
|
+
args[attribute] = "[#{value.map { stringify(_1) }.join(",")}]"
|
55
|
+
end
|
56
|
+
end
|
57
|
+
end
|
58
|
+
|
59
|
+
def validate_array_value!(attribute, value)
|
60
|
+
return if value.length.positive? && value.length <= 2
|
61
|
+
|
62
|
+
raise(
|
63
|
+
ArgumentError,
|
64
|
+
"Ranged searches should have either 1 or 2 values but " \
|
65
|
+
"#{attribute} has #{value.length}"
|
66
|
+
)
|
67
|
+
end
|
68
|
+
|
69
|
+
def stringify(value)
|
70
|
+
if value.respond_to?(:strftime)
|
71
|
+
value.strftime("%F")
|
72
|
+
else
|
73
|
+
value
|
74
|
+
end
|
75
|
+
end
|
76
|
+
end
|
77
|
+
end
|
@@ -0,0 +1,39 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative "logger"
|
4
|
+
|
5
|
+
module Fixably
|
6
|
+
class Config
|
7
|
+
attr_accessor :api_key
|
8
|
+
attr_accessor :subdomain
|
9
|
+
|
10
|
+
def logger
|
11
|
+
Logger.logger
|
12
|
+
end
|
13
|
+
|
14
|
+
def logger=(log)
|
15
|
+
Logger.logger = log
|
16
|
+
end
|
17
|
+
|
18
|
+
def require(param)
|
19
|
+
value = public_send(param)
|
20
|
+
return value unless value.nil? || value.empty?
|
21
|
+
|
22
|
+
require_error(param)
|
23
|
+
end
|
24
|
+
|
25
|
+
private
|
26
|
+
|
27
|
+
def require_error(param)
|
28
|
+
raise(
|
29
|
+
ArgumentError,
|
30
|
+
<<~MESSAGE
|
31
|
+
#{param} is required but hasn't been set.
|
32
|
+
Fixably.configure do |config|
|
33
|
+
config.#{param} = "value"
|
34
|
+
end
|
35
|
+
MESSAGE
|
36
|
+
)
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
@@ -0,0 +1,90 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Fixably
|
4
|
+
class CreateHasManyRecord
|
5
|
+
def self.call(record:, collection:)
|
6
|
+
new(record: record, collection: collection).call
|
7
|
+
end
|
8
|
+
|
9
|
+
attr_reader :record
|
10
|
+
attr_reader :collection
|
11
|
+
|
12
|
+
def initialize(record:, collection:)
|
13
|
+
@record = record
|
14
|
+
@collection = collection
|
15
|
+
end
|
16
|
+
|
17
|
+
def call
|
18
|
+
can_append!
|
19
|
+
save_record
|
20
|
+
collection.elements << record
|
21
|
+
end
|
22
|
+
|
23
|
+
private
|
24
|
+
|
25
|
+
def can_append!
|
26
|
+
instance_of_resource_class!
|
27
|
+
nested_resource!
|
28
|
+
parent_recource_known!
|
29
|
+
parent_association_known!
|
30
|
+
parent_is_persisted!
|
31
|
+
end
|
32
|
+
|
33
|
+
def instance_of_resource_class!
|
34
|
+
return if record.instance_of?(collection.resource_class)
|
35
|
+
|
36
|
+
raise(
|
37
|
+
TypeError,
|
38
|
+
"Appended record must be an instance of " \
|
39
|
+
"#{collection.resource_class.name}"
|
40
|
+
)
|
41
|
+
end
|
42
|
+
|
43
|
+
def nested_resource!
|
44
|
+
return if nested_resource?
|
45
|
+
|
46
|
+
raise(
|
47
|
+
ArgumentError,
|
48
|
+
"Can only appended resources nested one level deep"
|
49
|
+
)
|
50
|
+
end
|
51
|
+
|
52
|
+
def nested_resource?
|
53
|
+
name_parts = record.class.name.split("::")
|
54
|
+
name_parts.length.equal?(3)
|
55
|
+
end
|
56
|
+
|
57
|
+
def parent_recource_known!
|
58
|
+
return if collection.parent_resource
|
59
|
+
|
60
|
+
raise "A parent resource has not been set"
|
61
|
+
end
|
62
|
+
|
63
|
+
def parent_association_known!
|
64
|
+
return if collection.parent_association
|
65
|
+
|
66
|
+
raise "The association to the parent resource has not been set"
|
67
|
+
end
|
68
|
+
|
69
|
+
def parent_is_persisted!
|
70
|
+
if !collection.parent_resource.persisted?
|
71
|
+
raise "The parent resource has not been been persisted"
|
72
|
+
end
|
73
|
+
|
74
|
+
if !collection.parent_resource.id?
|
75
|
+
raise "Cannot find an ID for the parent resource"
|
76
|
+
end
|
77
|
+
end
|
78
|
+
|
79
|
+
def save_record
|
80
|
+
record.parent_association = collection.parent_association
|
81
|
+
record.prefix_options[parent_id_key] = collection.parent_resource.id
|
82
|
+
record.save!
|
83
|
+
end
|
84
|
+
|
85
|
+
def parent_id_key
|
86
|
+
"#{collection.parent_resource.class.name.split("::").last.underscore}_id".
|
87
|
+
to_sym
|
88
|
+
end
|
89
|
+
end
|
90
|
+
end
|
@@ -0,0 +1,38 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Fixably
|
4
|
+
module Encoding
|
5
|
+
# Since our monkey patch converts the keys to underscore, it is necessary to
|
6
|
+
# convert them back to camelcase when performing a create or update
|
7
|
+
def encode(_options = nil, attrs: nil)
|
8
|
+
attrs ||= attributes
|
9
|
+
remove_has_many_associations(attrs)
|
10
|
+
remove_unallowed_parameters(attrs)
|
11
|
+
nest_for_association(attrs).
|
12
|
+
deep_transform_keys { _1.camelize(:lower) }.
|
13
|
+
public_send("to_#{self.class.format.extension}")
|
14
|
+
end
|
15
|
+
|
16
|
+
protected
|
17
|
+
|
18
|
+
def remove_on_encode = %w[created_at href id]
|
19
|
+
|
20
|
+
private
|
21
|
+
|
22
|
+
def remove_has_many_associations(attrs)
|
23
|
+
reflections.select { _2.macro.equal?(:has_many) }.keys.each do
|
24
|
+
attrs.delete(_1)
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
def remove_unallowed_parameters(attrs)
|
29
|
+
remove_on_encode.each { attrs.delete(_1) }
|
30
|
+
end
|
31
|
+
|
32
|
+
def nest_for_association(attrs)
|
33
|
+
return attrs unless parent_association
|
34
|
+
|
35
|
+
{ parent_association.to_s => [attrs] }
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|