From fccede0889cb6cb2893db931e18d05a04f52501e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kasper=20Overg=C3=A5rd=20Nielsen?= Date: Wed, 17 Apr 2024 09:40:12 +0200 Subject: [PATCH 1/8] Named params cannot start with _ --- .../lib/src/realm_model_info.dart | 48 +++++++++++-------- .../good_test_data/private_fields.expected | 4 +- 2 files changed, 30 insertions(+), 22 deletions(-) diff --git a/packages/realm_generator/lib/src/realm_model_info.dart b/packages/realm_generator/lib/src/realm_model_info.dart index eb87a9389..e2dec10ae 100644 --- a/packages/realm_generator/lib/src/realm_model_info.dart +++ b/packages/realm_generator/lib/src/realm_model_info.dart @@ -11,6 +11,10 @@ extension on Iterable { Iterable except(bool Function(T) test) => where((e) => !test(e)); } +extension on String { + String nonPrivate() => startsWith('_') ? substring(1) : this; +} + class RealmModelInfo { final String name; final String modelName; @@ -32,26 +36,29 @@ class RealmModelInfo { yield ''; } - bool requiredCondition(RealmFieldInfo f) => f.isRequired || f.isPrimaryKey; - final required = allSettable.where(requiredCondition); - final notRequired = allSettable.except(requiredCondition); + bool required(RealmFieldInfo f) => f.isRequired || f.isPrimaryKey; + bool usePositional(RealmFieldInfo f) => required(f); + String paramName(RealmFieldInfo f) => usePositional(f) ? f.name : f.name.nonPrivate(); + final positional = allSettable.where(usePositional); + final named = allSettable.except(usePositional); // Constructor yield '$name('; { - yield* required.map((f) => '${f.mappedTypeName} ${f.name},'); - if (notRequired.isNotEmpty) { + yield* positional.map((f) => '${f.mappedTypeName} ${paramName(f)},'); + if (named.isNotEmpty) { yield '{'; - yield* notRequired.map((f) { - if (f.isRealmCollection) { - final collectionPrefix = f.type.isDartCoreList - ? 'Iterable<' - : f.type.isDartCoreSet - ? 'Set<' - : 'Map ${f.name}${f.initializer},'; - } - return '${f.mappedTypeName} ${f.name}${f.initializer},'; + yield* named.map((f) { + final requiredPrefix = required(f) ? 'required ' : ''; + final param = paramName(f); + final collectionPrefix = switch(f) { + _ when f.isDartCoreList => 'Iterable<', + _ when f.isDartCoreSet => 'Set<', + _ when f.isDartCoreMap => 'Map '', + }; + final typePrefix = f.isRealmCollection ? '$collectionPrefix${f.type.basicMappedName}>' : f.mappedTypeName; + return '$requiredPrefix$typePrefix $param${f.initializer},'; }); yield '}'; } @@ -67,13 +74,14 @@ class RealmModelInfo { } yield* allSettable.map((f) { + final param = paramName(f); if (f.type.isUint8List && f.hasDefaultValue) { - return "RealmObjectBase.set(this, '${f.realmName}', ${f.name} ?? ${f.fieldElement.initializerExpression});"; + return "RealmObjectBase.set(this, '${f.realmName}', $param ?? ${f.fieldElement.initializerExpression});"; } if (f.isRealmCollection) { - return "RealmObjectBase.set<${f.mappedTypeName}>(this, '${f.realmName}', ${f.mappedTypeName}(${f.name}));"; + return "RealmObjectBase.set<${f.mappedTypeName}>(this, '${f.realmName}', ${f.mappedTypeName}($param));"; } - return "RealmObjectBase.set(this, '${f.realmName}', ${f.name});"; + return "RealmObjectBase.set(this, '${f.realmName}', $param);"; }); } yield '}'; @@ -129,8 +137,8 @@ class RealmModelInfo { } yield '} => $name('; { - yield* required.map((f) => 'fromEJson(${f.name}),'); - yield* notRequired.map((f) => '${f.name}: fromEJson(${f.name}),'); + yield* positional.map((f) => 'fromEJson(${f.name}),'); + yield* named.map((f) => '${f.name}: fromEJson(${f.name}),'); } yield '),'; yield '_ => raiseInvalidEJson(ejson),'; diff --git a/packages/realm_generator/test/good_test_data/private_fields.expected b/packages/realm_generator/test/good_test_data/private_fields.expected index ba36387bc..368fd483e 100644 --- a/packages/realm_generator/test/good_test_data/private_fields.expected +++ b/packages/realm_generator/test/good_test_data/private_fields.expected @@ -13,7 +13,7 @@ class WithPrivateFields extends _WithPrivateFields WithPrivateFields( String _plain, { - int _withDefault = 0, + int withDefault = 0, }) { if (!_defaultsSet) { _defaultsSet = RealmObjectBase.setDefaults({ @@ -21,7 +21,7 @@ class WithPrivateFields extends _WithPrivateFields }); } RealmObjectBase.set(this, '_plain', _plain); - RealmObjectBase.set(this, '_withDefault', _withDefault); + RealmObjectBase.set(this, '_withDefault', withDefault); } WithPrivateFields._(); From 16af902c1e842835267453ce52b5b3fcb9cd4a2b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kasper=20Overg=C3=A5rd=20Nielsen?= Date: Wed, 17 Apr 2024 10:05:41 +0200 Subject: [PATCH 2/8] Introduce GeneratorConfig class This can be used to configure the style of constructor to use when generating realm object classes. A new RealmModel.using ctor allows the user to pass an GeneratorConfig instance when annotating a class. --- .../lib/src/realm_common_base.dart | 45 ++++++++++++++++--- .../realm_generator/lib/src/dart_type_ex.dart | 2 +- 2 files changed, 40 insertions(+), 7 deletions(-) diff --git a/packages/realm_common/lib/src/realm_common_base.dart b/packages/realm_common/lib/src/realm_common_base.dart index caea8a336..ab9ad07cf 100644 --- a/packages/realm_common/lib/src/realm_common_base.dart +++ b/packages/realm_common/lib/src/realm_common_base.dart @@ -23,7 +23,6 @@ enum ObjectType { const ObjectType([this._className = 'Unknown', this._flags = -1]); final String _className; - final int _flags; } @@ -33,15 +32,49 @@ extension ObjectTypeInternal on ObjectType { String get className => _className; } +/// An enum controlling the constructor type generated for a [RealmModel]. +enum CtorStyle { + /// Generate a constructor with only optional parameters named. + /// All required parameters will be positional. + /// This is the default, unless overridden in the build config. + onlyOptionalNamed, + + /// Generate a constructor with all parameters named. + allNamed, +} + +/// Class used to define the desired constructor behavior for a [RealmModel]. +/// +/// {@category Annotations} +class GeneratorConfig { + /// The style to use for the generated constructor + final CtorStyle ctorStyle; + + const GeneratorConfig({this.ctorStyle = CtorStyle.onlyOptionalNamed}); +} + /// Annotation class used to define `Realm` data model classes and their properties /// /// {@category Annotations} class RealmModel { - /// The base type of the object - final ObjectType type; - - /// Creates a new instance of [RealmModel] specifying the desired base type. - const RealmModel([this.type = ObjectType.realmObject]); + /// The base type of the generated object class + final ObjectType baseType; + + /// The generator configuration to use for this model + final GeneratorConfig generatorConfig; + + // NOTE: To avoid a breaking change, we keep this old constructor and add a new one + /// Creates a new instance of [RealmModel] optionally specifying the [baseType]. + const RealmModel([ + ObjectType baseType = ObjectType.realmObject, + ]) : this.using(baseType: baseType); + + /// Creates a new instance of [RealmModel] optionally specifying the [baseType] + /// and [generatorConfig]. + const RealmModel.using({ + this.baseType = ObjectType.realmObject, + this.generatorConfig = const GeneratorConfig(), + }); } /// MapTo annotation for class level and class member level. diff --git a/packages/realm_generator/lib/src/dart_type_ex.dart b/packages/realm_generator/lib/src/dart_type_ex.dart index 2cdf2a179..5a0b5de6e 100644 --- a/packages/realm_generator/lib/src/dart_type_ex.dart +++ b/packages/realm_generator/lib/src/dart_type_ex.dart @@ -24,7 +24,7 @@ extension DartTypeEx on DartType { if (element == null) return null; final realmModelAnnotation = realmModelChecker.firstAnnotationOfExact(element!); if (realmModelAnnotation == null) return null; // not a RealmModel - final index = realmModelAnnotation.getField('type')!.getField('index')!.toIntValue()!; + final index = realmModelAnnotation.getField('baseType')!.getField('index')!.toIntValue()!; return ObjectType.values[index]; } From e112bbf85d0547727b89ca82a51ca096f57fd7fd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kasper=20Overg=C3=A5rd=20Nielsen?= Date: Wed, 17 Apr 2024 10:08:37 +0200 Subject: [PATCH 3/8] Fix oddly optional params on ObjectType --- packages/realm_common/lib/src/realm_common_base.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/realm_common/lib/src/realm_common_base.dart b/packages/realm_common/lib/src/realm_common_base.dart index ab9ad07cf..e89b97b27 100644 --- a/packages/realm_common/lib/src/realm_common_base.dart +++ b/packages/realm_common/lib/src/realm_common_base.dart @@ -20,7 +20,7 @@ enum ObjectType { /// to query or modify it. asymmetricObject('AsymmetricObject', 2); - const ObjectType([this._className = 'Unknown', this._flags = -1]); + const ObjectType(this._className, this._flags); final String _className; final int _flags; From d33b858e543de13b50676999ead34339bfffc031 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kasper=20Overg=C3=A5rd=20Nielsen?= Date: Thu, 18 Apr 2024 11:09:17 +0200 Subject: [PATCH 4/8] Simplify with switch expression --- packages/realm_common/lib/src/realm_types.dart | 18 ++++++------------ 1 file changed, 6 insertions(+), 12 deletions(-) diff --git a/packages/realm_common/lib/src/realm_types.dart b/packages/realm_common/lib/src/realm_types.dart index bc9304b67..48ab76ebf 100644 --- a/packages/realm_common/lib/src/realm_types.dart +++ b/packages/realm_common/lib/src/realm_types.dart @@ -85,18 +85,12 @@ enum RealmCollectionType { _3, // ignore: unused_field, constant_identifier_names map; - String get plural { - switch (this) { - case RealmCollectionType.list: - return "lists"; - case RealmCollectionType.set: - return "sets"; - case RealmCollectionType.map: - return "maps"; - default: - return "none"; - } - } + String get plural => switch (this) { + RealmCollectionType.list => 'lists', + RealmCollectionType.set => 'sets', + RealmCollectionType.map => 'maps', + _ => 'none' + }; } /// A base class of all Realm errors. From cbc4be02342e760b5b380942e1db97347af777d1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kasper=20Overg=C3=A5rd=20Nielsen?= Date: Thu, 18 Apr 2024 13:39:58 +0200 Subject: [PATCH 5/8] Read GeneratorConfig from RealmModel annotation --- .../lib/src/class_element_ex.dart | 14 +++++++++++++- .../lib/src/realm_model_info.dart | 16 ++++++++++++---- 2 files changed, 25 insertions(+), 5 deletions(-) diff --git a/packages/realm_generator/lib/src/class_element_ex.dart b/packages/realm_generator/lib/src/class_element_ex.dart index 00c8fd0e4..4f35d4b55 100644 --- a/packages/realm_generator/lib/src/class_element_ex.dart +++ b/packages/realm_generator/lib/src/class_element_ex.dart @@ -202,7 +202,19 @@ extension ClassElementEx on ClassElement { } } - return RealmModelInfo(name, modelName, realmName, mappedFields, objectType); + // Get the generator configuration + final index = realmModelInfo?.value.getField('generatorConfig')?.getField('ctorStyle')?.getField('index')?.toIntValue(); + final ctorStyle = index != null ? CtorStyle.values[index] : CtorStyle.onlyOptionalNamed; + final config = GeneratorConfig(ctorStyle: ctorStyle); + + return RealmModelInfo( + name, + modelName, + realmName, + mappedFields, + objectType, + config, + ); } on InvalidGenerationSourceError catch (_) { rethrow; } catch (e, s) { diff --git a/packages/realm_generator/lib/src/realm_model_info.dart b/packages/realm_generator/lib/src/realm_model_info.dart index e2dec10ae..776bc82c0 100644 --- a/packages/realm_generator/lib/src/realm_model_info.dart +++ b/packages/realm_generator/lib/src/realm_model_info.dart @@ -21,8 +21,16 @@ class RealmModelInfo { final String realmName; final List fields; final ObjectType baseType; + final GeneratorConfig config; - const RealmModelInfo(this.name, this.modelName, this.realmName, this.fields, this.baseType); + const RealmModelInfo( + this.name, + this.modelName, + this.realmName, + this.fields, + this.baseType, + this.config, + ); Iterable toCode() sync* { yield 'class $name extends $modelName with RealmEntity, RealmObjectBase, ${baseType.className} {'; @@ -37,7 +45,7 @@ class RealmModelInfo { } bool required(RealmFieldInfo f) => f.isRequired || f.isPrimaryKey; - bool usePositional(RealmFieldInfo f) => required(f); + bool usePositional(RealmFieldInfo f) => config.ctorStyle == CtorStyle.allNamed ? false : required(f); String paramName(RealmFieldInfo f) => usePositional(f) ? f.name : f.name.nonPrivate(); final positional = allSettable.where(usePositional); final named = allSettable.except(usePositional); @@ -51,12 +59,12 @@ class RealmModelInfo { yield* named.map((f) { final requiredPrefix = required(f) ? 'required ' : ''; final param = paramName(f); - final collectionPrefix = switch(f) { + final collectionPrefix = switch (f) { _ when f.isDartCoreList => 'Iterable<', _ when f.isDartCoreSet => 'Set<', _ when f.isDartCoreMap => 'Map '', - }; + }; final typePrefix = f.isRealmCollection ? '$collectionPrefix${f.type.basicMappedName}>' : f.mappedTypeName; return '$requiredPrefix$typePrefix $param${f.initializer},'; }); From 498a46a58faee38c62b53303ee6206a2a371ba30 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kasper=20Overg=C3=A5rd=20Nielsen?= Date: Thu, 18 Apr 2024 13:46:02 +0200 Subject: [PATCH 6/8] Add test for CtorStyle.allNamed --- .../test/good_test_data/all_named_ctor.dart | 12 +++ .../good_test_data/all_named_ctor.expected | 82 +++++++++++++++++++ .../good_test_data/all_named_ctor.realm.dart | 2 + 3 files changed, 96 insertions(+) create mode 100644 packages/realm_generator/test/good_test_data/all_named_ctor.dart create mode 100644 packages/realm_generator/test/good_test_data/all_named_ctor.expected create mode 100644 packages/realm_generator/test/good_test_data/all_named_ctor.realm.dart diff --git a/packages/realm_generator/test/good_test_data/all_named_ctor.dart b/packages/realm_generator/test/good_test_data/all_named_ctor.dart new file mode 100644 index 000000000..0ca97194d --- /dev/null +++ b/packages/realm_generator/test/good_test_data/all_named_ctor.dart @@ -0,0 +1,12 @@ +import 'package:realm_common/realm_common.dart'; + +part 'all_named_ctor.realm.dart'; + +const config = GeneratorConfig(ctorStyle: CtorStyle.allNamed); +const realmModel = RealmModel.using(baseType: ObjectType.realmObject, generatorConfig: config); + +@realmModel +class _Person { + late String name; + int age = 42; +} diff --git a/packages/realm_generator/test/good_test_data/all_named_ctor.expected b/packages/realm_generator/test/good_test_data/all_named_ctor.expected new file mode 100644 index 000000000..295a2722f --- /dev/null +++ b/packages/realm_generator/test/good_test_data/all_named_ctor.expected @@ -0,0 +1,82 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'all_named_ctor.dart'; + +// ************************************************************************** +// RealmObjectGenerator +// ************************************************************************** + +// ignore_for_file: type=lint +class Person extends _Person with RealmEntity, RealmObjectBase, RealmObject { + static var _defaultsSet = false; + + Person({ + required String name, + int age = 42, + }) { + if (!_defaultsSet) { + _defaultsSet = RealmObjectBase.setDefaults({ + 'age': 42, + }); + } + RealmObjectBase.set(this, 'name', name); + RealmObjectBase.set(this, 'age', age); + } + + Person._(); + + @override + String get name => RealmObjectBase.get(this, 'name') as String; + @override + set name(String value) => RealmObjectBase.set(this, 'name', value); + + @override + int get age => RealmObjectBase.get(this, 'age') as int; + @override + set age(int value) => RealmObjectBase.set(this, 'age', value); + + @override + Stream> get changes => + RealmObjectBase.getChanges(this); + + @override + Stream> changesFor([List? keyPaths]) => + RealmObjectBase.getChangesFor(this, keyPaths); + + @override + Person freeze() => RealmObjectBase.freezeObject(this); + + EJsonValue toEJson() { + return { + 'name': name.toEJson(), + 'age': age.toEJson(), + }; + } + + static EJsonValue _toEJson(Person value) => value.toEJson(); + static Person _fromEJson(EJsonValue ejson) { + return switch (ejson) { + { + 'name': EJsonValue name, + 'age': EJsonValue age, + } => + Person( + name: fromEJson(name), + age: fromEJson(age), + ), + _ => raiseInvalidEJson(ejson), + }; + } + + static final schema = () { + RealmObjectBase.registerFactory(Person._); + register(_toEJson, _fromEJson); + return SchemaObject(ObjectType.realmObject, Person, 'Person', [ + SchemaProperty('name', RealmPropertyType.string), + SchemaProperty('age', RealmPropertyType.int), + ]); + }(); + + @override + SchemaObject get objectSchema => RealmObjectBase.getSchema(this) ?? schema; +} diff --git a/packages/realm_generator/test/good_test_data/all_named_ctor.realm.dart b/packages/realm_generator/test/good_test_data/all_named_ctor.realm.dart new file mode 100644 index 000000000..a5a96725a --- /dev/null +++ b/packages/realm_generator/test/good_test_data/all_named_ctor.realm.dart @@ -0,0 +1,2 @@ + +part of 'all_named_ctor.dart'; From 9ca12977343d23c83e3cf6140741bb638ee52238 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kasper=20Overg=C3=A5rd=20Nielsen?= Date: Thu, 18 Apr 2024 14:09:03 +0200 Subject: [PATCH 7/8] Update CHANGELOG --- CHANGELOG.md | 20 +++++++++++++++++++- 1 file changed, 19 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 7843f25de..c892b82ba 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,7 +1,25 @@ ## vNext (TBD) ### Enhancements -* None +* Allow configuration of generator per model class. Currently support specifying the constructor style to use. + ```dart + const config = GeneratorConfig(ctorStyle: CtorStyle.allNamed); + const realmModel = RealmModel.using(baseType: ObjectType.realmObject, generatorConfig: config); + + @realmModel + class _Person { + late String name; + int age = 42; + } + ``` + will generate a constructor like: + ```dart + Person({ + required String name, + int age = 42, + }) { ... } + ``` + (Issue [#292](https://github.com/realm/realm-dart/issues/292)) ### Fixed * None From 36a43af7c738da4f0a61f949c39dd2365d202726 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kasper=20Overg=C3=A5rd=20Nielsen?= Date: Thu, 18 Apr 2024 14:49:11 +0200 Subject: [PATCH 8/8] Update packages/realm_generator/lib/src/realm_model_info.dart Co-authored-by: Nikola Irinchev --- packages/realm_generator/lib/src/realm_model_info.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/realm_generator/lib/src/realm_model_info.dart b/packages/realm_generator/lib/src/realm_model_info.dart index 776bc82c0..b86decdb2 100644 --- a/packages/realm_generator/lib/src/realm_model_info.dart +++ b/packages/realm_generator/lib/src/realm_model_info.dart @@ -45,7 +45,7 @@ class RealmModelInfo { } bool required(RealmFieldInfo f) => f.isRequired || f.isPrimaryKey; - bool usePositional(RealmFieldInfo f) => config.ctorStyle == CtorStyle.allNamed ? false : required(f); + bool usePositional(RealmFieldInfo f) => config.ctorStyle != CtorStyle.allNamed && required(f); String paramName(RealmFieldInfo f) => usePositional(f) ? f.name : f.name.nonPrivate(); final positional = allSettable.where(usePositional); final named = allSettable.except(usePositional);