json_api_resource 3.0.1 → 3.1.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (25) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +103 -0
  3. data/lib/json_api_resource.rb +2 -0
  4. data/lib/json_api_resource/associatable.rb +60 -0
  5. data/lib/json_api_resource/associations.rb +17 -0
  6. data/lib/json_api_resource/associations/base.rb +79 -0
  7. data/lib/json_api_resource/associations/belongs_to.rb +29 -0
  8. data/lib/json_api_resource/associations/has_many.rb +26 -0
  9. data/lib/json_api_resource/associations/has_many_prefetched.rb +34 -0
  10. data/lib/json_api_resource/associations/has_one.rb +30 -0
  11. data/lib/json_api_resource/associations/preloader.rb +58 -0
  12. data/lib/json_api_resource/associations/preloaders.rb +12 -0
  13. data/lib/json_api_resource/associations/preloaders/base.rb +38 -0
  14. data/lib/json_api_resource/associations/preloaders/belongs_to_preloader.rb +17 -0
  15. data/lib/json_api_resource/associations/preloaders/distributors.rb +11 -0
  16. data/lib/json_api_resource/associations/preloaders/distributors/base.rb +33 -0
  17. data/lib/json_api_resource/associations/preloaders/distributors/distributor_by_object_id.rb +28 -0
  18. data/lib/json_api_resource/associations/preloaders/distributors/distributor_by_target_id.rb +32 -0
  19. data/lib/json_api_resource/associations/preloaders/has_many_prefetched_preloader.rb +17 -0
  20. data/lib/json_api_resource/associations/preloaders/has_many_preloader.rb +17 -0
  21. data/lib/json_api_resource/associations/preloaders/has_one_preloader.rb +17 -0
  22. data/lib/json_api_resource/errors.rb +27 -0
  23. data/lib/json_api_resource/resource.rb +3 -1
  24. data/lib/json_api_resource/version.rb +1 -1
  25. metadata +21 -3
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: d614a8696dc5d0a00cec60e488cc0bf2525b90f0
4
- data.tar.gz: 98c045571f29a19bc8923631bc536a5605c049b4
3
+ metadata.gz: 550d3bf8b009a60ef20065c2131601ec524a0761
4
+ data.tar.gz: 822dca3ef57832f3fecc4c18c0bb1f52e846c1bf
5
5
  SHA512:
6
- metadata.gz: f1ab7d5d28bcd0d327469b3d8a0c96900df625288b0f5e7cd926dbc2b867a282ee23b1ed69834c3192a8361451451bf11cb7011fa52fd59a683e3116e83b778f
7
- data.tar.gz: 3a8b5589a2cab870d1a1548a7afdb659f2dde36fa655d16d2d36e7e1959703a1beede2621791197cbd6709c60917bdd3884ae9dab60a46c3ef9794b3f43b8a99
6
+ metadata.gz: 21426386df0e1e7bda458f3b3aaeff65cf415cdca85c03792b6ac9079b75a9f8d249d40161b572070463ff596ba30ec7841e9b7d05ba9b18f2609a24138556b2
7
+ data.tar.gz: f559ca29883f3f3f5f08800c19825402fdc7520a26f28020c8e26eb53097f6250302a673cd9b03ce2076a1df3fa67724c592d3510f9ca8ab20cd816457866b18
data/README.md CHANGED
@@ -140,6 +140,109 @@ NOTE: all properties are optional. If you don't have it defined and it's in the
140
140
 
141
141
  The object will still reply to `id`, `name` and `permissions`, but you won't be able to assume they are there, and they will not appear when you do `ResourceClass.new`.
142
142
 
