Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

22 changes: 19 additions & 3 deletions native/dart/packages/blocks_codegen/lib/src/builder.dart
Original file line number Diff line number Diff line change
Expand Up @@ -47,10 +47,15 @@ class SealedClassType extends ResolvedType {
String name;
final String discriminant;
final List<SealedVariant> variants;

/// True when the discriminant is a boolean enum (true/false) rather than a
/// string enum. Drives bool (vs String) emission in the generator.
final bool discriminantIsBoolean;
SealedClassType({
required this.name,
required this.discriminant,
required this.variants,
this.discriminantIsBoolean = false,
});
}

Expand Down Expand Up @@ -330,7 +335,7 @@ class CodegenModelBuilder {
final fk = fieldKeys.map((f) => '${f.name}:${_typeKey(f.type)}${f.isRequired ? '!' : ''}').join(',');
return '${v.discriminantValue}{$fk}';
});
return 'sealed[${sealed.discriminant}]{${parts.join('|')}}';
return 'sealed[${sealed.discriminant}${sealed.discriminantIsBoolean ? ':bool' : ''}]{${parts.join('|')}}';
}

ResolvedType _resolveEnum(List<String> values, String? hint, [String? path]) {
Expand Down Expand Up @@ -405,7 +410,13 @@ class CodegenModelBuilder {
final variants = groups.entries.map((entry) {
final discValue = entry.key;
final group = entry.value;
final className = '${_capitalize(discValue)}${_inferSuffix(name)}';
// String discriminants keep the established `<Value><SealedName>` scheme
// (e.g. `EmailGetNotificationResult`). A boolean discriminant has no
// descriptive value, so name the arm after the field + value
// (e.g. `IsUpdatedTrue`/`IsUpdatedFalse`), mirroring the Kotlin output.
final className = ref.discriminantIsBoolean
? '${_capitalize(ref.discriminant)}${_capitalize(discValue)}'
: '${_capitalize(discValue)}${_inferSuffix(name)}';
final variantPath = path == null ? null : '$path>$discValue';

final List<RecordField> fields;
Expand Down Expand Up @@ -454,7 +465,12 @@ class CodegenModelBuilder {
);
}).toList();

final sealed = SealedClassType(name: name, discriminant: ref.discriminant, variants: variants);
final sealed = SealedClassType(
name: name,
discriminant: ref.discriminant,
variants: variants,
discriminantIsBoolean: ref.discriminantIsBoolean,
);

// Structural dedup: reuse an existing sealed class with the same shape
// (ported from #682 — recursive structural + sealed-class dedup).
Expand Down
68 changes: 61 additions & 7 deletions native/dart/packages/blocks_codegen/lib/src/generator.dart
Original file line number Diff line number Diff line change
Expand Up @@ -265,12 +265,23 @@ class DartCodeGenerator {
buf.writeln(' const ${sealed.name}();');
buf.writeln(' Map<String, dynamic> toJson();');
buf.writeln(' static ${sealed.name} fromJson(Map<String, dynamic> json) {');
buf.writeln(" switch (json['${sealed.discriminant}'] as String) {");
for (final v in sealed.variants) {
buf.writeln(" case '${v.discriminantValue}': return ${v.className}.fromJson(json);");
if (sealed.discriminantIsBoolean) {
// Boolean discriminant: switch on the real bool; case labels and the
// serialized value are unquoted true/false literals. true/false are
// exhaustive for a bool, so no (unreachable) default clause is emitted.
buf.writeln(" switch (json['${sealed.discriminant}'] as bool) {");
for (final v in sealed.variants) {
buf.writeln(" case ${v.discriminantValue}: return ${v.className}.fromJson(json);");
}
buf.writeln(' }');
} else {
buf.writeln(" switch (json['${sealed.discriminant}'] as String) {");
for (final v in sealed.variants) {
buf.writeln(" case '${v.discriminantValue}': return ${v.className}.fromJson(json);");
}
buf.writeln(" default: throw ArgumentError('Unknown ${sealed.discriminant}: \${json['${sealed.discriminant}']}');");
buf.writeln(' }');
}
buf.writeln(" default: throw ArgumentError('Unknown ${sealed.discriminant}: \${json['${sealed.discriminant}']}');");
buf.writeln(' }');
buf.writeln(' }');
buf.writeln('}');
buf.writeln();
Expand Down Expand Up @@ -328,7 +339,11 @@ class DartCodeGenerator {
buf.writeln(' @override');
buf.writeln(' Map<String, dynamic> toJson() {');
buf.writeln(' return {');
buf.writeln(" '${sealed.discriminant}': '${v.discriminantValue}',");
if (sealed.discriminantIsBoolean) {
buf.writeln(" '${sealed.discriminant}': ${v.discriminantValue},");
} else {
buf.writeln(" '${sealed.discriminant}': '${v.discriminantValue}',");
}
for (final f in v.fields) {
final ident = _escapeIdentifier(f.name);
final expr = _toJsonExpr(ident, f.type, allTypes, !f.isRequired);
Expand Down Expand Up @@ -712,7 +727,7 @@ class DartCodeGenerator {
PrimitiveType(dartType: final dt) => dt == 'int' ? '($accessor as num).toInt()' : '$accessor as $dt',
NullableType(inner: final inner) => _deserializeNullable(accessor, inner, allTypes),
ListType(items: final items) => _deserializeList(accessor, items, allTypes),
MapType(valueType: final vt) => '($accessor as Map<String, dynamic>).map((k, v) => MapEntry(k, v as ${_dartTypeStr(vt, allTypes)}))',
MapType(valueType: final vt) => '($accessor as Map<String, dynamic>).map((k, v) => MapEntry(k, ${_mapValueFromJson('v', vt, allTypes)}))',
RecordType(name: final name) => '$name.fromJson($accessor as Map<String, dynamic>)',
SchemaReference(name: final name) => _deserializeSchema(accessor, name, allTypes),
SealedClassType(name: final name) => '$name.fromJson($accessor as Map<String, dynamic>)',
Expand All @@ -729,10 +744,49 @@ class DartCodeGenerator {
'$accessor == null ? null : $name.fromJson($accessor as Map<String, dynamic>)',
SchemaReference(name: final name) =>
'$accessor == null ? null : $name.fromJson($accessor as Map<String, dynamic>)',
SealedClassType(name: final name) =>
'$accessor == null ? null : $name.fromJson($accessor as Map<String, dynamic>)',
_ => '$accessor',
};
}

/// Per-value deserialization for a `Map<String, V>` entry value `v` (dynamic).
/// Primitive value types keep the plain `v as T` cast (byte-identical to the
/// prior output); object-like value types (records, sealed classes) — and the
/// nullable variants thereof — are decoded via their generated `fromJson`.
String _mapValueFromJson(String v, ResolvedType valueType, Map<String, ResolvedType> allTypes) {
switch (valueType) {
case SealedClassType(name: final n):
return '$n.fromJson($v as Map<String, dynamic>)';
case RecordType(name: final n):
return '$n.fromJson($v as Map<String, dynamic>)';
case SchemaReference(name: final n):
final resolved = allTypes[n];
if (resolved is RecordType || resolved is SealedClassType) {
return '$n.fromJson($v as Map<String, dynamic>)';
}
if (resolved is EnumType) {
return '$n.fromJson($v as String)';
}
return '$v as $n';
case NullableType(inner: final inner):
final innerName = switch (inner) {
SealedClassType(name: final n) => n,
RecordType(name: final n) => n,
SchemaReference(name: final n)
when allTypes[n] is RecordType || allTypes[n] is SealedClassType =>
n,
_ => null,
};
if (innerName != null) {
return '$v == null ? null : $innerName.fromJson($v as Map<String, dynamic>)';
}
return '$v as ${_dartTypeStr(valueType, allTypes)}';
default:
return '$v as ${_dartTypeStr(valueType, allTypes)}';
}
}

String _deserializeList(String accessor, ResolvedType items, Map<String, ResolvedType> allTypes) {
final itemExpr = switch (items) {
RecordType(name: final name) => '(e) => $name.fromJson(e as Map<String, dynamic>)',
Expand Down
Loading
Loading