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,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
|