143
+ ### Associations
144
+
145
+ #### interface
146
+
147
+ * `belongs_to` - your resource has the `#{object}_id` for the association.
148
+ * calls `object_class.find `
149
+ * `has_one` - will call the server with `opts.merge "#{root_class}_id => root_object.id` and grab the first thing that comes back
150
+ * calls `object_class.where`
151
+ * `has_many` - same as `has_one` but will give you the full array of objects
152
+ * calls 'object_class.where`
153
+
154
+ #### options
155
+
156
+ * `:foreign_key` - this is what the server will get as the key for `has_one` and `has_many`. `belongs_to` is weird and will send the key to the resource object. for example
157
+ ```ruby
158
+ class Admin < JsonApiResource::Resource
159
+ wraps Service::Client::Admin
160
+
161
+ property user_id
162
+ # this one is a little weird
163
+ belongs_to :person, foreign_key: :user_id, class: User
164
+ end
165
+ ```
166
+ * `:action` - If you have a custom action you're using for lookup, you can override the default `where` and `find`. For example
167
+ ```ruby
168
+ class Service::Client::Superuser < Service::Client::Base
169
+ # i don't know why you would have this, but whatever
170
+ custom_endpoint :superuser_from_user_id, :on=> :collection, :request_method=> :get
171
+ end
172
+
173
+ class User
174
+ wraps Service::Client::User
175
+ has_one :superuser, action: :superuser_from_user_id
176
+ end
177
+ ```
178
+ *NOTE: keep in mind, this will still make the call with the `opts.merge foreign_key => root_object.id` hash. If you want to override the query, you may want to consider 1: if the API is RESTful 2: rolling your own association.*
179
+
180
+ * `:prefetched_ids` *(`has_many` only)* - in the case that the root object has a collection of ids that come preloaded in it
181
+ ```ruby
182
+ class User < JsonApiResource::Resource
183
+ wraps Service::Client::User
184
+ property address_ids, []
185
+
186
+ has_many :addresses, prefetched_ids: :address_ids
187
+ end
188
+ ```
189
+
190
+
191
+ #### notes
192
+ * `:through` is not supported, nor will it ever be, because of the hidden complexity and cost of n HTTP calls. if you want to implement `through` do
193
+
194
+ * the servers you're preloading from/associating have controller entities that respond to show(`find`) and index(`where`)
195
+ * __important__: the index(`where`) action has to support `ingore_pagination`. Otherwise you may lose associations as they get capped by the per-page limit
196
+
197
+ ### Preloader
198
+
199
+ Sometimes you have many objects that you need to fetch associations for. It's expensive to have to iterate over them one by one and annoying to have to assign the results. Well, now there's a `Preloader` that can do all of that for you
200
+
201
+ #### example
202
+
203
+ Let's say you have `user`s who have `address`es.
204
+ ```ruby
205
+ class User < JsonApiResource::Resource
206
+ wraps Service::Client::User
207
+
208
+ # shiny new function yay
209
+ has_many :addresses
210
+ end
211
+
212
+ class Address < JsonApiResource::Resource
213
+ wraps Service::Client::Address
214
+ end
215
+ ```
216
+
217
+ With the associations in place, you can now use them to preload all the addresses for your users in a single query.
218
+
219
+ ```ruby
220
+ @users = User.where id: [1, 2, 3, 4, 5]
221
+
222
+ # that's it. that's all you have to do.
223
+ JsonApiClient::Associations::Preloader.preload @users, :addresses
224
+
225
+ # all the users now have addresses assigned to them and will not hit the server again
226
+ puts @users.map &:addresses
227
+ ```
228
+
229
+ #### interface
230
+
231
+ `Preloader.preload( objects, preloads )` takes
232
+
233
+ * the objects you want the associations to be tied to (`@users` in our example)
234
+ * the list of associations you want bulk fetched from the server as symbols.
235
+ * can be a single symbol, or a list
236
+ * has to have a corresponding association on the objects, and the name has to match
237
+
238
+ #### notes
239
+
240
+ This is a simple tool so don't expect too much magic. The Preloader will explode if
241
+
242
+ * the objects aren't the same class
243
+ * the results aren't the same class (although i don't know how that would be possible)
244
+ * the result set can't be matched to the objects
245
+
143
246
  ### Error Handling
144
247
 
145
248
  On an unsuccessful call to the server (this means that if you have multiple connections, they will *necessarily* **all** have to fail), errors will be routed through an overridable `handle_failed_request(e)` method. By default it will re-raise the error, but you can handle it any way you want.
@@ -1,6 +1,8 @@
1
1
  require 'json_api_client'
2
2
 
3
3
  module JsonApiResource
4
+ autoload :Associatable, 'json_api_resource/associatable'
5
+ autoload :Associations, 'json_api_resource/associations'
4
6
  autoload :Cacheable, 'json_api_resource/cacheable'
5
7
  autoload :Clientable, 'json_api_resource/clientable'
6
8
  autoload :Connections, 'json_api_resource/connections'
