From 844f1855a566c91a8a8e10479ed9951a4e416737 Mon Sep 17 00:00:00 2001 From: Simon Binder Date: Wed, 7 Aug 2024 18:45:59 +0200 Subject: [PATCH] Support string and null as schema representation --- pkgs/dart_model/lib/src/dart_model.g.dart | 52 +++++++++++++++ .../lib/generate_dart_model.dart | 66 +++++++++++++++---- 2 files changed, 105 insertions(+), 13 deletions(-) diff --git a/pkgs/dart_model/lib/src/dart_model.g.dart b/pkgs/dart_model/lib/src/dart_model.g.dart index 2e048060..7d95b8e4 100644 --- a/pkgs/dart_model/lib/src/dart_model.g.dart +++ b/pkgs/dart_model/lib/src/dart_model.g.dart @@ -85,6 +85,16 @@ extension type Model.fromJson(Map node) { Map get uris => (node['uris'] as Map).cast(); } +/// A Dart type of the form `T?` for an inner type `T` +extension type NullableType.fromJson(Map node) { + NullableType({ + StaticType? inner, + }) : this.fromJson({ + if (inner != null) 'inner': inner, + }); + StaticType get inner => node['inner'] as StaticType; +} + /// Set of boolean properties. extension type Properties.fromJson(Map node) { Properties({ @@ -131,3 +141,45 @@ extension type QualifiedName.fromJson(String string) { extension type Query.fromJson(Map node) { Query() : this.fromJson({}); } + +enum StaticTypeType { + unknown, + nullableType, + voidType; +} + +extension type StaticType.fromJson(Map node) { + static StaticType nullableType(NullableType nullableType) => + StaticType.fromJson({'type': 'NullableType', 'value': nullableType.node}); + static StaticType voidType(VoidType voidType) => + StaticType.fromJson({'type': 'VoidType', 'value': voidType.string}); + StaticTypeType get type { + switch (node['type'] as String) { + case 'NullableType': + return StaticTypeType.nullableType; + case 'VoidType': + return StaticTypeType.voidType; + default: + return StaticTypeType.unknown; + } + } + + NullableType get asNullableType { + if (node['type'] != 'NullableType') { + throw StateError('Not a NullableType.'); + } + return NullableType.fromJson(node['value'] as Map); + } + + VoidType get asVoidType { + if (node['type'] != 'VoidType') { + throw StateError('Not a VoidType.'); + } + return VoidType.fromJson(node['value'] as String); + } +} + +/// The type-hierarchy representation of the type `void`. +extension type VoidType.fromJson(String string) { + VoidType(String string) : this.fromJson(string); +} diff --git a/tool/dart_model_generator/lib/generate_dart_model.dart b/tool/dart_model_generator/lib/generate_dart_model.dart index 70cd116f..e054c42a 100644 --- a/tool/dart_model_generator/lib/generate_dart_model.dart +++ b/tool/dart_model_generator/lib/generate_dart_model.dart @@ -37,16 +37,45 @@ String generate(String schemaJson, final schema = JsonSchema.create(schemaJson, refProvider: LocalRefProvider(dartModelJson ?? File('schemas/dart_model.schema.json').readAsStringSync())); - for (final def in schema.defs.entries) { - if (def.value.oneOf.isNotEmpty) { - result.add(_generateUnion(def.key, def.value.oneOf)); + final allDefinitions = { + for (final def in schema.defs.entries) def.key: def.value, + }; + + for (final MapEntry(:key, :value) in allDefinitions.entries) { + if (value.oneOf.isNotEmpty) { + result.add( + _generateUnion(key, value.oneOf, allDefinitions: allDefinitions)); } else { - result.add(_generateExtensionType(def.key, def.value)); + result.add(_generateExtensionType(key, value)); } } return DartFormatter().formatSource(SourceCode(result.join('\n'))).text; } +/// The Dart type used to represent the JSON value for [definition]. +/// +/// This is most commonly a `Map, but can also be a JSON +/// primitive type for simpler definitions. +String _dartJsonType(JsonSchema definition) { + return switch (definition.type) { + SchemaType.object => 'Map', + SchemaType.string => 'String', + SchemaType.nullValue => 'Null', + _ => throw UnsupportedError('Unsupported type: ${definition.type}'), + }; +} + +/// Expands a [wrapper] expression evaluating to a generated extension type +/// instance to obtain the underlying JSON representation. +String _dartWrapperToJson(String wrapper, JsonSchema definition) { + return switch (definition.type) { + SchemaType.object => '$wrapper.node', + SchemaType.string => '$wrapper.string', + SchemaType.nullValue => 'null', + _ => throw UnsupportedError('Unsupported type: ${definition.type}'), + }; +} + String _generateExtensionType(String name, JsonSchema definition) { final result = StringBuffer(); @@ -55,6 +84,7 @@ String _generateExtensionType(String name, JsonSchema definition) { final jsonType = switch (definition.type) { SchemaType.object => 'Map node', SchemaType.string => 'String string', + SchemaType.nullValue => 'Null _', _ => throw UnsupportedError('Schema type ${definition.type}.'), }; if (definition.description != null) { @@ -96,6 +126,8 @@ String _generateExtensionType(String name, JsonSchema definition) { } case SchemaType.string: result.writeln('$name(String string) : this.fromJson(string);'); + case SchemaType.nullValue: + result.writeln('$name(): this.fromJson(null);'); default: throw UnsupportedError('Unsupported type: ${definition.type}'); } @@ -142,32 +174,39 @@ String _generateExtensionType(String name, JsonSchema definition) { /// and `asFoo` getters that "cast" to each type. /// /// On the wire the union type is: `{"type": , "value": }` -String _generateUnion(String name, List oneOf) { +String _generateUnion( + String name, + List oneOf, { + required Map allDefinitions, +}) { final result = StringBuffer(); - final types = - oneOf.map((s) => _refName(s.schemaMap![r'$ref'] as String)).toList(); + final unionEntries = oneOf + .map((s) => _refName(s.schemaMap![r'$ref'] as String)) + .map((name) => (name, allDefinitions[name]!)); // TODO(davidmorgan): add description(s). result ..writeln('enum ${name}Type {') - ..writeln(['unknown'].followedBy(types.map(_firstToLowerCase)).join(', ')) + ..writeln(['unknown'] + .followedBy(unionEntries.map((e) => _firstToLowerCase(e.$1))) + .join(', ')) ..writeln(';}'); // TODO(davidmorgan): add description. result.writeln('extension type $name.fromJson(Map node) {'); - for (final type in types) { + for (final (type, def) in unionEntries) { final lowerType = _firstToLowerCase(type); result ..writeln('static $name $lowerType($type $lowerType) =>') ..writeln('$name.fromJson({') ..writeln("'type': '$type',") - ..writeln("'value': $lowerType.node});"); + ..writeln("'value': ${_dartWrapperToJson(lowerType, def)}});"); } result ..writeln('${name}Type get type {') ..writeln("switch(node['type'] as String) {"); - for (final type in types) { + for (final (type, _) in unionEntries) { final lowerType = _firstToLowerCase(type); result.writeln("case '$type': return ${name}Type.$lowerType;"); } @@ -176,12 +215,13 @@ String _generateUnion(String name, List oneOf) { ..writeln('}') ..writeln('}'); - for (final type in types) { + for (final (type, schema) in unionEntries) { result ..writeln('$type get as$type {') ..writeln("if (node['type'] != '$type') " "{ throw StateError('Not a $type.'); }") - ..writeln("return $type.fromJson(node['value'] as Map);") + ..writeln( + "return $type.fromJson(node['value'] as ${_dartJsonType(schema)});") ..writeln('}'); } result.writeln('}');