computed_model 0.1.1 → 0.2.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
  SHA256:
3
- metadata.gz: 5fbb65b996358c1bad2d9f03cde8c2fa0e326341d5ef6964a273370d111bc90f
4
- data.tar.gz: 26869abf1e1ddf74b3c94e836d192379fdc1159328b9c260c14a8968b95ec9fd
3
+ metadata.gz: 827856e649d8af3b892979456fb1acbcfc9ab746934713422903e68342872371
4
+ data.tar.gz: 68b14dcca2f7e31880ac54b26e0f3c92bfeebf90ca9387be741f158d7fbeb5ee
5
5
  SHA512:
6
- metadata.gz: bcc03d655e79ef7941b1fc765f9dc56c698aa1afa832c9dac43b409b7a104332a77d9d87ff288146bcb348597af225cc666320eac51d543805cd8d0c39f92a43
7
- data.tar.gz: e0677afa15a0c1e0b33ce94e2543b9101a985ef2a9ce852c62e1bf69de54321f3fa825c38332a7e3f22810ac90f0beb8c4900ca8b4d509ed6607872028b5bb98
6
+ metadata.gz: 7a5ea72e1d06f357866dfde5a453591d43bad5a9ebd178cad88db09e351fc6ae3830d380514ee00ce582f45cb80339980e90c363298b910bfb2d2ccf3765008f
7
+ data.tar.gz: b2a953f74000b6b40f76ae43920e516c39c24856a2c29d89f63ee8253878c1c0ba847342b7915f134de6df3caa620f520f076951e10fed445726f793098153de
@@ -1,4 +1,7 @@
1
1
  ## Unreleased
2
+ - **BREAKING CHANGE** Make define_loader more concise interface like GraphQL's DataLoader.
3
+ - Introduce primary loader through `#define_primary_loader`.
4
+ - **BREAKING CHANGE** Change `#bulk_load_and_compute` signature to support primary loader.
2
5
 
3
6
  ## 0.1.1
4
7
 
data/README.md CHANGED
@@ -42,25 +42,54 @@ end
42
42
  They your model class will be able to define the two kinds of special attributes:
43
43
 
44
44
  - **Loaded attributes** for external data. You define batch loading strategies for loaded attributes.
45
+ - Among them, there is a special **primary model** for listing up the models from certain criteria.
45
46
  - **Computed attributes** for data derived from loaded attributes or other computed attributes.
46
47
  You define a usual `def` with special dependency annotations.
47
48
 
48
49
  ## Loaded attributes
49
50
 
50
- Use `ComputedModel::ClassMethods#define_loader` to define loaded attributes.
51
+ Use `ComputedModel::ClassMethods#define_primary_loader`
52
+ or `ComputedModel::ClassMethods#define_loader` to define loaded attributes.
51
53
 
52
54
  ```ruby
55
+ # Create a User instance
56
+ def initialize(raw_user)
57
+ @id = raw_user.id
58
+ @raw_user = raw_user
59
+ end
60
+
53
61
  # Example: pulling data from ActiveRecord
54
- define_loader :raw_user do |users, subdeps, **options|
55
- user_ids = users.map(&:id)
56
- raw_users = RawUser.where(id: user_ids).preload(subdeps).index_by(&:id)
57
- users.each do |user|
58
- # Even if it doesn't exist, you must explicitly assign nil to the field.
59
- user.raw_user = raw_users[user.id]
60
- end
62
+ define_primary_loader :raw_user do |subdeps, ids:, **options|
63
+ RawUser.where(id: ids).preload(subdeps).map { |raw_user| User.new(raw_user) }
64
+ end
65
+
66
+ # Example: pulling auxiliary data from ActiveRecord
67
+ define_loader :user_aux_data, key: -> { id } do |user_ids, subdeps, **options|
68
+ UserAuxData.where(user_id: user_ids).preload(subdeps).group_by(&:id)
61
69
  end
62
70
  ```