@@ -0,0 +1,60 @@
1
+ module JsonApiResource
2
+ module Associatable
3
+ extend ActiveSupport::Concern
4
+
5
+ included do
6
+
7
+ class_attribute :_associations
8
+ self._associations = {}
9
+
10
+ attr_accessor :_cached_associations
11
+
12
+ class << self
13
+ def belongs_to( name, opts = {} )
14
+ process Associations::BelongsTo.new( self, name, opts )
15
+ end
16
+
17
+ def has_one( name, opts = {} )
18
+ process Associations::HasOne.new( self, name, opts )
19
+ end
20
+
21
+ def has_many( name, opts = {} )
22
+ if opts[:prefetched_ids]
23
+ process Associations::HasManyPrefetched.new( self, name, opts )
24
+ else
25
+ process Associations::HasMany.new( self, name, opts )
26
+ end
27
+ end
28
+
29
+ private
30
+
31
+ def process(association)
32
+ add_association association
33
+ methodize association
34
+ end
35
+
36
+ def add_association(association)
37
+ self._associations = _associations.merge association.name => association
38
+ end
39
+
40
+ def methodize( association )
41
+ define_method association.name do
42
+ self._cached_associations ||= {}
43
+ unless self._cached_associations.has_key? association.name
44
+ if association.callable?(self)
45
+ result = association.klass.send( association.action, association.query(self) )
46
+ result = association.post_process result
47
+ else
48
+ result = association.nil_default
49
+ end
50
+
51
+ self._cached_associations[association.name] = result
52
+ end
53
+ self._cached_associations[association.name]
54
+ end
55
+ end
56
+ end
57
+ end
58
+ end
59
+ end
60
+
@@ -0,0 +1,17 @@
1
+ module JsonApiResource
2
+ module Associations
3
+ autoload :Base, 'json_api_resource/associations/base'
4
+ autoload :BelongsTo, 'json_api_resource/associations/belongs_to'
5
+ autoload :HasManyPrefetched, 'json_api_resource/associations/has_many_prefetched'
6
+ autoload :HasOne, 'json_api_resource/associations/has_one'
7
+ autoload :HasMany, 'json_api_resource/associations/has_many'
8
+ autoload :Preloader, 'json_api_resource/associations/preloader'
9
+ autoload :Preloaders, 'json_api_resource/associations/preloaders'
10
+
11
+
12
+ BELONGS_TO = :belongs_to
13
+ HAS_ONE = :has_one
14
+ HAS_MANY = :has_many
15
+ HAS_MANY_PREFETCHED = :has_many_prefetched
16
+ end
17
+ end
@@ -0,0 +1,79 @@
1
+ module JsonApiResource
2
+ module Associations
3
+ class Base
4
+ include ::JsonApiResource::Errors
5
+ attr_accessor :name, :action, :key, :root
6
+
7
+ def initialize(associated_class, name, opts = {})
8
+ self.name = name.to_sym
9
+ self.root = associated_class
10
+ @opts = opts.merge( skip_pagination: true )
11
+
12
+ self.action = @opts.delete :action do default_action end
13
+ self.key = @opts.delete :foreign_key do server_key end
14
+
15
+ self.key = self.key.try :to_sym
16
+ validate_options
17
+ end
18
+
19
+ def type
20
+ raise NotImplementedError
21
+ end
22
+
23
+ def query( root_insatnce )
24
+ raise NotImplementedError
25
+ end
26
+
27
+ def callable?( root_insatnce )
28
+ raise NotImplementedError
29
+ end
30
+
31
+ def default_nil
32
+ raise NotImplementedError
33
+ end
34
+
35
+ # klass has to be lazy initted for circular dependencies
36
+ def klass
37
+ @klass ||= @opts.delete :class do derived_class end
38
+ end
39
+
40
+ def post_process( value )
41
+ value
42
+ end
43
+
44
+ def opts
45
+ @opts.except *ASSOCIATION_OPTS
46
+ end
47
+
48
+ protected
49
+
50
+ def server_key
51
+ raise NotImplementedError
52
+ end
53
+
54
+ def default_action
55
+ raise NotImplementedError
56
+ end
57
+
58
+ def derived_class
59
+ module_string = self.root.to_s.split("::")[0 ... -1].join("::")
60
+ class_string = name.to_s.singularize.camelize
61
+
62
+ # we don't necessarily want to add :: to classes, in case they have a relative path or something
63
+ class_string = [module_string, class_string].select{|s| s.present? }.join "::"
64
+ class_string.constantize
65
+ end
66
+
67
+ ASSOCIATION_OPTS = [:class, :action, :foreign_key, :prefetched_ids]
68
+
69
+ RESERVED_KEYWORDS = [:attributes, :_associations, :_cached_associations, :schema, :client]
70
+
71
+ def validate_options
72
+ raise_unless action.present?, "Invalid action: #{self.root}.#{name}"
73
+ raise_unless key.present?, "Invalid foreign_key for #{self.root}.#{name}"
74
+
75
+ raise_if RESERVED_KEYWORDS.include?(name), "'#{name}' is a reserved keyword for #{self.root}"
76
+ end
77
+ end
78
+ end
79
+ end
@@ -0,0 +1,29 @@
1
+ module JsonApiResource
2
+ module Associations
3
+ class BelongsTo < Base
4
+ def default_action
5
+ :find
6
+ end
7
+
8
+ def server_key
9
+ "#{name}_id"
10
+ end
11
+
12
+ def query( root_instance )
13
+ root_instance.send key
14
+ end
15
+
16
+ def callable?( root_instance )
17
+ root_instance.send(key).present?
18
+ end
19
+
20
+ def nil_default
21
+ nil
22
+ end
23
+
24
+ def type
25
+ JsonApiResource::Associations::BELONGS_TO
26
+ end
27
+ end
28
+ end
29
+ end
@@ -0,0 +1,26 @@
1
+ module JsonApiResource
2
+ module Associations
3
+ class HasMany < Base
4
+ def default_action
5
+ :where
6
+ end
7
+
8
+ def server_key
9
+ class_name = self.root.to_s.demodulize.underscore
10
+ "#{class_name}_id"
11
+ end
12
+
13
+ def callable?( root_instance )
14
+ true
15
+ end
16
+
17
+ def query( root_instance )
18
+ { key => root_instance.id }.merge(opts)
19
+ end
20
+
21
+ def type
22
+ JsonApiResource::Associations::HAS_MANY
23
+ end
24
+ end
25
+ end
26
+ end
@@ -0,0 +1,34 @@
1
+ module JsonApiResource
2
+ module Associations
3
+ class HasManyPrefetched < Base
4
+ def default_action
5
+ :where
6
+ end
7
+
8
+ def server_key
9
+ @opts[:prefetched_ids]
10
+ end
11
+
12
+ def query( root_instance )
13
+ { id: root_instance.send(key) }.merge(opts)
14
+ end
15
+
16
+ def callable?( root_instance )
17
+ root_instance.send(key).present?
18
+ end
19
+
20
+ def nil_default
21
+ []
22
+ end
23
+
24
+ def type
25
+ JsonApiResource::Associations::HAS_MANY_PREFETCHED
26
+ end
27
+
28
+ def validate_options
29
+ raise_unless key == server_key, "#{root}.#{name} cannot specify both prefetched_ids and a foreign key"
30
+ super
31
+ end
32
+ end
33
+ end
34
+ end
@@ -0,0 +1,30 @@
1
+ module JsonApiResource
2
+ module Associations
3
+ class HasOne < Base
4
+ def post_process( value )
5
+ Array(value).first
6
+ end
7
+
8
+ def default_action
9
+ :where
10
+ end
11
+
12
+ def server_key
13
+ class_name = self.root.to_s.demodulize.underscore
14
+ "#{class_name}_id"
15
+ end
16
+
17
+ def query( root_instance )
18
+ { key => root_instance.id }.merge(opts)
19
+ end
20
+
21
+ def callable?( root_instance )
22
+ true
23
+ end
24
+
25
+ def type
26
+ JsonApiResource::Associations::HAS_ONE
27
+ end
28
+ end
29
+ end
30
+ end
@@ -0,0 +1,58 @@
1
+ module JsonApiResource
2
+ module Associations
3
+ class Preloader
4
+ include JsonApiResource::Errors
5
+
6
+ class << self
7
+ def preload ( objects, preloads )
8
+ objects = Array(objects)
9
+ preloads = Array(preloads)
10
+
11
+ preloads.each do |preload|
12
+
13
+ association = association_for objects, preload
14
+
15
+ preloader = preloader_for association
16
+
17
+ preloader.preload( objects )
18
+
19
+ end
20
+ end
21
+
22
+ private
23
+
24
+ def association_for( objects, preload )
25
+ # let's assert the objects are of a single class
26
+ verify_object_homogenity!(objects)
27
+
28
+ obj_class = objects.first.class
29
+ association = obj_class._associations[preload]
30
+
31
+ raise_if association.nil?, "'#{preload}' is not a valid association on #{obj_class}"
32
+
33
+ association
34
+ end
35
+
36
+ def preloader_for( association )
37
+ preloader_class = PREOLOADERS_FOR_ASSOCIATIONS[association.type]
38
+ preloader_class.new association
39
+ end
40
+
41
+ def verify_object_homogenity!( objects )
42
+ obj_class = objects.first.class
43
+
44
+ objects.each do |obj|
45
+ raise_unless obj.is_a?(obj_class), "JsonApiResource::Associations::Preloader.preload called with a heterogenious array of objects."
46
+ end
47
+ end
48
+
49
+ PREOLOADERS_FOR_ASSOCIATIONS = {
50
+ JsonApiResource::Associations::BELONGS_TO => JsonApiResource::Associations::Preloaders::BelongsToPreloader,
51
+ JsonApiResource::Associations::HAS_ONE => JsonApiResource::Associations::Preloaders::HasOnePreloader,
52
+ JsonApiResource::Associations::HAS_MANY => JsonApiResource::Associations::Preloaders::HasManyPreloader,
53
+ JsonApiResource::Associations::HAS_MANY_PREFETCHED => JsonApiResource::Associations::Preloaders::HasManyPrefetchedPreloader,
54
+ }
55
+ end
56
+ end
57
+ end
58
+ end
@@ -0,0 +1,12 @@
1
+ module JsonApiResource
2
+ module Associations
3
+ module Preloaders
4
+ autoload :Base, 'json_api_resource/associations/preloaders/base'
5
+ autoload :BelongsToPreloader, 'json_api_resource/associations/preloaders/belongs_to_preloader'
6
+ autoload :Distributors, 'json_api_resource/associations/preloaders/distributors'
7
+ autoload :HasManyPrefetchedPreloader, 'json_api_resource/associations/preloaders/has_many_prefetched_preloader'
8
+ autoload :HasManyPreloader, 'json_api_resource/associations/preloaders/has_many_preloader'
9
+ autoload :HasOnePreloader, 'json_api_resource/associations/preloaders/has_one_preloader'
10
+ end
11
+ end
12
+ end
@@ -0,0 +1,38 @@
1
+ module JsonApiResource
2
+ module Associations
3
+ module Preloaders
4
+ class Base
5
+ include JsonApiResource::Errors
6
+
7
+ attr_accessor :association, :distributor
8
+ delegate :klass, :name, :action, :key, :opts, :post_process, to: :association
9
+
10
+ def initialize(association)
11
+ self.association = association
12
+ self.distributor = distributor_class.new association
13
+ end
14
+
15
+ def preload( objects )
16
+ query = bulk_query( objects )
17
+ results = klass.send action, query
18
+
19
+ distributor.distribute objects, results
20
+ end
21
+
22
+ private
23
+
24
+ def assign( objects, results )
25
+ raise NotImplementedError
26
+ end
27
+
28
+ def bulk_query( objects )
29
+ raise NotImplementedError
30
+ end
31
+
32
+ def distributor_class
33
+ raise NotImplementedError
34
+ end
35
+ end
36
+ end
37
+ end
38
+ end
@@ -0,0 +1,17 @@
1
+ module JsonApiResource
2
+ module Associations
3
+ module Preloaders
4
+ class BelongsToPreloader < Base
5
+
6
+ def bulk_query( objects )
7
+ ids = objects.map{ |o| o.send(key) }.flatten.uniq
8
+ { id: ids }.merge(opts)
9
+ end
10
+
11
+ def distributor_class
12
+ JsonApiResource::Associations::Preloaders::Distributors::DistributorByTargetId
13
+ end
14
+ end
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,11 @@
1
+ module JsonApiResource
2
+ module Associations
3
+ module Preloaders
4
+ module Distributors
5
+ autoload :Base, 'json_api_resource/associations/preloaders/distributors/base'
6
+ autoload :DistributorByObjectId, 'json_api_resource/associations/preloaders/distributors/distributor_by_object_id'
7
+ autoload :DistributorByTargetId, 'json_api_resource/associations/preloaders/distributors/distributor_by_target_id'
8
+ end
9
+ end
10
+ end
11
+ end
@@ -0,0 +1,33 @@
1
+ module JsonApiResource
2
+ module Associations
3
+ module Preloaders
4
+ module Distributors
5
+ class Base
6
+ include JsonApiResource::Errors
7
+
8
+ attr_accessor :association
9
+ delegate :key, :post_process, :root, :name, to: :association
10
+
11
+ def initialize(association)
12
+ self.association = association
13
+ end
14
+
15
+ def distribute( targets, resutls )
16
+ validate_assignability!( resutls )
17
+ assign( targets, resutls )
18
+ end
19
+
20
+ private
21
+
22
+ def validate_assignability!( resutls )
23
+ raise NotImplementedError
24
+ end
25
+
26
+ def assign( targets, resutls )
27
+ raise NotImplementedError
28
+ end
29
+ end
30
+ end
31
+ end
32
+ end
33
+ end
@@ -0,0 +1,28 @@
1
+ module JsonApiResource
2
+ module Associations
3
+ module Preloaders
4
+ module Distributors
5
+ class DistributorByObjectId < Base
6
+
7
+ def assign( targets, results )
8
+ targets.each do |target|
9
+
10
+ id = target.id
11
+
12
+ result = results.select{ |r| r.send(key) == id }
13
+
14
+ target._cached_associations ||= {}
15
+ target._cached_associations[name] = post_process result
16
+ end
17
+ end
18
+
19
+ def validate_assignability!( results )
20
+ results.each do |obj|
21
+ raise_unless obj.respond_to?(key), "preloading #{root}.#{name} failed: results don't respond to '#{key}'"
22
+ end
23
+ end
24
+ end
25
+ end
26
+ end
27
+ end
28
+ end
@@ -0,0 +1,32 @@
1
+ module JsonApiResource
2
+ module Associations
3
+ module Preloaders
4
+ module Distributors
5
+ class DistributorByTargetId < Base
6
+
7
+ def assign( targets, results )
8
+ targets.each do |target|
9
+
10
+ ids = Array(target.send(key))
11
+
12
+ # this obejct doesn't have this association. skip
13
+ next unless ids.present?
14
+
15
+ result = results.select{ |r| ids.include? r.id }
16
+
17
+ target._cached_associations ||= {}
18
+ target._cached_associations[name] = post_process result
19
+
20
+ end
21
+ end
22
+
23
+ def validate_assignability!( results )
24
+ results.each do |obj|
25
+ raise_unless obj.respond_to?(:id), "preloading #{root}.#{name} failed: results don't respond to 'id'"
26
+ end
27
+ end
28
+ end
29
+ end
30
+ end
31
+ end
32
+ end
@@ -0,0 +1,17 @@
1
+ module JsonApiResource
2
+ module Associations
3
+ module Preloaders
4
+ class HasManyPrefetchedPreloader < Base
5
+
6
+ def bulk_query( objects )
7
+ ids = objects.map{ |o| o.send(key) }.flatten.uniq
8
+ { id: ids }.merge(opts)
9
+ end
10
+
11
+ def distributor_class
12
+ JsonApiResource::Associations::Preloaders::Distributors::DistributorByTargetId
13
+ end
14
+ end
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,17 @@
1
+ module JsonApiResource
2
+ module Associations
3
+ module Preloaders
4
+ class HasManyPreloader < Base
5
+
6
+ def bulk_query( objects )
7
+ ids = objects.map(&:id)
8
+ { key => ids }.merge(opts)
9
+ end
10
+
11
+ def distributor_class
12
+ JsonApiResource::Associations::Preloaders::Distributors::DistributorByObjectId
13
+ end
14
+ end
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,17 @@
1
+ module JsonApiResource
2
+ module Associations
3
+ module Preloaders
4
+ class HasOnePreloader < Base
5
+
6
+ def bulk_query( objects )
7
+ ids = objects.map(&:id)
8
+ { key => ids }.merge(opts)
9
+ end
10
+
11
+ def distributor_class
12
+ JsonApiResource::Associations::Preloaders::Distributors::DistributorByObjectId
13
+ end
14
+ end
15
+ end
16
+ end
17
+ end
@@ -1,5 +1,29 @@
1
1
  module JsonApiResource
