n1_loader 1.2.0 → 1.4.1

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
  SHA256:
3
- metadata.gz: 8b9b61121f94d9dba30e969c1b9a3fa9c812292d618434a98262c90aef43e6cd
4
- data.tar.gz: a1ecf5359a1a53200e354b30289566a437cf372c54ccf3f13c2e0adc2cb71640
3
+ metadata.gz: fc06934b6c230d9b48dce18c7e9ee49afad4ab32d51d4550eb3e2916fe977656
4
+ data.tar.gz: 456575999736bba2b4b8f33ff02a4908a7705b2cc31875bca9c80f3dbeeb4125
5
5
  SHA512:
6
- metadata.gz: 5c545cc2033dc8e75edddd5c948eed78fc1d965464522147841d30e3c6a02aebb0b6758d3f801b020c25d62618f7ae4bbb7ff11441748f798546351b1bf34d85
7
- data.tar.gz: a60fa813399debf1ae9c8f839272a683c2a7f733ea392b4b1bd4169af55369a1d3be2fd8648a548e50093005e3d2df962437adf08f1f9fdf1338fa4ab6217df7
6
+ metadata.gz: 4640cffcc0fe6e03672ba46fc16e9fc29fc33d1c18b6d9e54e4c6026cd0d0b465846a60e0b75ab0dec46fd4db21ae914a0f92d3cc4623e622b4782094d6ce680
7
+ data.tar.gz: a010ffac1ce7604ce9434de734fa20f0e5162ff7307f6ddcafe0b9be1119a4f37d3774b7dd4beb8d36affd56e4f30830d01904406ce1f2b2fc2989d7f9910fa1
data/CHANGELOG.md CHANGED
@@ -1,4 +1,24 @@
1
- ## [1.2.0]
1
+ ## [1.4.1] - 2022-02-24
2
+
3
+ - Fix preloading of invalid objects
4
+
5
+ ## [1.4.0] - 2022-02-22
6
+
7
+ - add support of optional arguments
8
+
9
+ BREAKING CHANGES:
10
+ - rework arguments to use single definition through `argument <name>` only
11
+ - use keyword arguments
12
+
13
+ ## [1.3.0] - 2022-02-22
14
+
15
+ - add support of named arguments with `argument <name>`
16
+
17
+ BREAKING CHANGES:
18
+ - rename `n1_load` to `n1_optimized`
19
+ - rework `def self.arguments_key` to `cache_key`
20
+
21
+ ## [1.2.0] - 2022-01-14
2
22
 
3
23
  - Introduce arguments support.
4
24
 
data/README.md CHANGED
@@ -45,7 +45,7 @@ class User
45
45
  include N1Loader::Loadable
46
46
 
47
47
  # with inline loader
48
- n1_loader :orders_count do |users|
48
+ n1_optimized :orders_count do |users|
49
49
  orders_per_user = Order.where(user: users).group(:user_id).count
50
50
 
51
51
  users.each { |user| fulfill(user, orders_per_user[user.id]) }
@@ -69,7 +69,7 @@ class User
69
69
  include N1Loader::Loadable
70
70
 
71
71
  # with inline loader
72
- n1_loader :orders_count do |users|
72
+ n1_optimized :orders_count do |users|
73
73
  orders_per_user = Order.where(user: users).group(:user_id).count
74
74
 
75
75
  users.each { |user| fulfill(user, orders_per_user[user.id]) }
@@ -98,14 +98,14 @@ end
98
98
 
99
99
  class User
100
100
  include N1Loader::Loadable
101
-
102
- n1_loader :orders_count, OrdersCountLoader
101
+
102
+ n1_optimized :orders_count, OrdersCountLoader
103
103
  end
104
104
 
105
105
  class Customer
106
106
  include N1Loader::Loadable
107
-
108
- n1_loader :orders_count, OrdersCountLoader
107
+
108
+ n1_optimized :orders_count, OrdersCountLoader
109
109
  end
110
110
 
111
111
  User.new.orders_count # => works
@@ -119,7 +119,7 @@ class User
119
119
  include N1Loader::Loadable
120
120
 
121
121
  # with inline loader
