computed_model 0.1.0 → 0.3.0

Sign up to get free protection for your applications and to get access to all the features.
data/CONCEPTS.md ADDED
@@ -0,0 +1,330 @@
1
+ # Basic concepts and features
2
+
3
+ [日本語版](CONCEPTS.ja.md)
4
+
5
+ ## Wrapping classes
6
+
7
+ We don't (yet) support directly including `ComputedModel::Model` into ActiveRecord classes or similar ones.
8
+ In that case, we recommend creating a wrapper class and reference the original class via the primary loader
9
+ (described later).
10
+
11
+ ## Fields
12
+
13
+ **Field** are certain attributes managed by ComputedModel. It's a unit of dependency resolution and
14
+ there are three kinds of fields:
15
+
16
+ - computed fields
17
+ - loaded fields
18
+ - primary fields
19
+
20
+ ### computed fields
21
+
22
+ A computed field is a field in which it's value is derived from other fields.
23
+ It's calculated independently per record.
24
+
25
+ ```ruby
26
+ class User
27
+ dependency :preference, :profile
28
+ computed def display_name
29
+ "#{preference.title} #{profile.name}"
30
+ end
31
+ end
32
+ ```
33
+
34
+ ### loaded fields
35
+
36
+ A loaded field is a field in which we obtain values in batches.
37
+
38
+ ```ruby
39
+ class User
40
+ define_loader :preference, key: -> { id } do |ids, _subfields, **|
41
+ Preference.where(user_id: ids).index_by(&:user_id)
42
+ end
43
+ end
44
+ ```
45
+
46
+ ### primary fields
47
+
48
+ A primary field is responsible in searching/enumerating the whole records,
49
+ in addition to the usual responsibility of loaded fields.
50
+
51
+ Consider a hypothetical `User` class for example. In this case you might want to inquiry somewhere (a data source)
52
+ whether a user with a certain id exists.
53
+
54
+ If it were a hypothetical ActiveRecord class `RawUser`, the primary field would be defined as follows:
55
+
56
+ ```ruby
57
+ class User
58
+ def initialize(raw_user)
59
+ @raw_user = raw_user
60
+ end
61
+
62
+ define_primary_loader :raw_user do |_subfields, ids:, **|
63
+ # You need to set @raw_user in User#initialize.
64
+ RawUser.where(id: ids).map { |u| User.new(u) }
65
+ end
66
+ end
67
+ ```
68
+
69
+ ## When computation is done
70
+
71
+ All necessary fields are computed eagerly when ComputedModel's `bulk_load_and_compute` is called.
72
+
73
+ It doesn't (yet) provide lazy loading functionality.
74
+
75
+ ## Dependency
76
+
77
+ You can declare dependencies on a field.
78
+ As an exception, the primary field cannot have a dependency (but it can have dependents, of course).
79
+
80
+ ```ruby
81
+ class User
82
+ dependency :preference, :profile
83
+ computed def display_name
84
+ "#{preference.title} #{profile.name}"
85
+ end
86
+ end
87
+ ```
88
+
89
+ In `computed def` or `define_loader`, among all fields, you can only read values of explicitly declared dependencies.
90
+ You cannot read other fields even if it happens to be present (such as indirect dependencies).
91
+
92
+ ## `bulk_load_and_compute`
93
+
94
+ `bulk_load_and_compute` is the very method you need to load ComputedModel records.
95
+ We recommend wrapping the method in each model class.
96
+ This is mostly because there is a lot of freedom in the format of the batch-loading parameters (described later)
97
+ and it will likely cause mistakes if used directly.
98
+
99
+ ```ruby
100
+ class User
101
+ # You can specify an array of fields like [:display_name, :title] in the `with` parameter.
102
+ def self.list(ids, with:)
103
+ bulk_load_and_compute(with, ids: ids)
104
+ end
105
+
106
+ def self.get(id, with:)
107
+ list([id], with: with).first
108
+ end
109
+
110
+ def self.get!(id, with:)
111
+ get(id, with: with) || (raise User::NotFound)
112
+ end
113
+ end
114
+ ```
115
+
116
+ There is no such method as load a single record. You can easily implement it by wrapping `bulk_load_and_compute`.
117
+ If you want a certain optimization for single-record cases, you may want to write conditionals in `define_loader` or `define_primary_loader`.
118
+
119
+ ## Subfield selectors
120
+
121
+ Subfield selectors (or subdependencies) are additional information attached to a dependency.
122
+
123
+ Implementation-wise they're just arbitrary messages sent from a field to its dependency.
124
+ Nonetheless we expect them to be used to request "subfields" as the name suggests.
125
+
126
+ ```ruby
127
+ class User
128
+ define_loader :profile, key: -> { id } do |ids, subfields, **|
129
+ Profile.preload(subfields).where(user_id: ids).index_by(&:user_id)
130
+ end
131
+
132
+ # [:contact_phones] will be passed to the loader of `profile`.
133
+ dependency profile: :contact_phones
134
+ computed def contact_phones
135
+ profile.contact_phones
136
+ end
137
+ end
138
+ ```
139
+
140
+ You can also receive subfield selectors in a computed field. See the "Dynamic dependencies" section later.
141
+
142
+ ## Batch-loading parameters
143
+
144
+ The keyword parameters given to `bulk_load_and_compute` is passed through to the blocks of `define_primary_loader` or `define_loader`.
145
+ You can use it for various purposes, some of which we present below:
146
+
147
+ ### Searching records by conditions other than id
148
+
149
+ You can pass multiple different search conditions through the keyword parameters.
150
+
151
+ ```ruby
152
+ class User
153
+ def self.list(ids, with:)
154
+ bulk_load_and_compute(with, ids: ids, emails: nil)
155
+ end
156
+
157
+ def self.list_by_emails(emails, with:)
158
+ bulk_load_and_compute(with, ids: nil, emails: emails)
159
+ end
160
+
161
+ define_primary_loader :raw_user do |_subfields, ids:, emails:, **|
162
+ s = User.all
163
+ s = s.where(id: ids) if ids
164
+ s = s.where(email: emails) if emails
165
+ s.map { |u| User.new(u) }
166
+ end
167
+ end
168
+ ```
169
+
170
+ ### Current user
171
+
172
+ Consider a situation where we want to present different information depending on whom the user is logging in as.
173
+ You can implement it by including the current user in the keyword parameters.
174
+
175
+ ```ruby
176
+ class User
177
+ def initialize(raw_user, current_user_id)
178
+ @raw_user = raw_user
179
+ @current_user_id = current_user_id
180
+ end
181
+
182
+ define_primary_loader :raw_user do |_subfields, current_user_id:, ids:, **|
183
+ # ...
184
+ end
185
+
186
+ define_loader :profile, key: -> { id } do |ids, _subfields, current_user_id:, **|
187
+ # ...
188
+ end
189
+ end
190
+ ```
191
+
192
+ ## Dynamic dependencies
193
+
194
+ You can configure dynamic dependencies by specifying Procs as subfield selectors.
195
+
196
+ ### Conditional dependencies
197
+
198
+ Dependencies which are conditionally enabled based on incoming subfield selectors:
199
+
200
+ ```ruby
201
+
202
+ class User
203
+ dependency(
204
+ :blog_articles,
205
+ # Load image_permissions only when it receives `image` subfield selector.
206
+ image_permissions: -> (subfields) { subfields.normalized[:image].any? }
207
+ )
208
+ computed def filtered_blog_articles
209
+ if current_subfields.normalized[:image].any?
210
+ # ...
211
+ end
212
+ # ...
213
+ end
214
+ end
215
+ ```
216
+
217
+ ### Subfield selectors passthrough
218
+
219
+ Passing through incoming subfield selectors to another field:
220
+
221
+ ```ruby
222
+
223
+ class User
224
+ dependency(
225
+ blog_articles: -> (subfields) { subfields }
226
+ )
227
+ computed def filtered_blog_articles
228
+ if current_subfields.normalized[:image].any?
229
+ # ...
230
+ end
231
+ # ...
232
+ end
233
+ end
234
+ ```
235
+
236
+ ### Subfield selectors mapping
237
+
238
+ Processing incoming subfield selectors and pass the result as outgoing subfield selectors to another field:
239
+
240
+ ```ruby
241
+ class User
242
+ dependency(
243
+ # Always load blog_articles, but
244
+ # if the incoming subfield selectors contain `blog_articles`, pass them down to the dependency.
245
+ blog_articles: [true, -> (subfields) { subfields.normalized[:blog_articles] }],
246
+ # Always load wiki_articles, but
247
+ # if the incoming subfield selectors contain `wiki_articles`, pass them down to the dependency.
248
+ wiki_articles: [true, -> (subfields) { subfields.normalized[:wiki_articles] }]
249
+ )
250
+ computed def articles
251
+ (blog_articles + wiki_articles).sort_by { |article| article.created_at }.reverse
252
+ end
253
+ end
254
+ ```
255
+
256
+ ### Detailed dependency format
257
+
258
+ You can pass 0 or more arguments to `dependency`.
259
+ They're pushed into an internal array and will be consumed by the next `computed def` or `define_loader`.
260
+ So they have the same meaning:
261
+
262
+ ```ruby
263
+ dependency :profile
264
+ dependency :preference
265
+ computed def display_name; ...; end
266
+ ```
267
+
268
+ ```ruby
269
+ dependency :profile, :preference
270
+ computed def display_name; ...; end
271
+ ```
272
+
273
+ The resulting array will be normalized as a hash by `ComputedModel.normalize_dependencies`. The rules are:
274
+
275
+ - If it's a Symbol, convert it to a singleton hash containing the key. (`:foo` → `{ foo: [true] }`)
276
+ - If it's a Hash, convert the values as follows:
277
+ - If the value is an empty array, replace it with `[true]`. (`{ foo: [] }` → `{ foo: [true] }`)
278
+ - If the value is not an array, convert it to the singleton array. (`{ foo: :bar }` → `{ foo: [:bar] }`)
279
+ - If the value is a non-empty array, keep it as-is.
280
+ - If it's an Array, convert each element following the rules above and merge the keys of the hashes. Hash values are always arrays and will be simply concatenated.
281
+ - `[:foo, :bar]` → `{ foo: [true], bar: [true] }`
282
+ - `[{ foo: :foo }, { foo: :bar }]` → `{ foo: [:foo, :bar] }`
283
+
284
+ We interpret the resulting hash as a dictionary from a field name (dependency names) and it's subfield selectors.
285
+
286
+ Each subfield selector is interpreted as below:
287
+
288
+ - If it contains `#call`able objects (such as Proc), call them with `subfields` (incoming subfield selectors) as their argument.
289
+ - Expand the result if it's an Array. (`{ foo: [-> { [:bar, :baz] }] }` → `{ foo: [:bar, :baz] }`)
290
+ - Otherwise push the result. (`{ foo: [-> { :bar }] }` → `{ foo: [:bar] }`)
291
+ - After Proc substitution, check if it contains any truthy value (value other than `nil` or `false`).
292
+ - If no truthy value is found, we don't use the dependency as the condition is not met.
293
+ - Otherwise (if truthy value is found), use the dependency. Subfield selectors (after substitution) are sent to the dependency as-is.
294
+
295
+ For that reason, in most cases subfield selectors contain `true`. As a special case we remove them in the following cases:
296
+
297
+ - We'll remove `nil`, `false`, `true` from the subfield selectors before passed to a `define_loader` or `define_primary_loader` block.
298
+ - In certain cases you can use `subfields.normalize` to get a hash from the subfield selectors array. This is basically `ComputedModel.normalize_dependencies` but `nil`, `false`, `true` will be removed as part of preprocessing.
299
+
300
+ ## Inheritance
301
+
302
+ You can also define partial ComputedModel class/module. You can then inherit/include it in a different class and complete the definition.
303
+
304
+ ```ruby
305
+ module UserLikeConcern
306
+ extends ActiveSupport::Concern
307
+ include ComputedModel::Model
308
+
309
+ dependency :preference, :profile
310
+ computed def display_name
311
+ "#{preference.title} #{profile.name}"
312
+ end
313
+ end
314
+
315
+ class User
316
+ include UserLikeConcern
317
+
318
+ define_loader :preference, key: -> { id } do ... end
319
+ define_loader :profile, key: -> { id } do ... end
320
+ end
321
+
322
+ class Admin
323
+ include UserLikeConcern
324
+
325
+ define_loader :preference, key: -> { id } do ... end
326
+ define_loader :profile, key: -> { id } do ... end
327
+ end
328
+ ```
329
+
330
+ Note that in certain cases overriding might work incorrectly (because `computed def` internally renames the given method)
data/Migration-0.3.md ADDED
@@ -0,0 +1,343 @@
1
+ # v0.3.0 migration guide
2
+
3
+ computed_model 0.3.0 comes with a number of breaking changes.
4
+ This guide will help you upgrade the library,
5
+ but please test your program before deploying to production.
6
+
7
+ ## Major breaking: `ComputedModel` is now `ComputedModel::Model`
8
+
9
+ https://github.com/wantedly/computed_model/pull/17
10
+
11
+ Before:
12
+
13
+ ```ruby
14
+ class User
15
+ include ComputedModel
16
+ end
17
+ ```
18
+
19
+ After:
20
+
21
+ ```ruby
22
+ class User
23
+ include ComputedModel::Model
24
+ end
25
+ ```
26
+
27
+
28
+ ## Major breaking: Indirect dependencies are now rejected
29
+
30
+ computed_model 0.3 checks if the requested field is a direct dependency.
31
+ If not, it raises `ComputedModel::ForbiddenDependency`.
32
+
33
+ https://github.com/wantedly/computed_model/pull/23
34
+
35
+ ### Case 1
36
+
37
+ This will mostly affect the following "indirect dependency" case:
38
+
39
+ Before:
40
+
41
+ ```ruby
42
+ class User
43
+ dependency :bar
44
+ computed def foo
45
+ baz # Accepted in computed_model 0.2
46
+ # ...
47
+ end
48
+
49
+ dependency :baz
50
+ computed def bar
51
+ # ...
52
+ end
53
+ end
54
+ ```
55
+
56
+ After:
57
+
58
+ ```ruby
59
+ class User
60
+ dependency :bar, :baz # Specify dependencies correctly
61
+ computed def foo
62
+ baz
63
+ # ...
64
+ end
65
+
66
+ dependency :baz
67
+ computed def bar
68
+ # ...
69
+ end
70
+ end
71
+ ```
72
+
73
+ ### Case 2
74
+
75
+ Before:
76
+
77
+ ```ruby
78
+ class User
79
+ dependency :bar
80
+ computed def foo
81
+ # ...
82
+ end
83
+ end
84
+
85
+ users = User.bulk_load_and_compute([:foo], ...)
86
+ users[0].bar # Accepted in computed_model 0.2
87
+ ```
88
+
89
+ After:
90
+
91
+ ```ruby
92
+ class User
93
+ dependency :bar
94
+ computed def foo
95
+ # ...
96
+ end
97
+ end
98
+
99
+ users = User.bulk_load_and_compute([:foo, :bar], ...) # Specify dependencies correctly
100
+ users[0].bar
101
+ ```
102
+
103
+ ### Other cases
104
+
105
+ Previously, it sometimes happens to work depending on the order in which fields are loaded.
106
+
107
+ ```ruby
108
+ class User
109
+ # No dependency between foo and bar
110
+
111
+ dependency :raw_user
112
+ computed def foo
113
+ # ...
114
+ end
115
+
116
+ dependency :raw_user
117
+ computed def bar
118
+ foo
119
+ # ...
120
+ end
121
+ end
122
+ ```
123
+
124
+ It was already fragile in computed_model 0.2.
125
+ However, in computed_model 0.3,
126
+ it always leads to `ComputedModel::ForbiddenDependency`.
127
+
128
+
129
+ ## Major breaking: `subdeps` are now called `subfields`
130
+
131
+ https://github.com/wantedly/computed_model/pull/31
132
+
133
+ Before:
134
+
135
+ ```ruby
136
+ class User
137
+ delegate_dependency :name, to: :raw_user, include_subdeps: true
138
+ end
139
+ ```
140
+
141
+ After:
142
+
143
+ ```ruby
144
+ class User
145
+ delegate_dependency :name, to: :raw_user, include_subfields: true
146
+ end
147
+ ```
148
+
149
+ We also recommend renaming block parameters named `subdeps` as `subfields`,
150
+ although not strictly necessary.
151
+
152
+
153
+ ## Minor breaking: `computed_model_error` has been removed
154
+
155
+ It was useful in computed_model 0.1 but no longer needed in computed_model 0.2.
156
+
157
+ https://github.com/wantedly/computed_model/pull/18
158
+
159
+ ```ruby
160
+ # No longer possible
161
+ self.computed_model_error = User::NotFound.new
162
+ ```
163
+
164
+ ## Minor breaking: Behavior of `dependency` not directly followed by `computed def` has been changed.
165
+
166
+ It doesn't effect you if all `dependency` declarations are followed by `computed def`.
167
+
168
+ ```ruby
169
+ # Keeps working
170
+ dependency :foo
171
+ computed def bar
172
+ end
173
+ ```
174
+
175
+ Otherwise `dependency` might be consumed by the next `define_loader` or `define_primary_loader`.
176
+
177
+ https://github.com/wantedly/computed_model/pull/20
178
+
179
+ Before:
180
+
181
+ ```ruby
182
+ dependency :foo # dependency of bar in computed_model 0.2
183
+
184
+ define_loader :quux, key: -> { id } do
185
+ # ...
186
+ end
187
+
188
+ computed def bar
189
+ # ...
190
+ end
191
+ ```
192
+
193
+ After:
194
+
195
+ ```ruby
196
+ # This would be interpreted as a dependency of quux
197
+ # dependency :foo
198
+
199
+ define_loader :quux, key: -> { id } do
200
+ # ...
201
+ end
202
+
203
+ dependency :foo # Place it here
204
+ computed def bar
205
+ # ...
206
+ end
207
+ ```
208
+
209
+ Additionally, `dependency` before `define_primary_loader` will be an error.
210
+
211
+ ## Minor breaking: Cyclic dependency is an error even if it is unused
212
+
213
+ https://github.com/wantedly/computed_model/pull/24
214
+
215
+ Before:
216
+
217
+ ```ruby
218
+ class User
219
+
220
+ # Cyclic dependency is allowed as long as it's unused
221
+
222
+ dependency :bar
223
+ computed def foo
224
+ end
225
+
226
+ dependency :foo
227
+ computed def bar
228
+ end
229
+ end
230
+
231
+ users = User.bulk_load_and_compute([], ...) # Neither :foo nor :bar is used
232
+ ```
233
+
234
+ After:
235
+
236
+ ```ruby
237
+ class User
238
+ # Remove cyclic dependency altogether
239
+ end
240
+
241
+ users = User.bulk_load_and_compute([], ...) # Neither :foo nor :bar is used
242
+ ```
243
+
244
+ ## Minor breaking: `nil`, `true`, `false` and `Proc` in subdeps are treated differently
245
+
246
+ They now have special meaning, so you should avoid using them as a normal subdependency.
247
+
248
+ https://github.com/wantedly/computed_model/pull/25
249
+
250
+ ### `nil` and `false`
251
+
252
+ They are constantly false condition in conditional dependency. Unless otherwise enabled, the dependency won't be used.
253
+
254
+ Before:
255
+
256
+ ```ruby
257
+ dependency foo: [nil, false] # foo will be used
258
+ computed def bar
259
+ end
260
+ ```
261
+
262
+ After:
263
+
264
+ ```ruby
265
+ # dependency foo: [nil, false] # foo won't be used
266
+ dependency foo: [:nil, :false] # Use other values instead
267
+ computed def bar
268
+ end
269
+ ```
270
+
271
+ ### `true`
272
+
273
+ They are constantly true condition in conditional dependency. It's filtered out before passed to a loader or a primary loader.
274
+
275
+ Before:
276
+
277
+ ```ruby
278
+ dependency foo: [true] # true is given to "define_loader :foo do ... end"
279
+ computed def bar
280
+ end
281
+ ```
282
+
283
+ After:
284
+
285
+ ```ruby
286
+ # dependency foo: [true] # true is ignored
287
+ dependency foo: [:true] # Use other values instead
288
+ computed def bar
289
+ end
290
+ ```
291
+
292
+ ### Proc
293
+
294
+ Callable objects (objects implementing `#call`), including instances of `Proc`, is interpreted as a dynamic dependency.
295
+
296
+ Before:
297
+
298
+ ```ruby
299
+ dependency foo: -> { raise "foo" } # Passed to foo as-is
300
+ computed def bar
301
+ end
302
+ ```
303
+
304
+ After:
305
+
306
+ ```ruby
307
+ # dependency foo: -> { raise "foo" } # Dynamic dependency. Called during dependency resolution.
308
+ dependency foo: { callback: -> { raise "foo" } } # Wrap it in something
309
+ computed def bar
310
+ end
311
+ ```
312
+
313
+
314
+ ## Behavioral change: The order in which fields are loaded is changed
315
+
316
+ https://github.com/wantedly/computed_model/pull/24
317
+
318
+ Independent fields may be loaded in an arbitrary order. But implementation-wise, this is to some degree predictable.
319
+
320
+ computed_model 0.3 uses different dependency resolution algorithm and may produce different orders.
321
+ As a result, if your model is accidentally order-dependent, it may break with computed_model 0.3.
322
+
323
+
324
+ ## Behavioral change: `ComputedModel::Model` now uses `ActiveSupport::Concern`
325
+
326
+ It won't affect you if you simply did `include ComputedModel::Model` (previously `include ComputedModel`) and nothing more.
327
+ Be cautious if you have a more complex inheritance/inclusion graph than that.
328
+
329
+
330
+ ## Recommendation: `#verify_dependencies`
331
+
332
+ `#verify_dependencies` allows you to check the graph in the initialization process, rather than just before loading records.
333
+ We recommend doing this at the end of the class definition.
334
+
335
+ ```ruby
336
+ class User
337
+ define_loader ...
338
+
339
+ computed def ... end
340
+
341
+ verify_dependencies # HERE
342
+ end
343
+ ```