2
2
  module Errors
3
+ extend ActiveSupport::Concern
4
+ included do
5
+
6
+ def raise_if( condition, message, error = JsonApiResource::Errors::InvalidAssociation )
7
+ self.class.raise_if condition, message, error
8
+ end
9
+
10
+ def raise_unless( condition, message, error = JsonApiResource::Errors::InvalidAssociation )
11
+ raise_if !condition, message, error
12
+ end
13
+
14
+
15
+ class << self
16
+
17
+ def raise_if( condition, message, error = JsonApiResource::Errors::InvalidAssociation )
18
+ raise error.new(class: self, message: message ) if condition
19
+ end
20
+
21
+ def raise_unless( condition, message, error = JsonApiResource::Errors::InvalidAssociation )
22
+ raise_if !condition, message, error
23
+ end
24
+ end
25
+ end
26
+
3
27
  class JsonApiResourceError < StandardError
4
28
  def initialize(opts = {})
5
29
  @klass = opts.fetch :class, JsonApiResource::Resource
@@ -13,5 +37,8 @@ module JsonApiResource
13
37
 
14
38
  class UnsuccessfulRequest < JsonApiResourceError
15
39
  end
40
+
41
+ class InvalidAssociation < JsonApiResourceError
42
+ end
16
43
  end
