acfs 1.3.3 → 1.3.4

Sign up to get free protection for your applications and to get access to all the features.
Files changed (96) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +339 -0
  3. data/LICENSE +22 -0
  4. data/README.md +335 -0
  5. data/acfs.gemspec +46 -0
  6. data/lib/acfs.rb +51 -0
  7. data/lib/acfs/adapter/base.rb +24 -0
  8. data/lib/acfs/adapter/typhoeus.rb +69 -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 +127 -0
  13. data/lib/acfs/global.rb +101 -0
  14. data/lib/acfs/location.rb +82 -0
  15. data/lib/acfs/middleware/base.rb +24 -0
  16. data/lib/acfs/middleware/json.rb +29 -0
  17. data/lib/acfs/middleware/logger.rb +25 -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 +83 -0
  22. data/lib/acfs/request.rb +39 -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 +269 -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 +132 -0
  39. data/lib/acfs/resource/operational.rb +23 -0
  40. data/lib/acfs/resource/persistence.rb +260 -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 +39 -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 +97 -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 +194 -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 +65 -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 +181 -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 +43 -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 +136 -3
@@ -0,0 +1,23 @@
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
+ delegate :operation, to: :'self.class'
15
+
16
+ module ClassMethods
17
+ # Invoke CRUD operation.
18
+ def operation(action, opts = {}, &block)
19
+ Acfs.runner.process ::Acfs::Operation.new self, action, opts, &block
20
+ end
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,260 @@
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(*args)
61
+ save!(*args)
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
+ private
248
+
249
+ def update_with(data)
250
+ self.attributes = data
251
+ loaded!
252
+ end
253
+
254
+ def check_loaded!(opts = {})
255
+ return if loaded? || opts[:force]
256
+
257
+ raise ::Acfs::ResourceNotLoaded.new resource: self
258
+ end
259
+ end
260
+ 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.merge(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, NoMethodError
262
+ raise Acfs::ResourceTypeError.new type_name: type, base_class: self
263
+ end
264
+ end
265
+ end
266
+ end