json_api_model 0.4.0 → 0.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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