computed_model 0.1.0 → 0.3.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 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 が内部的にメソッドのリネームを行っているため)