17
44
  end
@@ -24,6 +24,8 @@ module JsonApiResource
24
24
  include JsonApiResource::Conversions
25
25
  include JsonApiResource::Cacheable
26
26
  include JsonApiResource::ErrorHandleable
27
+ include JsonApiResource::Errors
28
+ include JsonApiResource::Associatable
27
29
 
28
30
  attr_accessor :client, :cache_expires_in
29
31
  class_attribute :per_page
@@ -31,7 +33,7 @@ module JsonApiResource
31
33
  delegate :to_json, to: :attributes
32
34
 
33
35
  def initialize(opts={})
34
- raise( JsonApiResource::Errors::JsonApiResourceError, class: self.class, message: "A resource must have a client class" ) unless client_class.present?
36
+ raise_unless client_class.present?, "A resource must have a client class", JsonApiResource::Errors::JsonApiResourceError
35
37
 
36
38
  self.attributes = opts
37
39
  self.errors = ActiveModel::Errors.new(self)
@@ -1,3 +1,3 @@
1
1
  module JsonApiResource
2
- VERSION = "3.0.1"
2
+ VERSION = "3.1.0"
3
3
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: json_api_resource
3
3
  version: !ruby/object:Gem::Version
4
- version: 3.0.1
4
+ version: 3.1.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Brandon Sislow
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2016-05-16 00:00:00.000000000 Z
11
+ date: 2016-07-22 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: json_api_client
@@ -104,6 +104,24 @@ files:
104
104
  - README.md
