Skip to content

Commit 546d349

Browse files
Kasper Overgård Nielsennirinchev
Kasper Overgård Nielsen
andauthored
RDART-1062: Allow missing values (implicit null) (#1736)
* Allow missing values (implicit null) * Update .expected files * Rerun builder in example * Update CHANGELOG * Support default values during EJson deserialization * Rerun builder in example * Add tests * Fix Decimal128 and RealmValue * register allow super types to be specified * Support DBRef * Extend RealmValue serialization tests * Accept a allowCustom argument on fromEJson * Lookup PK dynmically using object schema, during RealmValue of RealmObject serialization * Support deserializing RealmValue from DBKey * Support Set * Make SchemaObject const constructable again (unrelated to fix) * Avoid allowCustom argument * Update packages/realm_dart/test/serialization_test.dart Co-authored-by: Nikola Irinchev <[email protected]> * Update CHANGELOG.md --------- Co-authored-by: Nikola Irinchev <[email protected]>
1 parent 98e77a5 commit 546d349

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

50 files changed

+976
-1029
lines changed

CHANGELOG.md

+1
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
### Enhancements
44
* Added a new parameter of type `SyncTimeoutOptions` to `AppConfiguration`. It allows users to control sync timings, such as ping/pong intervals as well various connection timeouts. (Issue [#1763](https://github.com/realm/realm-dart/issues/1763))
55
* Added a new parameter `cancelAsyncOperationsOnNonFatalErrors` on `Configuration.flexibleSync` that allows users to control whether non-fatal errors such as connection timeouts should be surfaced in the form of errors or if sync should try and reconnect in the background. (PR [#1764](https://github.com/realm/realm-dart/pull/1764))
6+
* Allow nullable and other optional fields to be absent in EJson, when deserializing realm objects. (Issue [#1735](https://github.com/realm/realm-dart/issues/1735))
67

78
### Fixed
89
* Fixed an issue where creating a flexible sync configuration with an embedded object not referenced by any top-level object would throw a "No such table" exception with no meaningful information about the issue. Now a `RealmException` will be thrown that includes the offending object name, as well as more precise text for what the root cause of the error is. (PR [#1748](https://github.com/realm/realm-dart/pull/1748))

packages/CHANGELOG.ejson.md

+7
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,10 @@
1+
## 0.4.0
2+
3+
- `fromEJson<T>` now accepts a `defaultValue` argument that is returned if
4+
`null` is passed as `ejson`.
5+
- `register<T>` takes an optional `superTypes` argument to specify the super
6+
types of `T` if needed.
7+
18
## 0.3.1
29

310
- Update sane_uuid dependency to ^1.0.0 (compensate for breaking change)

packages/ejson/lib/src/configuration.dart

+2-2
Original file line numberDiff line numberDiff line change
@@ -9,8 +9,8 @@ import 'encoding.dart';
99

1010
/// Register custom EJSON [encoder] and [decoder] for a type [T].
1111
/// The last registered codec pair for a given type [T] will be used.
12-
void register<T>(EJsonEncoder<T> encoder, EJsonDecoder<T> decoder) {
13-
TypePlus.add<T>();
12+
void register<T>(EJsonEncoder<T> encoder, EJsonDecoder<T> decoder, {Iterable<Type>? superTypes}) {
13+
TypePlus.add<T>(superTypes: superTypes);
1414
customEncoders[T] = encoder;
1515
customDecoders[T] = decoder;
1616
}

packages/ejson/lib/src/decoding.dart

+40-23
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ const _commonDecoders = {
1919
Object: _decodeAny,
2020
Iterable: _decodeArray,
2121
List: _decodeArray,
22+
Set: _decodeSet,
2223
bool: _decodeBool,
2324
DateTime: _decodeDate,
2425
Defined: _decodeDefined,
@@ -32,34 +33,42 @@ const _commonDecoders = {
3233
Symbol: _decodeSymbol,
3334
Uint8List: _decodeBinary,
3435
Uuid: _decodeUuid,
36+
DBRef: _decodeDBRef,
3537
Undefined: _decodeUndefined,
3638
UndefinedOr: _decodeUndefinedOr,
3739
};
3840

39-
/// Custom decoders for specific types. Use `register` to add a custom decoder.
40-
final customDecoders = <Type, Function>{};
41-
42-
final _decoders = () {
41+
/// Predefined decoders for common types
42+
final commonDecoders = () {
4343
// register extra common types on first access
4444
undefinedOr<T>(dynamic f) => f<UndefinedOr<T>>();
4545
TypePlus.addFactory(undefinedOr);
4646
TypePlus.addFactory(<T>(dynamic f) => f<Defined<T>>(), superTypes: [undefinedOr]);
4747
TypePlus.addFactory(<T>(dynamic f) => f<Undefined<T>>(), superTypes: [undefinedOr]);
48+
TypePlus.addFactory(<T>(dynamic f) => f<DBRef<T>>());
4849
TypePlus.add<BsonKey>();
4950
TypePlus.add<ObjectId>();
5051
TypePlus.add<Uint8List>();
5152
TypePlus.add<Uuid>();
5253

53-
return CombinedMapView([customDecoders, _commonDecoders]);
54+
return _commonDecoders;
5455
}();
5556

57+
/// Custom decoders for specific types. Use `register` to add a custom decoder.
58+
final customDecoders = <Type, Function>{};
59+
60+
final _decoders = CombinedMapView([customDecoders, commonDecoders]);
61+
5662
/// Converts [ejson] to type [T].
5763
///
64+
/// [defaultValue] is returned if set, and [ejson] is `null`.
65+
///
5866
/// Throws [InvalidEJson] if [ejson] is not valid for [T].
5967
/// Throws [MissingDecoder] if no decoder is registered for [T].
60-
T fromEJson<T>(EJsonValue ejson) {
68+
T fromEJson<T>(EJsonValue ejson, {T? defaultValue}) {
6169
final type = T;
6270
final nullable = type.isNullable;
71+
if (ejson == null && defaultValue != null) return defaultValue;
6372
final decoder = nullable ? _decodeNullable : _decoders[type.base];
6473
if (decoder == null) {
6574
throw MissingDecoder._(ejson, type);
@@ -94,32 +103,30 @@ dynamic _decodeAny(EJsonValue ejson) {
94103
{'\$numberDouble': _} => _decodeDouble(ejson),
95104
{'\$numberInt': _} => _decodeInt(ejson),
96105
{'\$numberLong': _} => _decodeInt(ejson),
106+
{'\$ref': _, '\$id': _} => _decodeDBRef<dynamic>(ejson),
97107
{'\$regex': _} => _decodeString(ejson),
98108
{'\$symbol': _} => _decodeSymbol(ejson),
99109
{'\$undefined': _} => _decodeUndefined<dynamic>(ejson),
100110
{'\$oid': _} => _decodeObjectId(ejson),
101111
{'\$binary': {'base64': _, 'subType': '04'}} => _decodeUuid(ejson),
102112
{'\$binary': _} => _decodeBinary(ejson),
103-
List<dynamic> _ => _decodeArray<dynamic>(ejson),
104-
Map<dynamic, dynamic> _ => _tryDecodeCustom(ejson) ?? _decodeDocument<String, dynamic>(ejson), // other maps goes last!!
113+
List _ => _decodeArray<dynamic>(ejson),
114+
Set _ => _decodeSet<dynamic>(ejson),
115+
Map _ => _decodeDocument<String, dynamic>(ejson), // other maps goes last!!
105116
_ => raiseInvalidEJson<dynamic>(ejson),
106117
};
107118
}
108119

109-
dynamic _tryDecodeCustom(EJsonValue ejson) {
110-
for (final decoder in customDecoders.values) {
111-
try {
112-
return decoder(ejson);
113-
} catch (_) {
114-
// ignore
115-
}
116-
}
117-
return null;
120+
List<T> _decodeArray<T>(EJsonValue ejson) {
121+
return switch (ejson) {
122+
Iterable i => i.map((ejson) => fromEJson<T>(ejson)).toList(),
123+
_ => raiseInvalidEJson(ejson),
124+
};
118125
}
119126

120-
List<T> _decodeArray<T>(EJsonValue ejson) {
127+
Set<T> _decodeSet<T>(EJsonValue ejson) {
121128
return switch (ejson) {
122-
Iterable<dynamic> i => i.map((ejson) => fromEJson<T>(ejson)).toList(),
129+
Iterable i => i.map((ejson) => fromEJson<T>(ejson)).toSet(),
123130
_ => raiseInvalidEJson(ejson),
124131
};
125132
}
@@ -139,14 +146,24 @@ DateTime _decodeDate(EJsonValue ejson) {
139146
};
140147
}
141148

149+
DBRef<KeyT> _decodeDBRef<KeyT>(EJsonValue ejson) {
150+
switch (ejson) {
151+
case {'\$ref': String collection, '\$id': EJsonValue id}:
152+
KeyT key = fromEJson(id);
153+
return DBRef.new.callWith(parameters: [collection, key], typeArguments: [key.runtimeType]) as DBRef<KeyT>;
154+
default:
155+
return raiseInvalidEJson(ejson);
156+
}
157+
}
158+
142159
Defined<T> _decodeDefined<T>(EJsonValue ejson) {
143160
if (ejson case {'\$undefined': 1}) return raiseInvalidEJson(ejson);
144-
return Defined<T>(fromEJson<T>(ejson));
161+
return Defined(fromEJson(ejson));
145162
}
146163

147164
Map<K, V> _decodeDocument<K, V>(EJsonValue ejson) {
148165
return switch (ejson) {
149-
Map<dynamic, dynamic> m => m.map((key, value) => MapEntry(key as K, fromEJson<V>(value))),
166+
Map m => m.map((key, value) => MapEntry(key as K, fromEJson(value))),
150167
_ => raiseInvalidEJson(ejson),
151168
};
152169
}
@@ -229,14 +246,14 @@ Symbol _decodeSymbol(EJsonValue ejson) {
229246

230247
Undefined<T> _decodeUndefined<T>(EJsonValue ejson) {
231248
return switch (ejson) {
232-
{'\$undefined': 1} => Undefined<T>(),
249+
{'\$undefined': 1} => Undefined(),
233250
_ => raiseInvalidEJson(ejson),
234251
};
235252
}
236253

237254
UndefinedOr<T> _decodeUndefinedOr<T>(EJsonValue ejson) {
238255
return switch (ejson) {
239-
{'\$undefined': 1} => Undefined<T>(),
256+
{'\$undefined': 1} => Undefined(),
240257
_ => _decodeDefined(ejson),
241258
};
242259
}

packages/ejson/lib/src/encoding.dart

+15-1
Original file line numberDiff line numberDiff line change
@@ -34,12 +34,13 @@ EJsonValue _encodeAny(Object? value) {
3434
null => null,
3535
bool b => _encodeBool(b),
3636
DateTime d => _encodeDate(d),
37+
DBRef d => _encodeDBRef(d),
3738
Defined<dynamic> d => _encodeDefined(d),
3839
double d => _encodeDouble(d),
3940
int i => _encodeInt(i),
4041
BsonKey k => _encodeKey(k),
4142
Uint8List b => _encodeBinary(b, subtype: '00'),
42-
Iterable<dynamic> l => _encodeArray(l),
43+
Iterable<dynamic> l => _encodeArray(l), // handles List and Set as well
4344
Map<dynamic, dynamic> m => _encodeDocument(m),
4445
ObjectId o => _encodeObjectId(o),
4546
String s => _encodeString(s),
@@ -71,6 +72,13 @@ EJsonValue _encodeDate(DateTime value) {
7172
};
7273
}
7374

75+
EJsonValue _encodeDBRef(DBRef<dynamic> d) {
76+
return {
77+
'\$ref': d.collection,
78+
'\$id': toEJson(d.id),
79+
};
80+
}
81+
7482
EJsonValue _encodeDefined(Defined<dynamic> defined) => toEJson(defined.value);
7583

7684
EJsonValue _encodeDocument(Map<dynamic, dynamic> map) => map.map((k, v) => MapEntry(k, toEJson(v)));
@@ -150,6 +158,12 @@ extension DateTimeEJsonEncoderExtension on DateTime {
150158
EJsonValue toEJson() => _encodeDate(this);
151159
}
152160

161+
extension DBRefEJsonEncoderExtension on DBRef<dynamic> {
162+
/// Converts this [DBRef] to EJson
163+
@pragma('vm:prefer-inline')
164+
EJsonValue toEJson() => _encodeDBRef(this);
165+
}
166+
153167
extension DefinedEJsonEncoderExtension on Defined<dynamic> {
154168
/// Converts this [Defined] to EJson
155169
@pragma('vm:prefer-inline')

packages/ejson/lib/src/types.dart

+20-4
Original file line numberDiff line numberDiff line change
@@ -5,24 +5,24 @@ enum EJsonType {
55
array,
66
binary,
77
boolean,
8-
date,
8+
databaseRef,
9+
date, // use this instead of timestamp
910
decimal128,
1011
document,
1112
double,
1213
int32,
1314
int64,
1415
maxKey,
1516
minKey,
17+
nil, // aka. null
1618
objectId,
1719
string,
1820
symbol,
19-
nil, // aka. null
2021
undefined,
2122
// TODO: The following is not supported yet
2223
// code,
2324
// codeWithScope,
24-
// databasePointer,
25-
// databaseRef,
25+
// databasePointer, // deprecated
2626
// regularExpression,
2727
// timestamp, // This is not what you think, see https://www.mongodb.com/docs/manual/reference/mongodb-extended-json/#mongodb-bsontype-Timestamp
2828
}
@@ -31,6 +31,22 @@ enum EJsonType {
3131
/// and [MinKey](https://www.mongodb.com/docs/manual/reference/mongodb-extended-json/#mongodb-bsontype-MinKey)
3232
enum BsonKey { min, max }
3333

34+
/// See [DBRef](https://github.com/mongodb/specifications/blob/master/source/dbref.md)
35+
/// This is not technically a BSON type, but a common convention.
36+
final class DBRef<KeyT> {
37+
// Do we need to support the database name?
38+
final String collection;
39+
final KeyT id;
40+
41+
const DBRef(this.collection, this.id);
42+
43+
@override
44+
int get hashCode => Object.hash(collection, id);
45+
46+
@override
47+
bool operator ==(Object other) => other is DBRef<KeyT> && collection == other.collection && id == other.id;
48+
}
49+
3450
sealed class UndefinedOr<T> {
3551
const UndefinedOr();
3652
}

packages/ejson/test/ejson_test.dart

+55-1
Original file line numberDiff line numberDiff line change
@@ -10,9 +10,41 @@ import 'package:ejson/ejson.dart';
1010
import 'package:objectid/objectid.dart';
1111
import 'package:sane_uuid/uuid.dart';
1212
import 'package:test/test.dart';
13+
import 'package:type_plus/type_plus.dart';
1314

1415
import 'person.dart';
1516

17+
bool _canDecodeAny<T>([Type? type]) {
18+
commonDecoders; // ensure common types has been registered;
19+
type ??= T;
20+
if (type.isNullable) return _canDecodeAny(type.base);
21+
if ([
22+
dynamic,
23+
Null,
24+
Object,
25+
bool,
26+
double,
27+
int,
28+
num,
29+
String,
30+
DateTime,
31+
BsonKey,
32+
Symbol,
33+
ObjectId,
34+
Uuid,
35+
Uint8List,
36+
].contains(type)) return true;
37+
if ([
38+
List,
39+
Set,
40+
Map,
41+
DBRef,
42+
Undefined,
43+
UndefinedOr,
44+
].contains(type.base)) return type.args.every(_canDecodeAny);
45+
return false;
46+
}
47+
1648
void _testCase<T>(T value, EJsonValue expected) {
1749
test('encode from $value of type $T', () {
1850
expect(toEJson(value), expected);
@@ -46,7 +78,7 @@ void _testCase<T>(T value, EJsonValue expected) {
4678
expect(() => fromEJson(expected), returnsNormally);
4779
});
4880

49-
if (value is! Defined) {
81+
if (_canDecodeAny<T>()) {
5082
test('roundtrip $value of type $T as dynamic', () {
5183
// no <T> here, so dynamic
5284
expect(fromEJson(toEJson(value)), value);
@@ -82,6 +114,7 @@ void main() {
82114
expect([1, 2, 3].toEJson(), toEJson([1, 2, 3]));
83115
expect({'a': 1, 'b': 2}.toEJson(), toEJson({'a': 1, 'b': 2}));
84116
expect(DateTime(1974, 4, 10, 2, 42, 12, 202).toEJson(), toEJson(DateTime(1974, 4, 10, 2, 42, 12, 202)));
117+
expect(DBRef('collection', 42).toEJson(), toEJson(DBRef('collection', 42)));
85118
expect((#sym).toEJson(), toEJson(#sym));
86119
expect(BsonKey.max.toEJson(), toEJson(BsonKey.max));
87120
expect(BsonKey.min.toEJson(), toEJson(BsonKey.min));
@@ -105,11 +138,17 @@ void main() {
105138
group('invalid', () {
106139
_invalidTestCase<bool>();
107140
_invalidTestCase<DateTime>();
141+
_invalidTestCase<DBRef>();
142+
_invalidTestCase<DBRef<int>>();
108143
_invalidTestCase<double>({'\$numberDouble': 'foobar'});
109144
_invalidTestCase<double>();
110145
_invalidTestCase<int>();
111146
_invalidTestCase<BsonKey>();
147+
_invalidTestCase<List>();
112148
_invalidTestCase<List<int>>();
149+
_invalidTestCase<Set>();
150+
_invalidTestCase<Set<int>>();
151+
_invalidTestCase<Map>([]);
113152
_invalidTestCase<Map<int, int>>([]);
114153
_invalidTestCase<Null>();
115154
_invalidTestCase<num>();
@@ -118,6 +157,7 @@ void main() {
118157
_invalidTestCase<String>();
119158
_invalidTestCase<Symbol>();
120159
_invalidTestCase<Uint8List>();
160+
_invalidTestCase<Undefined>();
121161
_invalidTestCase<Undefined<int>>();
122162
_invalidTestCase<Uuid>();
123163

@@ -151,6 +191,16 @@ void main() {
151191
]
152192
: [1, 2, 3],
153193
);
194+
_testCase(
195+
{1, 2, 3},
196+
canonical
197+
? {
198+
{'\$numberInt': '1'},
199+
{'\$numberInt': '2'},
200+
{'\$numberInt': '3'},
201+
}
202+
: {1, 2, 3},
203+
);
154204
_testCase(
155205
[1, 1.1],
156206
canonical
@@ -190,6 +240,10 @@ void main() {
190240
_testCase(#sym, {'\$symbol': 'sym'});
191241
_testCase(BsonKey.max, {'\$maxKey': 1});
192242
_testCase(BsonKey.min, {'\$minKey': 1});
243+
_testCase(const DBRef<int>('collection', 42), {
244+
'\$ref': 'collection',
245+
'\$id': canonical ? {'\$numberInt': '42'} : 42,
246+
});
193247
_testCase(undefined, {'\$undefined': 1});
194248
_testCase(const Undefined<int?>(), {'\$undefined': 1});
195249
_testCase(Undefined<int?>(), {'\$undefined': 1});

packages/ejson_generator/test/ctor_test.dart

-7
Original file line numberDiff line numberDiff line change
@@ -85,13 +85,6 @@ void _testCase<T>(T value, EJsonValue expected) {
8585
test('roundtrip $expected as $T', () {
8686
expect(toEJson(fromEJson<T>(expected)), expected);
8787
});
88-
89-
test('roundtrip $expected of type $T as dynamic', () {
90-
// no <T> here, so dynamic
91-
final decoded = fromEJson<dynamic>(expected);
92-
expect(decoded, isA<T>());
93-
expect(toEJson(decoded), expected);
94-
});
9588
}
9689

9790
void main() {

0 commit comments

Comments
 (0)