Skip to content

Commit

Permalink
Support string and null as schema representation (#30)
Browse files Browse the repository at this point in the history
* Support string and null as schema representation

* Add definitions for static types

* Add static types as example

* Add dot, fix analysis hint
  • Loading branch information
simolus3 authored Aug 9, 2024
1 parent 731c449 commit 1bfa4b3
Show file tree
Hide file tree
Showing 3 changed files with 157 additions and 13 deletions.
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) {
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

0 comments on commit 1bfa4b3

Please sign in to comment.