diff --git a/pkgs/dart_model/lib/src/dart_model.g.dart b/pkgs/dart_model/lib/src/dart_model.g.dart index 2e048060..b39a4aa2 100644 --- a/pkgs/dart_model/lib/src/dart_model.g.dart +++ b/pkgs/dart_model/lib/src/dart_model.g.dart @@ -85,6 +85,21 @@ extension type Model.fromJson(Map node) { Map get uris => (node['uris'] as Map).cast(); } +/// Representation of the bottom type [Never]. +extension type NeverType.fromJson(Null _) { + NeverType() : this.fromJson(null); +} + +/// 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 +146,57 @@ extension type QualifiedName.fromJson(String string) { extension type Query.fromJson(Map node) { Query() : this.fromJson({}); } + +enum StaticTypeType { + unknown, + neverType, + nullableType, + voidType; +} + +extension type StaticType.fromJson(Map node) { + static StaticType neverType(NeverType neverType) => + StaticType.fromJson({'type': 'NeverType', 'value': null}); + 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 'NeverType': + return StaticTypeType.neverType; + case 'NullableType': + return StaticTypeType.nullableType; + case 'VoidType': + return StaticTypeType.voidType; + default: + return StaticTypeType.unknown; + } + } + + NeverType get asNeverType { + if (node['type'] != 'NeverType') { + throw StateError('Not a NeverType.'); + } + return NeverType.fromJson(node['value'] as Null); + } + + 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/schemas/dart_model.schema.json b/schemas/dart_model.schema.json index dc18e5f9..8280422e 100644 --- a/schemas/dart_model.schema.json +++ b/schemas/dart_model.schema.json @@ -86,6 +86,19 @@ } } }, + "NeverType": { + "type": "null", + "description": "Representation of the bottom type [Never]." + }, + "NullableType": { + "type": "object", + "description": "A Dart type of the form `T?` for an inner type `T`.", + "properties": { + "inner": { + "$ref": "#/$defs/StaticType" + } + } + }, "Properties": { "type": "object", "description": "Set of boolean properties.", @@ -123,6 +136,25 @@ "Query": { "type": "object", "description": "Query about a corpus of Dart source code. TODO(davidmorgan): this is a placeholder." + }, + "StaticType": { + "description": "A resolved type as it appears in Dart's type hierarchy.", + "properties": { + "type": { + "type": "string" + }, + "value": { + "oneOf": [ + {"$ref": "#/$defs/NeverType"}, + {"$ref": "#/$defs/NullableType"}, + {"$ref": "#/$defs/VoidType"} + ] + } + } + }, + "VoidType": { + "description": "The type-hierarchy representation of the type `void`.", + "type": "string" } } } diff --git a/tool/dart_model_generator/lib/generate_dart_model.dart b/tool/dart_model_generator/lib/generate_dart_model.dart index 9ff69250..0490a78a 100644 --- a/tool/dart_model_generator/lib/generate_dart_model.dart +++ b/tool/dart_model_generator/lib/generate_dart_model.dart @@ -37,16 +37,48 @@ 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 (_isUnion(def.value)) { - result.add(_generateUnion(def.key, def.value.properties['value']!.oneOf)); + final allDefinitions = { + for (final def in schema.defs.entries) def.key: def.value, + }; + + for (final MapEntry(:key, :value) in allDefinitions.entries) { + if (_isUnion(value)) { + result.add(_generateUnion( + key, + value.properties['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 +87,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 +129,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}'); } @@ -150,32 +185,39 @@ bool _isUnion(JsonSchema schema) => /// 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;"); } @@ -184,12 +226,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('}');