json_api_model 0.4.0 → 0.5.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 0dcc8c7cf054ebbc3e13ff7f11e526703d8763fd
4
- data.tar.gz: d11d0c76be9f5f0e7ff0b9545731e0534c66485c
3
+ metadata.gz: 61778944ededa50685c1d990d6f6a66c78254d4f
4
+ data.tar.gz: bd95c987db432e21b75457dddd973e3bcb3394eb
5
5
  SHA512:
6
- metadata.gz: 6d6330ad95f23f9e0e3d97945f00d26f37c0a9d6f79598fba478b901a477718ce1c771121fe96b54c21d7510b8092a98cd0f817bddda3b527bae518449c9c38a
7
- data.tar.gz: 55d212fb0693ae234aebc7cfd51c0e7e4f85d7cafd6a860261be22f2c26e8e1ca1f286aa733d03eeaae36c9974de4149c6ffdeae6f82aad55ae40b6ab46f552d
6
+ metadata.gz: f98a1388dee293f467b2af6b75134e8523ffc1c785a874e9a369b0b01a18139b75a20b9c6f804ab34bc07342f0bc0df2761e3234efa467d8a459e8c21202beff
7
+ data.tar.gz: 9d78a9616a2c80e2b67ae51f47951d641e690e7cc412bdad82377e624b30efdf2a3dd581a03e8c019e8e3b0bd296350bea11cc1d1479dc70c8cbd0ee3b57c7fb
data/Gemfile.lock CHANGED
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- json_api_model (0.4.0)
4
+ json_api_model (0.5.0)
5
5
  activesupport
6
6
  json_api_client
7
7
 
data/README.md CHANGED
@@ -184,6 +184,58 @@ class MyModelInRussianConroller < ApplicationController
184
184
  end
