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.
- checksums.yaml +4 -4
- data/.github/dependabot.yml +18 -0
- data/.github/workflows/test.yml +24 -0
- data/.yardopts +6 -0
- data/CHANGELOG.md +126 -0
- data/CONCEPTS.ja.md +324 -0
- data/CONCEPTS.md +330 -0
- data/Migration-0.3.md +343 -0
- data/README.ja.md +168 -0
- data/README.md +112 -70
- data/Rakefile +14 -0
- data/computed_model.gemspec +10 -2
- data/lib/computed_model.rb +78 -153
- data/lib/computed_model/dep_graph.rb +245 -0
- data/lib/computed_model/model.rb +447 -0
- data/lib/computed_model/plan.rb +48 -0
- data/lib/computed_model/version.rb +1 -1
- metadata +102 -7
- data/.travis.yml +0 -6
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
|
+
```
|