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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 0d4c9e19dfb66454b769cf0cb92e95fb01de3aeace7276953182fcebf82fdcd2
4
- data.tar.gz: 836d822953b14420ee7ed6f4dd3d17fba896b41e2caa638eb4fe9e1d59323df5
3
+ metadata.gz: abe05016ded2805bb131703f0ac56757d84b3d341bb79d041184703dd2d61d73
4
+ data.tar.gz: 81f95364fee17dd9aff3dc37fb1036c679ad59f5006f734cd4b0bcbe79b6a4f7
5
5
  SHA512:
6
- metadata.gz: e1d34a7a00c560ea97d96927e643d40e02164f94181601e39bcde334cd86f9c5f618c7d6c62556adb5331f2f3f8e61bc015d295f82ee03237d75d4dc8c1d3240
7
- data.tar.gz: 556daff44be9ae2408c9e5d11590fecd440d802067071b53563ee6646ca01296bafa01944df8e691bc770f9c7b613a66631cf60cffa5472abb9673b1ef82b95c
6
+ metadata.gz: e5cc071ca1645543721eb8ba0c50811930ac00f3ff542b9119b9919c328e3c22a8d127310cd3b8fca04d1ad32122b01a708a7b8210d5afe12d31f7580dbeb023
7
+ data.tar.gz: 431edf1ed4d6993b75fd67a8367f031e00eb210a57e9c40a87a28b393ecbc0fce54d3a364e80353a440687eabff65bbd7e213830d5b1bdbc7b4e669540f9eaef
@@ -0,0 +1,18 @@
1
+ version: 2
2
+ updates:
3
+ - package-ecosystem: bundler
4
+ directory: "/"
5
+ schedule:
6
+ interval: daily
7
+ time: "20:00"
8
+ open-pull-requests-limit: 10
9
+ reviewers:
10
+ - qnighy
11
+ - package-ecosystem: github-actions
12
+ directory: "/"
13
+ schedule:
14
+ interval: daily
15
+ time: "20:00"
16
+ open-pull-requests-limit: 10
17
+ reviewers:
18
+ - qnighy
@@ -0,0 +1,24 @@
1
+ name: test
2
+
3
+ on: [push, pull_request]
4
+
5
+ jobs:
6
+ test:
7
+ runs-on: ubuntu-latest
8
+
9
+ strategy:
10
+ matrix:
11
+ ruby: ["2.5", "2.6", "2.7", "3.0"]
12
+
13
+ steps:
14
+ - uses: actions/checkout@v2.3.4
15
+ - uses: ruby/setup-ruby@v1
16
+ with:
17
+ ruby-version: ${{ matrix.ruby }}
18
+ - run: bundle install
19
+ - run: bundle exec rake
20
+ - name: Upload coverage
21
+ uses: codecov/codecov-action@v1.5.0
22
+ with:
23
+ token: ${{ secrets.CODECOV_TOKEN }}
24
+ files: coverage/coverage.lcov
data/.yardopts ADDED
@@ -0,0 +1,6 @@
1
+ --markup markdown
2
+ -
3
+ README.md
4
+ README.ja.md
5
+ CONCEPTS.md
6
+ CONCEPTS.ja.md
data/CHANGELOG.md CHANGED
@@ -1,5 +1,131 @@
1
1
  ## Unreleased
2
2
 