105
105
  - Rakefile
106
106
  - lib/json_api_resource.rb
107
+ - lib/json_api_resource/associatable.rb
108
+ - lib/json_api_resource/associations.rb
109
+ - lib/json_api_resource/associations/base.rb
110
+ - lib/json_api_resource/associations/belongs_to.rb
111
+ - lib/json_api_resource/associations/has_many.rb
112
+ - lib/json_api_resource/associations/has_many_prefetched.rb
113
+ - lib/json_api_resource/associations/has_one.rb
114
+ - lib/json_api_resource/associations/preloader.rb
115
+ - lib/json_api_resource/associations/preloaders.rb
116
+ - lib/json_api_resource/associations/preloaders/base.rb
117
+ - lib/json_api_resource/associations/preloaders/belongs_to_preloader.rb
118
+ - lib/json_api_resource/associations/preloaders/distributors.rb
119
+ - lib/json_api_resource/associations/preloaders/distributors/base.rb
120
+ - lib/json_api_resource/associations/preloaders/distributors/distributor_by_object_id.rb
121
+ - lib/json_api_resource/associations/preloaders/distributors/distributor_by_target_id.rb
122
+ - lib/json_api_resource/associations/preloaders/has_many_prefetched_preloader.rb
123
+ - lib/json_api_resource/associations/preloaders/has_many_preloader.rb
124
+ - lib/json_api_resource/associations/preloaders/has_one_preloader.rb
107
125
  - lib/json_api_resource/cacheable.rb
108
126
  - lib/json_api_resource/clientable.rb
109
127
  - lib/json_api_resource/connections.rb
@@ -140,7 +158,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
140
158
  version: '0'
141
159
  requirements: []
142
160
  rubyforge_project:
143
- rubygems_version: 2.4.6
161
+ rubygems_version: 2.5.1
144
162
  signing_key:
145
163
  specification_version: 4
146
164
  summary: Build wrapper/adapter objects around JsonApiClient instances