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,26 @@
1
+ # frozen_string_literal: true
2
+
3
+ class Acfs::Resource
4
+ # @api private
5
+ #
6
+ # Provide methods for creating and processing CRUD operations and
7
+ # handling responses. That includes error handling as well as
8
+ # handling stubbed resources.
9
+ #
10
+ # Should only be used internal.
11
+ #
12
+ module Operational
13
+ extend ActiveSupport::Concern
14
+
15
+ def operation(*args, **kwargs, &block)
16
+ self.class.operation(*args, **kwargs, &block)
17
+ end
18
+
19
+ module ClassMethods
20
+ # Invoke CRUD operation.
21
+ def operation(action, **opts, &block)
22
+ Acfs.runner.process ::Acfs::Operation.new(self, action, **opts, &block)
23
+ end
24
+ end
25
+ end
26
+ end
@@ -0,0 +1,258 @@
1
+ # frozen_string_literal: true
2
+
3
+ class Acfs::Resource
4
+ #
5
+ # Allow to track the persistence state of a model.
6
+ #
7
+ module Persistence
8
+ extend ActiveSupport::Concern
9
+
10
+ # @api public
11
+ #
12
+ # Check if the model is persisted. A model is persisted if
13
+ # it is saved after being created
14
+ #
15
+ # @example Newly created resource:
16
+ # user = User.new name: "John"
17
+ # user.persisted? # => false
18
+ # user.save
19
+ # user.persisted? # => true
20
+ #
21
+ # @example Modified resource:
22
+ # user2 = User.find 5
23
+ # user2.persisted? # => true
24
+ # user2.name = 'Amy'
25
+ # user2.persisted? # => true
26
+ # user2.save
27
+ # user2.persisted? # => true
28
+ #
29
+ # @return [Boolean] True if resource has been saved
30
+ #
31
+ def persisted?
32
+ !new?
33
+ end
34
+
35
+ # @api public
36
+ #
37
+ # Return true if model is a new record and was not saved yet.
38
+ #
39
+ # @return [Boolean] True if resource is newly created,
40
+ # false otherwise.
41
+ #
42
+ def new?
43
+ !loaded?
44
+ end
45
+ alias new_record? new?
46
+
47
+ # @api public
48
+ #
49
+ # Saves the resource.
50
+ #
51
+ # It will PUT to the service to update the resource or send
52
+ # a POST to create a new one if the resource is new.
53
+ #
54
+ # Saving a resource is a synchronous operation.
55
+ #
56
+ # @return [Boolean] True if save operation was successful,
57
+ # false otherwise.
58
+ # @see #save! See {#save!} for available options.
59
+ #
60
+ def save(**opts)
61
+ save!(**opts)
62
+ true
63
+ rescue Acfs::Error
64
+ false
65
+ end
66
+
67
+ # @api public
68
+ #
69
+ # Saves the resource. Raises an error if something happens.
70
+ #
71
+ # Saving a resource is a synchronous operation.
72
+ #
73
+ # @param opts [Hash] Hash with additional options.
74
+ # @option opts [Hash] :data Data to send to remote service.
75
+ # Default will be resource attributes.
76
+ #
77
+ # @raise [Acfs::InvalidResource]
78
+ # If remote services respond with 422 response. Will fill
79
+ # errors with data from response
80
+ # @raise [Acfs::ErroneousResponse]
81
+ # If remote service respond with not successful response.
82
+ #
83
+ # @see #save
84
+ #
85
+ def save!(**opts)
86
+ opts[:data] = attributes unless opts[:data]
87
+
88
+ operation((new? ? :create : :update), **opts) do |data|
89
+ update_with data
90
+ end
91
+ rescue ::Acfs::InvalidResource => e
92
+ self.remote_errors = e.errors
93
+ raise e
94
+ end
95
+
96
+ # @api public
97
+ #
98
+ # Update attributes with given data and save resource.
99
+ #
100
+ # Saving a resource is a synchronous operation.
101
+ #
102
+ # @param attrs [Hash] Hash with attributes to write.
103
+ # @param opts [Hash] Options passed to `save`.
104
+ #
105
+ # @return [Boolean]
106
+ # True if save operation was successful, false otherwise.
107
+ #
108
+ # @see #save
109
+ # @see #attributes=
110
+ # @see #update_attributes!
111
+ #
112
+ def update_attributes(attrs, **opts)
113
+ check_loaded!(**opts)
114
+
115
+ self.attributes = attrs
116
+ save(**opts)
117
+ end
118
+
119
+ # @api public
120
+ #
121
+ # Update attributes with given data and save resource.
122
+ #
123
+ # Saving a resource is a synchronous operation.
124
+ #
125
+ # @param [Hash] attrs Hash with attributes to write.
126
+ # @param [Hash] opts Options passed to `save!`.
127
+ #
128
+ # @raise [Acfs::InvalidResource]
129
+ # If remote services respond with 422 response. Will fill
130
+ # errors with data from response
131
+ #
132
+ # @raise [Acfs::ErroneousResponse]
133
+ # If remote service respond with not successful response.
134
+ #
135
+ # @see #save!
136
+ # @see #attributes=
137
+ # @see #update_attributes
138
+ #
139
+ def update_attributes!(attrs, **opts)
140
+ check_loaded! opts
141
+
142
+ self.attributes = attrs
143
+ save!(**opts)
144
+ end
145
+
146
+ # @api public
147
+ #
148
+ # Destroy resource by sending a DELETE request.
149
+ #
150
+ # Deleting a resource is a synchronous operation.
151
+ #
152
+ # @return [Boolean]
153
+ # @see #delete!
154
+ #
155
+ def delete(**opts)
156
+ delete!(**opts)
157
+ true
158
+ rescue Acfs::Error
159
+ false
160
+ end
161
+
162
+ # @api public
163
+ #
164
+ # Destroy resource by sending a DELETE request.
165
+ # Will raise an error in case something goes wrong.
166
+ #
167
+ # Deleting a resource is a synchronous operation.
168
+
169
+ # @raise [Acfs::ErroneousResponse]
170
+ # If remote service respond with not successful response.
171
+ #
172
+ # @return [undefined]
173
+ # @see #delete
174
+ #
175
+ def delete!(**opts)
176
+ opts[:params] ||= {}
177
+ opts[:params] = attributes_for_url(:delete).merge opts[:params]
178
+
179
+ operation(:delete, **opts) do |data|
180
+ update_with data
181
+ freeze
182
+ end
183
+ end
184
+
185
+ private
186
+
187
+ def attributes_for_url(action)
188
+ arguments_for_url = self.class.location(action: action).arguments
189
+ attributes.slice(*arguments_for_url)
190
+ end
191
+
192
+ module ClassMethods
193
+ # @api public
194
+ #
195
+ # Create a new resource sending given data. If resource cannot be
196
+ # created an error will be thrown.
197
+ #
198
+ # Saving a resource is a synchronous operation.
199
+ #
200
+ # @param data [Hash{Symbol, String => Object}]
201
+ # Data to send in create request.
202
+ #
203
+ # @return [self] Newly resource object.
204
+ #
205
+ # @raise [Acfs::InvalidResource]
206
+ # If remote services respond with 422 response. Will fill
207
+ # errors with data from response
208
+ #
209
+ # @raise [Acfs::ErroneousResponse]
210
+ # If remote service respond with not successful response.
211
+ #
212
+ # @see Acfs::Model::Persistence#save! Available options. `:data`
213
+ # will be overridden with provided data hash.
214
+ # @see #create
215
+ #
216
+ def create!(data, _opts = {})
217
+ new(data).tap(&:save!)
218
+ end
219
+
220
+ # @api public
221
+ #
222
+ # Create a new resource sending given data. If resource cannot be
223
+ # create model will be returned and error hash contains response
224
+ # errors if available.
225
+ #
226
+ # Saving a resource is a synchronous operation.
227
+ #
228
+ # @param data [Hash{Symbol, String => Object}]
229
+ # Data to send in create request.
230
+ #
231
+ # @return [self] Newly resource object.
232
+ #
233
+ # @raise [Acfs::ErroneousResponse]
234
+ # If remote service respond with not successful response.
235
+ #
236
+ # @see Acfs::Model::Persistence#save! Available options. `:data`
237
+ # will be overridden with provided data hash.
238
+ # @see #create!
239
+ #
240
+ def create(data, _opts = {})
241
+ model = new data
242
+ model.save
243
+ model
244
+ end
245
+ end
246
+
247
+ def update_with(data)
248
+ self.attributes = data
249
+ loaded!
250
+ end
251
+
252
+ def check_loaded!(opts = {})
253
+ return if loaded? || opts[:force]
254
+
255
+ raise ::Acfs::ResourceNotLoaded.new resource: self
256
+ end
257
+ end
258
+ end
@@ -0,0 +1,266 @@
1
+ # frozen_string_literal: true
2
+
3
+ class Acfs::Resource
4
+ # Methods providing the query interface for finding resouces.
5
+ #
6
+ # @example
7
+ # class MyUser < Acfs::Resource
8
+ # end
9
+ #
10
+ # MyUser.find(5) # Find single resource
11
+ # MyUser.all # Full or partial collection of
12
+ # # resources
13
+ # Comment.where(user: user.id) # Collection with additional parameter
14
+ # # to filter resources
15
+ #
16
+ module QueryMethods
17
+ extend ActiveSupport::Concern
18
+
19
+ module ClassMethods
20
+ # @api public
21
+ #
22
+ # @overload find(id, opts = {})
23
+ # Find a single resource by given ID.
24
+ #
25
+ # @example
26
+ # user = User.find(5) # Will query `http://base.url/users/5`
27
+ #
28
+ # @param id [Fixnum] Resource IDs to fetch from remote service.
29
+ # @param params [Hash] Additional options.
30
+ # @option opts [Hash] :params Additional parameters added to
31
+ # request. `:id` will be overridden with given ID.
32
+ #
33
+ # @yield [resource] Callback block to be executed after resource
34
+ # was fetched successfully.
35
+ # @yieldparam resource [self] Fetched resources.
36
+ #
37
+ # @return [self] Resource object if only one ID was given.
38
+ #
39
+ # @overload find(ids, opts = {})
40
+ # Load collection of specified resources by given IDs.
41
+ #
42
+ # @example
43
+ # User.find([1, 2, 5]) # Will return collection and will request
44
+ # # `http://base.url/users/1`,
45
+ # # `http://base.url/users/2`
46
+ # # and `http://base.url/users/5` parallel
47
+ #
48
+ # @param ids [Array<Integer>] List of resource IDs.
49
+ # @param opts [Hash] Additional options.
50
+ # @option opts [Hash] :params Additional parameters added to
51
+ # request. `:id` will be overridden with individual resource ID.
52
+ #
53
+ # @yield [collection] Callback block to be executed after collection
54
+ # was fetched successfully.
55
+ # @yieldparam resource [Collection] Collection with fetched resources.
56
+ #
57
+ # @return [Collection] Collection of requested resources.
58
+ #
59
+ def find(id_or_ids, **opts, &block)
60
+ if id_or_ids.respond_to? :each
61
+ find_multiple id_or_ids, opts, &block
62
+ else
63
+ find_single id_or_ids, opts, &block
64
+ end
65
+ end
66
+
67
+ # @api public
68
+ #
69
+ # Try to load all resources.
70
+ #
71
+ # @param params [Hash] Request parameters that will be send to
72
+ # remote service.
73
+ #
74
+ # @yield [collection] Callback block to be executed when resource
75
+ # collection was loaded successfully.
76
+ # @yieldparam collection [Collection] Collection of fetched resources.
77
+ #
78
+ # @return [Collection] Collection of requested resources.
79
+ #
80
+ def all(params = {}, opts = {}, &block)
81
+ collection = ::Acfs::Collection.new self
82
+ collection.__callbacks__ << block if block
83
+
84
+ operation(:list, **opts, params: params) do |data, response|
85
+ data.each {|obj| collection << create_resource(obj) }
86
+ collection.process_response response
87
+ collection.loaded!
88
+ collection.__invoke__
89
+ end
90
+
91
+ collection
92
+ end
93
+ alias where all
94
+
95
+ # @api public
96
+ #
97
+ # Try to load first resource. Return nil if no object can be loaded.
98
+ #
99
+ # @param params [Hash] Request parameters that will be send
100
+ # to remote service.
101
+ #
102
+ # @yield [resource] Callback block to be executed after
103
+ # resource was fetched (even if nil).
104
+ # @yieldparam resource [self] Fetched resource, nil
105
+ # if empty list is returned
106
+ #
107
+ # @return [self] Resource object, nil if empty list is returned
108
+ #
109
+ def find_by(params, &block)
110
+ Acfs::Util::ResourceDelegator.new(new).tap do |m|
111
+ m.__callbacks__ << block unless block.nil?
112
+ operation(:list, params: params) do |data|
113
+ if data.empty?
114
+ m.__setobj__ nil
115
+ else
116
+ m.__setobj__ create_resource(data.first, origin: m.__getobj__)
117
+ end
118
+ m.__invoke__
119
+ end
120
+ end
121
+ end
122
+
123
+ # @api public
124
+ #
125
+ # Try to load first resource. Raise Acfs::ResourceNotFound
126
+ # exception if no object can be loaded.
127
+ #
128
+ # @param params [Hash] Request parameters that will be send
129
+ # to remote service.
130
+ #
131
+ # @yield [resource] Callback block to be executed after
132
+ # resource was fetched successfully.
133
+ # @yieldparam resource [self] Fetched resource, nil
134
+ # if empty list is returned
135
+ #
136
+ # @return [self] Resource object, nil if empty list is returned
137
+ #
138
+ def find_by!(params, &block)
139
+ find_by params do |m|
140
+ if m.nil?
141
+ raise Acfs::ResourceNotFound.new message: 'Received erroneous ' \
142
+ "response: no `#{name}` with params #{params} found"
143
+ end
144
+ block&.call m
145
+ end
146
+ end
147
+
148
+ # @api public
149
+ #
150
+ # Iterates over all pages returned by index action.
151
+ #
152
+ # Server must return a paginated resource.
153
+ #
154
+ # @example
155
+ # User.each_page do |page|
156
+ # p page.size
157
+ # end
158
+ # Acfs.run
159
+ # # => 50
160
+ # # => 50
161
+ # # => 42
162
+ #
163
+ # @param opts [Hash] Options passed to {#where}.
164
+ #
165
+ # @yield [collection] Callback that will be invoked for each page.
166
+ # @yieldparam collection [Collection] Paginated collection.
167
+ #
168
+ # @return [Collection] First page.
169
+ #
170
+ def each_page(opts = {})
171
+ cb = proc do |collection|
172
+ yield collection
173
+ collection.next_page(&cb)
174
+ end
175
+ where opts, &cb
176
+ end
177
+
178
+ # @api public
179
+ #
180
+ # Iterates over all items of all pages returned by index action.
181
+ #
182
+ # Server must return a paginated resource.
183
+ #
184
+ # @example
185
+ # index = 0
186
+ # User.each_item do |page|
187
+ # index += 1
188
+ # end
189
+ # Acfs.run
190
+ # print index
191
+ # # => 142
192
+ #
193
+ # @param opts [Hash] Options passed to {#each_page}.
194
+ #
195
+ # @yield [item] Callback that will be invoked for each item.
196
+ # @yieldparam item [self] Resource.
197
+ # @yieldparam collection [Acfs::Collection] Collection.
198
+ #
199
+ def each_item(opts = {})
200
+ each_page(opts) do |collection|
201
+ collection.each do |item|
202
+ yield item, collection
203
+ end
204
+ end
205
+ end
206
+
207
+ private
208
+
209
+ def find_single(id, opts, &block)
210
+ model = Acfs::Util::ResourceDelegator.new new
211
+
212
+ opts[:params] ||= {}
213
+ opts[:params].merge! id: id unless id.nil?
214
+
215
+ model.__callbacks__ << block unless block.nil?
216
+
217
+ operation(:read, **opts) do |data|
218
+ model.__setobj__ create_resource data, origin: model.__getobj__
219
+ model.__invoke__
220
+ end
221
+
222
+ model
223
+ end
224
+
225
+ def find_multiple(ids, opts, &block)
226
+ ::Acfs::Collection.new(self).tap do |collection|
227
+ collection.__callbacks__ << block unless block.nil?
228
+
229
+ counter = 0
230
+ ids.each_with_index do |id, index|
231
+ find_single(id, opts) do |resource|
232
+ collection[index] = resource
233
+ if (counter += 1) == ids.size
234
+ collection.loaded!
235
+ collection.__invoke__
236
+ end
237
+ end
238
+ end
239
+ end
240
+ end
241
+
242
+ def create_resource(data, **opts)
243
+ type = data.delete 'type'
244
+ klass = resource_class_lookup(type)
245
+ (opts[:origin].is_a?(klass) ? opts[:origin] : klass.new).tap do |m|
246
+ m.write_attributes(data, **opts)
247
+ m.loaded!
248
+ end
249
+ end
250
+
251
+ def resource_class_lookup(type)
252
+ return self if type.nil?
253
+
254
+ klass = type.camelize.constantize
255
+
256
+ unless klass <= self
257
+ raise Acfs::ResourceTypeError.new type_name: type, base_class: self
258
+ end
259
+
260
+ klass
261
+ rescue NameError
262
+ raise Acfs::ResourceTypeError.new type_name: type, base_class: self
263
+ end
264
+ end
265
+ end
266
+ end