185
185
  ```
186
186
 
187
+ ### Preloading
188
+
189
+ If you have a model that you need to bulk load along with its associations, in `ActiveRecord`, you can do `MyModel.where( args ).preload( :association, other_association: :nested association )`. Well, you can do that here as well.
190
+
191
+ __NOTE__(1): consider the complexity, especially for remote models
192
+
193
+ __NOTE__(2): for remote models, the preloader does not currently autopage
194
+
195
+ #### Example
196
+
197
+ Let's say you have a mixed environemnt of local and remote models, like so:
198
+
199
+ ```ruby
200
+ module Example
201
+ class User < JsonApiModel::Model
202
+ wraps Example::Client::User
203
+
204
+ # remote model
205
+ belongs_to :org, class_name: "Example::Org"
206
+
207
+ # local model
208
+ has_one :profile
209
+ end
210
+
211
+ class Org < JsonApiModel::Model
212
+ wraps Example::Client::Org
213
+ end
214
+ end
215
+
216
+ class Profile < ActiveRecord::Base
217
+ belongs_to :user
218
+ end
219
+ ```
220
+
221
+ You can bulk prefetch associations for a block of `User`s
222
+
223
+ ```ruby
224
+ Example::User.where( name: [ "Greg", "Mike" ] ).preload( :org, :profile )
225
+ ```
226
+
227
+ Will make 3 calls:
228
+
229
+ * `User` to fetch `.where( name: [ "Greg", "Mike" ])`
230
+ * `Org` to fetch the orgs associated with those users, as returned in their relationship blocks
231
+ * `Profile` to fetch `where( user_id: users.map( &:id ) )`
232
+
233
+ Or if you already have your `users` loaded, you can call
234
+
235
+ ```ruby
236
+ JsonApiModel::Associations::Preloader.preload( users, :org, :profile )
237
+ ```
238
+ and that'll do the same thing, minus the `User` call
187
239
 
188
240
  ### Configuration
189
241
 
@@ -57,12 +57,15 @@ module JsonApiModel
57
57
  self.__cached_associations ||= {}
58
58
 
59
59
  unless self.__cached_associations.has_key? association.name
60
- result = association.fetch( self )
61
-
62
- self.__cached_associations[association.name] = result
60
+ self.send( "#{association.name}=", association.fetch( self ) )
63
61
  end
64
62
  self.__cached_associations[association.name]
65
63
  end
64
+
65
+ define_method "#{association.name}=" do | value |
66
+ self.__cached_associations ||= {}
67
+ self.__cached_associations[association.name] = value
68
+ end
66
69
  end
67
70
  end
68
71
  end
@@ -6,5 +6,7 @@ module JsonApiModel
6
6
  autoload :Has, 'json_api_model/associations/has'
7
7
  autoload :HasMany, 'json_api_model/associations/has_many'
8
8
  autoload :HasOne, 'json_api_model/associations/has_one'
9
+ autoload :Preloader, 'json_api_model/associations/preloader'
10
+ autoload :Preloaders, 'json_api_model/associations/preloaders'
9
11
  end
10
12
  end
@@ -2,41 +2,46 @@ module JsonApiModel
2
2
  module Associations
3
3
  class Base
4
4
 
5
- attr_accessor :name, :opts, :key
5
+ attr_accessor :name, :opts, :key, :base_class
6
+ delegate :preload, to: :preloader
6
7
 
7
8
  def initialize( base_class, name, opts = {} )
8
- self.name = name
9
- self.opts = opts
10
- self.key = idify( base_class )
9
+ self.name = name
10
+ self.opts = opts
11
+ self.key = idify( base_class )
12
+ self.base_class = base_class
11
13
 
12
- sanitize_opts( base_class )
14
+ validate_opts!
13
15
  end
14
16
 
15
17
  def fetch( instance )
16
- process klass.send( action, query( instance ) )
18
+ process association_class.send( action, query( instance ) )
17
19
  end
18
20
 
19
- protected
20
-
21
- def klass
22
- @klass ||= association_class( name, opts )
21
+ def json_relationship?
22
+ association_class < JsonApiModel::Model
23
23
  end
24
24
 
25
- def association_class( name, opts = {} )
26
- a_class = opts[:class] if opts.has_key? :class
27
- a_class_name = opts[:class_name].constantize if opts.has_key? :class_name
28
-
29
- a_class || a_class_name || derived_class_for( name )
25
+ def association_class
26
+ @associated_class ||= opts[:class] ||
27
+ opts[:class_name]&.constantize ||
28
+ derived_class
30
29
  end
31
30
 
32
- def derived_class_for( name )
33
- name.to_s.singularize.classify.constantize
31
+ def process( results )
32
+ results
34
33
  end
35
34
 
35
+ protected
36
+
36
37
  def idify( class_name )
37
38
  "#{class_name.to_s.demodulize.underscore}_id"
38
39
  end
39
40
 
41
+ def derived_class
42
+ name.to_s.singularize.classify.constantize
43
+ end
44
+
40
45
  def supported_options
41
46
  [ :class, :class_name ] + additional_options
42
47
  end
@@ -45,21 +50,14 @@ module JsonApiModel
45
50
  []
46
51
  end
47
52
 
48
- def sanitize_opts( base_class )
53
+ def validate_opts!
49
54
  if name.to_s == "object"
50
55
  raise "#{base_class}: 'object_id' is a reserved keyword in ruby and cannot be overridden"
51
56
  end
52
- invalid_options = (opts.keys - supported_options)
53
- if invalid_options.present?
54
- list = invalid_options.map{|o|"'#{o}'"}.to_sentence
55
- plural = invalid_options.count > 1
56
- raise "#{base_class}: #{list} #{plural ? "are" : "is"} not supported."
57
+ (opts.keys - supported_options).each do | opt |
58
+ raise "#{base_class}: #{opt} is not supported."
57
59
  end
58
60
  end
59
-
60
- def process( results )
61
- results
62
- end
63
61
  end
64
62
  end
65
63
  end
@@ -11,13 +11,17 @@ module JsonApiModel
11
11
  "#{name}_id"
12
12
  end
13
13
 
14
- def query( instance )
15
- if instance.has_relationship_ids? name
14
+ def ids( instance )
15
+ if json_relationship?
16
16
  instance.relationship_ids( name ).first
17
17
  else
18
18
  instance.send key
19
19
  end
20
20
  end
21
+
22
+ def query( instance )
23
+ ids( instance )
24
+ end
21
25
  end
22
26
  end
23
27
  end
@@ -6,20 +6,32 @@ module JsonApiModel
6
6
  :where
7
7
  end
8
8
 
9
- def query( instance )
10
- if instance.has_relationship_ids? name
11
- { id: instance.relationship_ids( name ) }
12
- elsif through?
13
- { id: target_ids( instance ) }
9
+ def additional_options
10
+ [ :as, :through ]
11
+ end
12
+
13
+ def key
14
+ if json_relationship? || through?
15
+ :id
14
16
  elsif as?
15
- { "#{as}_id" => instance.id }
17
+ "#{as}_id"
16
18
  else
17
- { key => instance.id }
19
+ idify( base_class )
18
20
  end
19
21
  end
20
22
 
21
- def additional_options
22
- [ :as, :through ]
23
+ def ids( instance )
24
+ if json_relationship?
25
+ instance.relationship_ids( name )
26
+ elsif through?
27
+ target_ids( instance )
28
+ else
29
+ instance.id
30
+ end
31
+ end
32
+
33
+ def query( instance )
34
+ { key => ids( instance ) }
23
35
  end
24
36
 
25
37
  protected
@@ -41,15 +53,15 @@ module JsonApiModel
41
53
  end
42
54
 
43
55
  def target_ids( instance )
44
- intermadiates = Array(instance.send( through ) )
45
-
46
- intermadiates.map do | intermediate |
56
+ intermediates = Array(instance.send( through ) )
57
+
58
+ intermediates.map do | intermediate |
47
59
  intermediate.send( through_key )
48
60
  end
49
61
  end
50
62
 
51
63
  def through_key
52
- idify klass
64
+ idify association_class
53
65
  end
54
66
  end
55
67
  end
@@ -0,0 +1,59 @@
1
+ module JsonApiModel
2
+ module Associations
3
+ class Preloader
4
+
5
+ class << self
6
+ def preload( objects, *preloads )
7
+ new( objects, preloads ).preload
8
+ end
9
+ end
10
+
11
+ attr_accessor :objects, :preloads
12
+
13
+ def initialize( objects, preloads )
14
+ @objects = Array( objects ).compact
15
+ @preloads = preloads
16
+
17
+ validate_homogenity!
18
+ end
19
+
20
+ def preload
21
+ @preloads.each do | preload |
22
+ case preload
23
+ when Hash
24
+ preload.each do | preload, subpreloads |
25
+ preloader = Preloaders.preloader_for( @objects, preload )
26
+
27
+ subobjects = preloader.load
28
+ preloader.assign subobjects
29
+
30
+ subobjects.preload( subpreloads )
31
+ end
32
+ else
33
+ Preloaders.preloader_for( @objects, preload ).fetch
34
+ end
35
+ end
36
+
37
+ @objects
38
+ end
39
+
40
+ private
41
+
42
+ def validate_homogenity!
43
+ unless homogeneous?
44
+ raise "JsonApiModel::Associations::Preloader.preload called with a heterogeneous array of objects."
45
+ end
46
+ end
47
+
48
+ def object_class
49
+ @object_class ||= @objects.first.class
50
+ end
51
+
52
+ def homogeneous?
53
+ @objects.all? do |obj|
54
+ obj.is_a?(object_class)
55
+ end
56
+ end
57
+ end
58
+ end
59
+ end
@@ -0,0 +1,32 @@
1
+ module JsonApiModel
2
+ module Associations
3
+ module Preloaders
4
+ autoload :Base, 'json_api_model/associations/preloaders/base'
5
+ autoload :BelongsTo, 'json_api_model/associations/preloaders/belongs_to'
6
+ autoload :Has, 'json_api_model/associations/preloaders/has'
7
+
8
+ class << self
9
+ def preloader_for( objects, preload )
10
+ klass = object_class( objects )
11
+ association = klass.__associations.fetch preload
12
+
13
+ PREOLOADERS[ association.class ].new( objects, association )
14
+ rescue KeyError
15
+ raise "#{klass}##{preload.to_s} is not a valid association"
16
+ end
17
+
18
+ private
19
+
20
+ def object_class( objects )
21
+ objects.first.class
22
+ end
23
+ end
24
+
25
+ PREOLOADERS = {
26
+ JsonApiModel::Associations::BelongsTo => JsonApiModel::Associations::Preloaders::BelongsTo,
27
+ JsonApiModel::Associations::HasOne => JsonApiModel::Associations::Preloaders::Has,
28
+ JsonApiModel::Associations::HasMany => JsonApiModel::Associations::Preloaders::Has
29
+ }
30
+ end
31
+ end
32
+ end
@@ -0,0 +1,46 @@
1
+ module JsonApiModel
2
+ module Associations
3
+ module Preloaders
4
+ class Base
5
+
6
+ attr_accessor :association
7
+ delegate :key, :name, :association_class, :action, :process, :ids, :base_class, to: :association
8
+
9
+ def initialize( objects, association )
10
+ @objects = Array( objects )
11
+ @association = association
12
+ end
13
+
14
+ def fetch
15
+ assign load
16
+ end
17
+
18
+ def load
19
+ association_class.send( action, query( @objects ) )
20
+ end
21
+
22
+ def assign( results )
23
+ validate_assignability!( results )
24
+ @objects.each do | object |
25
+
26
+ associated_objects = results.to_a.select do |r|
27
+ associated_key( r ).in? Array( ids( object ) )
28
+ end
29
+
30
+ object.send( "#{name}=", process( associated_objects ) )
31
+ end
32
+ end
33
+
34
+ protected
35
+
36
+ def validate_assignability!( results )
37
+ results.each do | object |
38
+ unless associated_key( object )
39
+ raise "Preloading #{association_class}.#{lookup} failed: results don't identify an association."
40
+ end
41
+ end
42
+ end
43
+ end
44
+ end
45
+ end
46
+ end
@@ -0,0 +1,23 @@
1
+ module JsonApiModel
2
+ module Associations
3
+ module Preloaders
4
+ class BelongsTo < Base
5
+ def associated_key( object )
6
+ object.id
7
+ rescue => e
8
+ nil
9
+ end
10
+
11
+ def lookup
12
+ :id
13
+ end
14
+
15
+ def query( instances )
16
+ instances.map do | instance |
17
+ ids( instance )
18
+ end.uniq
19
+ end
20
+ end
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,26 @@
1
+ module JsonApiModel
2
+ module Associations
3
+ module Preloaders
4
+ class Has < Base
5
+ def associated_key( object )
6
+ object.respond_to?( key ) ? object.send( key ) : object.id
7
+ rescue => e
8
+ nil
9
+ end
10
+
11
+ def query( instances )
12
+ instances.each_with_object( { key => [] } ) do | instance, query |
13
+ query[ key ] += Array( ids( instance ) )
14
+ query[ key ].uniq!
15
+ end
16
+ end
17
+
18
+ protected
19
+
20
+ def lookup
21
+ base_class.to_s.demodulize.underscore
22
+ end
23
+ end
24
+ end
25
+ end
26
+ end
@@ -1,7 +1,7 @@
1
1
  module JsonApiModel
2
2
  class Model
3
3
  class << self
4
- delegate :where, :order, :includes, :select, :all, :paginate, :page, :with_params, :first, :find, :last, to: :__new_scope
4
+ delegate :where, :order, :includes, :select, :all, :paginate, :page, :with_params, :first, :find, :last, :preload, to: :__new_scope
5
5
 
6
6
  attr_reader :client_class
7
7
  def wraps( client_class )
@@ -17,6 +17,10 @@ module JsonApiModel
17
17
  end
18
18
  end
19
19
 
20
+ def preload( *args )
21
+ JsonApiModel::Associations::Preloader.preload( self, *args )
22
+ end
23
+
20
24
  def meta
21
25
  @set.meta.attributes
22
26
  end
@@ -1,3 +1,3 @@
1
1
  module JsonApiModel
2
- VERSION = "0.4.0"
2
+ VERSION = "0.5.0"
3
3
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: json_api_model
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.4.0
4
+ version: 0.5.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Greg Orlov
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2018-08-27 00:00:00.000000000 Z
11
+ date: 2018-09-05 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: activesupport
@@ -135,6 +135,11 @@ files:
135
135
  - lib/json_api_model/associations/has.rb
136
136
  - lib/json_api_model/associations/has_many.rb
137
137
  - lib/json_api_model/associations/has_one.rb
138
+ - lib/json_api_model/associations/preloader.rb
139
+ - lib/json_api_model/associations/preloaders.rb
140
+ - lib/json_api_model/associations/preloaders/base.rb
141
+ - lib/json_api_model/associations/preloaders/belongs_to.rb
142
+ - lib/json_api_model/associations/preloaders/has.rb
138
143
  - lib/json_api_model/instrumenter.rb
139
144
  - lib/json_api_model/instrumenter/null_instrumenter.rb
140
145
  - lib/json_api_model/model.rb