acfs 1.3.3 → 1.6.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.
Files changed (96) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +372 -0
  3. data/LICENSE +22 -0
  4. data/README.md +321 -0
  5. data/acfs.gemspec +38 -0
  6. data/lib/acfs.rb +51 -0
  7. data/lib/acfs/adapter/base.rb +26 -0
  8. data/lib/acfs/adapter/typhoeus.rb +82 -0
  9. data/lib/acfs/collection.rb +28 -0
  10. data/lib/acfs/collections/paginatable.rb +76 -0
  11. data/lib/acfs/configuration.rb +120 -0
  12. data/lib/acfs/errors.rb +147 -0
  13. data/lib/acfs/global.rb +101 -0
  14. data/lib/acfs/location.rb +76 -0
  15. data/lib/acfs/middleware/base.rb +24 -0
  16. data/lib/acfs/middleware/json.rb +31 -0
  17. data/lib/acfs/middleware/logger.rb +23 -0
  18. data/lib/acfs/middleware/msgpack.rb +32 -0
  19. data/lib/acfs/middleware/print.rb +23 -0
  20. data/lib/acfs/middleware/serializer.rb +41 -0
  21. data/lib/acfs/operation.rb +96 -0
  22. data/lib/acfs/request.rb +32 -0
  23. data/lib/acfs/request/callbacks.rb +54 -0
  24. data/lib/acfs/resource.rb +39 -0
  25. data/lib/acfs/resource/attributes.rb +270 -0
  26. data/lib/acfs/resource/attributes/base.rb +29 -0
  27. data/lib/acfs/resource/attributes/boolean.rb +39 -0
  28. data/lib/acfs/resource/attributes/date_time.rb +32 -0
  29. data/lib/acfs/resource/attributes/dict.rb +39 -0
  30. data/lib/acfs/resource/attributes/float.rb +33 -0
  31. data/lib/acfs/resource/attributes/integer.rb +29 -0
  32. data/lib/acfs/resource/attributes/list.rb +36 -0
  33. data/lib/acfs/resource/attributes/string.rb +26 -0
  34. data/lib/acfs/resource/attributes/uuid.rb +48 -0
  35. data/lib/acfs/resource/dirty.rb +37 -0
  36. data/lib/acfs/resource/initialization.rb +31 -0
  37. data/lib/acfs/resource/loadable.rb +35 -0
  38. data/lib/acfs/resource/locatable.rb +135 -0
  39. data/lib/acfs/resource/operational.rb +26 -0
  40. data/lib/acfs/resource/persistence.rb +258 -0
  41. data/lib/acfs/resource/query_methods.rb +266 -0
  42. data/lib/acfs/resource/service.rb +44 -0
  43. data/lib/acfs/resource/validation.rb +49 -0
  44. data/lib/acfs/response.rb +30 -0
  45. data/lib/acfs/response/formats.rb +27 -0
  46. data/lib/acfs/response/status.rb +33 -0
  47. data/lib/acfs/rspec.rb +13 -0
  48. data/lib/acfs/runner.rb +102 -0
  49. data/lib/acfs/service.rb +94 -0
  50. data/lib/acfs/service/middleware.rb +58 -0
  51. data/lib/acfs/service/middleware/stack.rb +65 -0
  52. data/lib/acfs/singleton_resource.rb +85 -0
  53. data/lib/acfs/stub.rb +199 -0
  54. data/lib/acfs/util.rb +22 -0
  55. data/lib/acfs/version.rb +16 -0
  56. data/lib/acfs/yard.rb +6 -0
  57. data/spec/acfs/adapter/typhoeus_spec.rb +55 -0
  58. data/spec/acfs/collection_spec.rb +157 -0
  59. data/spec/acfs/configuration_spec.rb +53 -0
  60. data/spec/acfs/global_spec.rb +140 -0
  61. data/spec/acfs/location_spec.rb +25 -0
  62. data/spec/acfs/middleware/json_spec.rb +79 -0
  63. data/spec/acfs/middleware/msgpack_spec.rb +62 -0
  64. data/spec/acfs/operation_spec.rb +12 -0
  65. data/spec/acfs/request/callbacks_spec.rb +48 -0
  66. data/spec/acfs/request_spec.rb +79 -0
  67. data/spec/acfs/resource/attributes/boolean_spec.rb +58 -0
  68. data/spec/acfs/resource/attributes/date_time_spec.rb +51 -0
  69. data/spec/acfs/resource/attributes/dict_spec.rb +77 -0
  70. data/spec/acfs/resource/attributes/float_spec.rb +61 -0
  71. data/spec/acfs/resource/attributes/integer_spec.rb +36 -0
  72. data/spec/acfs/resource/attributes/list_spec.rb +60 -0
  73. data/spec/acfs/resource/attributes/uuid_spec.rb +42 -0
  74. data/spec/acfs/resource/attributes_spec.rb +179 -0
  75. data/spec/acfs/resource/dirty_spec.rb +49 -0
  76. data/spec/acfs/resource/initialization_spec.rb +36 -0
  77. data/spec/acfs/resource/loadable_spec.rb +22 -0
  78. data/spec/acfs/resource/locatable_spec.rb +118 -0
  79. data/spec/acfs/resource/persistance_spec.rb +322 -0
  80. data/spec/acfs/resource/query_methods_spec.rb +548 -0
  81. data/spec/acfs/resource/validation_spec.rb +129 -0
  82. data/spec/acfs/response/formats_spec.rb +52 -0
  83. data/spec/acfs/response/status_spec.rb +71 -0
  84. data/spec/acfs/runner_spec.rb +95 -0
  85. data/spec/acfs/service/middleware_spec.rb +35 -0
  86. data/spec/acfs/service_spec.rb +48 -0
  87. data/spec/acfs/singleton_resource_spec.rb +17 -0
  88. data/spec/acfs/stub_spec.rb +345 -0
  89. data/spec/acfs_spec.rb +205 -0
  90. data/spec/fixtures/config.yml +14 -0
  91. data/spec/spec_helper.rb +42 -0
  92. data/spec/support/hash.rb +11 -0
  93. data/spec/support/response.rb +12 -0
  94. data/spec/support/service.rb +92 -0
  95. data/spec/support/shared/find_callbacks.rb +50 -0
  96. metadata +159 -26