63
71
 
72
+ ### `define_primary_loader`
73
+
74
+ At most one primary loader can be defined on a model class.
75
+
76
+ The primary loader's job is to list up models from user-defined criteria, along with
77
+ requested data loaded to the primary attribute.
78
+
79
+ Search criteria can be passed as a keyword argument to `bulk_load_and_compute`
80
+ and it will be passed to the loader as-is.
81
+
82
+ Most typically you receive `ids`, an array of integers, and use it like
83
+ `.where(id: ids)`. Instead you may want to accept a non-primary-key criterion
84
+ such as `group_ids` and `.where(group_id: group_ids)`.
85
+
86
+ The loader must return an array of instances of the model being defined.
87
+ Each instance must have the primary attribute assigned at that time.
88
+ In the example above, the block for `define_primary_loader :raw_user`
89
+ must return an array of `User`s, each of which already have `@raw_user`.
90
+
91
+ ### `define_loader`
92
+
64
93
  The first argument to the block is an array of the model instances.
65
94
  The loader's job is to assign something to the corresponding field of each instance.
66
95
 
@@ -94,13 +123,7 @@ Typically you need to create a wrapper for the batch loader like:
94
123
 
95
124
  ```ruby
96
125
  def self.list(ids, with:)
97
- # Create placeholder objects.
98
- objs = ids.map { |id| User.new(id) }
99
- # Batch-load attributes into the objects.
100
- bulk_load_and_compute(objs, Array(with) + [:raw_user])
101
- # Reject objects without primary model.
102
- objs.reject! { |u| u.raw_user.nil? }
103
- objs
126
+ bulk_load_and_compute(Array(with), ids: ids)
104
127
  end
105
128
  ```
106
129
 
@@ -116,6 +139,8 @@ This library is distributed under MIT license.
116
139
 
117
140
  Copyright (c) 2020 Masaki Hara
118
141
 
142
+ Copyright (c) 2020 Masayuki Izumi
143
+
119
144
  Copyright (c) 2020 Wantedly, Inc.
120
145
 
121
146
  ## Development
@@ -7,8 +7,8 @@ require "computed_model/version"
7
7
  Gem::Specification.new do |spec|
8
8
  spec.name = "computed_model"
9
9
  spec.version = ComputedModel::VERSION
10
- spec.authors = ["Masaki Hara", "Wantedly, Inc."]
11
- spec.email = ["ackie.h.gmai@gmail.com", "dev@wantedly.com"]
10
+ spec.authors = ["Masaki Hara", "Masayuki Izumi", "Wantedly, Inc."]
11
+ spec.email = ["ackie.h.gmai@gmail.com", "m@izum.in", "dev@wantedly.com"]
12
12
 
13
13
  spec.summary = %q{Batch loader with dependency resolution and computed fields}
14
14
  spec.description = <<~DSC
@@ -27,6 +27,11 @@ module ComputedModel
27
27
  # A return value from {ComputedModel::ClassMethods#computing_plan}.
28
28
  Plan = Struct.new(:load_order, :subdeps_hash)
29
29
 
30
+ # An object for storing procs for loaded attributes.
31
+ Loader = Struct.new(:key_proc, :load_proc)
32
+
33
+ private_constant :Loader
34
+
30
35
  # A set of class methods for {ComputedModel}. Automatically included to the
31
36
  # singleton class when you include {ComputedModel}.
32
37
  module ClassMethods
@@ -136,7 +141,7 @@ module ComputedModel
136
141
  end
137
142
  end
138
143
 
139
- # Declares a loaded attribute. See {#dependency} too.
144
+ # Declares a loaded attribute. See {#dependency} and {#define_primary_loader} too.
140
145
  #
141
146
  # `define_loader :foo do ... end` generates a reader `foo` and a writer `foo=`.
142
147
  # The writer is only meant to be used in the loader.
