computed_model 0.1.1 → 0.2.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 +4 -4
- data/CHANGELOG.md +3 -0
- data/README.md +40 -15
- data/computed_model.gemspec +2 -2
- data/lib/computed_model.rb +80 -11
- data/lib/computed_model/version.rb +1 -1
- metadata +4 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 827856e649d8af3b892979456fb1acbcfc9ab746934713422903e68342872371
|
4
|
+
data.tar.gz: 68b14dcca2f7e31880ac54b26e0f3c92bfeebf90ca9387be741f158d7fbeb5ee
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 7a5ea72e1d06f357866dfde5a453591d43bad5a9ebd178cad88db09e351fc6ae3830d380514ee00ce582f45cb80339980e90c363298b910bfb2d2ccf3765008f
|
7
|
+
data.tar.gz: b2a953f74000b6b40f76ae43920e516c39c24856a2c29d89f63ee8253878c1c0ba847342b7915f134de6df3caa620f520f076951e10fed445726f793098153de
|
data/CHANGELOG.md
CHANGED
@@ -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#
|
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
|
-
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
-
|
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
|
-
|
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
|
data/computed_model.gemspec
CHANGED
@@ -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
|
data/lib/computed_model.rb
CHANGED
@@ -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 [
|
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
|
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
|
-
@
|
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 [
|
184
|
-
|
185
|
-
|
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 @
|
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
|
-
|
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
|
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.
|
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-
|
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: []
|