122
- n1_loader :orders_count do |users|
122
+ n1_optimized :orders_count do |users|
123
123
  orders_per_user = Order.where(user: users).group(:user_id).count
124
124
 
125
125
  users.each { |user| fulfill(user, orders_per_user[user.id]) }
@@ -159,8 +159,8 @@ end
159
159
  ```ruby
160
160
  class User
161
161
  include N1Loader::Loadable
162
-
163
- n1_loader :orders_count do # no arguments passed to the block, so we can override both perform and single.
162
+
163
+ n1_optimized :orders_count do # no arguments passed to the block, so we can override both perform and single.
164
164
  def perform(users)
165
165
  orders_per_user = Order.where(user: users).group(:user_id).count
166
166
 
@@ -188,21 +188,25 @@ users.map(&:orders_count) # perform will be used once without N+1
188
188
  class User
189
189
  include N1Loader::Loadable
190
190
 
191
- n1_loader :orders_count do |users, type|
192
- orders_per_user = Order.where(type: type, user: users).group(:user_id).count
193
-
194
- users.each { |user| fulfill(user, orders_per_user[user.id]) }
191
+ n1_optimized :orders_count do
192
+ argument :type
193
+
194
+ def perform(users)
195
+ orders_per_user = Order.where(type: type, user: users).group(:user_id).count
196
+
197
+ users.each { |user| fulfill(user, orders_per_user[user.id]) }
198
+ end
195
199
  end
196
200
  end
197
201
 
198
202
  user = User.new
199
- user.orders_count(:gifts) # The loader will be performed first time for this argument
200
- user.orders_count(:sales) # The loader will be performed first time for this argument
201
- user.orders_count(:gifts) # The cached value will be used
203
+ user.orders_count(type: :gifts) # The loader will be performed first time for this argument
204
+ user.orders_count(type: :sales) # The loader will be performed first time for this argument
205
+ user.orders_count(type: :gifts) # The cached value will be used
202
206
 
203
207
  users = [User.new, User.new]
204
208
  N1Loader::Preloader.new(users).preload(:orders_count)
205
- users.map { |user| user.orders_count(:gifts) } # No N+1 here
209
+ users.map { |user| user.orders_count(type: :gifts) } # No N+1 here
206
210
  ```
207
211
 
208
212
  _Note_: By default, we use `arguments.map(&:object_id)` to identify arguments but in some cases,
@@ -212,22 +216,22 @@ you may want to override it, for example:
212
216
  class User
213
217
  include N1Loader::Loadable
214
218
 
215
- n1_loader :orders_count do
216
- def perform(users, sale)
219
+ n1_optimized :orders_count do
220
+ argument :sale
221
+
222
+ cache_key { sale.id }
223
+
224
+ def perform(users)
217
225
  orders_per_user = Order.where(sale: sale, user: users).group(:user_id).count
218
226
 
219
227
  users.each { |user| fulfill(user, orders_per_user[user.id]) }
220
228
  end
221
-
222
- def self.arguments_key(sale)
223
- sale.id
224
- end
225
229
  end
226
230
  end
227
231
 
228
232
  user = User.new
229
- user.orders_count(Sale.first) # perform will be executed and value will be cached
230
- user.orders_count(Sale.first) # the cached value will be returned
233
+ user.orders_count(sale: Sale.first) # perform will be executed and value will be cached
234
+ user.orders_count(sale: Sale.first) # the cached value will be returned
231
235
  ```
232
236
 
233
237
 
@@ -235,11 +239,13 @@ user.orders_count(Sale.first) # the cached value will be returned
235
239
 
236
240
  ### [ActiveRecord][5]
237
241
 
242
+ _Note_: Rails 7 support is coming soon! Stay tuned!
243
+
238
244
  ```ruby
239
245
  class User < ActiveRecord::Base
240
246
  include N1Loader::Loadable
241
-
242
- n1_loader :orders_count do |users|
247
+
248
+ n1_optimized :orders_count do |users|
243
249
  orders_per_user = Order.where(user: users).group(:user_id).count
244
250
 
245
251
  users.each { |user| fulfill(user, orders_per_user[user.id]) }