@@ -145,9 +150,45 @@ module ComputedModel
145
150
  # or set `computed_model_error` otherwise.
146
151
  #
147
152
  # @param meth_name [Symbol] the name of the loaded attribute.
153
+ # @param key [Proc] The proc to collect keys.
154
+ # @return [void]
155
+ # @yield [keys, subdeps, **options]
156
+ # @yieldparam objects [Array] The ids of the loaded attributes.
157
+ # @yieldparam subdeps [Hash] sub-dependencies
158
+ # @yieldparam options [Hash] A verbatim copy of what is passed to {#bulk_load_and_compute}.
159
+ # @yieldreturn [Hash]
160
+ #
161
+ # @example define a loader for ActiveRecord-based models
162
+ # define_loader :user_aux_data, key: -> { id } do |user_ids, subdeps, **options|
163
+ # UserAuxData.where(user_id: user_ids).preload(subdeps).group_by(&:id)
164
+ # end
165
+ def define_loader(meth_name, key:, &block)
166
+ raise ArgumentError, "No block given" unless block
167
+
168
+ var_name = :"@#{meth_name}"
169
+
170
+ @__computed_model_loaders[meth_name] = Loader.new(key, block)
171
+
172
+ define_method(meth_name) do
173
+ raise NotLoaded, "the field #{meth_name} is not loaded" unless instance_variable_defined?(var_name)
174
+ instance_variable_get(var_name)
175
+ end
176
+ attr_writer meth_name
177
+ end
178
+
179
+ # Declares a primary attribute. See {#define_loader} and {#dependency} too.
180
+ #
181
+ # `define_primary_loader :foo do ... end` generates a reader `foo` and
182
+ # a writer `foo=`.
183
+ # The writer is only meant to be used in the loader.
184
+ #
185
+ # The responsibility of the primary loader is to list up all the relevant
186
+ # primary models, and initialize instances of the subclass of ComputedModel
187
+ # with `@foo` set to the primary model which is just being found.
188
+ #
189
+ # @param meth_name [Symbol] the name of the loaded attribute.
148
190
  # @return [void]
149
- # @yield [objects, **options]
150
- # @yieldparam objects [Array] The objects to preload the attribute into.
191
+ # @yield [**options]
151
192
  # @yieldparam options [Hash] A verbatim copy of what is passed to {#bulk_load_and_compute}.
152
193
  # @yieldreturn [void]
153
194
  #
@@ -160,12 +201,14 @@ module ComputedModel
160
201
  # user.raw_user = raw_users[user.id]
161
202
  # end
162
203
  # end
163
- def define_loader(meth_name, &block)
204
+ def define_primary_loader(meth_name, &block)
164
205
  raise ArgumentError, "No block given" unless block
206
+ raise ArgumentError, "Primary loader has already been defined" if @__computed_model_primary_attribute
165
207
 
166
208
  var_name = :"@#{meth_name}"
167
209
 
168
- @__computed_model_loaders[meth_name] = block
210
+ @__computed_model_primary_loader = block
211
+ @__computed_model_primary_attribute = meth_name
169
212
 
170
213
  define_method(meth_name) do
171
214
  raise NotLoaded, "the field #{meth_name} is not loaded" unless instance_variable_defined?(var_name)
@@ -176,26 +219,44 @@ module ComputedModel
176
219
 
177
220
  # The core routine for batch-loading.
178
221
  #
179
- # @param objs [Array] The objects to preload attributes into.
180
222
  # @param deps [Array<Symbol, Hash{Symbol=>Array}>] A set of dependencies.
181
223
  # @param options [Hash] An arbitrary hash to pass to loaders
182
224
  # defined by {#define_loader}.
183
- # @return [void]
184
- def bulk_load_and_compute(objs, deps, **options)
185
- objs = objs.dup
225
+ # @return [Array<Object>] The array of the requested models.
226
+ # Based on what the primary loader returns.
227
+ def bulk_load_and_compute(deps, **options)
228
+ unless @__computed_model_primary_attribute
229
+ raise ArgumentError, "No primary loader defined"
230
+ end
231
+
232
+ objs = orig_objs = nil
186
233
  plan = computing_plan(deps)
