arkenstone-open 3.0.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (45) hide show
  1. checksums.yaml +7 -0
  2. data/.github/workflows/codacy-analysis.yml +46 -0
  3. data/.gitignore +19 -0
  4. data/.rubocop.yml +5 -0
  5. data/.ruby-version +1 -0
  6. data/.travis.yml +16 -0
  7. data/Gemfile +21 -0
  8. data/Gemfile.lock +101 -0
  9. data/LICENSE.txt +11 -0
  10. data/README.md +87 -0
  11. data/Rakefile +36 -0
  12. data/arkenstone.gemspec +27 -0
  13. data/lib/arkenstone/associations/resources.rb +76 -0
  14. data/lib/arkenstone/associations.rb +389 -0
  15. data/lib/arkenstone/document.rb +289 -0
  16. data/lib/arkenstone/enumerable/query_list.rb +20 -0
  17. data/lib/arkenstone/errors/no_url_error.rb +14 -0
  18. data/lib/arkenstone/helpers.rb +18 -0
  19. data/lib/arkenstone/network/env.rb +19 -0
  20. data/lib/arkenstone/network/hook.rb +74 -0
  21. data/lib/arkenstone/network/network.rb +72 -0
  22. data/lib/arkenstone/query_builder.rb +84 -0
  23. data/lib/arkenstone/queryable.rb +38 -0
  24. data/lib/arkenstone/timestamps.rb +25 -0
  25. data/lib/arkenstone/validation/validation_error.rb +34 -0
  26. data/lib/arkenstone/validation/validations.rb +192 -0
  27. data/lib/arkenstone/version.rb +5 -0
  28. data/lib/arkenstone.rb +22 -0
  29. data/test/associations/test_document_overrides.rb +50 -0
  30. data/test/associations/test_has_and_belongs_to_many.rb +80 -0
  31. data/test/dummy/app/models/association.rb +35 -0
  32. data/test/dummy/app/models/superuser.rb +8 -0
  33. data/test/dummy/app/models/user.rb +10 -0
  34. data/test/spec_helper.rb +16 -0
  35. data/test/test_arkenstone.rb +354 -0
  36. data/test/test_arkenstone_hook_inheritance.rb +39 -0
  37. data/test/test_associations.rb +327 -0
  38. data/test/test_enumerables.rb +36 -0
  39. data/test/test_environment.rb +14 -0
  40. data/test/test_helpers.rb +18 -0
  41. data/test/test_hooks.rb +104 -0
  42. data/test/test_query_builder.rb +163 -0
  43. data/test/test_queryable.rb +59 -0
  44. data/test/test_validations.rb +197 -0
  45. metadata +133 -0
