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

Support string and null as schema representation #30

Merged
merged 4 commits into from
Aug 9, 2024
Merged
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
69 changes: 69 additions & 0 deletions pkgs/dart_model/lib/src/dart_model.g.dart
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,21 @@ extension type Model.fromJson(Map<String, Object?> node) {
Map<String, Library> 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<String, Object?> 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<String, Object?> node) {
Properties({
Expand Down Expand Up @@ -131,3 +146,57 @@ extension type QualifiedName.fromJson(String string) {
extension type Query.fromJson(Map<String, Object?> node) {
Query() : this.fromJson({});
}

enum StaticTypeType {
unknown,
neverType,
nullableType,
voidType;
}

extension type StaticType.fromJson(Map<String, Object?> node) {
Copy link
Contributor

Choose a reason for hiding this comment

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

It would be useful to see schema change + code change + generator together, but I don't see the schema change--could you add it please?

There isn't any other test coverage for the generator besides what is in the schema, so it probably makes sense to add something to the schema even if it's just showing the new functionality. In that case please note in the description that the type is currently just there to show the union functionality :)

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I didn't mean to commit the type changes in this PR, but I've added a few types with their schema definitions to test the generator.

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<String, Object?>);
}

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);
}
32 changes: 32 additions & 0 deletions schemas/dart_model.schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -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.",
Expand Down Expand Up @@ -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"
}
}
}
69 changes: 56 additions & 13 deletions tool/dart_model_generator/lib/generate_dart_model.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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 = <String, JsonSchema>{
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<String, Object?>`, but can also be a JSON
/// primitive type for simpler definitions.
String _dartJsonType(JsonSchema definition) {
return switch (definition.type) {
SchemaType.object => 'Map<String, Object?>',
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();

Expand All @@ -55,6 +87,7 @@ String _generateExtensionType(String name, JsonSchema definition) {
final jsonType = switch (definition.type) {
SchemaType.object => 'Map<String, Object?> node',
SchemaType.string => 'String string',
SchemaType.nullValue => 'Null _',
_ => throw UnsupportedError('Schema type ${definition.type}.'),
};
if (definition.description != null) {
Expand Down Expand Up @@ -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}');
}
Expand Down Expand Up @@ -150,32 +185,39 @@ bool _isUnion(JsonSchema schema) =>
/// and `asFoo` getters that "cast" to each type.
///
/// On the wire the union type is: `{"type": <name>, "value": <value>}`
String _generateUnion(String name, List<JsonSchema> oneOf) {
String _generateUnion(
String name,
List<JsonSchema> oneOf, {
required Map<String, JsonSchema> 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<String, Object?> 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;");
}
Expand All @@ -184,12 +226,13 @@ String _generateUnion(String name, List<JsonSchema> 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<String, Object?>);")
..writeln(
"return $type.fromJson(node['value'] as ${_dartJsonType(schema)});")
..writeln('}');
}
result.writeln('}');
Expand Down