187
234
  plan.load_order.each do |dep_name|
188
- if @__computed_model_dependencies.key?(dep_name)
235
+ if @__computed_model_primary_attribute == dep_name
236
+ orig_objs = @__computed_model_primary_loader.call(plan.subdeps_hash[dep_name], **options)
237
+ objs = orig_objs.dup
238
+ elsif @__computed_model_dependencies.key?(dep_name)
239
+ raise "Bug: objs is nil" if objs.nil?
240
+
189
241
  objs.each do |obj|
190
242
  obj.send(:"compute_#{dep_name}")
191
243
  end
192
244
  elsif @__computed_model_loaders.key?(dep_name)
193
- @__computed_model_loaders[dep_name].call(objs, plan.subdeps_hash[dep_name], **options)
245
+ raise "Bug: objs is nil" if objs.nil?
246
+
247
+ l = @__computed_model_loaders[dep_name]
248
+ keys = objs.map { |o| o.instance_exec(&(l.key_proc)) }
249
+ subobj_by_key = l.load_proc.call(keys, plan.subdeps_hash[dep_name], **options)
250
+ objs.zip(keys) do |obj, key|
251
+ obj.send(:"#{dep_name}=", subobj_by_key[key])
252
+ end
194
253
  else
195
254
  raise "No dependency info for #{self}##{dep_name}"
196
255
  end
197
256
  objs.reject! { |obj| !obj.computed_model_error.nil? }
198
257
  end
258
+
259
+ orig_objs
199
260
  end
200
261
 
201
262
  # @param deps [Array]
@@ -206,6 +267,12 @@ module ComputedModel
206
267
  subdeps_hash = {}
207
268
  visiting = Set[]
208
269
  visited = Set[]
270
+ if @__computed_model_primary_attribute
271
+ load_order << @__computed_model_primary_attribute
272
+ visiting.add @__computed_model_primary_attribute
273
+ visited.add @__computed_model_primary_attribute
274
+ subdeps_hash[@__computed_model_primary_attribute] ||= []
275
+ end
209
276
  normalized.each do |dep_name, dep_subdeps|
210
277
  computing_plan_dfs(dep_name, dep_subdeps, load_order, subdeps_hash, visiting, visited)
211
278
  end
@@ -271,5 +338,7 @@ module ComputedModel
271
338
  klass.extend ClassMethods
272
339
  klass.instance_variable_set(:@__computed_model_dependencies, {})
273
340
  klass.instance_variable_set(:@__computed_model_loaders, {})
341
+ klass.instance_variable_set(:@__computed_model_primary_loader, nil)
342
+ klass.instance_variable_set(:@__computed_model_primary_attribute, nil)
274
343
  end
275
344
  end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module ComputedModel
4
- VERSION = "0.1.1"
4
+ VERSION = "0.2.0"
5
5
  end
metadata CHANGED
@@ -1,15 +1,16 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: computed_model
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.1
4
+ version: 0.2.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Masaki Hara
8
+ - Masayuki Izumi
8
9
  - Wantedly, Inc.
9
10
  autorequire:
10
11
  bindir: exe
11
12
  cert_chain: []
12
- date: 2020-03-16 00:00:00.000000000 Z
13
+ date: 2020-03-26 00:00:00.000000000 Z
13
14
  dependencies:
14
15
  - !ruby/object:Gem::Dependency
15
16
  name: bundler
@@ -62,6 +63,7 @@ description: |
62
63
  ActiveRecord and remote server (such as ActiveResource).
63
64
  email:
64
65
  - ackie.h.gmai@gmail.com
66
+ - m@izum.in
65
67
  - dev@wantedly.com
66
68
  executables: []
67
69
  extensions: []