Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

RDART-1010: Allow configuration of generator per realm model class #1641

Merged
merged 8 commits into from
Apr 19, 2024
Merged
Show file tree
Hide file tree
Changes from 7 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
20 changes: 19 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -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
Expand Down
47 changes: 40 additions & 7 deletions packages/realm_common/lib/src/realm_common_base.dart
Original file line number Diff line number Diff line change
Expand Up @@ -20,10 +20,9 @@ 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;
}

Expand All @@ -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;
Comment on lines +60 to +61
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Are you okay with this?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think it's a big deal. I don't imagine anyone has any code that reads this value.


/// 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.
Expand Down
18 changes: 6 additions & 12 deletions packages/realm_common/lib/src/realm_types.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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'
};
Comment on lines +88 to +93
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A small drive by edit. Not related to this PR

}

/// A base class of all Realm errors.
Expand Down
14 changes: 13 additions & 1 deletion packages/realm_generator/lib/src/class_element_ex.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down
2 changes: 1 addition & 1 deletion packages/realm_generator/lib/src/dart_type_ex.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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];
}

Expand Down
58 changes: 37 additions & 21 deletions packages/realm_generator/lib/src/realm_model_info.dart
Original file line number Diff line number Diff line change
Expand Up @@ -11,14 +11,26 @@ extension<T> on Iterable<T> {
Iterable<T> 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;
final String realmName;
final List<RealmFieldInfo> 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<String> toCode() sync* {
yield 'class $name extends $modelName with RealmEntity, RealmObjectBase, ${baseType.className} {';
Expand All @@ -32,26 +44,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) => config.ctorStyle == CtorStyle.allNamed ? false : required(f);
nielsenko marked this conversation as resolved.
Show resolved Hide resolved
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<String, ';
return '$collectionPrefix${f.type.basicMappedName}> ${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<String,',
_ => '',
};
final typePrefix = f.isRealmCollection ? '$collectionPrefix${f.type.basicMappedName}>' : f.mappedTypeName;
return '$requiredPrefix$typePrefix $param${f.initializer},';
});
yield '}';
}
Expand All @@ -67,13 +82,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 '}';
Expand Down Expand Up @@ -129,8 +145,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),';
Expand Down
12 changes: 12 additions & 0 deletions packages/realm_generator/test/good_test_data/all_named_ctor.dart
Original file line number Diff line number Diff line change
@@ -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;
}
Original file line number Diff line number Diff line change
@@ -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,
}) {
Comment on lines +13 to +16
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The interesting bit

if (!_defaultsSet) {
_defaultsSet = RealmObjectBase.setDefaults<Person>({
'age': 42,
});
}
RealmObjectBase.set(this, 'name', name);
RealmObjectBase.set(this, 'age', age);
}

Person._();

@override
String get name => RealmObjectBase.get<String>(this, 'name') as String;
@override
set name(String value) => RealmObjectBase.set(this, 'name', value);

@override
int get age => RealmObjectBase.get<int>(this, 'age') as int;
@override
set age(int value) => RealmObjectBase.set(this, 'age', value);

@override
Stream<RealmObjectChanges<Person>> get changes =>
RealmObjectBase.getChanges<Person>(this);

@override
Stream<RealmObjectChanges<Person>> changesFor([List<String>? keyPaths]) =>
RealmObjectBase.getChangesFor<Person>(this, keyPaths);

@override
Person freeze() => RealmObjectBase.freezeObject<Person>(this);

EJsonValue toEJson() {
return <String, dynamic>{
'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;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@

part of 'all_named_ctor.dart';
Original file line number Diff line number Diff line change
Expand Up @@ -13,15 +13,15 @@ class WithPrivateFields extends _WithPrivateFields

WithPrivateFields(
String _plain, {
int _withDefault = 0,
int withDefault = 0,
Copy link
Contributor Author

@nielsenko nielsenko Apr 18, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is actually fixing a bug in version 2.1.0. Named arguments cannot start with underscore.

}) {
if (!_defaultsSet) {
_defaultsSet = RealmObjectBase.setDefaults<WithPrivateFields>({
'_withDefault': 0,
});
}
RealmObjectBase.set(this, '_plain', _plain);
RealmObjectBase.set(this, '_withDefault', _withDefault);
RealmObjectBase.set(this, '_withDefault', withDefault);
}

WithPrivateFields._();
Expand Down