n1_loader 1.3.0 → 1.4.2

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: cc47894a27d425f70a38dc43b9b40053d823f4815fe8e135a7cb1cf5afbde0f3
4
- data.tar.gz: 132d5851c19763db2f1910ebeeb6a368437aba6d3340c2c69ad2ffe348791011
3
+ metadata.gz: f72c7428f78489c25222fb2ab8d56fde519c680fdc010115b952637c2291ca93
4
+ data.tar.gz: 7149852481ffed8d1fd0fc25e28fc045eda0a0f2936e748461dd7f43d4f709f1
5
5
  SHA512:
6
- metadata.gz: 945258069b57bc7cd0fe26416411322886d734db0ed4eead1c88a3acced70bdb14ffc92eb9403e5d346c0853d64dc09e5afb8039a9026c9b4c09a1cc6f5fc9e8
7
- data.tar.gz: 036e5a8a4b2d63bed2aee558164fdd042ac8c0c68b0ecdb1d5ec9f64231ca9f6841dc56cd6cdc63b87fdb9bf964e4f42089f89d9783cf2513cf81243a473150a
6
+ metadata.gz: 0d0802914b84d4c25a97c8c328ff2ae4453cf8fcd4eee87de1955b06b810c874d9982498b5fc80ecf1fa77238971e51ef5f162f809914876a1811084b366ddfe
7
+ data.tar.gz: fa001ab34b7165ee35ba31ef449c837418d538e52977ee984b38e35b767df06c05ef0cb01949c9290c17ae40bcd3e3644f00374cf7f66d7e6811f094515c37da
data/CHANGELOG.md CHANGED
@@ -1,3 +1,19 @@
1
+ ## [1.4.2] - 2022-03-01
2
+
3
+ - Add n1_clear_cache method which is useful for cases like reload in ActiveRecord
4
+
5
+ ## [1.4.1] - 2022-02-24
6
+
7
+ - Fix preloading of invalid objects
8
+
9
+ ## [1.4.0] - 2022-02-22
10
+
11
+ - add support of optional arguments
12
+
13
+ BREAKING CHANGES:
14
+ - rework arguments to use single definition through `argument <name>` only
15
+ - use keyword arguments
16
+
1
17
  ## [1.3.0] - 2022-02-22
2
18
 
3
19
  - add support of named arguments with `argument <name>`
data/README.md CHANGED
@@ -129,6 +129,9 @@ end
129
129
  user = User.new
130
130
  user.orders_count # => loader is executed first time and value was cached
131
131
  user.orders_count(reload: true) # => loader is executed again and a new value was cached
132
+ # or
133
+ user.n1_clear_cache
134
+ user.orders_count
132
135
 
133
136
  users = [User.new, User.new]
134
137
  N1Loader::Preloader.new(users).preload(:orders_count) # => loader was initialized but not yet executed
@@ -188,21 +191,25 @@ users.map(&:orders_count) # perform will be used once without N+1
188
191
  class User
189
192
  include N1Loader::Loadable
190
193
 
191
- n1_optimized :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]) }
194
+ n1_optimized :orders_count do
195
+ argument :type
196
+
197
+ def perform(users)
198
+ orders_per_user = Order.where(type: type, user: users).group(:user_id).count
199
+
200
+ users.each { |user| fulfill(user, orders_per_user[user.id]) }
201
+ end
195
202
  end
196
203
  end
197
204
 
198
205
  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
206
+ user.orders_count(type: :gifts) # The loader will be performed first time for this argument
207
+ user.orders_count(type: :sales) # The loader will be performed first time for this argument
208
+ user.orders_count(type: :gifts) # The cached value will be used
202
209
 
203
210
  users = [User.new, User.new]
204
211
  N1Loader::Preloader.new(users).preload(:orders_count)
205
- users.map { |user| user.orders_count(:gifts) } # No N+1 here
212
+ users.map { |user| user.orders_count(type: :gifts) } # No N+1 here
206
213
  ```
207
214
 
208
215
  _Note_: By default, we use `arguments.map(&:object_id)` to identify arguments but in some cases,
@@ -217,7 +224,7 @@ class User
217
224
 
218
225
  cache_key { sale.id }
219
226
 
220
- def perform(users, sale)
227
+ def perform(users)
221
228
  orders_per_user = Order.where(sale: sale, user: users).group(:user_id).count
222
229
 
223
230
  users.each { |user| fulfill(user, orders_per_user[user.id]) }
@@ -226,8 +233,8 @@ class User
226
233
  end
227
234
 
228
235
  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
236
+ user.orders_count(sale: Sale.first) # perform will be executed and value will be cached
237
+ user.orders_count(sale: Sale.first) # the cached value will be returned
231
238
  ```
232
239
 
233
240
 
@@ -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
@@ -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?
@@ -32,6 +32,12 @@ module N1Loader
32
32
  send("#{name}_loader=", loader_collection)
33
33
  end
34
34
 
35
+ def n1_clear_cache
36
+ self.class.n1_loaders.each do |name|
37
+ n1_loader_set(name, nil)
38
+ end
39
+ end
40
+
35
41
  def self.included(base)