@@ -0,0 +1,32 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'acfs/request/callbacks'
4
+
5
+ module Acfs
6
+ # Encapsulate all data required to make up a request to the
7
+ # underlaying http library.
8
+ #
9
+ class Request
10
+ attr_accessor :body, :format
11
+ attr_reader :url, :headers, :params, :data, :method, :operation
12
+
13
+ include Request::Callbacks
14
+ def initialize(url, **options, &block)
15
+ @url = URI.parse(url.to_s).tap do |_url|
16
+ @data = options.delete(:data) || nil
17
+ @format = options.delete(:format) || :json
18
+ @headers = options.delete(:headers) || {}
19
+ @params = options.delete(:params) || {}
20
+ @method = options.delete(:method) || :get
21
+ end.to_s
22
+
23
+ @operation = options.delete(:operation) || nil
24
+
25
+ on_complete(&block) if block_given?
26
+ end
27
+
28
+ def data?
29
+ !data.nil?
30
+ end
31
+ end
32
+ end
@@ -0,0 +1,54 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Acfs
4
+ class Request
5
+ # Module containing callback handling for Requests.
6
+ # Current the only callback type is `on_complete`:
7
+ #
8
+ # request = Request.new 'URL'
9
+ # request.on_complete { |response| ... }
10
+ #
11
+ module Callbacks
12
+ # Add a new `on_complete` callback for this request.
13
+ #
14
+ # @example Set on_complete.
15
+ # request.on_complete { |response| print response.body }
16
+ #
17
+ # @param [ Block ] block The callback block to execute.
18
+ #
19
+ # @yield [ Acfs::Response ]
20
+ #
21
+ # @return [ Acfs::Request ] The request itself.
22
+ #
23
+ def on_complete(&block)
24
+ callbacks.insert 0, block if block_given?
25
+ self
26
+ end
27
+
28
+ # Return array of all callbacks.
29
+ #
30
+ # @return [ Array<Block> ] All callbacks.
31
+ #
32
+ def callbacks
33
+ @callbacks ||= []
34
+ end
35
+
36
+ # Trigger all callback for given response.
37
+ #
38
+ # @return [ Acfs::Request ] The request itself.
39
+ #
40
+ def complete!(response)
41
+ call_callback response, 0
42
+ self
43
+ end
44
+
45
+ private
46
+
47
+ def call_callback(res, index)
48
+ return if index >= callbacks.size
49
+
50
+ callbacks[index].call(res, proc {|bres| call_callback bres, index + 1 })
51
+ end
52
+ end
53
+ end
54
+ end
@@ -0,0 +1,39 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'active_model'
4
+
5
+ # @api public
6
+ #
7
+ class Acfs::Resource
8
+ require 'acfs/resource/initialization'
9
+ require 'acfs/resource/attributes'
10
+ require 'acfs/resource/dirty'
11
+ require 'acfs/resource/loadable'
12
+ require 'acfs/resource/locatable'
13
+ require 'acfs/resource/operational'
14
+ require 'acfs/resource/persistence'
15
+ require 'acfs/resource/query_methods'
16
+ require 'acfs/resource/service'
17
+ require 'acfs/resource/validation'
18
+
19
+ if ActiveModel::VERSION::MAJOR >= 4
20
+ include ActiveModel::Model
21
+ else
22
+ extend ActiveModel::Naming
23
+ extend ActiveModel::Translation
24
+ include ActiveModel::Conversion
25
+ include ActiveModel::Validations
26
+ end
27
+
28
+ include Initialization
29
+
30
+ include Attributes
31
+ include Loadable
32
+ include Persistence
33
+ include Locatable
34
+ include Operational
35
+ include QueryMethods
36
+ include Service
37
+ include Dirty
38
+ include Validation
39
+ end
@@ -0,0 +1,270 @@
1
+ # frozen_string_literal: true
2
+
3
+ class Acfs::Resource
4
+ #
5
+ # = Acfs Attributes
6
+ #
7
+ # Allows to specify attributes of a class with default
8
+ # values and type safety.
9
+ #
10
+ # @example
11
+ # class User < Acfs::Resource
12
+ # attribute :name, :string, default: 'Anon'
13
+ # attribute :age, :integer
14
+ # attribute :special, My::Special::Type
15
+ # end
16
+ #
17
+ # For each attribute a setter and getter will be created and values will be
18
+ # type casted when set.
19
+ #
20
+ module Attributes
21
+ extend ActiveSupport::Concern
22
+ include ActiveModel::AttributeMethods
23
+
24
+ # @api public
25
+ #
26
+ # Write default attributes defined in resource class.
27
+ #
28
+ # @see #write_attributes
29
+ # @see ClassMethods#attributes
30
+ #
31
+ def initialize(*attrs)
32
+ write_attributes self.class.attributes
33
+ reset_changes
34
+ super
35
+ end
36
+
37
+ # @api public
38
+ #
39
+ # Returns ActiveModel compatible list of attributes and values.
40
+ #
41
+ # @example
42
+ # class User < Acfs::Resource
43
+ # attribute :name, type: String, default: 'Anon'
44
+ # end
45
+ # user = User.new(name: 'John')
46
+ # user.attributes # => { "name" => "John" }
47
+ #
48
+ # @return [HashWithIndifferentAccess{Symbol => Object}]
49
+ # Attributes and their values.
50
+ #
51
+ # rubocop:disable Naming/MemoizedInstanceVariableName
52
+ def attributes
53
+ @_attrs ||= HashWithIndifferentAccess.new
54
+ end
55
+ # rubocop:enable Naming/MemoizedInstanceVariableName
56
+
57
+ # @api public
58
+ #
59
+ # Update all attributes with given hash. Attribute values will be casted
60
+ # to defined attribute type.
61
+ #
62
+ # @example
63
+ # user.attributes = { :name => 'Adam' }
64
+ # user.name # => 'Adam'
65
+ #
66
+ # @param [Hash{String, Symbol => Object}, #each{|key, value|}]
67
+ # Attributes to set in resource.
68
+ # @see #write_attributes Delegates attributes hash to {#write_attributes}.
69
+ #
70
+ def attributes=(attributes)
71
+ write_attributes(attributes)
72
+ end
73
+
74
+ # @api public
75
+ #
76
+ # Read an attribute from instance variable.
77
+ #
78
+ # @param [Symbol, String] name Attribute name.
79
+ # @return [Object] Attribute value.
80
+ #
81
+ def read_attribute(name)
82
+ attributes[name.to_s]
83
+ end
84
+
85
+ # @api public
86
+ #
87
+ # Write a hash of attributes and values.
88
+ #
89
+ # If attribute value is a `Proc` it will be evaluated in the context
90
+ # of the resource after all non-proc attribute values are set. Values
91
+ # will be casted to defined attribute type.
92
+ #
93
+ # The behavior is used to apply default attributes from resource
94
+ # class definition.
95
+ #
96
+ # @example
97
+ # user.write_attributes name: 'john', email: ->{ "#{name}@example.org" }
98
+ # user.name # => 'john'
99
+ # user.email # => 'john@example.org'
100
+ #
101
+ # @param [Hash{String, Symbol => Object, Proc}, #each{|key, value|}]
102
+ # Attributes to write.
103
+ #
104
+ # @see #write_attribute Delegates attribute values to `#write_attribute`.
105
+ #
106
+ def write_attributes(attributes, **opts)
107
+ unless attributes.respond_to?(:each) && attributes.respond_to?(:keys)
108
+ return false
109
+ end
110
+
111
+ if opts.fetch(:unknown, :ignore) == :raise &&
112
+ (attributes.keys.map(&:to_s) - self.class.attributes.keys).any?
113
+ missing = attributes.keys - self.class.attributes.keys
114
+ missing.map!(&:inspect)
115
+ raise ArgumentError.new "Unknown attributes: #{missing.join(', ')}"
116
+ end
117
+
118
+ procs = {}
119
+
120
+ attributes.each do |key, _|
121
+ if attributes[key].is_a? Proc
122
+ procs[key] = attributes[key]
123
+ else
124
+ write_local_attribute(key, attributes[key], **opts)
125
+ end
126
+ end
127
+
128
+ procs.each do |key, proc|
129
+ write_local_attribute(key, instance_exec(&proc), **opts)
130
+ end
131
+
132
+ true
133
+ end
134
+
135
+ # @api private
136
+ #
137
+ # Check if a public getter for attribute exists that should be called to
138
+ # write it or of {#write_attribute} should be called directly. This is
139
+ # necessary as {#write_attribute} should go though setters but can also
140
+ # handle unknown attribute that will not have a generated setter method.
141
+ #
142
+ def write_local_attribute(name, value, opts = {})
143
+ method = "#{name}="
144
+ if respond_to? method, true
145
+ public_send method, value
146
+ else
147
+ write_attribute name, value, opts
148
+ end
149
+ end
150
+
151
+ # @api public
152
+ #
153
+ # Write single attribute with given value. Value will be casted
154
+ # to defined attribute type.
155
+ #
156
+ # @param [String, Symbol] name Attribute name.
157
+ # @param [Object] value Value to write.
158
+ # @raise [ArgumentError] If no attribute with given name is defined.
159
+ #
160
+ def write_attribute(name, value, opts = {})
161
+ attr_type = self.class.defined_attributes[name.to_s]
162
+ if attr_type
163
+ write_raw_attribute name, attr_type.cast(value), opts
164
+ else
165
+ write_raw_attribute name, value, opts
166
+ end
167
+ end
168
+
169
+ # @api private
170
+ #
171
+ # Write an attribute without checking type or existence or casting
172
+ # value to attributes type. Value be stored in an instance variable
173
+ # named after attribute name.
174
+ #
175
+ # @param [String, Symbol] name Attribute name.
176
+ # @param [Object] value Attribute value.
177
+ #
178
+ def write_raw_attribute(name, value, _ = {})
179
+ attributes[name.to_s] = value
180
+ end
181
+
182
+ module ClassMethods
183
+ ATTR_CLASS_BASE = '::Acfs::Resource::Attributes'
184
+
185
+ #
186
+ # @api public
187
+ #
188
+ # Define a model attribute by name and type. Will create getter and
189
+ # setter for given attribute name. Existing methods will be overridden.
190
+ #
191
+ # Available types can be found in `Acfs::Model::Attributes::*`.
192
+ #
193
+ # @example
194
+ # class User < Acfs::Resource
195
+ # attribute :name, :string, default: 'Anon'
196
+ # attribute :email, :string, default: lambda{ "#{name}@example.org"}
197
+ # end
198
+ #
199
+ # @param [#to_sym] name Attribute name.
200
+ # @param [Symbol, String, Class] type Attribute
201
+ # type identifier or type class.
202
+ #
203
+ def attribute(name, type, **opts)
204
+ if type.is_a?(Symbol) || type.is_a?(String)
205
+ type = "#{ATTR_CLASS_BASE}::#{type.to_s.classify}".constantize
206
+ end
207
+
208
+ define_attribute(name.to_sym, type, **opts)
209
+ end
210
+
211
+ # @api public
212
+ #
213
+ # Return list of possible attributes and default
214
+ # values for this model class.
215
+ #
216
+ # @example
217
+ # class User < Acfs::Resource
218
+ # attribute :name, :string
219
+ # attribute :age, :integer, default: 25
220
+ # end
221
+ # User.attributes # => { "name": nil, "age": 25 }
222
+ #
223
+ # @return [Hash{String => Object, Proc}]
224
+ # Attributes with default values.
225
+ #
226
+ def attributes
227
+ defined_attributes.each_with_object({}) do |(key, attr), hash|
228
+ hash[key] = attr.default_value
229
+ end
230
+ end
231
+
232
+ def defined_attributes
233
+ if superclass.respond_to?(:defined_attributes)
234
+ superclass.defined_attributes.merge(local_attributes)
235
+ else
236
+ local_attributes
237
+ end
238
+ end
239
+
240
+ private
241
+
242
+ def local_attributes
243
+ @local_attributes ||= {}
244
+ end
245
+
246
+ def define_attribute(name, type, **opts)
247
+ name = name.to_s
248
+ attribute = type.new(**opts)
249
+
250
+ local_attributes[name] = attribute
251
+ define_attribute_method name
252
+
253
+ send :define_method, name do
254
+ read_attribute name
255
+ end
256
+
257
+ send :define_method, :"#{name}=" do |value|
258
+ write_attribute name, value
259
+ end
260
+ end
261
+ end
262
+ end
263
+ end
264
+
265
+ # Load attribute type classes.
266
+ #
267
+ Dir[File.join(__dir__, 'attributes/*.rb')].sort.each do |path|
268
+ filename = File.basename(path)
269
+ require "acfs/resource/attributes/#{filename}"
270
+ end
@@ -0,0 +1,29 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Acfs::Resource::Attributes
4
+ class Base
5
+ attr_reader :default
6
+
7
+ def initialize(default: nil)
8
+ @default = default
9
+ end
10
+
11
+ def cast(value)
12
+ cast_value(value) unless value.nil?
13
+ end
14
+
15
+ def default_value
16
+ if default.respond_to? :call
17
+ default
18
+ else
19
+ cast default
20
+ end
21
+ end
22
+
23
+ private
24
+
25
+ def cast_value(_value)
26
+ raise NotImplementedError
27
+ end
28
+ end
29
+ end
@@ -0,0 +1,39 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Acfs::Resource::Attributes
4
+ # @api public
5
+ #
6
+ # Boolean attribute type. Use it in your model as an attribute type:
7
+ #
8
+ # @example
9
+ # class User < Acfs::Resource
10
+ # attribute :name, :boolean
11
+ # end
12
+ #
13
+ # Given objects will be converted to string. The following strings
14
+ # are considered true, everything else false:
15
+ #
16
+ # true, on, yes
17
+ #
18
+ class Boolean < Base
19
+ FALSE_VALUES = [false, 0, '0', 'f', 'F', 'false', 'FALSE', 'off', 'OFF', 'no', 'NO'].to_set
20
+
21
+ # @api public
22
+ #
23
+ # Cast given object to boolean.
24
+ #
25
+ # @param [Object] value Object to cast.
26
+ # @return [TrueClass, FalseClass] Casted boolean.
27
+ #
28
+ def cast_value(value)
29
+ return true if value == true
30
+ return false if value == false
31
+
32
+ if value.blank?
33
+ nil
34
+ else
35
+ !FALSE_VALUES.include?(value)
36
+ end
37
+ end
38
+ end
39
+ end