@@ -266,8 +272,8 @@ users.map(&:orders_count)
266
272
  ```ruby
267
273
  class User < ActiveRecord::Base
268
274
  include N1Loader::Loadable
269
-
270
- n1_loader :orders_count do |users|
275
+
276
+ n1_optimized :orders_count do |users|
271
277
  orders_per_user = Order.where(user: users).group(:user_id).count
272
278
 
273
279
  users.each { |user| fulfill(user, orders_per_user[user.id]) }
@@ -1,9 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  N1Loader::LoaderCollection.define_method :preloaded_records do
4
- unless loader_class.instance_method(:perform).arity == 1
5
- raise N1Loader::ActiveRecord::InvalidPreloading, "Cannot preload loader with arguments"
6
- end
4
+ raise N1Loader::ActiveRecord::InvalidPreloading, "Cannot preload loader with arguments" if loader_class.arguments
7
5
 
8
6
  with.preloaded_records
9
7
  end
@@ -4,7 +4,7 @@ module N1Loader
4
4
  module ArLazyPreload
5
5
  module Loadable
6
6
  module ClassMethods # :nodoc:
7
- def n1_load(name, loader = nil, &block)
7
+ def n1_optimized(name, loader = nil, &block)
8
8
  name, loader_name, loader_variable_name = super
9
9
 
10
10
  define_method(loader_name) do
@@ -6,7 +6,7 @@ module N1Loader
6
6
  module LoaderCollectionPatch
7
7
  attr_accessor :context_setup
8
8
 
9
- def with(*args)
9
+ def with(**args)
10
10
  result = super
11
11
 
12
12
  result.context_setup = context_setup if context_setup && result.context_setup.nil?
@@ -45,9 +45,9 @@ module N1Loader
45
45
  respond_to?("#{name}_loader")
46
46
  end
47
47
 
48
- def n1_load(name, loader = nil, &block) # rubocop:disable Metrics/MethodLength, Metrics/AbcSize
48
+ def n1_optimized(name, loader = nil, &block) # rubocop:disable Metrics/MethodLength, Metrics/AbcSize
49
49
  loader ||= Class.new(N1Loader::Loader) do
50
- if block&.arity&.positive?
50
+ if block.arity == 1
51
51
  define_method(:perform, &block)
52
52
  else
53
53
  class_eval(&block)
@@ -73,10 +73,10 @@ module N1Loader
73
73
  instance_variable_get(loader_variable_name) || send("#{loader_name}_reload")
74
74
  end
75
75
 
76
- define_method(name) do |*args, reload: false|
76
+ define_method(name) do |reload: false, **args|
77
77
  send("#{loader_name}_reload") if reload
78
78
 
79
- send(loader_name).with(*args).for(self)
79
+ send(loader_name).with(**args).for(self)
80
80
  end
81
81
 
82
82
  [name, loader_name, loader_variable_name]
@@ -6,11 +6,35 @@ module N1Loader
6
6
  # Subclasses must define +perform+ method that accepts single argument
7
7
  # and returns hash where key is the element and value is what we want to load.
8
8
  class Loader
9
- def self.arguments_key(*args)
10
- args.map(&:object_id)
9
+ class << self
10
+ attr_reader :arguments
11
+
12
+ # Defines an argument that can be accessed within the loader.
13
+ #
14
+ # First defined argument will have the value of first passed argument,
15
+ # meaning the order is important.
16
+ #
17
+ # @param name [Symbol]
18
+ # @param opts [Hash]
19
+ # @option opts [Boolean] optional false by default
20
+ def argument(name, **opts)
21
+ @arguments ||= []
22
+
23
+ define_method(name) { args[name] }
24
+
25
+ @arguments << opts.merge(name: name)
26
+ end
27
+
28
+ # Defines a custom cache key that is calculated for passed arguments.
29
+ def cache_key(&block)
30
+ define_method(:cache_key) do
31
+ check_arguments!
32
+ instance_exec(&block)
33
+ end
34
+ end
11
35
  end
12
36
 
13
- def initialize(elements, *args)
37
+ def initialize(elements, **args)
14
38
  @elements = elements
15
39
  @args = args
16
40
  end
@@ -24,10 +48,48 @@ module N1Loader
24
48
  loaded[element]
25
49
  end
26
50
 
51
+ def cache_key
52
+ check_arguments!
53
+ args.values.map(&:object_id)
54
+ end
55
+
27
56
  private
28
57
 
29
58
  attr_reader :elements, :args
30
59
 
60
+ def check_missing_arguments! # rubocop:disable Metrics/AbcSize, Metrics/MethodLength
61
+ return unless (arguments = self.class.arguments)
62
+
63
+ min = arguments.count { |argument| !argument[:optional] }
64
+ max = arguments.count
65
+
66
+ return if args.size >= min && args.size <= max
67
+
68
+ str =
69
+ if min == max
70
+ max.to_s
71
+ else
72
+ "#{min}..#{max}"
73
+ end
74
+
75
+ raise MissingArgument, "Loader requires #{str} arguments but #{args.size} were given"
76
+ end
77
+
78
+ def check_arguments!
79
+ check_missing_arguments!
80
+ check_invalid_arguments!
81
+ end
82
+
83
+ def check_invalid_arguments!
84
+ return unless (arguments = self.class.arguments)
85
+
86
+ args.each_key do |arg|
87
+ next if arguments.find { |argument| argument[:name] == arg }
88
+
89
+ raise InvalidArgument, "Loader doesn't define #{arg} argument"
90
+ end
91
+ end
92
+
31
93
  def perform(_elements)
32
94
  raise NotImplemented, "Subclasses have to implement the method"
33
95
  end
@@ -39,12 +101,14 @@ module N1Loader
39
101
  def loaded
40
102
  return @loaded if @loaded
41
103
 
104
+ check_arguments!
105
+
42
106
  @loaded = {}.compare_by_identity
43
107
 
44
108
  if elements.size == 1 && respond_to?(:single)
45
- fulfill(elements.first, single(elements.first, *args))
109
+ fulfill(elements.first, single(elements.first))
46
110
  elsif elements.any?
47
- perform(elements, *args)
111
+ perform(elements)
48
112
  end
49
113
 
50
114
  @loaded
@@ -10,8 +10,10 @@ module N1Loader
10
10
  @elements = elements
11
11
  end
12
12
 
13
- def with(*args)
14
- loaders[loader_class.arguments_key(*args)] ||= loader_class.new(elements, *args)
13
+ def with(**args)
14
+ loader = loader_class.new(elements, **args)
15
+
16
+ loaders[loader.cache_key] ||= loader
15
17
  end
16
18
 
17
19
  private
@@ -17,13 +17,23 @@ module N1Loader
17
17
  def preload(*keys)
18
18
  keys.flatten(1).flat_map do |key|
19
19
  elements
20
- .group_by { |element| element.class.n1_loader(key) }
20
+ .group_by { |element| loader_class(element, key) }
21
21
  .map do |loader_class, grouped_elements|
22
+ next unless loader_class
23
+
22
24
  loader_collection = N1Loader::LoaderCollection.new(loader_class, grouped_elements)
23
25
  grouped_elements.each { |grouped_element| grouped_element.n1_loader_set(key, loader_collection) }
24
26
  loader_collection
25
- end
27
+ end.compact
26
28
  end
27
29
  end
30
+
31
+ private
32
+
33
+ def loader_class(element, key)
34
+ element.class.respond_to?(:n1_loader_defined?) &&
35
+ element.class.n1_loader_defined?(key) &&
36
+ element.class.n1_loader(key)
37
+ end
28
38
  end
29
39
  end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module N1Loader
4
- VERSION = "1.2.0"
4
+ VERSION = "1.4.1"
5
5
  end
data/lib/n1_loader.rb CHANGED
@@ -12,4 +12,6 @@ module N1Loader # :nodoc:
12
12
  class NotImplemented < Error; end
13
13
  class NotLoaded < Error; end
14
14
  class NotFilled < Error; end
15
+ class MissingArgument < Error; end
16
+ class InvalidArgument < Error; end
15
17
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: n1_loader
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.2.0
4
+ version: 1.4.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Evgeniy Demin
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2022-01-14 00:00:00.000000000 Z
11
+ date: 2022-02-24 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: activerecord