3
+ ## 0.3.0
4
+
5
+ computed_model 0.3 comes with a great number of improvements, and a bunch of breaking changes.
6
+
7
+ - Breaking changes
8
+ - `include ComputedModel` is now `include ComputedModel::Model`.
9
+ - Indirect dependencies are now rejected.
10
+ - `computed_model_error` was removed.
11
+ - `dependency` before `define_loader` will be consumed and ignored.
12
+ - `dependency` before `define_primary_loader` will be an error.
13
+ - Cyclic dependency is an error even if it is unused.
14
+ - `nil`, `true`, and `false` in subdeps will be filtered out before passed to a loader.
15
+ - `ComputedModel.normalized_dependencies` now returns `[true]` instead of `[]` as an empty value.
16
+ - `include_subdeps` is now `include_subfields`
17
+ - Notable behavioral changes
18
+ - The order in which fields are loaded is changed.
19
+ - `ComputedModel::Model` now uses `ActiveSupport::Concern`.
20
+ - Changed
21
+ - Separate `ComputedModel::Model` from `ComputedModel` https://github.com/wantedly/computed_model/pull/17
22
+ - Remove `computed_model_error` https://github.com/wantedly/computed_model/pull/18
23
+ - Improve behavior around dependency-field pairing https://github.com/wantedly/computed_model/pull/20
24
+ - Implement strict field access https://github.com/wantedly/computed_model/pull/23
25
+ - Preprocess graph with topological sorting https://github.com/wantedly/computed_model/pull/24
26
+ - Implement conditional dependencies and subdependency mapping/passthrough https://github.com/wantedly/computed_model/pull/25
27
+ - Use `ActiveSupport::Concern` https://github.com/wantedly/computed_model/pull/26
28
+ - Rename subdeps as subfields https://github.com/wantedly/computed_model/pull/31
29
+ - Added
30
+ - `ComputedModel::Model#verify_dependencies`
31
+ - Loader dependency https://github.com/wantedly/computed_model/pull/28
32
+ - Support computed model inheritance https://github.com/wantedly/computed_model/pull/29
33
+ - Refactored
34
+ - Extract `DepGraph` from `Model` https://github.com/wantedly/computed_model/pull/19
35
+ - Define loader as a singleton method https://github.com/wantedly/computed_model/pull/21
36
+ - Refactor `ComputedModel::Plan` https://github.com/wantedly/computed_model/pull/22
37
+ - Misc
38
+ - Collect coverage https://github.com/wantedly/computed_model/pull/12 https://github.com/wantedly/computed_model/pull/16
39
+ - Refactor tests https://github.com/wantedly/computed_model/pull/10 https://github.com/wantedly/computed_model/pull/15
40
+ - Add tests https://github.com/wantedly/computed_model/pull/27
41
+ - Add documentation https://github.com/wantedly/computed_model/pull/30
42
+
43
+ See [Migration-0.3.md](Migration-0.3.md) for migration.
44
+
45
+ ### New feature: dynamic dependencies
46
+
47
+ Previously, subdeps are only useful for loaded fields and primary fields. Now computed fields can make use of subdeps!
48
+
49
+ ```ruby
50
+
51
+ class User
52
+ # Delegate subdeps
53
+ dependency(
54
+ blog_articles: -> (subdeps) { subdeps }
55
+ )
56
+ computed def filtered_blog_articles
57
+ if current_subfields.normalized[:image].any?
58
+ # ...
59
+ end
60
+ # ...
61
+ end
62
+ end
63
+ ```
64
+
65
+ See [CONCEPTS.md](CONCEPTS.md) for more usages.
66
+
67
+ ### New feature: loader dependency
68
+
69
+ You can specify dependency from a loaded field.
70
+
71
+ ```ruby
72
+ class User
73
+ dependency :raw_user # dependency of :raw_books
74
+ define_loader :raw_books, key: -> { id } do |subdeps, **|
75
+ # ...
76
+ end
77
+ end
78
+ ```
79
+
80
+ ### New feature: computed model inheritance
81
+
82
+ Now you can reuse computed model definitions via inheritance.
83
+
84
+ ```ruby
85
+ module UserLikeConcern
86
+ extends ActiveSupport::Concern
87
+ include ComputedModel::Model
88
+
89
+ dependency :preference, :profile
90
+ computed def display_name
91
+ "#{preference.title} #{profile.name}"
92
+ end
93
+ end
94
+
95
+ class User
96
+ include UserLikeConcern
97
+
98
+ define_loader :preference, key: -> { id } do ... end
99
+ define_loader :profile, key: -> { id } do ... end
100
+ end
101
+
102
+ class Admin
103
+ include UserLikeConcern
104
+
105
+ define_loader :preference, key: -> { id } do ... end
106
+ define_loader :profile, key: -> { id } do ... end
107
+ end
108
+ ```
109
+
110
+ ## 0.2.2
111
+
112
+ - [#7](https://github.com/wantedly/computed_model/pull/7) Accept Hash as a `with` parameter
113
+
114
+ ## 0.2.1
115
+
116
+ - Fix problem with `prefix` option in `delegate_dependency` not working
117
+
118
+ ## 0.2.0
119
+
120
+ - **BREAKING CHANGE** Make define_loader more concise interface like GraphQL's DataLoader.
121
+ - Introduce primary loader through `#define_primary_loader`.
122
+ - **BREAKING CHANGE** Change `#bulk_load_and_compute` signature to support primary loader.
123
+
124
+ ## 0.1.1
125
+
126
+ - Expand docs.
127
+ - Add `ComputedModel#computed_model_error` for load cancellation
128
+
3
129
  ## 0.1.0
4
130
 
5
131
  Initial release.
data/CONCEPTS.ja.md ADDED
@@ -0,0 +1,324 @@
1
+ # 基本概念と機能
2
+
3
+ [English version](CONCEPTS.md)
4
+
5
+ ## ラッパークラス
6
+
7
+ ComputedModelは、ActiveRecordクラスなどに直接includeして使うことを(今のところ)想定していません。
8
+ その場合ラッパークラスを作成し、元のクラスのオブジェクトは主ローダー (後述) として定義するのがよいでしょう。
9
+
10
+ ## フィールド
11
+
12
+ **フィールド**はComputedModelの管理下にある属性のことで、依存管理の基本単位です。以下の3種類のフィールドがあります。
13
+
14
+ - computed field (計算フィールド)
15
+ - loaded field (読み込みフィールド)
16
+ - primary field (主フィールド)
17
+
18
+ ### computed field (計算フィールド)
19
+
20
+ 別のフィールドの組み合わせで算出されるフィールドです。各レコードごとに独立に計算されます。
21
+
22
+ ```ruby
23
+ class User
24
+ dependency :preference, :profile
25
+ computed def display_name
26
+ "#{preference.title} #{profile.name}"
27
+ end
28
+ end
29
+ ```
30
+
31
+ ### loaded field (読み込みフィールド)
32
+
33
+ 複数レコードに対してまとめて読み込むフィールドです。
34
+
35
+ ```ruby
36
+ class User
37
+ define_loader :preference, key: -> { id } do |ids, _subfields, **|
38
+ Preference.where(user_id: ids).index_by(&:user_id)
39
+ end
40
+ end
41
+ ```
42
+
43
+ ### primary field (主フィールド)
44
+
45
+ loaded fieldの機能に加えて、レコードの検索・列挙機能を担う特別なフィールドです。
46
+
47
+ たとえば `User` の場合、あるidのユーザーが存在するかどうかはどこかのデータソースに問い合わせる必要があるはずです。
48
+ それがたとえばActiveRecordの `RawUser` クラスである場合、主フィールドは以下のように定義されます。
49
+
50
+ ```ruby
51
+ class User
52
+ def initialize(raw_user)
53
+ @raw_user = raw_user
54
+ end
55
+
56
+ define_primary_loader :raw_user do |_subfields, ids:, **|
57
+ # User#initialize 内で @raw_user をセットする必要がある
58
+ RawUser.where(id: ids).map { |u| User.new(u) }
59
+ end
60
+ end
61
+ ```
62
+
63
+ ## 計算タイミング
64
+
65
+ ComputedModelの `bulk_load_and_compute` が呼ばれたタイミングで全ての必要なフィールドが計算されます。
66
+
67
+ 遅延ロードは今のところサポートしていません。
68
+
69
+ ## 依存関係
70
+
71
+ フィールドには依存関係を宣言することができます。
72
+ ただし、主フィールドは依存関係を持つことができません。 (他のフィールドから主フィールドに依存することはできます。)
73
+
74
+ ```ruby
75
+ class User
76
+ dependency :preference, :profile
77
+ computed def display_name
78
+ "#{preference.title} #{profile.name}"
79
+ end
80
+ end
81
+ ```
82
+
83
+ `computed def` 内や `define_loader` のブロック内では、 `dependency` で宣言したフィールドのみ参照できます。
84
+ 間接依存しているフィールドなど、たまたまロードされている場合でもアクセスはブロックされます。
85
+
86
+ ## `bulk_load_and_compute`
87
+
88
+ ComputedModelの読み込みを行うメソッドが `bulk_load_and_compute` です。
89
+ `bulk_load_and_compute` をそのまま使うのではなく、各モデルでラッパー関数を実装することが推奨されます。
90
+ (これは後述するバッチロード引数の自由度が高く、そのままでは使い間違いが起きやすいからです)
91
+
92
+ ```ruby
93
+ class User
94
+ # with には [:display_name, :title] のようにフィールド名の配列を指定する
95
+ def self.list(ids, with:)
96
+ bulk_load_and_compute(with, ids: ids)
97
+ end
98
+
99
+ def self.get(id, with:)
100
+ list([id], with: with).first
101
+ end
102
+
103
+ def self.get!(id, with:)
104
+ get(id, with: with) || (raise User::NotFound)
105
+ end
106
+ end
107
+ ```
108
+
109
+ 単独のレコードを読み込むための専用のメソッドはありません。こちらも `bulk_load_and_compute` のラッパーを実装することで実現してください。
110
+ もし単独のレコードであることを利用した最適化が必要な場合は、 `define_loader` や `define_primary_loader` で分岐を実装するとよいでしょう。
111
+
112
+ ## 下位フィールドセレクタ
113
+
114
+ 下位フィールドセレクタ (subfield selector) または 下位依存 (subdependency) は依存関係につけられる追加の情報です。
115
+
116
+ 実装上はフィールドから依存先フィールドに任意のメッセージを送ることができる仕組みになっていますが、
117
+ 名前の通りフィールドにぶら下がっている情報の取得に使うことを想定しています。
118
+
119
+ ```ruby
120
+ class User
121
+ define_loader :profile, key: -> { id } do |ids, subfields, **|
122
+ Profile.preload(subfields).where(user_id: ids).index_by(&:user_id)
123
+ end
124
+
125
+ # profileのローダーに [:contact_phones] が渡される
126
+ dependency profile: :contact_phones
127
+ computed def contact_phones
128
+ profile.contact_phones
129
+ end
130
+ end
131
+ ```
132
+
133
+ 計算フィールドでも下位フィールドセレクタを使うことができます。 (「高度な依存関係」で後述)
134
+
135
+ ## バッチロード引数
136
+
137
+ `bulk_load_and_compute` のキーワード引数は、 `define_primary_loader` や `define_loader` のブロックにそのまま渡されます。
138
+ 状況にあわせて色々な使い方が考えられます。
139
+
140
+ ### id以外での検索
141
+
142
+ 複数の検索条件を与えることもできます。
143
+
144
+ ```ruby
145
+ class User
146
+ def self.list(ids, with:)
147
+ bulk_load_and_compute(with, ids: ids, emails: nil)
148
+ end
149
+
150
+ def self.list_by_emails(emails, with:)
151
+ bulk_load_and_compute(with, ids: nil, emails: emails)
152
+ end
153
+
154
+ define_primary_loader :raw_user do |_subfields, ids:, emails:, **|
155
+ s = User.all
156
+ s = s.where(id: ids) if ids
157
+ s = s.where(email: emails) if emails
158
+ s.map { |u| User.new(u) }
159
+ end
160
+ end
161
+ ```
162
+
163
+ ### カレントユーザー
164
+
165
+ 「今どのユーザーでログインしているか」によって情報の見え方が違う、というような状況を考えます。これはカレントユーザー情報をバッチロード引数に含めることで実現可能です。
166
+
167
+ ```ruby
168
+ class User
169
+ def initialize(raw_user, current_user_id)
170
+ @raw_user = raw_user
171
+ @current_user_id = current_user_id
172
+ end
173
+
174
+ define_primary_loader :raw_user do |_subfields, current_user_id:, ids:, **|
175
+ # ...
176
+ end
177
+
178
+ define_loader :profile, key: -> { id } do |ids, _subfields, current_user_id:, **|
179
+ # ...
180
+ end
181
+ end
182
+ ```
183
+
184
+ ## 高度な依存関係
185
+
186
+ 下位フィールドセレクタにprocを指定することで、より高度な制御をすることができます。
187
+
188
+ ### 条件つき依存
189
+
190
+ 受け取った下位フィールドセレクタの内容にもとづいて、条件つき依存関係を定義することができます。
191
+
192
+ ```ruby
193
+
194
+ class User
195
+ dependency(
196
+ :blog_articles,
197
+ # image 下位フィールドセレクタがあるときのみ image_permissions フィールド を読み込む
198
+ image_permissions: -> (subfields) { subfields.normalized[:image].any? }
199
+ )
200
+ computed def filtered_blog_articles
201
+ if current_subfields.normalized[:image].any?
202
+ # ...
203
+ end
204
+ # ...
205
+ end
206
+ end
207
+ ```
208
+
209
+ ### 下位フィールドセレクタのパススルー
210
+
211
+ 下位フィールドセレクタを別のフィールドにそのまま流すことができます。
212
+
213
+ ```ruby
214
+
215
+ class User
216
+ dependency(
217
+ blog_articles: -> (subfields) { subfields }
218
+ )
219
+ computed def filtered_blog_articles
220
+ if current_subfields.normalized[:image].any?
221
+ # ...
222
+ end
223
+ # ...
224
+ end
225
+ end
226
+ ```
227
+
228
+ ### 下位フィールドセレクタのマッピング
229
+
230
+ 下位フィールドセレクタを加工して別のフィールドに流すこともできます。
231
+
232
+ ```ruby
233
+ class User
234
+ dependency(
235
+ # blog_articles を必ずロードするが、
236
+ # 特に下位フィールドセレクタが blog_articles キーを持つ場合はそれを blog_articles の下位フィールドセレクタとして流す
237
+ blog_articles: [true, -> (subfields) { subfields.normalized[:blog_articles] }],
238
+ # wiki_articles を必ずロードするが、
239
+ # 特に下位フィールドセレクタが wiki_articles キーを持つ場合はそれを wiki_articles の下位フィールドセレクタとして流す
240
+ wiki_articles: [true, -> (subfields) { subfields.normalized[:wiki_articles] }]
241
+ )
242
+ computed def articles
243
+ (blog_articles + wiki_articles).sort_by { |article| article.created_at }.reverse
244
+ end
245
+ end
246
+ ```
247
+
248
+ ### 依存関係のフォーマット
249
+
250
+ `dependency` には0個以上の引数を渡すことができます。
251
+ これらは内部で配列に積まれていき、直後の `computed def` や `define_loader` によって消費されます。
252
+ そのため、以下は同じ意味です。
253
+
254
+ ```ruby
255
+ dependency :profile
256
+ dependency :preference
257
+ computed def display_name; ...; end
258
+ ```
259
+
260
+ ```ruby
261
+ dependency :profile, :preference
262
+ computed def display_name; ...; end
263
+ ```
264
+
265
+ 渡された配列は `ComputedModel.normalize_dependencies` によってハッシュに正規化されます。これは以下のようなルールになっています。
266
+
267
+ - Symbolの場合はそのシンボルをキーとするHashとみなす。 (`:foo` → `{ foo: [true] }`)
268
+ - Hashの場合は中の値を以下のように変換する。
269
+ - 空配列の場合は `[true]` に変換する。 (`{ foo: [] }` → `{ foo: [true] }`)
270
+ - 配列以外の場合はそれを単独で含む配列に変換する。 (`{ foo: :bar }` → `{ foo: [:bar] }`)
271
+ - 空以外の配列の場合はそのまま。
272
+ - 配列の場合は個々の要素を上記のルールに従って変換したあと、ハッシュのキーをマージする。ハッシュの値は配列なのでそのまま結合される。
273
+ - `[:foo, :bar]` → `{ foo: [true], bar: [true] }`
274
+ - `[{ foo: :foo }, { foo: :bar }]` → `{ foo: [:foo, :bar] }`
275
+
276
+ このようにして得られたハッシュのキーは依存するフィールド名、値は下位フィールドセレクタとして解釈されます。
277
+
278
+ 下位フィールドセレクタは以下のように解釈します。
279
+
280
+ - Procなど `#call` を持つオブジェクトがある場合、引数に `subfields` (下位フィールドセレクタの配列) を渡して実行する。
281
+ - 配列が返ってきた場合はフラットに展開する。 (`{ foo: [-> { [:bar, :baz] }] }` → `{ foo: [:bar, :baz] }`)
282
+ - それ以外の値が返ってきた場合はその要素で置き換える。 (`{ foo: [-> { :bar }] }` → `{ foo: [:bar] }`)
283
+ - Procの置き換え後、真値 (nilとfalse以外の値) が1つ以上含まれているかを判定する。
284
+ - 真値がひとつもない場合は、条件つき依存の判定が偽になったとみなし、その依存関係は使わない。
285
+ - それ以外の場合は依存関係を使う。Procの置き換え後に得られた下位フィールドセレクタはそのまま依存先フィールドに送られる。
286
+
287
+ そのため下位フィールドセレクタには通常 `true` が含まれています。特別な条件として以下の場合は取り除かれます。
288
+
289
+ - `define_loader` や `define_primary_loader` のブロックに渡されるときは、下位フィールドセレクタに含まれる `nil`, `false`, `true` は
290
+ 取り除かれます。
291
+ - いくつかの場面では `subfields.normalize` という特別なメソッドが使えることがあります。これは下位フィールドセレクタに含まれる
292
+ `nil`, `false`, `true` を取り除いたあと、 `ComputedModel.normalize_dependencies` の正規化にかけたハッシュを返します。
293
+
294
+ ## 継承
295
+
296
+ ComputedModelで部分的にフィールドを定義したクラス (モジュール) を作り、それを継承 (インクルード) したクラスで定義を完成させることができます。
297
+
298
+ ```ruby
299
+ module UserLikeConcern
300
+ extends ActiveSupport::Concern
301
+ include ComputedModel::Model
302
+
303
+ dependency :preference, :profile
304
+ computed def display_name
305
+ "#{preference.title} #{profile.name}"
306
+ end
307
+ end
308
+
309
+ class User
310
+ include UserLikeConcern
311
+
312
+ define_loader :preference, key: -> { id } do ... end
313
+ define_loader :profile, key: -> { id } do ... end
314
+ end
315
+
316
+ class Admin
317
+ include UserLikeConcern
318
+
319
+ define_loader :preference, key: -> { id } do ... end
320
+ define_loader :profile, key: -> { id } do ... end
321
+ end
322
+ ```
323
+
324
+ オーバーライドは正しく動かない可能性があります。 (computed def が内部的にメソッドのリネームを行っているため)