-
-
Notifications
You must be signed in to change notification settings - Fork 4
/
model_ref.dart
474 lines (443 loc) · 18.5 KB
/
model_ref.dart
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
part of katana_model;
/// Class for defining relationships between models.
///
/// You can have that relationship as data by passing a query for the related document to [modelQuery].
///
/// Since it is a mutable class and has an interface to [DocumentBase], it can be replaced by [DocumentBase] by implementing [ModelRef] and mixing in [ModelRefMixin].
///
/// モデル間のリレーションを定義するためのクラス。
///
/// [modelQuery]に関連するドキュメントのクエリーを渡すことでそのリレーションをデータとして持つことができます。
///
/// ミュータブルクラスでかつ[DocumentBase]のインターフェースを備えているため[ModelRef]を実装し、[ModelRefMixin]をミックスインすることで[DocumentBase]で置き換えることが可能です。
typedef ModelRef<T> = ModelRefBase<T>?;
class _ModelRefBase<T> extends ModelRefBase<T>
with ModelFieldValueAsMapMixin<T?> {
const _ModelRefBase(super.modelQuery);
}
/// Class for defining relationships between models.
///
/// You can have that relationship as data by passing a query for the related document to [modelQuery].
///
/// Since it is a mutable class and has an interface to [DocumentBase], it can be replaced by [DocumentBase] by implementing [ModelRefBase] and mixing in [ModelRefMixin].
///
/// モデル間のリレーションを定義するためのクラス。
///
/// [modelQuery]に関連するドキュメントのクエリーを渡すことでそのリレーションをデータとして持つことができます。
///
/// ミュータブルクラスでかつ[DocumentBase]のインターフェースを備えているため[ModelRefBase]を実装し、[ModelRefMixin]をミックスインすることで[DocumentBase]で置き換えることが可能です。
class ModelRefBase<T> extends ModelFieldValue<T?> {
/// Class for defining relationships between models.
///
/// You can have that relationship as data by passing a query for the related document to [modelQuery].
///
/// Since it is a mutable class and has an interface to [DocumentBase], it can be replaced by [DocumentBase] by implementing [ModelRefBase] and mixing in [ModelRefMixin].
///
/// モデル間のリレーションを定義するためのクラス。
///
/// [modelQuery]に関連するドキュメントのクエリーを渡すことでそのリレーションをデータとして持つことができます。
///
/// ミュータブルクラスでかつ[DocumentBase]のインターフェースを備えているため[ModelRefBase]を実装し、[ModelRefMixin]をミックスインすることで[DocumentBase]で置き換えることが可能です。
const ModelRefBase(this.modelQuery);
/// Class for defining relationships between models.
///
/// By passing the path of the related document in [path], you can have that relationship as data.
///
/// It is also possible to set a model adapter by specifying [adapter].
///
/// Since it is a mutable class and has an interface to [DocumentBase], it can be replaced by [DocumentBase] by implementing [ModelRefBase] and mixing in [ModelRefMixin].
///
/// モデル間のリレーションを定義するためのクラス。
///
/// [path]に関連するドキュメントのパスを渡すことでそのリレーションをデータとして持つことができます。
///
/// また[adapter]を指定してモデルアダプターを設定することが可能です。
///
/// ミュータブルクラスでかつ[DocumentBase]のインターフェースを備えているため[ModelRefBase]を実装し、[ModelRefMixin]をミックスインすることで[DocumentBase]で置き換えることが可能です。
factory ModelRefBase.fromPath(String path, [ModelAdapter? adapter]) {
return _ModelRefBase(
DocumentModelQuery(
path.trimQuery().trimString("/"),
adapter: adapter,
),
);
}
/// Convert from [json] map to [ModelRefBase].
///
/// [json]のマップから[ModelRefBase]に変換します。
factory ModelRefBase.fromJson(Map<String, dynamic> json) {
return ModelRefBase.fromPath(json.get(ModelRefBase._kRefKey, ""));
}
static const _kRefKey = "@ref";
/// [DocumentModelQuery] of the associated document.
///
/// 関連するドキュメントの[DocumentModelQuery].
final DocumentModelQuery modelQuery;
@override
Map<String, dynamic> toJson() => {
kTypeFieldKey: (ModelRefBase).toString(),
ModelRefBase._kRefKey: modelQuery.path.trimQuery().trimString("/"),
};
/// Actual value.
///
/// [Null] is returned.
///
/// 実際の値。
///
/// [Null]が返されます。
@override
T? get value {
return null;
}
/// Returns the ID for the document path.
///
/// ドキュメントのパス用のIDを返します。
String get uid {
return modelQuery.path.trimQuery().trimString("/").split("/").lastOrNull ??
"";
}
@override
String toString() {
return modelQuery.path;
}
}
/// [ModelFieldValueConverter] to enable automatic conversion of [ModelRefBase] as [ModelFieldValue].
///
/// [ModelRefBase]を[ModelFieldValue]として自動変換できるようにするための[ModelFieldValueConverter]。
@immutable
class ModelRefConverter extends ModelFieldValueConverter<ModelRefBase> {
/// [ModelFieldValueConverter] to enable automatic conversion of [ModelRefBase] as [ModelFieldValue].
///
/// [ModelRefBase]を[ModelFieldValue]として自動変換できるようにするための[ModelFieldValueConverter]。
const ModelRefConverter();
@override
ModelRefBase fromJson(Map<String, Object?> map) {
return ModelRefBase.fromJson(map);
}
@override
Map<String, Object?> toJson(ModelRefBase value) {
return value.toJson();
}
}
/// Filter class to make [ModelRefBase] available to [ModelQuery.filters].
///
/// [ModelRefBase]を[ModelQuery.filters]で利用できるようにするためのフィルタークラス。
@immutable
class ModelRefFilter extends ModelFieldValueFilter<ModelRefBase> {
/// Filter class to make [ModelRefBase] available to [ModelQuery.filters].
///
/// [ModelRefBase]を[ModelQuery.filters]で利用できるようにするためのフィルタークラス。
const ModelRefFilter();
@override
int? compare(dynamic a, dynamic b) {
return _hasMatch(a, b, (a, b) => a.compareTo(b));
}
@override
bool? hasMatch(ModelQueryFilter filter, dynamic source) {
final target = filter.value;
switch (filter.type) {
case ModelQueryFilterType.equalTo:
return _hasMatch(source, target, (source, target) => source == target);
case ModelQueryFilterType.notEqualTo:
return _hasMatch(source, target, (source, target) => source != target);
case ModelQueryFilterType.arrayContains:
if (source is List) {
if (source.any((s) =>
_hasMatch(s, target, (source, target) => source == target) ??
false)) {
return true;
}
}
break;
case ModelQueryFilterType.arrayContainsAny:
if (source is List && target is List && target.isNotEmpty) {
if (source.any((s) => target.any((t) =>
_hasMatch(s, t, (source, target) => source == target) ??
false))) {
return true;
}
}
break;
case ModelQueryFilterType.whereIn:
if (target is List && target.isNotEmpty) {
final matches = target.mapAndRemoveEmpty((t) =>
_hasMatch(source, t, (source, target) => source == target));
if (matches.isNotEmpty) {
return matches.any((element) => element);
}
}
break;
case ModelQueryFilterType.whereNotIn:
if (target is List && target.isNotEmpty) {
final matches = target.mapAndRemoveEmpty((t) =>
_hasMatch(source, t, (source, target) => source == target));
if (matches.isNotEmpty) {
return !matches.any((element) => element);
}
}
break;
default:
return null;
}
return null;
}
T? _hasMatch<T>(
dynamic source,
dynamic target,
T Function(String source, String target) filter,
) {
if (source is ModelRefBase && target is ModelRefBase) {
return filter(source.modelQuery.path, target.modelQuery.path);
} else if (source is ModelRefBase && target is DocumentModelQuery) {
return filter(source.modelQuery.path, target.path);
} else if (source is DocumentModelQuery && target is ModelRefBase) {
return filter(source.path, target.modelQuery.path);
} else if (source is ModelRefBase && target is String) {
return filter(source.modelQuery.path, target);
} else if (source is String && target is ModelRefBase) {
return filter(source, target.modelQuery.path);
} else if (source is ModelRefBase &&
target is DynamicMap &&
target.get(kTypeFieldKey, "") == (ModelRefBase).toString()) {
return filter(source.modelQuery.path,
ModelRefBase.fromJson(target).modelQuery.path);
} else if (source is DynamicMap &&
target is ModelRefBase &&
source.get(kTypeFieldKey, "") == (ModelRefBase).toString()) {
return filter(ModelRefBase.fromJson(source).modelQuery.path,
target.modelQuery.path);
} else if (source is DynamicMap &&
target is DynamicMap &&
source.get(kTypeFieldKey, "") == (ModelRefBase).toString() &&
target.get(kTypeFieldKey, "") == (ModelRefBase).toString()) {
return filter(ModelRefBase.fromJson(source).modelQuery.path,
ModelRefBase.fromJson(target).modelQuery.path);
}
return null;
}
}
/// A mix-in to define that it is a relationship between models in [DocumentBase], etc.
///
/// Mix in the document to which you are relating.
///
/// [DocumentBase]などにモデル間のリレーションであるということを定義するためのミックスイン。
///
/// リレーション先のドキュメントにミックスインしてください。
mixin ModelRefMixin<T>
implements ModelRefDocumentBase<T>, ModelRefBase<T>, DocumentBase<T> {
@override
Map<String, dynamic> toJson() => {
kTypeFieldKey: (ModelRefBase).toString(),
ModelRefBase._kRefKey: modelQuery.path.trimQuery().trimString("/"),
};
}
/// Define a document base including [ModelRefBase] and [DocumentBase].
///
/// [ModelRefBase]と[DocumentBase]を含めたドキュメントベースを定義します。
abstract class ModelRefDocumentBase<T>
implements ModelRefBase<T>, DocumentBase<T> {}
/// It is available by mixing in when using [ModelRefBase] in [DocumentBase.value].
///
/// When data is loaded in [DocumentBase.load], the data in [ModelRefBase] is automatically loaded and stored.
///
/// Define [builder] to store relevant documents and data.
///
/// Mix in the document from which you are relaying.
///
/// [DocumentBase.value]で[ModelRefBase]を利用している際にミックスインすることで利用できます。
///
/// [DocumentBase.load]でデータをロードする際に合わせて[ModelRefBase]内のデータを自動でロードして格納します。
///
/// [builder]を定義して関連するドキュメントとデータの格納を行ってください。
///
/// リレーション元のドキュメントにミックスインしてください。
mixin ModelRefLoaderMixin<T> implements DocumentBase<T> {
final _modelRefBuilderCache = <DocumentModelQuery, ModelRefMixin>{};
/// ModelRefBuilder], which implements the definition of the loading method.
///
/// Data is loaded and stored in the order listed.
///
/// ロード方法の定義を実装した[ModelRefBuilder]。
///
/// リストの順番通りにデータのロードと格納が行われます。
List<ModelRefBuilderBase<T>> get builder;
@override
@protected
@mustCallSuper
Future<T?> filterOnDidLoad(T? value) async {
final builderList = builder;
var tmpValue = value;
for (final build in builderList) {
if (tmpValue == null) {
continue;
}
tmpValue = await build._build(
val: tmpValue,
cacheList: _modelRefBuilderCache,
onDidLoad: (query, modelRefMixin) {
modelRefMixin.addListener(notifyListeners);
_modelRefBuilderCache[query] = modelRefMixin;
},
loaderModelQuery: modelQuery,
);
}
return tmpValue;
}
@override
@protected
@mustCallSuper
void dispose() {
for (final cache in _modelRefBuilderCache.values) {
cache.removeListener(notifyListeners);
cache.dispose();
}
}
}
/// Builder for granting relationships between models and loading data.
///
/// Define [ModelRefLoaderMixin] to match the mix-in.
///
/// The procedure is;
///
/// 1. Returns a [ModelRefBase] containing only the relation information stored in [TSource] via [modelRef].
/// 2. Generates and returns a mixed-in [DocumentBase] with [ModelRefMixin<TResult>] based on [DocumentModelQuery] via [document].
/// 3. Store the [DocumentBase] generated via [value] in [TSource] and return the updated [TSource].
///
/// モデル間のリレーションを付与しデータのロードを行うためのビルダー。
///
/// [ModelRefLoaderMixin]をミックスインしたときに合わせて定義します。
///
/// 手順としては
///
/// 1. [TSource]に保存されているリレーション情報のみ入った[ModelRefBase]を[modelRef]経由で返します。
/// 2. [DocumentModelQuery]を元に[ModelRefMixin<TResult>]をミックスインした[DocumentBase]を[document]経由で生成し返します。
/// 3. [value]経由で生成された[DocumentBase]を[TSource]に保存して、更新した[TSource]を返すようにします。
///
/// ```dart
/// @override
/// List<ModelRefBuilderBase<StreamModel>> get builder => [
/// ModelRefBuilder(
/// modelRef: (value) => value.user,
/// document: (query) => UserModelDocument(query),
/// value: (value, document) {
/// return value.copyWith(user: document);
/// },
/// )
/// ];
/// ```
@immutable
class ModelRefBuilder<TSource, TResult> extends ModelRefBuilderBase<TSource> {
/// Builder for granting relationships between models and loading data.
///
/// Define [ModelRefLoaderMixin] to match the mix-in.
///
/// The procedure is;
///
/// 1. Returns a [ModelRefBase] containing only the relation information stored in [TSource] via [modelRef].
/// 2. Generates and returns a mixed-in [DocumentBase] with [ModelRefMixin<TResult>] based on [DocumentModelQuery] via [document].
/// 3. Store the [DocumentBase] generated via [value] in [TSource] and return the updated [TSource].
///
/// モデル間のリレーションを付与しデータのロードを行うためのビルダー。
///
/// [ModelRefLoaderMixin]をミックスインしたときに合わせて定義します。
///
/// 手順としては
///
/// 1. [TSource]に保存されているリレーション情報のみ入った[ModelRefBase]を[modelRef]経由で返します。
/// 2. [DocumentModelQuery]を元に[ModelRefMixin<TResult>]をミックスインした[DocumentBase]を[document]経由で生成し返します。
/// 3. [value]経由で生成された[DocumentBase]を[TSource]に保存して、更新した[TSource]を返すようにします。
///
/// ```dart
/// @override
/// List<ModelRefBuilderBase<StreamModel>> get builder => [
/// ModelRefBuilder(
/// modelRef: (value) => value.user,
/// document: (query) => UserModelDocument(query),
/// value: (value, document) {
/// return value.copyWith(user: document);
/// },
/// )
/// ];
/// ```
const ModelRefBuilder({
required this.modelRef,
required this.document,
required this.value,
});
/// Callback to retrieve [ModelRefBase] stored in [TSource].
///
/// [TSource]に格納されている[ModelRefBase]を取得するためのコールバック。
final ModelRefBase? Function(TSource value) modelRef;
/// Callback to generate a [DocumentBase] that mixes in a [ModelRefMixin<TResult>] based on a [DocumentModelQuery] obtained from a [ModelRefBase].
///
/// [ModelRefBase]から取得された[DocumentModelQuery]を元に[ModelRefMixin<TResult>]をミックスインした[DocumentBase]を生成するためのコールバック。
final ModelRefMixin<TResult> Function(DocumentModelQuery modelQuery) document;
/// Callback to store the generated [ModelRefMixin<TResult>] in [TSource].
///
/// [TSource]に生成された[ModelRefMixin<TResult>]を格納するためのコールバック。
final TSource Function(
TSource value,
ModelRefMixin<TResult> document,
) value;
@override
Future<TSource?> _build({
required TSource? val,
required Map<DocumentModelQuery, ModelRefMixin> cacheList,
required void Function(
DocumentModelQuery query, ModelRefMixin modelRefMixin)
onDidLoad,
required DocumentModelQuery loaderModelQuery,
}) async {
if (val == null) {
return val;
}
final ref = modelRef(val);
if (ref == null) {
return val;
}
final modelQuery = DocumentModelQuery(
ref.modelQuery.path,
adapter: loaderModelQuery.adapter,
);
if (cacheList.containsKey(modelQuery)) {
final doc = cacheList[modelQuery];
if (doc is ModelRefMixin<TResult>) {
return value(val, doc);
}
}
final doc = document(modelQuery);
assert(
doc.modelQuery == modelQuery,
"The document was created with a different [DocumentModelQuery] than [ModelRef]. Please match [DocumentModelQuery]: ${doc.modelQuery}, $modelQuery",
);
await doc.load();
onDidLoad(modelQuery, doc);
return value(val, doc);
}
}
/// Base class for defining [ModelRefBuilder].
///
/// The actual definition is done using [ModelRefBuilder].
///
/// [ModelRefBuilder]を定義するためのベースクラス。
///
/// 実際の定義は[ModelRefBuilder]を利用します。
@immutable
abstract class ModelRefBuilderBase<TSource> {
/// Base class for defining [ModelRefBuilder].
///
/// The actual definition is done using [ModelRefBuilder].
///
/// [ModelRefBuilder]を定義するためのベースクラス。
///
/// 実際の定義は[ModelRefBuilder]を利用します。
const ModelRefBuilderBase();
Future<TSource?> _build({
required TSource? val,
required Map<DocumentModelQuery, ModelRefMixin> cacheList,
required void Function(
DocumentModelQuery query, ModelRefMixin modelRefMixin)
onDidLoad,
required DocumentModelQuery loaderModelQuery,
});
}