@@ -0,0 +1,389 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'active_support/inflector'
4
+
5
+ # TODO: consider splitting the bigger associations (has_many) into separate files
6
+ module Arkenstone
7
+ module Associations
8
+ class << self
9
+ def included(base)
10
+ base.send :include, Arkenstone::Associations::Resources
11
+ base.send :include, Arkenstone::Associations::InstanceMethods
12
+ base.extend Arkenstone::Associations::ClassMethods
13
+ end
14
+ end
15
+
16
+ module ClassMethods
17
+ # All association data is stored in a hash (@arkenstone_data) on the instance of the class. Each entry in the hash is keyed off the association name. The value of the hash key is a basic array. This can be wrapped up and extended if (when) more functionality is needed.
18
+ # `setup_arkenstone_data` creates the following *instance* methods on the class:
19
+ #
20
+ # `arkenstone_data` - the hash for the association data. Only use this if you're absolutely 100% sure that you don't need to get up to date data.
21
+ #
22
+ # `wipe_arkenstone_cache` - clears the cache for the association provided
23
+ def setup_arkenstone_data
24
+ define_method('arkenstone_data') do
25
+ @arkenstone_data = {} if @arkenstone_data.nil?
26
+ @arkenstone_data
27
+ end
28
+
29
+ define_method('wipe_arkenstone_cache') do |model_name|
30
+ arkenstone_data[model_name] = nil
31
+ end
32
+ end
33
+
34
+ # Creates a One to Many association with the supplied `child_model_name`. Example:
35
+ #
36
+ # class Flea
37
+ # end
38
+ #
39
+ # class Llama
40
+ # has_many :fleas
41
+ #
42
+ # end
43
+ #
44
+ # Once `has_many` has evaluated, the structure of `Llama` will look like this:
45
+ #
46
+ # class Llama
47
+ # url 'http://example.com/llamas'
48
+ #
49
+ # def cached_fleas
50
+ # #snip
51
+ # end
52
+ #
53
+ # def fleas
54
+ # #snip
55
+ # end
56
+ #
57
+ # def flea_ids
58
+ # [...] # all the ids of the fleas
59
+ # end
60
+ #
61
+ # def add_flea(new_flea)
62
+ # #snip
63
+ # end
64
+ #
65
+ # def remove_flea(flea_to_remove)
66
+ # #snip
67
+ # end
68
+ # end
69
+ #
70
+ # You can override the url of the association by passing in model_name: 'something'. This will change the URL it fetches from to use the `model_name` instead:
71
+ #
72
+ # has_many :fleas, model_name: 'bugs'
73
+ #
74
+ # Will fetch `fleas` from `http://example.com/llamas/:id/bugs.
75
+ def has_many(child_model_name, options = {})
76
+ setup_arkenstone_data
77
+ child_url_fragment = options[:model_name] || child_model_name
78
+ child_class_name = options[:class_name] || child_model_name
79
+
80
+ # The method for accessing the cached data is `cached_[name]`. If the cache is empty it creates a request to repopulate it from the server.
81
+ cached_child_name = "cached_#{child_model_name}"
82
+ add_association_method cached_child_name do
83
+ cache = arkenstone_data
84
+ if cache[child_model_name].nil?
85
+ child_instances = fetch_children child_class_name, child_url_fragment
86
+ attach_nested_has_many_resource_methods(child_instances, child_model_name, child_class_name)
87
+ cache[child_model_name] = child_instances
88
+ end
89
+ cache[child_model_name]
90
+ end
91
+
92
+ # The uncached version is the name supplied to has_many. It wipes the cache for the association and refetches it.
93
+ add_association_method child_model_name do
94
+ wipe_arkenstone_cache child_model_name
95
+ send cached_child_name
96
+ end
97
+
98
+ # Creates an array of the ids of the child models for quick access.
99
+ singular = child_model_name.to_s.singularize
100
+ add_association_method "#{singular}_ids" do
101
+ (send cached_child_name).map(&:id)
102
+ end
103
+
104
+ # Add a model to the association with add_[child_model_name]. It performs two network calls, one to add it, then another to refetch the association.
105
+ add_child_method_name = "add_#{singular}"
106
+ add_association_method add_child_method_name do |new_child|
107
+ add_child child_model_name, new_child.id
108
+ wipe_arkenstone_cache child_model_name
109
+ send cached_child_name
110
+ end
111
+
112
+ # Remove a model from the association with remove_[child_model_name]. It performs two network calls, one to add it, then another to refetch the association.
113
+ remove_child_method_name = "remove_#{singular}"
114
+ add_association_method remove_child_method_name do |child_to_remove|
115
+ remove_child child_model_name, child_to_remove.id
116
+ wipe_arkenstone_cache child_model_name
117
+ send cached_child_name
118
+ end
119
+ end
120
+
121
+ # Similar to `has_many` but for a One to One association. Example:
122
+ #
123
+ # class Hat
124
+ # end
125
+ #
126
+ # class Llama
127
+ # has_one :hat
128
+ # end
129
+ #
130
+ # Once `has_one` has evaluated, the structure of `Llama` will look like this:
131
+ #
132
+ # class Llama
133
+ # def cached_hat
134
+ # #snip
135
+ # end
136
+ #
137
+ # def hat
138
+ # #snip
139
+ # end
140
+ #
141
+ # def hat=(new_value)
142
+ # #snip
143
+ # end
144
+ # end
145
+ #
146
+ # If nil is passed into the setter method (`hat=` in the above example), the association is removed.
147
+ def has_one(child_model_name, options = {})
148
+ setup_arkenstone_data
149
+ child_url_fragment = options[:model_name] || child_model_name
150
+
151
+ # The method for accessing the cached single resource is `cached_[name]`. If the value is nil it creates a request to pull the value from the server.
152
+ cached_child_name = "cached_#{child_model_name}"
153
+ add_association_method cached_child_name do
154
+ cache = arkenstone_data
155
+ cache[child_model_name] = fetch_child child_model_name, child_url_fragment if cache[child_model_name].nil?
156
+ cache[child_model_name]
157
+ end
158
+
159
+ # The uncached version is retrieved by wiping the cache for the association, and then re-getting it.
160
+ add_association_method child_model_name do
161
+ arkenstone_data[child_model_name] = nil
162
+ send cached_child_name
163
+ end
164
+
165
+ # A single association is updated or removed with a setter method.
166
+ setter_method_name = "#{child_model_name}="
167
+ add_association_method setter_method_name do |new_value|
168
+ if new_value.nil?
169
+ old_model = send child_model_name
170
+ remove_child child_model_name, old_model.id
171
+ wipe_arkenstone_cache child_model_name
172
+ else
173
+ add_child child_model_name, new_value.id
174
+ wipe_arkenstone_cache child_model_name
175
+ send cached_child_name
176
+ end
177
+ end
178
+ end
179
+
180
+ # The opposite of a has_X relationship. Allows you to go back up the association tree. Example:
181
+ #
182
+ # class Hat
183
+ # belongs_to :llama
184
+ # end
185
+ #
186
+ # class Llama
187
+ # end
188
+ #
189
+ # Once `belongs_to` has been evaluated, the structure of `Hat` will look like this:
190
+ #
191
+ # class Hat
192
+ # def llama
193
+ # #snip
194
+ # end
195
+ # end
196
+ def belongs_to(parent_model_name)
197
+ setup_arkenstone_data
198
+
199
+ parent_model_field = "#{parent_model_name}_id"
200
+
201
+ self.arkenstone_attributes = [] unless arkenstone_attributes
202
+ arkenstone_attributes << parent_model_field.to_sym
203
+ class_eval("attr_accessor :#{parent_model_field}", __FILE__, __LINE__)
204
+
205
+ # The method for accessing the cached data is `cached_[name]`. If the cache is empty it creates a request to repopulate it from the server.
206
+ cached_parent_model_name = "cached_#{parent_model_name}"
207
+ add_association_method cached_parent_model_name do
208
+ cache = arkenstone_data
209
+ cache[parent_model_name] = fetch_parent parent_model_name if cache[parent_model_name].nil?
210
+ cache[parent_model_name]
211
+ end
212
+
213
+ # The uncached version is the name supplied to belongs_to. It wipes the cache for the association and refetches it.
214
+ add_association_method parent_model_name.to_s do
215
+ arkenstone_data[parent_model_name] = nil
216
+ send cached_parent_model_name
217
+ end
218
+
219
+ define_method("#{parent_model_name}=") do |parent_instance|
220
+ send "#{parent_model_field}=".to_sym, parent_instance.id
221
+ end
222
+ end
223
+
224
+ ### Support for `has_and_belongs_to_many` relationship
225
+ def has_and_belongs_to_many(model_klass_name)
226
+ # Gather the namespace
227
+ namespace = to_s.split(/::/)
228
+ model_klass_name = model_klass_name.to_s.singularize.underscore.to_sym
229
+ current_klass_name = namespace.pop.underscore.to_sym
230
+
231
+ # Build join class needs
232
+ join_klass_name = [model_klass_name, current_klass_name].sort.join('_')
233
+ join_klass_classified = join_klass_name.classify.to_sym
234
+ join_klass_pluralized = join_klass_name.pluralize
235
+ namespace = Kernel.const_get(namespace.join('::'))
236
+
237
+ # Create the join class if it doesn't exist already
238
+ unless namespace.constants.include?(join_klass_classified)
239
+ join_klass = namespace.const_set(join_klass_classified, Class.new)
240
+ join_klass.instance_eval { include Arkenstone::Document }
241
+
242
+ # The join class should belong to both foreign sides of the relationship
243
+ join_klass.send :belongs_to, model_klass_name
244
+ join_klass.send :belongs_to, current_klass_name
245
+ end
246
+
247
+ # This class should belong to the join table
248
+ send(:has_many, join_klass_pluralized.to_sym) unless respond_to?(join_klass_pluralized.to_sym)
249
+
250
+ # These are helper variables for the cached and uncached join `:through` instances
251
+ model_klass_pluralized = model_klass_name.to_s.pluralize
252
+ cached_instances_field = "cached_#{model_klass_pluralized}"
253
+
254
+ send :attr_accessor, cached_instances_field.to_sym
255
+
256
+ # Creates a `self.join_through_instances` helper method
257
+ #
258
+ # This actually pulls instances of the join model and then maps on the
259
+ # complimenting foreign key to gather all the foreign join instances
260
+ #
261
+ define_method model_klass_pluralized.to_s do
262
+ current_klass_instance = self # The instance calling this method
263
+ current_klass_pluralized = current_klass_name.to_s.pluralize
264
+
265
+ # Check for cached joined instances
266
+ cached_instances = current_klass_instance.send cached_instances_field
267
+ return cached_instances if cached_instances
268
+
269
+ # Get from joined instances
270
+ model_klass_instances = send("cached_#{join_klass_pluralized}".to_sym).map(&:"#{model_klass_pluralized}")
271
+
272
+ # Redefine `<<` so that you can something like `beer.tags << new_tag`
273
+ model_klass_instances.define_singleton_method :<< do |element|
274
+ # Use built in `push` for `Array.new`
275
+ push element
276
+
277
+ # Cache the result
278
+ current_klass_instance.send "#{cached_instances_field}=", self
279
+
280
+ # Add the current class instance in the other side of the join
281
+ # The equivelant of doing `beer.tags << tag` then `tag.beers << beer`
282
+ #
283
+ # Grab the current_klass_instances from element
284
+ element_current_klass_instances = element.send(current_klass_pluralized)
285
+
286
+ # Push the current_klass_instance to what element currently has
287
+ element_current_klass_instances.push(current_klass_instance)
288
+
289
+ # Save the new stack of current_klass_instances with element
290
+ element.send "#{current_klass_pluralized}=", element_current_klass_instances
291
+
292
+ # Return the new array
293
+ self
294
+ end
295
+ model_klass_instances
296
+ end
297
+
298
+ # This creates a setter helper to set all joined instances on the
299
+ # opposite side of the foreign join
300
+ #
301
+ define_method "#{model_klass_pluralized}=" do |elements|
302
+ current_klass_instance = self
303
+ current_klass_instance.send "#{cached_instances_field}=", elements
304
+ end
305
+ end
306
+
307
+ # Adds a method to a class unless that method is already defined.
308
+ def add_association_method(method_name, &method_definition)
309
+ define_method method_name, method_definition unless method_defined? method_name
310
+ end
311
+ end
312
+
313
+ module InstanceMethods
314
+ ### Fetches a `has_many` based resource
315
+ def fetch_children(child_model_name, child_url_fragment)
316
+ fetch_nested_resource child_model_name, child_url_fragment do |klass, response_body|
317
+ klass.parse_all response_body
318
+ end
319
+ end
320
+
321
+ ### Fetches a single `has_one` based resource
322
+ def fetch_child(child_model_name, child_url_fragment)
323
+ fetch_nested_resource child_model_name, child_url_fragment do |klass, response_body|
324
+ return nil if response_body.nil? || response_body.empty?
325
+
326
+ klass.build JSON.parse(response_body)
327
+ end
328
+ end
329
+
330
+ ### Fetches a single `belongs_to` parent resource.
331
+ def fetch_parent(parent_model_name)
332
+ klass_name = parent_model_name.to_s.classify
333
+ klass_name = prefix_with_class_module klass_name
334
+ klass = Kernel.const_get klass_name
335
+ parent_model_field = "#{parent_model_name}_id"
336
+ klass.send(:find, send(parent_model_field))
337
+ end
338
+
339
+ ### Calls the POST url for creating a nested_resource
340
+ def add_child(child_model_name, child_id)
341
+ url = build_nested_url child_model_name
342
+ body = { id: child_id }.to_json
343
+ self.class.send_request url, :post, body
344
+ end
345
+
346
+ ### Calls the DELETE route for a nested resource
347
+ def remove_child(child_model_name, child_id)
348
+ url = build_nested_url child_model_name, child_id
349
+ self.class.send_request url, :delete
350
+ end
351
+
352
+ private
353
+
354
+ ### Creates the network request for fetching a child resource. Hands parsing the response off to a callback.
355
+ def fetch_nested_resource(nested_resource_name, nested_resource_fragment = nested_resource_name, &parser)
356
+ url = build_nested_url nested_resource_fragment
357
+ response = self.class.send_request url, :get
358
+ return [] unless Arkenstone::Network.response_is_success response
359
+
360
+ klass_name = nested_resource_name.to_s.classify
361
+ klass_name = prefix_with_class_module klass_name
362
+ klass = Kernel.const_get klass_name
363
+ parser[klass, response.body]
364
+ end
365
+
366
+ # If the class is in a module, preserve the module namespace.
367
+ # Example:
368
+ #
369
+ # # for the class Zoo::Llama
370
+ # prefix_with_class_module('Hat') # 'Zoo::Hat'
371
+ def prefix_with_class_module(klass)
372
+ mod = self.class.name.deconstantize
373
+ klass = "#{mod}::#{klass}" unless mod.empty?
374
+ klass
375
+ end
376
+
377
+ # Builds a RESTful nested URL based on the instance URL.
378
+ # Example:
379
+ #
380
+ # build_nested_url('fleas') # http://example.com/llamas/100/fleas
381
+ # build_nested_url('fleas', 25) # http://example.com/llamas/100/fleas/25
382
+ def build_nested_url(child_name, child_id = nil)
383
+ url = "#{instance_url}/#{child_name}"
384
+ url += "/#{child_id}" unless child_id.nil?
385
+ url
386
+ end
387
+ end
388
+ end
389
+ end
@@ -0,0 +1,289 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'json'
4
+
5
+ module Arkenstone
6
+ # == Document
7
+ #
8
+ # A `Document` is the main entry point for Arkenstone. A `Document` is a model that is retrieved and/or stored on a RESTful service. For example, if you have a web service that has a URL structure like:
9
+ #
10
+ # http://example.com/users
11
+ #
12
+ # You can create a `User` model, include `Document` and it will automatically create methods to fetch and save data from that URL.
13
+ #
14
+ # class User
15
+ # include Arkenstone::Document
16
+ #
17
+ # url 'http://example.com/users'
18
+ #
19
+ # attributes :first_name, :last_name, :email
20
+ # end
21
+ #
22
+ # Attributes create properties on instances that match up with the data returned by the `url`. Properties on the web service are ignored if they are not present within the `attributes` list.
23
+ module Document
24
+ class << self
25
+ def included(base)
26
+ base.send :include, Arkenstone::Helpers
27
+ base.send :include, Arkenstone::Associations
28
+ base.send :include, Arkenstone::Document::InstanceMethods
29
+ base.send :include, Arkenstone::Network
30
+ base.extend Arkenstone::Document::ClassMethods
31
+ end
32
+ end
33
+
34
+ module InstanceMethods
35
+ ### The convention is for all Documents to have an id.
36
+ attr_accessor :id, :arkenstone_attributes, :arkenstone_server_errors
37
+
38
+ ### Easy access to all of the attributes defined for this Document.
39
+ def attributes
40
+ new_hash = {}
41
+ self.class.arkenstone_attributes.each do |key|
42
+ new_hash[key.to_sym] = send(key.to_s)
43
+ end
44
+ new_hash
45
+ end
46
+
47
+ ### Set attributes for a Document. If a key in the `options` hash is not present in the attributes list, it is ignored.
48
+ def attributes=(options)
49
+ options.each do |key, value|
50
+ setter = "#{key}="
51
+ send(setter.to_sym, value) if respond_to? setter
52
+ end
53
+ attributes
54
+ end
55
+
56
+ ### Returns true if this is a new object that has not been saved yet.
57
+ def new_record?
58
+ id.nil?
59
+ end
60
+
61
+ ### Serializes the attributes to json.
62
+ def to_json(options = {})
63
+ attributes.to_json(options)
64
+ end
65
+
66
+ ### If this is a new Document, create it with a POST request, otherwise update it with a PUT. Returns whether the server response was successful or not.
67
+ def save
68
+ self.class.check_for_url
69
+ timestamp if respond_to?(:timestampable)
70
+ response = new_record? ? post_document_data : put_document_data
71
+ self.attributes = JSON.parse(response.body)
72
+ Arkenstone::Network.response_is_success response
73
+ end
74
+
75
+ ### Reloading the document fetches the document again by it's id
76
+ def reload
77
+ reloaded_self = self.class.find(id)
78
+ self.attributes = reloaded_self.attributes
79
+ self
80
+ end
81
+
82
+ alias save! save
83
+
84
+ ### Update a single attribute. Performs validation (by calling `update_attributes`).
85
+ def update_attribute(key, value)
86
+ hash = { key.to_sym => value }
87
+ update_attributes hash
88
+ end
89
+
90
+ ### Update multiple attributes at once. Performs validation (if that is setup for this document).
91
+ def update_attributes(new_attributes)
92
+ attributes.merge! new_attributes
93
+ save
94
+ end
95
+
96
+ ### Checks if there is a `valid?` method.
97
+ def has_validation_method?
98
+ self.class.method_defined? :valid?
99
+ end
100
+
101
+ # Retrieves a RESTful URL for an instance, in this case by tacking an id onto the end of the `arkenstone_url`.
102
+ # Example:
103
+ #
104
+ # # arkenstone_url
105
+ # http://example.com/users
106
+ #
107
+ # # instance_url
108
+ # http://example.com/users/100
109
+ def instance_url
110
+ "#{full_url(self.class.arkenstone_url)}#{id}"
111
+ end
112
+
113
+ ### The full RESTful URL for a Document.
114
+ def class_url
115
+ full_url(self.class.arkenstone_url)
116
+ end
117
+
118
+ ### Save via POST.
119
+ def post_document_data
120
+ http_response class_url, :post
121
+ end
122
+
123
+ ### Save via PUT.
124
+ def put_document_data
125
+ http_response instance_url, :put
126
+ end
127
+
128
+ ### Sends a DELETE request to the `instance_url`.
129
+ def destroy
130
+ resp = http_response instance_url, :delete
131
+ Arkenstone::Network.response_is_success resp
132
+ end
133
+
134
+ ### Sends a network request with the `attributes` as the body.
135
+ def http_response(url, method = :post)
136
+ response = self.class.send_request url, method, saveable_attributes
137
+ self.arkenstone_server_errors = JSON.parse(response.body) if response.code == '500'
138
+ response
139
+ end
140
+
141
+ ### Runs any encoding hooks on the attributes if present.
142
+ def saveable_attributes
143
+ return attributes unless Arkenstone::Hook.has_hooks? self.class
144
+
145
+ attrs = {}
146
+ Arkenstone::Hook.all_hooks_for_class(self.class).each do |hook|
147
+ new_attrs = hook.encode_attributes(attributes)
148
+ attrs.merge! new_attrs unless new_attrs.nil?
149
+ end
150
+ attrs.empty? ? attributes : attrs
151
+ end
152
+
153
+ ### Creates a deep dupe of the document with the id set to nil
154
+ def dup
155
+ duped = super
156
+ duped.id = nil
157
+ duped
158
+ end
159
+ end
160
+
161
+ module ClassMethods
162
+ attr_accessor :arkenstone_url, :arkenstone_attributes, :arkenstone_hooks, :arkenstone_inherit_hooks
163
+
164
+ ### Sets the root url used for generating RESTful requests.
165
+ def url(new_url)
166
+ self.arkenstone_url = new_url
167
+ end
168
+
169
+ # == Hooks
170
+ #
171
+ # Hooks are used to allow you to call arbitrary code at various points in the object lifecycle. For example, if you need to massage some property names before they are sent off to the `url`, you can do that with a hook. A hook should extend `Arkenstone::Hook` and then override the method you want to hook into. There are three types of hooks:
172
+ # 1. `before_request` - Called before the request is sent to the web service. Passes in the request environment (an `Arkenstone::Environment`) as a parameter.
173
+ # 2. `after_complete` - Called after the request has been *successfully* completed. Passes in a Net::HTTPResponse as a parameter.
174
+ # 3. `on_error` - Called if the response returned an error. Passes in a Net::HTTPResponse as a parameter.
175
+ #
176
+ # Example:
177
+ #
178
+ # class ErrorLogger < Arkenstone::Hook
179
+ # def on_error(response)
180
+ # # log the error here
181
+ # end
182
+ # end
183
+ #
184
+ # class User
185
+ # include Arkenstone::Document
186
+ #
187
+ # url 'http://example.com/users'
188
+ # add_hook ErrorLogger.new
189
+ # end
190
+ def add_hook(hook)
191
+ self.arkenstone_hooks = [] if arkenstone_hooks.nil?
192
+ arkenstone_hooks << hook
193
+ end
194
+
195
+ # Hooks are applied **only** to the class they are added to. This can cause a problem if you have a base class and want to use the same hooks for subclasses. If you want to use the same hooks as a parent class, use `inherit_hooks`. This will tell Arkenstone to walk up the inheritance chain and call all of the hooks it can find.
196
+ # Example:
197
+ #
198
+ # class ErrorLogger < Arkenstone::Hook
199
+ # def on_error(response)
200
+ # # log the error here
201
+ # end
202
+ # end
203
+ #
204
+ # class BaseModel
205
+ # include Arkenstone::Document
206
+ #
207
+ # add_hook ErrorLogger.new
208
+ # add_hook SomeOtherHook.new
209
+ # end
210
+ #
211
+ # class User < BaseModel
212
+ # url 'http://example.com/users'
213
+ #
214
+ # inherit_hooks
215
+ # end
216
+ #
217
+ # This will use the hooks defined for `BaseModel` and any defined for `User` too.
218
+ def inherit_hooks(val: true)
219
+ self.arkenstone_inherit_hooks = val
220
+ end
221
+
222
+ ### Sets the attributes for an Arkenstone Document. These become `attr_accessors` on instances.
223
+ def attributes(*options)
224
+ self.arkenstone_attributes = options
225
+ options.each do |option|
226
+ send(:attr_accessor, option)
227
+ end
228
+ arkenstone_attributes
229
+ end
230
+
231
+ ### You can use Arkenstone without defining a `url`, but you won't be able to save a model without one. This raises an error if the url is not defined.
232
+ def check_for_url
233
+ raise NoUrlError.new, NoUrlError.default_message if arkenstone_url.nil?
234
+ end
235
+
236
+ ### Constructs a new instance with the provided attributes.
237
+ def build(options)
238
+ document = new
239
+ document.attributes = Hash(options).select do |key, _value|
240
+ document.respond_to? :"#{key}="
241
+ end
242
+ document
243
+ end
244
+
245
+ ### Builds a list of objects with attributes set from a JSON string or an array.
246
+ def parse_all(to_parse)
247
+ return [] if to_parse.nil? || to_parse.empty?
248
+
249
+ tree = if to_parse.is_a? String
250
+ JSON.parse to_parse
251
+ else
252
+ to_parse
253
+ end
254
+ tree = ensure_parseable_is_array tree
255
+ documents = tree.map { |document| build document }
256
+ Arkenstone::QueryList.new documents
257
+ end
258
+
259
+ def ensure_parseable_is_array(to_parse)
260
+ to_parse = [to_parse] if to_parse.is_a? Hash
261
+ to_parse
262
+ end
263
+
264
+ ### Creates and saves a single instance with the attribute values provided.
265
+ def create(options)
266
+ document = build(options)
267
+ document.save
268
+ document
269
+ end
270
+
271
+ ### Performs a GET request to the instance url with the supplied id. Builds an instance with the response.
272
+ def find(id)
273
+ check_for_url
274
+ url = full_url(arkenstone_url) + id.to_s
275
+ response = send_request url, :get
276
+ return nil unless Arkenstone::Network.response_is_success response
277
+
278
+ build JSON.parse(response.body)
279
+ end
280
+
281
+ ### Calls the `arkenstone_url` expecting to receive a json array of properties to deserialize into a list of objects.
282
+ def all
283
+ check_for_url
284
+ response = send_request arkenstone_url, :get
285
+ parse_all response.body
286
+ end
287
+ end
288
+ end
289
+ end
@@ -0,0 +1,20 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Arkenstone
4
+ # QueryList extends Array to provide more customized options for Arkenstone documents.
5
+ class QueryList < Array
6
+ ### If an array is provided, concatenate it onto the instance so that it becomes one long array. Otherwise, push it on.
7
+ def initialize(initial_value)
8
+ if initial_value.instance_of?(Array)
9
+ concat initial_value
10
+ else
11
+ push initial_value
12
+ end
13
+ end
14
+
15
+ # Assumes that every element is an Arkenstone::Document
16
+ def to_json(options = nil)
17
+ map(&:attributes).to_json(options)
18
+ end
19
+ end
20
+ end