36
42
  base.extend(ClassMethods)
37
43
  end
@@ -45,9 +51,13 @@ module N1Loader
45
51
  respond_to?("#{name}_loader")
46
52
  end
47
53
 
54
+ def n1_loaders
55
+ @n1_loaders ||= superclass.respond_to?(:n1_loaders) ? superclass.n1_loaders.dup : []
56
+ end
57
+
48
58
  def n1_optimized(name, loader = nil, &block) # rubocop:disable Metrics/MethodLength, Metrics/AbcSize
49
59
  loader ||= Class.new(N1Loader::Loader) do
50
- if block&.arity&.positive?
60
+ if block.arity == 1
51
61
  define_method(:perform, &block)
52
62
  else
53
63
  class_eval(&block)
@@ -56,6 +66,8 @@ module N1Loader
56
66
  loader_name = "#{name}_loader"
57
67
  loader_variable_name = "@#{loader_name}"
58
68
 
69
+ n1_loaders << name
70
+
59
71
  define_singleton_method(loader_name) do
60
72
  loader
61
73
  end
@@ -73,10 +85,10 @@ module N1Loader
73
85
  instance_variable_get(loader_variable_name) || send("#{loader_name}_reload")
74
86
  end
75
87
 
76
- define_method(name) do |*args, reload: false|
88
+ define_method(name) do |reload: false, **args|
77
89
  send("#{loader_name}_reload") if reload
78
90
 
79
- send(loader_name).with(*args).for(self)
91
+ send(loader_name).with(**args).for(self)
80
92
  end
81
93
 
82
94
  [name, loader_name, loader_variable_name]
@@ -13,11 +13,16 @@ module N1Loader
13
13
  #
14
14
  # First defined argument will have the value of first passed argument,
15
15
  # meaning the order is important.
16
- def argument(name)
16
+ #
17
+ # @param name [Symbol]
18
+ # @param opts [Hash]
19
+ # @option opts [Boolean] optional false by default
20
+ def argument(name, **opts)
17
21
  @arguments ||= []
18
- index = @arguments.size
19
- define_method(name) { args[index] }
20
- @arguments << name
22
+
23
+ define_method(name) { args[name] }
24
+
25
+ @arguments << opts.merge(name: name)
21
26
  end
22
27
 
23
28
  # Defines a custom cache key that is calculated for passed arguments.
@@ -29,7 +34,7 @@ module N1Loader
29
34
  end
30
35
  end
31
36
 
32
- def initialize(elements, *args)
37
+ def initialize(elements, **args)
33
38
  @elements = elements
34
39
  @args = args
35
40
  end
@@ -44,18 +49,45 @@ module N1Loader
44
49
  end
45
50
 
46
51
  def cache_key
47
- args.map(&:object_id)
52
+ check_arguments!
53
+ args.values.map(&:object_id)
48
54
  end
49
55
 
50
56
  private
51
57
 
52
58
  attr_reader :elements, :args
53
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
+
54
78
  def check_arguments!
55
- return unless (required = self.class.arguments)
56
- return if required.size == args.size
79
+ check_missing_arguments!
80
+ check_invalid_arguments!
81
+ end
57
82
 
58
- raise MissingArgument, "Loader defined #{required.size} arguments but #{args.size} were given"
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
59
91
  end
60
92
 
61
93
  def perform(_elements)
@@ -66,7 +98,7 @@ module N1Loader
66
98
  @loaded[element] = value
67
99
  end
68
100
 
69
- def loaded # rubocop:disable Metrics/AbcSize
101
+ def loaded
70
102
  return @loaded if @loaded
71
103
 
72
104
  check_arguments!
@@ -74,9 +106,9 @@ module N1Loader
74
106
  @loaded = {}.compare_by_identity
75
107
 
76
108
  if elements.size == 1 && respond_to?(:single)
77
- fulfill(elements.first, single(elements.first, *args))
109
+ fulfill(elements.first, single(elements.first))
78
110
  elsif elements.any?
79
- perform(elements, *args)
111
+ perform(elements)
80
112
  end
81
113
 
82
114
  @loaded
@@ -10,8 +10,8 @@ module N1Loader
10
10
  @elements = elements
11
11
  end
12
12
 
13
- def with(*args)
14
- loader = loader_class.new(elements, *args)
13
+ def with(**args)
14
+ loader = loader_class.new(elements, **args)
15
15
 
16
16
  loaders[loader.cache_key] ||= loader
17
17
  end
@@ -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.3.0"
4
+ VERSION = "1.4.2"
5
5
  end
data/lib/n1_loader.rb CHANGED
@@ -13,4 +13,5 @@ module N1Loader # :nodoc:
13
13
  class NotLoaded < Error; end
14
14
  class NotFilled < Error; end
15
15
  class MissingArgument < Error; end
16
+ class InvalidArgument < Error; end
16
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.3.0
4
+ version: 1.4.2
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-02-22 00:00:00.000000000 Z
11
+ date: 2022-03-01 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: activerecord