Line data Source code
1 : import 'package:collection/collection.dart';
2 : import 'package:tuple/tuple.dart';
3 : import 'package:uuid/uuid.dart';
4 :
5 : /// Getter function to retrieve a field from a [GenericModel]
6 : typedef Getter<T> = T Function();
7 :
8 : /// Setter function to set a field in a [GenericModel]
9 : typedef Setter<T> = void Function(T value);
10 4 : final bool Function(dynamic, dynamic) _equality =
11 2 : const DeepCollectionEquality(DefaultEquality<dynamic>()).equals;
12 :
13 : /// Base Class to be extended by
14 : abstract class GenericModel {
15 : /// The key for [type] in the result of [toMap]
16 : static const TYPE = 'type';
17 :
18 : /// The key for [id] in the result of [toMap]
19 : static const ID = 'id';
20 :
21 : /// Unique identifier for this model
22 : String? id;
23 :
24 : /// Used by [toMap] to generate the map
25 : late final Map<String, Tuple2<Getter<dynamic>, Setter<dynamic>>>
26 10 : getterSetterMap = _getterSetterMap;
27 5 : Map<String, Tuple2<Getter<dynamic>, Setter<dynamic>>> get _getterSetterMap {
28 5 : final getterSetterMap = getGetterSetterMap();
29 : assert(
30 10 : !getterSetterMap.containsKey(TYPE),
31 0 : '"$TYPE" is already used by GenericModel. Do not use it for extensions',
32 : );
33 : assert(
34 10 : !getterSetterMap.containsKey(ID),
35 0 : '"$ID" is already used by GenericModel. Do not use it for extensions',
36 : );
37 30 : getterSetterMap[ID] = Tuple2(() => id, (val) => id = val as String?);
38 : return getterSetterMap;
39 : }
40 :
41 : /// Converts this [GenericModel] into a Serializable Map.
42 : ///
43 : /// Can be coverted back into the Class version by calling [loadFromMap]
44 4 : Map<String, dynamic> toMap() {
45 4 : final map = <String, dynamic>{};
46 8 : map[TYPE] = type;
47 8 : getterSetterMap.keys
48 28 : .forEach((element) => map[element] = getterSetterMap[element]!.item1());
49 :
50 : return map;
51 : }
52 :
53 : /// Loads a Serializable map into the values of this [GenericModel]
54 : ///
55 : /// You can generate a value that can be passed into this by using [toMap]
56 : ///
57 : /// [respectType] will make a check to ensure that the TYPE entry is the same
58 : /// if true. This will throw an [ArgumentError] if they're not the same
59 4 : void loadFromMap(Map<String, dynamic> map, {bool respectType = true}) {
60 4 : if (respectType && map.containsKey(TYPE)) {
61 12 : if (map[TYPE] != type) {
62 0 : throw ArgumentError('Type in $map does not match $type');
63 : }
64 : }
65 8 : getterSetterMap.keys
66 28 : .forEach((element) => getterSetterMap[element]!.item2(map[element]));
67 : }
68 :
69 : /// Copies values from the given [model] into this model.
70 : ///
71 : /// If [allowDifferentTypes] is true, the method will continue even if the
72 : /// types and fields in [model] and myself are different. Otherwise,
73 : /// differences will be met with an error.
74 : ///
75 : /// [onlyFields] and [exceptFields] can be used to limit the fields that are
76 : /// copied. The two are mutually exclusive and an error will be thrown if both
77 : /// are specified.
78 4 : void copy<T extends GenericModel>(
79 : T model, {
80 : bool allowDifferentTypes = false,
81 : bool copyId = true,
82 : Iterable<String>? onlyFields,
83 : Iterable<String>? exceptFields,
84 : }) {
85 : if (!allowDifferentTypes) {
86 : assert(
87 12 : type == model.type,
88 0 : 'Types do not match! ("$type" and "${model.type}")',
89 : );
90 : }
91 4 : fieldsToEvaluate(onlyFields, exceptFields)
92 8 : .where((element) {
93 4 : if (element == ID) {
94 : return copyId;
95 : }
96 : return true;
97 : })
98 20 : .where((element) => model.getterSetterMap.keys.contains(element))
99 8 : .forEach((element) {
100 8 : getterSetterMap[element]!
101 24 : .item2(model.getterSetterMap[element]!.item1());
102 : });
103 : }
104 :
105 : /// Returns whether the given [model] has the same given fields as this model.
106 : ///
107 : /// [onlyFields] and [exceptFields] can be used to limit the fields that are
108 : /// compared. The two are mutually exclusive and an error will be thrown if
109 : /// both are specified.
110 2 : bool hasSameFields<T extends GenericModel>({
111 : required T model,
112 : Iterable<String>? onlyFields,
113 : Iterable<String>? exceptFields,
114 : }) {
115 2 : return fieldsToEvaluate(onlyFields, exceptFields)
116 2 : .map(
117 6 : (e) => _equality(
118 8 : getterSetterMap[e]?.item1(),
119 8 : model.getterSetterMap[e]?.item1(),
120 : ),
121 : )
122 4 : .reduce((value, element) => value && element);
123 : }
124 :
125 : /// Returns the fields that exist in this model.
126 : ///
127 : /// [onlyFields] and [exceptFields] can be used to limit the fields that are
128 : /// evaluated. The two are mutually exclusive and an error will be thrown if
129 : /// both are specified.
130 4 : Iterable<String> fieldsToEvaluate<T extends GenericModel>(
131 : Iterable<String>? onlyFields,
132 : Iterable<String>? exceptFields,
133 : ) {
134 : assert(
135 4 : onlyFields == null || exceptFields == null,
136 : 'onlyFields and exceptFields cannot both be specified at the same time',
137 : );
138 16 : return getterSetterMap.keys.where((element) {
139 : if (onlyFields != null) {
140 2 : return onlyFields.contains(element);
141 : }
142 : if (exceptFields != null) {
143 1 : return !exceptFields.contains(element);
144 : }
145 : return true;
146 : });
147 : }
148 :
149 : /// Returns [id] if that value is not null. Otherwise will automatically
150 : /// generate a new value for [id] with the [idSuffix] setter.
151 20 : String get autoGenId => id = id ?? prefixTypeForId(const Uuid().v4());
152 :
153 : /// Prefixes this [GenericModel]'s [type] to [idSuffix]
154 12 : String prefixTypeForId(String idSuffix) => '$type::$idSuffix';
155 :
156 : /// Sets the [id] to have [idSuffix] as a suffix. This will override the
157 : /// existing [id].
158 2 : set idSuffix(String? idSuffix) =>
159 4 : id = idSuffix == null ? null : prefixTypeForId(idSuffix);
160 :
161 : /// Returns the last part of this [id], which is always a unique identifier.
162 : ///
163 : /// Note that if multiple types are prefixed, none of those will be returned.
164 8 : String? get idSuffix => id?.split('::').last;
165 :
166 : /// Implemented by subclasses to map the getters and setters of the object.
167 : ///
168 : /// Cannot have keys that have the values [TYPE] or [ID]
169 : Map<String, Tuple2<Getter<dynamic>, Setter<dynamic>>> getGetterSetterMap();
170 :
171 : /// Unique type to give to the model. Whether or not collision is expected is
172 : /// dependent on the parameters of your system.
173 : String get type;
174 :
175 : /// Converts the pair of [Getter] and [Setter] for an enum into the
176 : /// appropriate pair for storage (a String)
177 : ///
178 : /// [values] is the list of possible values that the enum has
179 : /// (ex. ExampleEnum.values)
180 4 : static Tuple2<Getter<dynamic>, Setter<dynamic>>
181 : convertEnumToString<T extends Enum>(
182 : Getter<T?> getter,
183 : Setter<T?> setter,
184 : Iterable<T> values,
185 : ) {
186 4 : return Tuple2(
187 4 : () {
188 4 : final value = getter();
189 4 : return value?.name;
190 : },
191 8 : (val) => setter(
192 : val == null
193 : ? null
194 12 : : values.map<T?>((e) => e).firstWhere(
195 12 : (element) => val == element?.name,
196 1 : orElse: () => null,
197 : ),
198 : ),
199 : );
200 : }
201 :
202 : /// Converts the pair of [Getter] and [Setter] for a [GenericModel] into the
203 : /// appropriate serialized type.
204 : ///
205 : /// [supplier] should generate a new mutable version of this [GenericModel]
206 3 : static Tuple2<Getter<dynamic>, Setter<dynamic>> model<T extends GenericModel>(
207 : Getter<T?> getter,
208 : Setter<T?> setter,
209 : Getter<T> supplier,
210 : ) =>
211 3 : Tuple2(
212 9 : () => getter()?.toMap(),
213 6 : (val) => setter(
214 : val == null
215 : ? null
216 6 : : (supplier()..loadFromMap(val as Map<String, dynamic>)),
217 : ),
218 : );
219 :
220 : /// Converts the pair of [Getter] and [Setter] for a [List] of [GenericModel]
221 : /// into the appropriate serialized type.
222 : ///
223 : /// [supplier] should generate a new mutable version of this [GenericModel]
224 3 : static Tuple2<Getter<dynamic>, Setter<dynamic>>
225 : modelList<T extends GenericModel>(
226 : Getter<List<T>?> getter,
227 : Setter<List<T>?> setter,
228 : Getter<T> supplier,
229 : ) =>
230 3 : Tuple2(
231 14 : () => getter()?.map((e) => e.toMap()).toList(),
232 6 : (val) => setter(
233 : (val as List<Map<String, dynamic>>?)
234 6 : ?.map<T>((e) => supplier()..loadFromMap(e))
235 3 : .toList(),
236 : ),
237 : );
238 :
239 : /// Converts the pair of [Getter] and [Setter] for a [Map] of [GenericModel]
240 : /// into the appropriate serialized type.
241 : ///
242 : /// [supplier] should generate a new mutable version of this [GenericModel]
243 3 : static Tuple2<Getter<dynamic>, Setter<dynamic>>
244 : modelMap<T extends GenericModel>(
245 : Getter<Map<String, T>?> getter,
246 : Setter<Map<String, T>?> setter,
247 : Getter<T> supplier,
248 : ) =>
249 3 : Tuple2(
250 12 : () => getter()?.map((key, value) => MapEntry(key, value.toMap())),
251 6 : (val) => setter(
252 3 : (val as Map<String, dynamic>?)?.map<String, T>(
253 2 : (key, value) => MapEntry(
254 : key,
255 2 : supplier()..loadFromMap(value as Map<String, dynamic>),
256 : ),
257 : ),
258 : ),
259 : );
260 :
261 : /// Converts the pair of [Getter] and [Setter] for a [DateTime] into the
262 : /// appropriate serialized type. (microsecondsSinceEpoc)
263 4 : static Tuple2<Getter<dynamic>, Setter<dynamic>> dateTime(
264 : Getter<DateTime?> getter,
265 : Setter<DateTime?> setter,
266 : ) =>
267 4 : Tuple2(
268 12 : () => getter()?.microsecondsSinceEpoch,
269 8 : (val) => setter(
270 4 : val == null ? null : DateTime.fromMicrosecondsSinceEpoch(val as int),
271 : ),
272 : );
273 :
274 : /// Takes the pair of [Getter] and [Setter] for a primitive and puts them in
275 : /// a [Tuple2]. Convenience function if you don't want to rely on the tuple
276 : /// package directly.
277 5 : static Tuple2<Getter<dynamic>, Setter<dynamic>> primitive<T>(
278 : Getter<T?> getter,
279 : Setter<T?> setter,
280 : ) =>
281 25 : Tuple2(() => getter(), (val) => setter(val as T?));
282 : }
|