n1_loader 1.2.0 → 1.4.1

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
  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