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.
- checksums.yaml +4 -4
- data/README.md +103 -0
- data/lib/json_api_resource.rb +2 -0
- data/lib/json_api_resource/associatable.rb +60 -0
- data/lib/json_api_resource/associations.rb +17 -0
- data/lib/json_api_resource/associations/base.rb +79 -0
- data/lib/json_api_resource/associations/belongs_to.rb +29 -0
- data/lib/json_api_resource/associations/has_many.rb +26 -0
- data/lib/json_api_resource/associations/has_many_prefetched.rb +34 -0
- data/lib/json_api_resource/associations/has_one.rb +30 -0
- data/lib/json_api_resource/associations/preloader.rb +58 -0
- data/lib/json_api_resource/associations/preloaders.rb +12 -0
- data/lib/json_api_resource/associations/preloaders/base.rb +38 -0
- data/lib/json_api_resource/associations/preloaders/belongs_to_preloader.rb +17 -0
- data/lib/json_api_resource/associations/preloaders/distributors.rb +11 -0
- data/lib/json_api_resource/associations/preloaders/distributors/base.rb +33 -0
- data/lib/json_api_resource/associations/preloaders/distributors/distributor_by_object_id.rb +28 -0
- data/lib/json_api_resource/associations/preloaders/distributors/distributor_by_target_id.rb +32 -0
- data/lib/json_api_resource/associations/preloaders/has_many_prefetched_preloader.rb +17 -0
- data/lib/json_api_resource/associations/preloaders/has_many_preloader.rb +17 -0
- data/lib/json_api_resource/associations/preloaders/has_one_preloader.rb +17 -0
- data/lib/json_api_resource/errors.rb +27 -0
- data/lib/json_api_resource/resource.rb +3 -1
- data/lib/json_api_resource/version.rb +1 -1
- metadata +21 -3
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 550d3bf8b009a60ef20065c2131601ec524a0761
|
4
|
+
data.tar.gz: 822dca3ef57832f3fecc4c18c0bb1f52e846c1bf
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
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.
|
data/lib/json_api_resource.rb
CHANGED
@@ -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
|
-
|
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)
|
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
|
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-
|
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.
|
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
|