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,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,17 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Fixably
4
+ module Authorization
5
+ def headers
6
+ result = super()
7
+ result["Authorization"] = api_key
8
+ result
9
+ end
10
+
11
+ private
12
+
13
+ def api_key
14
+ Fixably.config.require(:api_key)
15
+ end
16
+ end
17
+ 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