From 4c2cc695ed290d6f5b0ba9a6ab6bf2b530a16691 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kasper=20Overg=C3=A5rd=20Nielsen?= Date: Fri, 18 Aug 2023 16:00:02 +0200 Subject: [PATCH] RealmValue interface changes to support collections in mixed --- common/lib/src/realm_types.dart | 153 +++++++++++++++++++++++++++----- test/realm_value_test.dart | 7 +- 2 files changed, 136 insertions(+), 24 deletions(-) diff --git a/common/lib/src/realm_types.dart b/common/lib/src/realm_types.dart index b3c1545ff..074de3d67 100644 --- a/common/lib/src/realm_types.dart +++ b/common/lib/src/realm_types.dart @@ -16,7 +16,6 @@ // //////////////////////////////////////////////////////////////////////////////// -import 'dart:ffi'; import 'dart:typed_data'; import 'package:objectid/objectid.dart'; import 'package:sane_uuid/uuid.dart'; @@ -172,10 +171,20 @@ abstract class AsymmetricObjectMarker implements RealmObjectBaseMarker {} /// } /// ``` class RealmValue { + static const true_ = RealmValue.bool(true); + static const false_ = RealmValue.bool(false); + static const null_ = RealmValue.nullValue(); + final Object? value; Type get type => value.runtimeType; T as() => value as T; // better for code completion + List get asList => as>(); + Map get asDictionary => as>(); + Set get asSet => as>(); + + T call() => as(); // is this useful? + // This is private, so user cannot accidentally construct an invalid instance const RealmValue._(this.value); @@ -191,34 +200,90 @@ class RealmValue { const RealmValue.decimal128(Decimal128 decimal) : this._(decimal); const RealmValue.uuid(Uuid uuid) : this._(uuid); const RealmValue.uint8List(Uint8List binary) : this._(binary); + const RealmValue.dictionary(Map dictionary) : this._(dictionary); + const RealmValue.list(Iterable list) : this._(list); + const RealmValue.set(Set set) : this._(set); + + static bool _isCollection(Object? value) => value is List || value is Set || value is Map; - /// Will throw [ArgumentError] + /// Will throw [ArgumentError] if the type is not supported factory RealmValue.from(Object? object) { - if (object == null || - object is bool || - object is String || - object is int || - object is Float || - object is double || - object is RealmObjectMarker || - object is DateTime || - object is ObjectId || - object is Decimal128 || - object is Uuid || - object is Uint8List) { - return RealmValue._(object); - } else { - throw ArgumentError.value(object, 'object', 'Unsupported type'); + return switch (object) { + Object? o + when o == null || + o is bool || + o is String || + o is int || + o is double || + o is RealmObjectMarker || + o is DateTime || + o is ObjectId || + o is Decimal128 || + o is Uuid || + o is Uint8List => + RealmValue._(o), + Map d => RealmValue.dictionary(d), + Map d => RealmValue.dictionary(Map.fromEntries(d.entries.map((e) => MapEntry(e.key, RealmValue.from(e.value))))), + List l => RealmValue.list(l), + List l => RealmValue.list(l.map((o) => RealmValue.from(o)).toList()), + Set s => RealmValue.set(s), + Set s => RealmValue.set(s.map((o) => RealmValue.from(o)).toSet()), + _ => throw ArgumentError.value(object.runtimeType, 'object', 'Unsupported type'), + }; + } + + RealmValue operator [](Object? index) { + final v = value; + return switch (index) { + int i when v is List => v[i], // throws on range error + String s when v is Map => v[s], + Iterable l when _isCollection(v) => this[l.first][l.skip(1)], + _ => throw ArgumentError.value(index, 'index', 'Unsupported type'), + } ?? + const RealmValue.nullValue(); + } + + void operator []=(Object? index, RealmValue value) { + final v = this.value; + switch (index) { + case int i when v is List: + v[i] = value; + break; + case String s when v is Map: + v[s] = value; + break; + case Iterable l when _isCollection(v): + this[l.first][l.skip(1)] = value; + break; + default: + throw ArgumentError.value(index, 'index', 'Unsupported type'); } } + RealmValue lookup(T item) { + final v = value as Set?; + if (v == null) throw ArgumentError.value(item, 'item', 'Unsupported type'); // TODO: Wrong exception + return v.lookup(item) ?? const RealmValue.nullValue(); + } + + bool add(T item) { + if (_isCollection(item)) throw ArgumentError.value(item, 'item', 'Unsupported type'); // TODO: Wrong exception + final v = value as Set?; + if (v == null) throw ArgumentError.value(item, 'item', 'Unsupported type'); // TODO: Wrong exception + return v.add(RealmValue.from(item)); + } + + bool remove(T item) { + final v = value as Set?; + if (v == null) throw ArgumentError.value(item, 'item', 'Unsupported type'); // TODO: Wrong exception + return v.remove(item); + } + @override operator ==(Object? other) { - if (other is RealmValue) { - return value == other.value; - } - - return value == other; + if (identical(this, other)) return true; + if (other is! RealmValue) return false; + return value == other.value; } @override @@ -227,3 +292,47 @@ class RealmValue { @override String toString() => 'RealmValue($value)'; } + +void demo() { + final any = RealmValue.from([ + 1, + 2, + { + 'x': {1, 2} + }, + ]); + + if (any[2]['x'].lookup(1) != const RealmValue.nullValue()) {} + + // access list element at index 2, + // then map element with key 'x', + // then set element 1, if it exists + // assuming an int, or null if not found + final x = any[2]['x'].lookup(1).as(); + assert(x == 1); + + // or, a bit shorter + final y = any[2]['x'].lookup(1)(); + assert(y == 1); + + // or, if you are sure + int z = any[2]['x'].lookup(1)(); // <-- shortest + assert(z == 1); + + // or, using a list of indexes + final u = any[[2, 'x']](); + assert(u == RealmValue.from({1, 2})); + + // which allows for a different layout + int v = any[[ + 2, + 'x', + ]] + .lookup(1)(); + assert(v == 1); + + any[1] = RealmValue.from({'z': 'foo'}); // replace int with a map + any[2]['x'].add(3); // add an element to the set + any[2]['x'] = RealmValue.from(1); // replace set with an int + any[2]['y'] = RealmValue.from(true); // add a new key to the map +} diff --git a/test/realm_value_test.dart b/test/realm_value_test.dart index 0f34ebdcc..0b865bedb 100644 --- a/test/realm_value_test.dart +++ b/test/realm_value_test.dart @@ -59,7 +59,10 @@ Future main([List? args]) async { ObjectId.fromTimestamp(now), Uuid.v4(), Decimal128.fromDouble(128.128), - Uint8List.fromList([1, 2, 0]) + Uint8List.fromList([1, 2, 0]), + [1, 2, 3], + {1, 2, 3}, + {'a': 1, 'b': 2}, ]; for (final x in values) { @@ -80,7 +83,7 @@ Future main([List? args]) async { test('Illegal value', () { final config = Configuration.local([AnythingGoes.schema, Stuff.schema, TuckedIn.schema]); final realm = getRealm(config); - expect(() => realm.write(() => realm.add(AnythingGoes(oneAny: RealmValue.from([1, 2])))), throwsArgumentError); + expect(() => realm.write(() => realm.add(AnythingGoes(oneAny: RealmValue.from((1, 2))))), throwsArgumentError); // records not supported }); test('Embedded object not allowed in RealmValue', () {