blob: b2c9a9f7bace6248e4ee542dccc74c6de8737a4c [file] [log] [blame]
// Copyright (c) 2015, the Dart project authors. Please see the AUTHORS file
// for details. All rights reserved. Use of this source code is governed by a
// BSD-style license that can be found in the LICENSE file.
// ignore_for_file: missing_whitespace_between_adjacent_strings
library discoveryapis_generator.dart_schemas;
import 'dart_api_library.dart';
import 'dart_comments.dart';
import 'generated_googleapis/discovery/v1.dart';
import 'namer.dart';
import 'null_safety.dart';
import 'utils.dart';
/// Class for keeping all named schemas. This is used for
/// - resolving forward references
/// - querying types by name
/// - access to built-in types
class DartSchemaTypeDB {
// Builtin types
final StringType stringType;
final IntegerType integerType;
final StringIntegerType stringIntegerType;
final DoubleType doubleType;
final BooleanType booleanType;
final DateType dateType;
final DateTimeType dateTimeType;
final AnyType anyType;
DartSchemaTypeDB(DartApiImports imports)
: stringType = StringType(imports),
integerType = IntegerType(imports),
stringIntegerType = StringIntegerType(imports),
doubleType = DoubleType(imports),
booleanType = BooleanType(imports),
dateType = DateType(imports),
dateTimeType = DateTimeType(imports),
anyType = AnyType(imports);
// List of all [DartSchemaType]s.
// TODO: This has to be in depth-first sorted traversal, right?
List<DartSchemaType> dartTypes = [];
// Original schema names to [DartSchemaType].
final Map<String, DartSchemaType> namedSchemaTypes = {};
// Name of dart class to [DartSchemaType].
final List<ComplexDartSchemaType> dartClassTypes = [];
DartSchemaType register(DartSchemaType type) {
if (type is! DartSchemaForwardRef) {
// Add [type] to list of all types.
dartTypes.add(type);
}
return type;
}
void registerTopLevel(String schemaName, DartSchemaType type) {
namedSchemaTypes[schemaName] = type;
}
}
/// Represents a property in a dart class.
class DartClassProperty {
final Identifier name;
final Comment comment;
final DartSchemaType type;
final String jsonName;
// If this property is a base64 encoded bytes, this identifier will represent
// the name used for a setter/getter.
final Identifier byteArrayAccessor;
DartClassProperty(this.name, this.comment, this.type, this.jsonName,
{this.byteArrayAccessor});
}
/// Represents the type declarations we use for representing json data.
abstract class JsonType {
final DartApiImports imports;
JsonType(this.imports);
String get declaration;
String get baseDeclaration => declaration;
}
class SimpleJsonType extends JsonType {
final String name;
SimpleJsonType(DartApiImports imports, this.name) : super(imports);
@override
String get declaration => '${imports.core.ref()}$name';
}
class StringJsonType extends SimpleJsonType {
StringJsonType(DartApiImports imports) : super(imports, 'String');
}
class IntJsonType extends SimpleJsonType {
IntJsonType(DartApiImports imports) : super(imports, 'int');
}
class BoolJsonType extends SimpleJsonType {
BoolJsonType(DartApiImports imports) : super(imports, 'bool');
}
class DoubleJsonType extends SimpleJsonType {
DoubleJsonType(DartApiImports imports) : super(imports, 'double');
@override
String get baseDeclaration => '${imports.core.ref()}num';
}
class MapJsonType extends JsonType {
final JsonType keyJsonType;
final JsonType valueJsonType;
MapJsonType(DartApiImports imports, this.keyJsonType, this.valueJsonType)
: super(imports);
@override
String get declaration => '${imports.core.ref()}Map'
'<${keyJsonType.declaration}, ${valueJsonType.declaration}$orNull>';
@override
String get baseDeclaration => '${imports.core.ref()}Map';
}
class ArrayJsonType extends JsonType {
final JsonType valueJsonType;
ArrayJsonType(DartApiImports imports, this.valueJsonType) : super(imports);
@override
String get declaration =>
'${imports.core.ref()}List<${valueJsonType.declaration}>';
@override
String get baseDeclaration => '${imports.core.ref()}List';
}
class AnyJsonType extends JsonType {
AnyJsonType(DartApiImports imports) : super(imports);
@override
String get declaration => '${imports.core.ref()}Object';
}
/// Represents an internal representation used for codegen.
///
/// [DartSchemaType] and it's subclasses are a representation for codegen of:
/// - dart class definitions
/// - dart type declarations
/// - dart expressions for encoding/decoding json
///
/// Before a [DartSchemaType] can be used, it's [resolve] method must be called
/// to resolve all forward references.
abstract class DartSchemaType {
// [className] is the name of the dart class this [DartSchemaType] represents
// or `null` if it does not represent a schema type represented by a custom
// dart class.
final Identifier className;
final Comment comment;
final DartApiImports imports;
bool _resolved = false;
DartSchemaType(this.imports, this.className, {Comment comment})
: comment = comment ?? Comment.empty;
DartSchemaType resolve(DartSchemaTypeDB db) {
if (!_resolved) {
_resolved = true;
return _resolve(db);
}
return this;
}
DartSchemaType _resolve(DartSchemaTypeDB db);
String get declaration;
JsonType get jsonType;
/// [value] is the string expression of this [DartSchemaType] that needs to be
/// encoded.
///
/// This method is used for encoding parameter types for the URI query part.
String primitiveEncoding(String value);
/// Whether this value needs a primitive encoding.
bool get needsPrimitiveEncoding => primitiveEncoding('foo') != 'foo';
/// [value] is the string expression of this [DartSchemaType] that needs to be
/// encoded.
String jsonEncode(String value);
/// [json] is the string expression of json data that needs to be decoded to
/// a [DartSchemaType].
String jsonDecode(String json);
/// Whether this value needs a JSON encoding or not.
bool get needsJsonEncoding => jsonEncode('foo') != 'foo';
/// Whether this value needs a JSON decoding or not.
bool get needsJsonDecoding => jsonDecode('foo') != 'foo';
String get coreMapJsonTypeArguments =>
'<${imports.core.ref()}String, ${imports.core.ref()}dynamic>';
String get coreMapJsonType =>
'${imports.core.ref()}Map$coreMapJsonTypeArguments';
}
/// Placeholder type for forward references.
class DartSchemaForwardRef extends DartSchemaType {
final String forwardRefName;
DartSchemaForwardRef(DartApiImports imports, this.forwardRefName)
: super(imports, null);
@override
DartSchemaType resolve(DartSchemaTypeDB db) {
var concreteType = db.namedSchemaTypes[forwardRefName];
while (concreteType is DartSchemaForwardRef) {
concreteType = db.namedSchemaTypes[
(concreteType as DartSchemaForwardRef).forwardRefName];
}
if (concreteType == null) {
throw StateError('Invalid forward reference: $forwardRefName');
}
return concreteType;
}
@override
DartSchemaType _resolve(DartSchemaTypeDB db) => null;
@override
JsonType get jsonType {
throw StateError('Type declarations can only be created after '
'resolving references.');
}
@override
String get declaration {
throw StateError('Type declarations can only be created after '
'resolving references.');
}
@override
String primitiveEncoding(String value) {
throw StateError('Encoding methods can only be called after '
'resolving references.');
}
@override
String jsonEncode(String value) {
throw StateError('JSON methods can only be called after '
'resolving references.');
}
@override
String jsonDecode(String json) {
throw StateError('JSON methods can only be called after '
'resolving references.');
}
}
/// Superclass for primitive types which will not be represented as custom dart
/// classes.
abstract class PrimitiveDartSchemaType extends DartSchemaType {
PrimitiveDartSchemaType(DartApiImports imports) : super(imports, null);
@override
DartSchemaType _resolve(DartSchemaTypeDB db) => this;
@override
String primitiveEncoding(String value) => "'\${$value}'";
@override
String jsonEncode(String value) => value;
@override
String jsonDecode(String json) => '$json as $declaration';
}
class BooleanType extends PrimitiveDartSchemaType {
@override
final JsonType jsonType;
BooleanType(DartApiImports imports)
: jsonType = BoolJsonType(imports),
super(imports);
@override
String get declaration => '${imports.core.ref()}bool';
}
class IntegerType extends PrimitiveDartSchemaType {
@override
final JsonType jsonType;
IntegerType(DartApiImports imports)
: jsonType = IntJsonType(imports),
super(imports);
@override
String get declaration => '${imports.core.ref()}int';
}
class StringIntegerType extends PrimitiveDartSchemaType {
@override
final JsonType jsonType;
StringIntegerType(DartApiImports imports)
: jsonType = StringJsonType(imports),
super(imports);
@override
String get declaration => '${imports.core.ref()}int';
@override
String jsonEncode(String value) => '"\${$value}"';
@override
String jsonDecode(String json) =>
'${imports.core.ref()}int.parse("\${$json}")';
}
class DoubleType extends PrimitiveDartSchemaType {
@override
final JsonType jsonType;
DoubleType(DartApiImports imports)
: jsonType = DoubleJsonType(imports),
super(imports);
@override
String get declaration => '${imports.core.ref()}double';
@override
String jsonDecode(String json) =>
'($json as ${imports.core.ref()}num).toDouble()';
}
class StringType extends PrimitiveDartSchemaType {
@override
final JsonType jsonType;
StringType(DartApiImports imports)
: jsonType = StringJsonType(imports),
super(imports);
@override
String primitiveEncoding(String value) => value;
@override
String get declaration => '${imports.core.ref()}String';
}
class EnumType extends StringType {
final List<String> enumValues;
final List<String> enumDescriptions;
factory EnumType(DartApiImports imports, List<String> enumValues,
List<String> enumDescriptions) {
enumDescriptions ??= enumValues.map((value) => 'A $value.').toList();
if (enumValues.length != enumDescriptions.length) {
throw ArgumentError('Number of enum values does not match number of '
'enum descriptions.');
}
return EnumType._(imports, enumValues, enumDescriptions);
}
EnumType._(DartApiImports imports, this.enumValues, this.enumDescriptions)
: super(imports);
}
class DateType extends StringType {
DateType(DartApiImports imports) : super(imports);
@override
String get declaration => '${imports.core.ref()}DateTime';
@override
String primitiveEncoding(String value) =>
'"\${($value).year.toString().padLeft(4, \'0\')}-'
'\${($value).month.toString().padLeft(2, \'0\')}-'
'\${($value).day.toString().padLeft(2, \'0\')}"';
@override
String jsonEncode(String value) => primitiveEncoding(value);
@override
String jsonDecode(String json) => '${imports.core.ref()}DateTime'
'.parse($json as ${imports.core.ref()}String)';
}
class DateTimeType extends StringType {
DateTimeType(DartApiImports imports) : super(imports);
@override
String get declaration => '${imports.core.ref()}DateTime';
@override
String primitiveEncoding(String value) => '$value.toIso8601String()';
@override
String jsonEncode(String value) => '$value.toIso8601String()';
@override
String jsonDecode(String json) => '${imports.core.ref()}DateTime'
'.parse($json as ${imports.core.ref()}String)';
}
/// Class representing "any" schema type.
///
/// A decoded any type object is the JSON the server sent. The any type object
/// a user supplies is expected to be JSON and transferred to the server "as is"
class AnyType extends PrimitiveDartSchemaType {
@override
final JsonType jsonType;
AnyType(DartApiImports imports)
: jsonType = AnyJsonType(imports),
super(imports);
@override
String get declaration => '${imports.core.ref()}Object';
}
/// Class representing non-primitive types.
///
/// Subclasses may be named dart classes or composed classes (e.g. List<X>).
abstract class ComplexDartSchemaType extends DartSchemaType {
ComplexDartSchemaType(DartApiImports imports, Identifier name,
{Comment comment})
: super(imports, name, comment: comment);
String get classDefinition;
@override
String get declaration;
@override
String primitiveEncoding(String value) {
throw UnsupportedError(
'Complex schema types do not have a primitive string encoding for URI'
'query parameters.');
}
}
abstract class HasInnertype implements ComplexDartSchemaType {
DartSchemaType innerType;
}
/// Represents an unnamed List<T> type with a given `T`.
class UnnamedArrayType extends ComplexDartSchemaType implements HasInnertype {
@override
DartSchemaType innerType;
UnnamedArrayType(DartApiImports imports, this.innerType)
: super(imports, null);
@override
DartSchemaType _resolve(DartSchemaTypeDB db) {
innerType = innerType.resolve(db);
return this;
}
@override
JsonType get jsonType => ArrayJsonType(imports, innerType.jsonType);
@override
String get classDefinition => null;
@override
String get declaration =>
'${imports.core.ref()}List<${innerType.declaration}>';
@override
String jsonEncode(String value) {
if (innerType.needsJsonEncoding) {
return '$value.map((value) => ${innerType.jsonEncode('value')})'
'.toList()';
} else {
// NOTE: The List from the user is already JSON. We have a big
// ASSUMPTION here: The user does not modify the list while we're
// converting JSON -> String (-> Bytes).
return value;
}
}
@override
String jsonDecode(String json) {
if (innerType.needsJsonDecoding) {
return '($json as ${imports.core.ref()}List)'
'.map<${innerType.declaration}>'
'((value) => ${innerType.jsonDecode('value')}).toList()';
} else {
// NOTE: The List returned from JSON.decode() transfers ownership to the
// user (i.e. we don't need to make a copy of it).
return '($json as ${imports.core.ref()}List)'
'.cast<${innerType.declaration}>()';
}
}
}
/// Represents a named List<T> type with a given `T`.
class NamedArrayType extends ComplexDartSchemaType implements HasInnertype {
@override
DartSchemaType innerType;
NamedArrayType(DartApiImports imports, Identifier name, this.innerType,
{Comment comment})
: super(imports, name, comment: comment);
@override
DartSchemaType _resolve(DartSchemaTypeDB db) {
innerType = innerType.resolve(db);
return this;
}
@override
JsonType get jsonType => ArrayJsonType(imports, innerType.jsonType);
@override
String get classDefinition {
final decode = StringBuffer();
decode.writeln(' $className.fromJson(${imports.core.ref()}List json)');
decode.writeln(' : _inner = json.map((value) => '
'${innerType.jsonDecode('value')}).toList();');
final encode = StringBuffer();
encode.writeln(' ${jsonType.declaration} toJson() {');
encode.writeln(' return _inner.map((value) => '
'${innerType.jsonEncode('value')}).toList();');
encode.write(' }');
final core = imports.core.ref();
final type = innerType.declaration;
return '''
${comment.asDartDoc(0)}class $className
extends ${imports.collection.ref()}ListBase<$type> {
final ${imports.core.ref()}List<$type> _inner;
$className() : _inner = [];
$decode
$encode
@${core}override
$type operator [](${imports.core.ref()}int key) => _inner[key];
@${core}override
void operator []=(${imports.core.ref()}int key, $type value) {
_inner[key] = value;
}
@${core}override
${imports.core.ref()}int get length => _inner.length;
@${core}override
set length(${imports.core.ref()}int newLength) {
_inner.length = newLength;
}
}
''';
}
@override
String get declaration => className.name;
@override
String jsonEncode(String value) {
if (innerType.needsJsonEncoding) {
return '$value.toJson()';
} else {
// NOTE: The List from the user can be encoded directly. We have a big
// ASSUMPTION here: The user does not modify the list while we're
// converting JSON -> String (-> Bytes).
return value;
}
}
@override
String jsonDecode(String json) =>
'$className.fromJson($json as ${imports.core.ref()}List)';
}
/// Represents an unnamed Map<F, T> type with given types `F` and `T`.
class UnnamedMapType extends ComplexDartSchemaType {
DartSchemaType fromType;
DartSchemaType toType;
UnnamedMapType(DartApiImports imports, this.fromType, this.toType)
: super(imports, null) {
if (fromType is! StringType) {
throw StateError('Violation of assumption: Keys in map types must '
'be Strings.');
}
}
@override
DartSchemaType _resolve(DartSchemaTypeDB db) {
fromType = fromType.resolve(db);
toType = toType.resolve(db);
return this;
}
@override
JsonType get jsonType =>
MapJsonType(imports, fromType.jsonType, toType.jsonType);
@override
String get classDefinition => null;
@override
String get declaration {
final from = fromType.declaration;
final to = toType.declaration;
return '${imports.core.ref()}Map<$from, $to>';
}
@override
String jsonEncode(String value) {
if (fromType.needsJsonEncoding || toType.needsJsonEncoding) {
return '$value.map((key, item) => '
'${imports.core.ref()}MapEntry(key, ${toType.jsonEncode('item')}))';
} else {
// NOTE: The Map from the user can be encoded directly. We have a big
// ASSUMPTION here: The user does not modify the map while we're
// converting JSON -> String (-> Bytes).
return value;
}
}
@override
String jsonDecode(String json) {
if (fromType.needsJsonDecoding || toType.needsJsonDecoding) {
// Null safe code is strict about types, so be more precise about generics
// This should also work in legacy mode, but keep as before to avoid churn
final toTypeJsonDeclaration = generateNullSafeCode
? toType.jsonType.declaration
: toType.jsonType.baseDeclaration;
return '''
($json as ${imports.core.ref()}Map)
.cast<${fromType.jsonType.baseDeclaration}, $toTypeJsonDeclaration>()
.map(
(key, item) => ${imports.core.ref()}MapEntry(
key,
${toType.jsonDecode('item')},
),
)''';
} else {
// NOTE: The Map returned from JSON.decode() transfers ownership to the
// user (i.e. we don't need to make a copy of it).
return '($json as $coreMapJsonType)'
'.cast<${fromType.declaration}, ${toType.declaration}>()';
}
}
}
/// Represents a named Map<F, T> type with given types `F` and `T`.
class NamedMapType extends ComplexDartSchemaType {
DartSchemaType fromType;
DartSchemaType toType;
NamedMapType(
DartApiImports imports, Identifier name, this.fromType, this.toType,
{Comment comment})
: super(imports, name, comment: comment) {
if (fromType is! StringType) {
throw StateError('Violation of assumption: Keys in map types must '
'be Strings.');
}
}
@override
DartSchemaType _resolve(DartSchemaTypeDB db) {
fromType = fromType.resolve(db);
toType = toType.resolve(db);
return this;
}
@override
JsonType get jsonType =>
MapJsonType(imports, fromType.jsonType, toType.jsonType);
@override
String get classDefinition {
final core = imports.core.ref();
final decode = StringBuffer();
decode.writeln(' $className.fromJson(');
decode.writeln(' ${imports.coreJsonMap} _json) {');
decode.writeln(' _json.forEach((${core}String key, value) {');
decode.writeln(' this[key] = ${toType.jsonDecode('value')};');
decode.writeln(' });');
decode.writeln(' }');
final encode = StringBuffer();
encode.writeln(' ${imports.coreJsonMap} toJson() =>');
encode.writeln(' ${imports.coreJsonMap}.of(this);');
final fromT = fromType.declaration;
final toT = toType.declaration;
return '''
${comment.asDartDoc(0)}class $className
extends ${imports.collection.ref()}MapBase<$fromT, $toT> {
final _innerMap = <$fromT, $toT>{};
$className();
$decode
$encode
@${core}override
${toType.declaration}$orNull operator [](${core}Object$orNull key)
=> _innerMap[key];
@${core}override
void operator []=($fromT key, $toT value) {
_innerMap[key] = value;
}
@${core}override
void clear() {
_innerMap.clear();
}
@${core}override
${core}Iterable<$fromT> get keys => _innerMap.keys;
@${core}override
$toT$orNull remove(${core}Object$orNull key) => _innerMap.remove(key);
}
''';
}
@override
String get declaration => '$className';
@override
String jsonEncode(String value) {
if (fromType.needsJsonEncoding || toType.needsJsonEncoding) {
return '$value.toJson()';
} else {
// NOTE: The Map from the user can be encoded directly. We have a big
// ASSUMPTION here: The user does not modify the map while we're
// converting JSON -> String (-> Bytes).
return value;
}
}
@override
String jsonDecode(String json) =>
'$className.fromJson($json as $coreMapJsonType)';
}
/// Represents a named custom dart class with a number of properties.
class ObjectType extends ComplexDartSchemaType {
final List<DartClassProperty> properties;
@override
final MapJsonType jsonType;
// Will be set by the superVariantType when resolving forward references.
AbstractVariantType superVariantType;
ObjectType(DartApiImports imports, Identifier name, this.properties,
{Comment comment})
: jsonType =
MapJsonType(imports, StringJsonType(imports), AnyJsonType(imports)),
super(imports, name, comment: comment);
@override
DartSchemaType _resolve(DartSchemaTypeDB db) {
for (var i = 0; i < properties.length; i++) {
final property = properties[i];
final resolvedProperty = DartClassProperty(property.name,
property.comment, property.type.resolve(db), property.jsonName,
byteArrayAccessor: property.byteArrayAccessor);
properties[i] = resolvedProperty;
}
return this;
}
@override
String get classDefinition {
var superClassString = '';
if (superVariantType != null) {
superClassString = ' extends ${superVariantType.declaration} ';
}
final propertyString = StringBuffer();
for (var property in properties) {
final comment = property.comment.asDartDoc(2);
var prefix = '', postfix = '';
if (isVariantDiscriminator(property)) {
prefix = 'final ';
postfix = ' = "${escapeString(discriminatorValue())}"';
}
propertyString.writeln(
'$comment $prefix${property.type.declaration}$orNull ${property.name}'
'$postfix;',
);
if (property.byteArrayAccessor != null) {
propertyString.writeln(
' ${imports.core.ref()}List<${imports.core.ref()}int> get '
'${property.byteArrayAccessor} =>');
propertyString.writeln('${imports.convert.ref()}base64.decode'
'(${property.name}$notNull);');
propertyString.writeln();
propertyString.write(' set ${property.byteArrayAccessor}');
propertyString.writeln(
'(${imports.core.ref()}List<${imports.core.ref()}int> _bytes) {');
propertyString.writeln(' ${property.name} = ${imports.convert.ref()}'
"base64.encode(_bytes).replaceAll('/', '_').replaceAll('+', '-');");
propertyString.writeln(' }');
}
}
final fromJsonString = StringBuffer();
final emptyPropertiesComment = properties.isEmpty
? ' // ignore: avoid_unused_constructor_parameters\n'
: '';
fromJsonString.writeln(
' $className.fromJson'
'($emptyPropertiesComment ${imports.core.ref()}Map _json)',
);
if (properties.isEmpty) {
fromJsonString.writeln(';');
} else {
fromJsonString.write('{');
for (var property in properties) {
// The super variant fromJson() will call this subclass constructor
// and the variant descriminator is final.
if (!isVariantDiscriminator(property)) {
final decodeString = property.type
.jsonDecode("_json['${escapeString(property.jsonName)}']");
fromJsonString.writeln(' if (_json.containsKey'
"('${escapeString(property.jsonName)}')) {");
fromJsonString.writeln(' ${property.name} = $decodeString;');
fromJsonString.writeln(' }');
}
}
fromJsonString.writeln(' }');
}
final toJsonString = StringBuffer();
toJsonString.writeln('${jsonType.declaration} toJson() =>');
toJsonString.writeln('{');
for (var property in properties) {
toJsonString.writeln('if (${property.name} != null)');
toJsonString.writeln("'${escapeString(property.jsonName)}':"
'${property.type.jsonEncode('${property.name}$notNull')},');
}
toJsonString.write('};');
return '''
${comment.asDartDoc(0)}class $className $superClassString{
$propertyString
$className();
$fromJsonString
$toJsonString
}
''';
}
@override
String get declaration => '$className';
@override
String jsonEncode(String value) => '$value.toJson()';
@override
String jsonDecode(String json) =>
'$className.fromJson($json as $coreMapJsonType)';
bool isVariantDiscriminator(DartClassProperty prop) =>
superVariantType != null &&
prop.jsonName == superVariantType.discriminant;
String discriminatorValue() {
for (var key in superVariantType.map.keys) {
final value = superVariantType.map[key];
if (value == this) return key;
}
throw StateError('Could not find my discriminator string.');
}
}
/// Represents a schema variant type.
class AbstractVariantType extends ComplexDartSchemaType {
final String discriminant;
final Map<String, DartSchemaType> map;
@override
final JsonType jsonType;
AbstractVariantType(
DartApiImports imports, Identifier name, this.discriminant, this.map,
{Comment comment})
: jsonType =
MapJsonType(imports, StringJsonType(imports), AnyJsonType(imports)),
super(imports, name, comment: comment);
@override
DartSchemaType _resolve(DartSchemaTypeDB db) {
map.forEach((String name, DartSchemaType ref) {
final resolvedType = ref.resolve(db);
if (resolvedType is ObjectType) {
map[name] = resolvedType;
// Set superclass to ourselves.
if (resolvedType.superVariantType == null) {
if (resolvedType is AbstractVariantType) {
throw StateError('Variant types cannot have subclasses which '
'are variant types themselves.');
}
resolvedType.superVariantType = this;
} else {
throw StateError('Superclass already set. A object type should '
'have only one superclass');
}
} else {
throw StateError('A variant type can only have concrete object '
'types as subclasses.');
}
});
return this;
}
@override
String get classDefinition {
final fromJsonString = StringBuffer();
fromJsonString.writeln(
' factory $className.fromJson(${imports.core.ref()}Map json) {');
fromJsonString.writeln(" var discriminant = json['$discriminant'];");
map.forEach((String name, DartSchemaType type) {
fromJsonString.writeln(' if (discriminant == "$name") {');
fromJsonString.writeln(' return ${type.declaration}'
'.fromJson(json);');
fromJsonString.writeln(' }');
});
fromJsonString.writeln(' throw ${imports.core.ref()}ArgumentError'
'("Invalid discriminant: \$discriminant!");');
fromJsonString.writeln(' }');
final toJsonString = StringBuffer();
toJsonString.writeln(' ${jsonType.declaration} toJson();');
return '''
${comment.asDartDoc(0)}abstract class $className {
$className();
$fromJsonString
$toJsonString
}
''';
}
@override
String get declaration => '$className';
@override
String jsonEncode(String value) => '$value.toJson()';
@override
String jsonDecode(String json) => '$className.fromJson($json)';
}
/// Parses all schemas in [description] and returns a [DartSchemaTypeDB].
DartSchemaTypeDB parseSchemas(
DartApiImports imports, RestDescription description) {
final namer = imports.namer;
final db = DartSchemaTypeDB(imports);
/*
* Primitive types "integer"/"boolean"/"double"/"number"/"string":
* { "type": "boolean" ... }
*
* Any type:
* { "type" : "any" ... }
*
* Array types:
* { "type": "array", "items" : {"type": ...}, ... }
*
* Map types:
* {
* "type": "object",
* "additionalProperties" : {"type": ...},
* ...
* }
* => key is always String
*
* Forward references:
* { "$ref" : "NamedSchemaType" }
*
* Normal objects:
* {
* "type" : "object",
* "properties": {"prop1" : {"type" : ...}, ... },
* ...
* }
*
* Variant objects:
* {
* "type" : 'object",
* "variant": {
* "discriminant" : "type",
* "map": [
* { "type_value" : "type_a", "$ref" : "NamedSchemaType" },
* { "type_value" : "type_b", "$ref" : "NamedSchemaType" }
* ]
* }
* }
*
* If these types appear on the top level, i.e. in the {"schemas" { XXX }},
* they are named, otherwise they are unnamed.
*/
DartSchemaType parse(String className, Scope classScope, JsonSchema schema,
{bool topLevel = false}) {
if (schema.repeated != null) {
throw ArgumentError('Only path/query parameters can be repeated.');
}
if (schema.type == 'object') {
var comment = Comment.header(schema.description, true);
if (schema.additionalProperties != null) {
final anonValueClassName = namer.schemaClassName('${className}Value');
final anonClassScope = namer.newClassScope();
final valueType = parse(
anonValueClassName,
anonClassScope,
schema.additionalProperties,
);
if (topLevel) {
if (schema.additionalProperties.description != null) {
comment = Comment(
'${comment.rawComment}\n\n'
'${schema.additionalProperties.description}',
);
}
// This is a named map type.
final classId = namer.schemaClass(className);
return db.register(
NamedMapType(
imports,
classId,
db.stringType,
valueType,
comment: comment,
),
);
} else {
// This is an unnamed map type.
return db.register(UnnamedMapType(imports, db.stringType, valueType));
}
} else if (schema.variant != null) {
// This is a variant type, declaring the type discriminant field and all
// subclasses.
final map = <String, DartSchemaType>{};
for (var mapItem in schema.variant.map) {
map[mapItem.typeValue] = DartSchemaForwardRef(imports, mapItem.P_ref);
}
final classId = namer.schemaClass(className);
return db.register(AbstractVariantType(
imports, classId, schema.variant.discriminant, map));
} else {
// This is a normal named schema class, we generate a normal
// [ObjectType] for it with the defined properties.
final classId = namer.schemaClass(className);
final properties = <DartClassProperty>[];
if (schema.properties != null) {
orderedForEach(schema.properties,
(String jsonPName, JsonSchema value) {
final propertyName = classScope.newIdentifier(jsonPName);
final propertyClass =
namer.schemaClassName(jsonPName, parent: className);
final propertyClassScope = namer.newClassScope();
final propertyType =
parse(propertyClass, propertyClassScope, value);
var comment = Comment.header(value.description, true);
comment = extendEnumComment(comment, propertyType);
comment = extendAnyTypeComment(comment, propertyType);
Identifier byteArrayAccessor;
if (value.format == 'byte' && value.type == 'string') {
byteArrayAccessor =
classScope.newIdentifier('${jsonPName}AsBytes');
}
final property = DartClassProperty(
propertyName, comment, propertyType, jsonPName,
byteArrayAccessor: byteArrayAccessor);
properties.add(property);
});
}
return db.register(
ObjectType(imports, classId, properties, comment: comment));
}
} else if (schema.type == 'array') {
final comment = Comment.header(schema.description, true);
if (topLevel) {
final elementClassName = namer.schemaClassName('${className}Element');
final classId = namer.schemaClass(className);
return db.register(NamedArrayType(imports, classId,
parse(elementClassName, namer.newClassScope(), schema.items),
comment: comment));
} else {
return db.register(UnnamedArrayType(
imports, parse(className, namer.newClassScope(), schema.items)));
}
} else if (schema.type == 'any') {
return db.anyType;
} else if (schema.P_ref != null) {
// This is a forward or backward reference, it will be resolved in
// another pass following the parsing.
return db.register(DartSchemaForwardRef(imports, schema.P_ref));
} else {
return parsePrimitive(imports, db, schema);
}
}
if (description.schemas != null) {
orderedForEach(description.schemas, (String name, JsonSchema schema) {
final className = namer.schemaClassName(name);
final classScope = namer.newClassScope();
db.registerTopLevel(
name, parse(className, classScope, schema, topLevel: true));
});
// Resolve all forward references and save list in [db.dartTypes].
db.dartTypes = db.dartTypes.map((type) => type.resolve(db)).toList();
// Build map of all top level dart schema classes which will be represented
// as named dart classes.
db.dartClassTypes.addAll(db.dartTypes
.where((type) => type.className != null)
.cast<ComplexDartSchemaType>());
}
return db;
}
// NOTE: This will be called for resolving parameter types in methods.
DartSchemaType parseResolved(
DartApiImports imports, DartSchemaTypeDB db, JsonSchema schema) {
if (schema.repeated != null && schema.repeated) {
final innerType = parsePrimitive(imports, db, schema);
return UnnamedArrayType(imports, innerType);
}
return parsePrimitive(imports, db, schema);
}
DartSchemaType parsePrimitive(
DartApiImports imports, DartSchemaTypeDB db, JsonSchema schema) {
switch (schema.type) {
case 'boolean':
return db.booleanType;
case 'string':
switch (schema.format) {
case 'date-time':
return db.dateTimeType;
case 'date':
return db.dateType;
case 'int64':
// 9007199254740991 == pow(2, 53) - 1; the maximum range for integers
// in javascript (which uses doubles to store integers)
if (schema.maximum != null &&
int.parse(schema.maximum) <= 9007199254740991 &&
schema.minimum != null &&
int.parse(schema.minimum) >= -9007199254740991) {
return db.stringIntegerType;
}
}
if (schema.enum_ != null) {
return db
.register(EnumType(imports, schema.enum_, schema.enumDescriptions));
}
return db.stringType;
case 'number':
if (!['float', 'double', null].contains(schema.format)) {
throw ArgumentError(
'Only number types with float/double format are supported.');
}
return db.doubleType;
case 'integer':
final format = schema.format;
if (format != null && !['int16', 'int32', 'uint32'].contains(format)) {
throw Exception('Integer format $format is not not supported.');
}
return db.integerType;
}
throw ArgumentError('Invalid JsonSchema.type (was: ${schema.type}).');
}
/// Generates the codegen'ed dart string for all schema classes.
String generateSchemas(DartSchemaTypeDB db) {
final sb = StringBuffer();
for (var value in db.dartClassTypes) {
final classDefinition = value.classDefinition;
if (classDefinition != null) {
sb.writeln(classDefinition);
}
}
return '$sb';
}
Comment extendEnumComment(Comment baseComment, DartSchemaType type) {
if (type is EnumType) {
final s = StringBuffer()
..writeln(baseComment.rawComment)
..writeln('Possible string values are:');
for (var i = 0; i < type.enumValues.length; i++) {
final description = type.enumDescriptions[i];
if (description != null && description.trim().isNotEmpty) {
s.writeln('- "${type.enumValues[i]}" : $description');
} else {
s.writeln('- "${type.enumValues[i]}"');
}
}
return Comment(bracketClean(s.toString()));
}
return baseComment;
}
Comment extendAnyTypeComment(Comment baseComment, DartSchemaType type,
{bool includeNamedTypes = false}) {
const _anyTypeComment =
'The values for Object must be JSON objects. It can consist of `num`, '
'`String`, `bool` and `null` as well as `Map` and `List` values.';
// This will detect if [type] contains usages of the general AnyType, e.g.
// - Object
// - List<List<Object>>
// - Map<String,List<Object>>
// - ...
bool traverseType(DartSchemaType type) {
if (includeNamedTypes) {
if (type is NamedArrayType) {
return traverseType(type.innerType);
} else if (type is NamedMapType) {
return traverseType(type.toType);
}
}
if (type is UnnamedArrayType) {
return traverseType(type.innerType);
} else if (type is UnnamedMapType) {
return traverseType(type.toType);
} else if (type is AnyType) {
return true;
}
return false;
}
if (traverseType(type)) {
return Comment('${baseComment.rawComment}\n\n$_anyTypeComment');
}
return baseComment;
}