diff --git a/packages/dynamite/dynamite/lib/src/builder/header_serializer.dart b/packages/dynamite/dynamite/lib/src/builder/header_serializer.dart new file mode 100644 index 00000000..af7fec10 --- /dev/null +++ b/packages/dynamite/dynamite/lib/src/builder/header_serializer.dart @@ -0,0 +1,136 @@ +import 'package:code_builder/code_builder.dart'; +import 'package:dynamite/src/builder/resolve_type.dart'; +import 'package:dynamite/src/builder/state.dart'; +import 'package:dynamite/src/helpers/dart_helpers.dart'; +import 'package:dynamite/src/helpers/dynamite.dart'; +import 'package:dynamite/src/models/open_api.dart'; +import 'package:dynamite/src/models/schema.dart'; +import 'package:dynamite/src/type_result/type_result.dart'; + +Spec buildHeaderSerializer(final State state, final String identifier, final OpenAPI spec, final Schema schema) => + Class( + (final b) => b + ..name = '_\$${state.classPrefix}${identifier}Serializer' + ..implements.add(refer('StructuredSerializer<${state.classPrefix}$identifier>')) + ..fields.addAll([ + Field( + (final b) => b + ..name = 'types' + ..modifier = FieldModifier.final$ + ..type = refer('Iterable') + ..annotations.add(refer('override')) + ..assignment = Code('const [${state.classPrefix}$identifier, _\$${state.classPrefix}$identifier]'), + ), + Field( + (final b) => b + ..name = 'wireName' + ..modifier = FieldModifier.final$ + ..type = refer('String') + ..annotations.add(refer('override')) + ..assignment = Code("r'${state.classPrefix}$identifier'"), + ), + ]) + ..methods.addAll([ + Method((final b) { + b + ..name = 'serialize' + ..returns = refer('Iterable') + ..annotations.add(refer('override')) + ..requiredParameters.addAll([ + Parameter( + (final b) => b + ..name = 'serializers' + ..type = refer('Serializers'), + ), + Parameter( + (final b) => b + ..name = 'object' + ..type = refer('${state.classPrefix}$identifier'), + ), + ]) + ..optionalParameters.add( + Parameter( + (final b) => b + ..name = 'specifiedType' + ..type = refer('FullType') + ..named = true + ..defaultTo = const Code('FullType.unspecified'), + ), + ) + ..body = const Code('throw UnimplementedError();'); + }), + Method((final b) { + b + ..name = 'deserialize' + ..returns = refer('${state.classPrefix}$identifier') + ..annotations.add(refer('override')) + ..requiredParameters.addAll([ + Parameter( + (final b) => b + ..name = 'serializers' + ..type = refer('Serializers'), + ), + Parameter( + (final b) => b + ..name = 'serialized' + ..type = refer('Iterable'), + ), + ]) + ..optionalParameters.add( + Parameter( + (final b) => b + ..name = 'specifiedType' + ..type = refer('FullType') + ..named = true + ..defaultTo = const Code('FullType.unspecified'), + ), + ); + List deserializeProperty(final MapEntry property) { + final propertyName = property.key; + final propertySchema = property.value; + final result = resolveType( + spec, + state, + '${identifier}_${toDartName(propertyName, uppercaseFirstCharacter: true)}', + propertySchema, + nullable: isDartParameterNullable(schema.required?.contains(propertyName), propertySchema), + ); + + return [ + Code("case '$propertyName':"), + if (result.className != 'String') ...[ + if (result is TypeResultBase || result is TypeResultEnum) ...[ + Code( + 'result.${toDartName(propertyName)} = ${result.deserialize(result.decode('value!'))};', + ), + ] else ...[ + Code( + 'result.${toDartName(propertyName)}.replace(${result.deserialize(result.decode('value!'))});', + ), + ], + ] else ...[ + Code( + 'result.${toDartName(propertyName)} = value!;', + ), + ], + ]; + } + + b.body = Block.of([ + Code('final result = new ${state.classPrefix}${identifier}Builder();'), + const Code(''), + const Code('final iterator = serialized.iterator;'), + const Code('while (iterator.moveNext()) {'), + const Code('final key = iterator.current! as String;'), + const Code('iterator.moveNext();'), + const Code('final value = iterator.current! as String;'), + const Code('switch (key) {'), + for (final property in schema.properties!.entries) ...deserializeProperty(property), + const Code('}'), + const Code('}'), + const Code(''), + const Code('return result.build();'), + ]); + }), + ]), + ); diff --git a/packages/dynamite/dynamite/lib/src/builder/ofs_builder.dart b/packages/dynamite/dynamite/lib/src/builder/ofs_builder.dart new file mode 100644 index 00000000..1b9d7bc9 --- /dev/null +++ b/packages/dynamite/dynamite/lib/src/builder/ofs_builder.dart @@ -0,0 +1,229 @@ +import 'package:built_collection/built_collection.dart'; +import 'package:code_builder/code_builder.dart'; +import 'package:dynamite/src/builder/resolve_type.dart'; +import 'package:dynamite/src/builder/state.dart'; +import 'package:dynamite/src/helpers/built_value.dart'; +import 'package:dynamite/src/helpers/dart_helpers.dart'; +import 'package:dynamite/src/models/open_api.dart'; +import 'package:dynamite/src/models/schema.dart'; +import 'package:dynamite/src/type_result/type_result.dart'; + +TypeResult resolveOfs( + final OpenAPI spec, + final State state, + final String identifier, + final Schema schema, { + final bool nullable = false, +}) { + final result = TypeResultObject( + '${state.classPrefix}$identifier', + nullable: nullable, + ); + + if (state.resolvedTypes.add(result)) { + final results = schema.ofs! + .map( + (final s) => resolveType( + spec, + state, + '$identifier${schema.ofs!.indexOf(s)}', + s, + nullable: !(schema.allOf?.contains(s) ?? false), + ), + ) + .toList(); + + final fields = {}; + for (final result in results) { + final dartName = toDartName(result.name.replaceFirst(state.classPrefix, '')); + fields[result.name] = toFieldName(dartName, result.name.replaceFirst(state.classPrefix, '')); + } + + state.output.addAll([ + buildBuiltClass( + '${state.classPrefix}$identifier', + BuiltList.build((final b) { + b.add( + Method( + (final b) { + b + ..name = 'data' + ..returns = refer('JsonObject') + ..type = MethodType.getter; + }, + ), + ); + + for (final result in results) { + b.add( + Method( + (final b) { + final s = schema.ofs![results.indexOf(result)]; + b + ..name = fields[result.name] + ..returns = refer(result.nullableName) + ..type = MethodType.getter + ..docs.addAll(s.formattedDescription); + }, + ), + ); + } + }), + customSerializer: true, + ), + Class( + (final b) => b + ..name = '_\$${state.classPrefix}${identifier}Serializer' + ..implements.add(refer('PrimitiveSerializer<${state.classPrefix}$identifier>')) + ..fields.addAll([ + Field( + (final b) => b + ..name = 'types' + ..modifier = FieldModifier.final$ + ..type = refer('Iterable') + ..annotations.add(refer('override')) + ..assignment = Code('const [${state.classPrefix}$identifier, _\$${state.classPrefix}$identifier]'), + ), + Field( + (final b) => b + ..name = 'wireName' + ..modifier = FieldModifier.final$ + ..type = refer('String') + ..annotations.add(refer('override')) + ..assignment = Code("r'${state.classPrefix}$identifier'"), + ), + ]) + ..methods.addAll([ + Method((final b) { + b + ..name = 'serialize' + ..returns = refer('Object') + ..annotations.add(refer('override')) + ..requiredParameters.addAll([ + Parameter( + (final b) => b + ..name = 'serializers' + ..type = refer('Serializers'), + ), + Parameter( + (final b) => b + ..name = 'object' + ..type = refer('${state.classPrefix}$identifier'), + ), + ]) + ..optionalParameters.add( + Parameter( + (final b) => b + ..name = 'specifiedType' + ..type = refer('FullType') + ..named = true + ..defaultTo = const Code('FullType.unspecified'), + ), + ) + ..body = const Code('return object.data.value;'); + }), + Method((final b) { + b + ..name = 'deserialize' + ..returns = refer('${state.classPrefix}$identifier') + ..annotations.add(refer('override')) + ..requiredParameters.addAll([ + Parameter( + (final b) => b + ..name = 'serializers' + ..type = refer('Serializers'), + ), + Parameter( + (final b) => b + ..name = 'data' + ..type = refer('Object'), + ), + ]) + ..optionalParameters.add( + Parameter( + (final b) => b + ..name = 'specifiedType' + ..type = refer('FullType') + ..named = true + ..defaultTo = const Code('FullType.unspecified'), + ), + ) + ..body = Code( + [ + 'final result = new ${state.classPrefix}${identifier}Builder()', + '..data = JsonObject(data);', + if (schema.allOf != null) ...[ + for (final result in results) ...[ + if (result is TypeResultBase || result is TypeResultEnum) ...[ + 'result.${fields[result.name]!} = ${result.deserialize('data')};', + ] else ...[ + 'result.${fields[result.name]!}.replace(${result.deserialize('data')});', + ], + ], + ] else ...[ + if (schema.discriminator != null) ...[ + 'if (data is! Iterable) {', + r"throw StateError('Expected an Iterable but got ${data.runtimeType}');", + '}', + '', + 'String? discriminator;', + '', + 'final iterator = data.iterator;', + 'while (iterator.moveNext()) {', + 'final key = iterator.current! as String;', + 'iterator.moveNext();', + 'final Object? value = iterator.current;', + "if (key == '${schema.discriminator!.propertyName}') {", + 'discriminator = value! as String;', + 'break;', + '}', + '}', + ], + for (final result in results) ...[ + if (schema.discriminator != null) ...[ + "if (discriminator == '${result.name.replaceFirst(state.classPrefix, '')}'", + if (schema.discriminator!.mapping != null && schema.discriminator!.mapping!.isNotEmpty) ...[ + for (final key in schema.discriminator!.mapping!.entries + .where( + (final entry) => + entry.value.endsWith('/${result.name.replaceFirst(state.classPrefix, '')}'), + ) + .map((final entry) => entry.key)) ...[ + " || discriminator == '$key'", + ], + ') {', + ], + ], + 'try {', + if (result is TypeResultBase || result is TypeResultEnum) ...[ + 'result._${fields[result.name]!} = ${result.deserialize('data')};', + ] else ...[ + 'result._${fields[result.name]!} = ${result.deserialize('data')}.toBuilder();', + ], + '} catch (_) {', + if (schema.discriminator != null) ...[ + 'rethrow;', + ], + '}', + if (schema.discriminator != null) ...[ + '}', + ], + ], + if (schema.oneOf != null) ...[ + "assert([${fields.values.map((final e) => 'result._$e').join(',')}].where((final x) => x != null).length >= 1, 'Need oneOf for \${result._data}');", + ], + if (schema.anyOf != null) ...[ + "assert([${fields.values.map((final e) => 'result._$e').join(',')}].where((final x) => x != null).length >= 1, 'Need anyOf for \${result._data}');", + ], + ], + 'return result.build();', + ].join(), + ); + }), + ]), + ), + ]); + } + + return result; +} diff --git a/packages/dynamite/dynamite/lib/src/builder/resolve_enum.dart b/packages/dynamite/dynamite/lib/src/builder/resolve_enum.dart new file mode 100644 index 00000000..533c7f03 --- /dev/null +++ b/packages/dynamite/dynamite/lib/src/builder/resolve_enum.dart @@ -0,0 +1,112 @@ +import 'package:code_builder/code_builder.dart'; +import 'package:dynamite/src/builder/resolve_type.dart'; +import 'package:dynamite/src/builder/state.dart'; +import 'package:dynamite/src/helpers/built_value.dart'; +import 'package:dynamite/src/helpers/dart_helpers.dart'; +import 'package:dynamite/src/helpers/type_result.dart'; +import 'package:dynamite/src/models/open_api.dart'; +import 'package:dynamite/src/models/schema.dart'; +import 'package:dynamite/src/type_result/type_result.dart'; + +TypeResult resolveEnum( + final OpenAPI spec, + final State state, + final String identifier, + final Schema schema, + final TypeResult subResult, { + final bool nullable = false, +}) { + if (state.resolvedTypes.add(TypeResultEnum('${state.classPrefix}$identifier', subResult))) { + state.output.add( + Class( + (final b) => b + ..name = '${state.classPrefix}$identifier' + ..extend = refer('EnumClass') + ..constructors.add( + Constructor( + (final b) => b + ..name = '_' + ..constant = true + ..requiredParameters.add( + Parameter( + (final b) => b + ..name = 'name' + ..toSuper = true, + ), + ), + ), + ) + ..fields.addAll( + schema.enum_!.map( + (final value) => Field( + (final b) { + final result = resolveType( + spec, + state, + '$identifier${toDartName(value.toString(), uppercaseFirstCharacter: true)}', + schema, + ignoreEnum: true, + ); + b + ..name = toDartName(value.toString()) + ..static = true + ..modifier = FieldModifier.constant + ..type = refer('${state.classPrefix}$identifier') + ..assignment = Code( + '_\$${toCamelCase('${state.classPrefix}$identifier')}${toDartName(value.toString(), uppercaseFirstCharacter: true)}', + ); + + if (toDartName(value.toString()) != value.toString()) { + if (result.name != 'String' && result.name != 'int') { + throw Exception( + 'Sorry enum values are a bit broken. ' + 'See https://github.com/google/json_serializable.dart/issues/616. ' + 'Please remove the enum values on ${state.classPrefix}$identifier.', + ); + } + b.annotations.add( + refer('BuiltValueEnumConst').call([], { + 'wireName': refer(valueToEscapedValue(result, value.toString())), + }), + ); + } + }, + ), + ), + ) + ..methods.addAll([ + Method( + (final b) => b + ..name = 'values' + ..returns = refer('BuiltSet<${state.classPrefix}$identifier>') + ..lambda = true + ..static = true + ..body = Code('_\$${toCamelCase('${state.classPrefix}$identifier')}Values') + ..type = MethodType.getter, + ), + Method( + (final b) => b + ..name = 'valueOf' + ..returns = refer('${state.classPrefix}$identifier') + ..lambda = true + ..static = true + ..requiredParameters.add( + Parameter( + (final b) => b + ..name = 'name' + ..type = refer(subResult.name), + ), + ) + ..body = Code('_\$valueOf${state.classPrefix}$identifier(name)'), + ), + buildSerializer('${state.classPrefix}$identifier'), + ]), + ), + ); + } + return TypeResultEnum( + '${state.classPrefix}$identifier', + subResult, + nullable: nullable, + ); +} diff --git a/packages/dynamite/dynamite/lib/src/builder/resolve_object.dart b/packages/dynamite/dynamite/lib/src/builder/resolve_object.dart index 1b58eb85..5bf5ab64 100644 --- a/packages/dynamite/dynamite/lib/src/builder/resolve_object.dart +++ b/packages/dynamite/dynamite/lib/src/builder/resolve_object.dart @@ -1,6 +1,9 @@ +import 'package:built_collection/built_collection.dart'; import 'package:code_builder/code_builder.dart'; +import 'package:dynamite/src/builder/header_serializer.dart'; import 'package:dynamite/src/builder/resolve_type.dart'; import 'package:dynamite/src/builder/state.dart'; +import 'package:dynamite/src/helpers/built_value.dart'; import 'package:dynamite/src/helpers/dart_helpers.dart'; import 'package:dynamite/src/helpers/dynamite.dart'; import 'package:dynamite/src/helpers/type_result.dart'; @@ -10,7 +13,6 @@ import 'package:dynamite/src/type_result/type_result.dart'; TypeResult resolveObject( final OpenAPI spec, - final String variablePrefix, final State state, final String identifier, final Schema schema, { @@ -22,291 +24,66 @@ TypeResult resolveObject( nullable: nullable, ); if (state.resolvedTypes.add(result)) { - state.output.add( - Class( - (final b) { - b - ..name = '${state.classPrefix}$identifier' - ..docs.addAll(schema.formattedDescription) - ..abstract = true - ..implements.add( - refer( - 'Built<${state.classPrefix}$identifier, ${state.classPrefix}${identifier}Builder>', - ), - ) - ..constructors.addAll([ - Constructor( - (final b) => b - ..factory = true - ..lambda = true - ..optionalParameters.add( - Parameter( - (final b) => b - ..name = 'b' - ..type = refer('void Function(${state.classPrefix}${identifier}Builder)?'), - ), - ) - ..redirect = refer('_\$${state.classPrefix}$identifier'), - ), - Constructor( - (final b) => b - ..name = '_' - ..constant = true, - ), - Constructor( - (final b) => b - ..factory = true - ..name = 'fromJson' - ..lambda = true - ..requiredParameters.add( - Parameter( - (final b) => b - ..name = 'json' - ..type = refer('Map'), - ), - ) - ..body = const Code('_jsonSerializers.deserializeWith(serializer, json)!'), - ), - ]) - ..methods.addAll([ - Method( - (final b) => b - ..name = 'toJson' - ..returns = refer('Map') - ..lambda = true - ..body = const Code('_jsonSerializers.serializeWith(serializer, this)! as Map'), - ), - for (final property in schema.properties!.entries) ...[ - Method( - (final b) { - final propertyName = property.key; - final propertySchema = property.value; - final result = resolveType( - spec, - variablePrefix, - state, - '${identifier}_${toDartName(propertyName, uppercaseFirstCharacter: true)}', - propertySchema, - nullable: isDartParameterNullable( - schema.required?.contains(propertyName), - propertySchema, - ), - ); - - b - ..name = toDartName(propertyName) - ..returns = refer(result.nullableName) - ..type = MethodType.getter - ..docs.addAll(propertySchema.formattedDescription); - - if (toDartName(propertyName) != propertyName) { - b.annotations.add( - refer('BuiltValueField').call([], { - 'wireName': literalString(propertyName), - }), - ); - } - }, - ), - ], - Method((final b) { - b - ..name = 'serializer' - ..returns = refer('Serializer<${state.classPrefix}$identifier>') - ..lambda = true - ..static = true - ..body = Code( - isHeader - ? '_\$${state.classPrefix}${identifier}Serializer()' - : "_\$${toCamelCase('${state.classPrefix}$identifier')}Serializer", - ) - ..type = MethodType.getter; - if (isHeader) { - b.annotations.add(refer('BuiltValueSerializer').call([], {'custom': refer('true')})); - } - }), - ]); + final defaults = []; + for (final property in schema.properties!.entries) { + final propertySchema = property.value; + if (propertySchema.default_ != null) { + final value = propertySchema.default_!.toString(); + final result = resolveType( + spec, + state, + propertySchema.type!, + propertySchema, + ); + defaults.add('..${toDartName(property.key)} = ${valueToEscapedValue(result, value)}'); + } + } - final defaults = []; + state.output.add( + buildBuiltClass( + '${state.classPrefix}$identifier', + BuiltList.build((final b) { for (final property in schema.properties!.entries) { - final propertySchema = property.value; - if (propertySchema.default_ != null) { - final value = propertySchema.default_!.toString(); - final result = resolveType( - spec, - variablePrefix, - state, - propertySchema.type!, - propertySchema, - ); - defaults.add('..${toDartName(property.key)} = ${valueToEscapedValue(result, value)}'); - } - } - if (defaults.isNotEmpty) { - b.methods.add( + b.add( Method( - (final b) => b - ..name = '_defaults' - ..returns = refer('void') - ..static = true - ..lambda = true - ..annotations.add( - refer('BuiltValueHook').call( - [], - { - 'initializeBuilder': refer('true'), - }, - ), - ) - ..requiredParameters.add( - Parameter( - (final b) => b - ..name = 'b' - ..type = refer('${state.classPrefix}${identifier}Builder'), - ), - ) - ..body = Code( - [ - 'b', - ...defaults, - ].join(), - ), - ), - ); - } - }, - ), - ); - if (isHeader) { - state.output.add( - Class( - (final b) => b - ..name = '_\$${state.classPrefix}${identifier}Serializer' - ..implements.add(refer('StructuredSerializer<${state.classPrefix}$identifier>')) - ..fields.addAll([ - Field( - (final b) => b - ..name = 'types' - ..modifier = FieldModifier.final$ - ..type = refer('Iterable') - ..annotations.add(refer('override')) - ..assignment = Code('const [${state.classPrefix}$identifier, _\$${state.classPrefix}$identifier]'), - ), - Field( - (final b) => b - ..name = 'wireName' - ..modifier = FieldModifier.final$ - ..type = refer('String') - ..annotations.add(refer('override')) - ..assignment = Code("r'${state.classPrefix}$identifier'"), - ), - ]) - ..methods.addAll([ - Method((final b) { - b - ..name = 'serialize' - ..returns = refer('Iterable') - ..annotations.add(refer('override')) - ..requiredParameters.addAll([ - Parameter( - (final b) => b - ..name = 'serializers' - ..type = refer('Serializers'), - ), - Parameter( - (final b) => b - ..name = 'object' - ..type = refer('${state.classPrefix}$identifier'), - ), - ]) - ..optionalParameters.add( - Parameter( - (final b) => b - ..name = 'specifiedType' - ..type = refer('FullType') - ..named = true - ..defaultTo = const Code('FullType.unspecified'), - ), - ) - ..body = const Code('throw UnimplementedError();'); - }), - Method((final b) { - b - ..name = 'deserialize' - ..returns = refer('${state.classPrefix}$identifier') - ..annotations.add(refer('override')) - ..requiredParameters.addAll([ - Parameter( - (final b) => b - ..name = 'serializers' - ..type = refer('Serializers'), - ), - Parameter( - (final b) => b - ..name = 'serialized' - ..type = refer('Iterable'), - ), - ]) - ..optionalParameters.add( - Parameter( - (final b) => b - ..name = 'specifiedType' - ..type = refer('FullType') - ..named = true - ..defaultTo = const Code('FullType.unspecified'), - ), - ); - List deserializeProperty(final MapEntry property) { + (final b) { final propertyName = property.key; final propertySchema = property.value; final result = resolveType( spec, - variablePrefix, state, '${identifier}_${toDartName(propertyName, uppercaseFirstCharacter: true)}', propertySchema, - nullable: isDartParameterNullable(schema.required?.contains(propertyName), propertySchema), + nullable: isDartParameterNullable( + schema.required?.contains(propertyName), + propertySchema, + ), ); - return [ - Code("case '$propertyName':"), - if (result.className != 'String') ...[ - if (result is TypeResultBase || result is TypeResultEnum) ...[ - Code( - 'result.${toDartName(propertyName)} = ${result.deserialize(result.decode('value!'))};', - ), - ] else ...[ - Code( - 'result.${toDartName(propertyName)}.replace(${result.deserialize(result.decode('value!'))});', - ), - ], - ] else ...[ - Code( - 'result.${toDartName(propertyName)} = value!;', - ), - ], - ]; - } + b + ..name = toDartName(propertyName) + ..returns = refer(result.nullableName) + ..type = MethodType.getter + ..docs.addAll(propertySchema.formattedDescription); - b.body = Block.of([ - Code('final result = new ${state.classPrefix}${identifier}Builder();'), - const Code(''), - const Code('final iterator = serialized.iterator;'), - const Code('while (iterator.moveNext()) {'), - const Code('final key = iterator.current! as String;'), - const Code('iterator.moveNext();'), - const Code('final value = iterator.current! as String;'), - const Code('switch (key) {'), - for (final property in schema.properties!.entries) ...deserializeProperty(property), - const Code('}'), - const Code('}'), - const Code(''), - const Code('return result.build();'), - ]); - }), - ]), - ), - ); + if (toDartName(propertyName) != propertyName) { + b.annotations.add( + refer('BuiltValueField').call([], { + 'wireName': literalString(propertyName), + }), + ); + } + }, + ), + ); + } + }), + defaults: defaults, + customSerializer: isHeader, + ), + ); + if (isHeader) { + state.output.add(buildHeaderSerializer(state, identifier, spec, schema)); } } return result; diff --git a/packages/dynamite/dynamite/lib/src/builder/resolve_type.dart b/packages/dynamite/dynamite/lib/src/builder/resolve_type.dart index 0f17b165..a69d4999 100644 --- a/packages/dynamite/dynamite/lib/src/builder/resolve_type.dart +++ b/packages/dynamite/dynamite/lib/src/builder/resolve_type.dart @@ -1,15 +1,13 @@ -import 'package:code_builder/code_builder.dart'; +import 'package:dynamite/src/builder/ofs_builder.dart'; +import 'package:dynamite/src/builder/resolve_enum.dart'; import 'package:dynamite/src/builder/resolve_object.dart'; import 'package:dynamite/src/builder/state.dart'; -import 'package:dynamite/src/helpers/dart_helpers.dart'; -import 'package:dynamite/src/helpers/type_result.dart'; import 'package:dynamite/src/models/open_api.dart'; import 'package:dynamite/src/models/schema.dart'; import 'package:dynamite/src/type_result/type_result.dart'; TypeResult resolveType( final OpenAPI spec, - final String variablePrefix, final State state, final String identifier, final Schema schema, { @@ -27,281 +25,22 @@ TypeResult resolveType( final name = schema.ref!.split('/').last; result = resolveType( spec, - variablePrefix, state, name, spec.components!.schemas![name]!, nullable: nullable, ); } else if (schema.ofs != null) { - result = TypeResultObject( - '${state.classPrefix}$identifier', + result = resolveOfs( + spec, + state, + identifier, + schema, nullable: nullable, ); - if (state.resolvedTypes.add(result)) { - final results = schema.ofs! - .map( - (final s) => resolveType( - spec, - variablePrefix, - state, - '$identifier${schema.ofs!.indexOf(s)}', - s, - nullable: !(schema.allOf?.contains(s) ?? false), - ), - ) - .toList(); - - final fields = {}; - for (final result in results) { - final dartName = toDartName(result.name.replaceFirst(state.classPrefix, '')); - fields[result.name] = toFieldName(dartName, result.name.replaceFirst(state.classPrefix, '')); - } - - state.output.addAll([ - Class( - (final b) { - b - ..name = '${state.classPrefix}$identifier' - ..abstract = true - ..implements.add( - refer( - 'Built<${state.classPrefix}$identifier, ${state.classPrefix}${identifier}Builder>', - ), - ) - ..constructors.addAll([ - Constructor( - (final b) => b - ..name = '_' - ..constant = true, - ), - Constructor( - (final b) => b - ..factory = true - ..lambda = true - ..optionalParameters.add( - Parameter( - (final b) => b - ..name = 'b' - ..type = refer('void Function(${state.classPrefix}${identifier}Builder)?'), - ), - ) - ..redirect = refer('_\$${state.classPrefix}$identifier'), - ), - ]) - ..methods.addAll([ - Method( - (final b) { - b - ..name = 'data' - ..returns = refer('JsonObject') - ..type = MethodType.getter; - }, - ), - for (final result in results) ...[ - Method( - (final b) { - final s = schema.ofs![results.indexOf(result)]; - b - ..name = fields[result.name] - ..returns = refer(result.nullableName) - ..type = MethodType.getter - ..docs.addAll(s.formattedDescription); - }, - ), - ], - Method( - (final b) => b - ..static = true - ..name = 'fromJson' - ..lambda = true - ..returns = refer('${state.classPrefix}$identifier') - ..requiredParameters.add( - Parameter( - (final b) => b - ..name = 'json' - ..type = refer('Object'), - ), - ) - ..body = const Code('_jsonSerializers.deserializeWith(serializer, json)!'), - ), - Method( - (final b) => b - ..name = 'toJson' - ..returns = refer('Map') - ..lambda = true - ..body = const Code('_jsonSerializers.serializeWith(serializer, this)! as Map'), - ), - Method( - (final b) => b - ..name = 'serializer' - ..returns = refer('Serializer<${state.classPrefix}$identifier>') - ..lambda = true - ..static = true - ..annotations.add(refer('BuiltValueSerializer').call([], {'custom': refer('true')})) - ..body = Code('_\$${state.classPrefix}${identifier}Serializer()') - ..type = MethodType.getter, - ), - ]); - }, - ), - Class( - (final b) => b - ..name = '_\$${state.classPrefix}${identifier}Serializer' - ..implements.add(refer('PrimitiveSerializer<${state.classPrefix}$identifier>')) - ..fields.addAll([ - Field( - (final b) => b - ..name = 'types' - ..modifier = FieldModifier.final$ - ..type = refer('Iterable') - ..annotations.add(refer('override')) - ..assignment = Code('const [${state.classPrefix}$identifier, _\$${state.classPrefix}$identifier]'), - ), - Field( - (final b) => b - ..name = 'wireName' - ..modifier = FieldModifier.final$ - ..type = refer('String') - ..annotations.add(refer('override')) - ..assignment = Code("r'${state.classPrefix}$identifier'"), - ), - ]) - ..methods.addAll([ - Method((final b) { - b - ..name = 'serialize' - ..returns = refer('Object') - ..annotations.add(refer('override')) - ..requiredParameters.addAll([ - Parameter( - (final b) => b - ..name = 'serializers' - ..type = refer('Serializers'), - ), - Parameter( - (final b) => b - ..name = 'object' - ..type = refer('${state.classPrefix}$identifier'), - ), - ]) - ..optionalParameters.add( - Parameter( - (final b) => b - ..name = 'specifiedType' - ..type = refer('FullType') - ..named = true - ..defaultTo = const Code('FullType.unspecified'), - ), - ) - ..body = const Code('return object.data.value;'); - }), - Method((final b) { - b - ..name = 'deserialize' - ..returns = refer('${state.classPrefix}$identifier') - ..annotations.add(refer('override')) - ..requiredParameters.addAll([ - Parameter( - (final b) => b - ..name = 'serializers' - ..type = refer('Serializers'), - ), - Parameter( - (final b) => b - ..name = 'data' - ..type = refer('Object'), - ), - ]) - ..optionalParameters.add( - Parameter( - (final b) => b - ..name = 'specifiedType' - ..type = refer('FullType') - ..named = true - ..defaultTo = const Code('FullType.unspecified'), - ), - ) - ..body = Code( - [ - 'final result = new ${state.classPrefix}${identifier}Builder()', - '..data = JsonObject(data);', - if (schema.allOf != null) ...[ - for (final result in results) ...[ - if (result is TypeResultBase || result is TypeResultEnum) ...[ - 'result.${fields[result.name]!} = ${result.deserialize('data')};', - ] else ...[ - 'result.${fields[result.name]!}.replace(${result.deserialize('data')});', - ], - ], - ] else ...[ - if (schema.discriminator != null) ...[ - 'if (data is! Iterable) {', - r"throw StateError('Expected an Iterable but got ${data.runtimeType}');", - '}', - '', - 'String? discriminator;', - '', - 'final iterator = data.iterator;', - 'while (iterator.moveNext()) {', - 'final key = iterator.current! as String;', - 'iterator.moveNext();', - 'final Object? value = iterator.current;', - "if (key == '${schema.discriminator!.propertyName}') {", - 'discriminator = value! as String;', - 'break;', - '}', - '}', - ], - for (final result in results) ...[ - if (schema.discriminator != null) ...[ - "if (discriminator == '${result.name.replaceFirst(state.classPrefix, '')}'", - if (schema.discriminator!.mapping != null && schema.discriminator!.mapping!.isNotEmpty) ...[ - for (final key in schema.discriminator!.mapping!.entries - .where( - (final entry) => - entry.value.endsWith('/${result.name.replaceFirst(state.classPrefix, '')}'), - ) - .map((final entry) => entry.key)) ...[ - " || discriminator == '$key'", - ], - ') {', - ], - ], - 'try {', - if (result is TypeResultBase || result is TypeResultEnum) ...[ - 'result._${fields[result.name]!} = ${result.deserialize('data')};', - ] else ...[ - 'result._${fields[result.name]!} = ${result.deserialize('data')}.toBuilder();', - ], - '} catch (_) {', - if (schema.discriminator != null) ...[ - 'rethrow;', - ], - '}', - if (schema.discriminator != null) ...[ - '}', - ], - ], - if (schema.oneOf != null) ...[ - "assert([${fields.values.map((final e) => 'result._$e').join(',')}].where((final x) => x != null).length >= 1, 'Need oneOf for \${result._data}');", - ], - if (schema.anyOf != null) ...[ - "assert([${fields.values.map((final e) => 'result._$e').join(',')}].where((final x) => x != null).length >= 1, 'Need anyOf for \${result._data}');", - ], - ], - 'return result.build();', - ].join(), - ); - }), - ]), - ), - ]); - } } else if (schema.isContentString) { final subResult = resolveType( spec, - variablePrefix, state, identifier, schema.contentSchema!, @@ -346,7 +85,6 @@ TypeResult resolveType( if (schema.items != null) { final subResult = resolveType( spec, - variablePrefix, state, identifier, schema.items!, @@ -379,7 +117,6 @@ TypeResult resolveType( } else { final subResult = resolveType( spec, - variablePrefix, state, identifier, schema.additionalProperties!, @@ -399,7 +136,6 @@ TypeResult resolveType( } else { result = resolveObject( spec, - variablePrefix, state, identifier, schema, @@ -411,105 +147,11 @@ TypeResult resolveType( if (result != null) { if (!ignoreEnum && schema.enum_ != null) { - if (state.resolvedTypes.add(TypeResultEnum('${state.classPrefix}$identifier', result))) { - state.output.add( - Class( - (final b) => b - ..name = '${state.classPrefix}$identifier' - ..extend = refer('EnumClass') - ..constructors.add( - Constructor( - (final b) => b - ..name = '_' - ..constant = true - ..requiredParameters.add( - Parameter( - (final b) => b - ..name = 'name' - ..toSuper = true, - ), - ), - ), - ) - ..fields.addAll( - schema.enum_!.map( - (final value) => Field( - (final b) { - final result = resolveType( - spec, - variablePrefix, - state, - '$identifier${toDartName(value.toString(), uppercaseFirstCharacter: true)}', - schema, - ignoreEnum: true, - ); - b - ..name = toDartName(value.toString()) - ..static = true - ..modifier = FieldModifier.constant - ..type = refer('${state.classPrefix}$identifier') - ..assignment = Code( - '_\$${toCamelCase('${state.classPrefix}$identifier')}${toDartName(value.toString(), uppercaseFirstCharacter: true)}', - ); - - if (toDartName(value.toString()) != value.toString()) { - if (result.name != 'String' && result.name != 'int') { - throw Exception( - 'Sorry enum values are a bit broken. ' - 'See https://github.com/google/json_serializable.dart/issues/616. ' - 'Please remove the enum values on ${state.classPrefix}$identifier.', - ); - } - b.annotations.add( - refer('BuiltValueEnumConst').call([], { - 'wireName': refer(valueToEscapedValue(result, value.toString())), - }), - ); - } - }, - ), - ), - ) - ..methods.addAll([ - Method( - (final b) => b - ..name = 'values' - ..returns = refer('BuiltSet<${state.classPrefix}$identifier>') - ..lambda = true - ..static = true - ..body = Code('_\$${toCamelCase('${state.classPrefix}$identifier')}Values') - ..type = MethodType.getter, - ), - Method( - (final b) => b - ..name = 'valueOf' - ..returns = refer('${state.classPrefix}$identifier') - ..lambda = true - ..static = true - ..requiredParameters.add( - Parameter( - (final b) => b - ..name = 'name' - ..type = refer(result!.name), - ), - ) - ..body = Code('_\$valueOf${state.classPrefix}$identifier(name)'), - ), - Method( - (final b) => b - ..name = 'serializer' - ..returns = refer('Serializer<${state.classPrefix}$identifier>') - ..lambda = true - ..static = true - ..body = Code("_\$${toCamelCase('${state.classPrefix}$identifier')}Serializer") - ..type = MethodType.getter, - ), - ]), - ), - ); - } - result = TypeResultEnum( - '${state.classPrefix}$identifier', + result = resolveEnum( + spec, + state, + identifier, + schema, result, nullable: nullable, ); diff --git a/packages/dynamite/dynamite/lib/src/builder/state.dart b/packages/dynamite/dynamite/lib/src/builder/state.dart index ad5acc77..75c9abe6 100644 --- a/packages/dynamite/dynamite/lib/src/builder/state.dart +++ b/packages/dynamite/dynamite/lib/src/builder/state.dart @@ -1,14 +1,15 @@ import 'package:code_builder/code_builder.dart'; +import 'package:dynamite/src/helpers/dart_helpers.dart'; import 'package:dynamite/src/type_result/type_result.dart'; class State { - State({ - required this.classPrefix, - required this.variablePrefix, - }); + State(final String prefix) + : classPrefix = toDartName(prefix, uppercaseFirstCharacter: true), + variablePrefix = toDartName(prefix); final String classPrefix; final String variablePrefix; + final output = []; final resolvedTypes = {}; } diff --git a/packages/dynamite/dynamite/lib/src/helpers/built_value.dart b/packages/dynamite/dynamite/lib/src/helpers/built_value.dart new file mode 100644 index 00000000..7ac0f247 --- /dev/null +++ b/packages/dynamite/dynamite/lib/src/helpers/built_value.dart @@ -0,0 +1,120 @@ +import 'package:built_collection/built_collection.dart'; +import 'package:code_builder/code_builder.dart'; +import 'package:dynamite/src/helpers/dart_helpers.dart'; + +Spec buildBuiltClass( + final String className, + final BuiltList methods, { + final Iterable? defaults, + final bool customSerializer = false, +}) => + Class( + (final b) { + b + ..name = className + ..abstract = true + ..implements.add( + refer( + 'Built<$className, ${className}Builder>', + ), + ) + ..constructors.addAll([ + builtValueConstructor(className), + hiddenConstructor, + fromJsonConstructor, + ]) + ..methods.addAll([ + toJsonMethod, + ...methods, + buildSerializer(className, isCustom: customSerializer), + ]); + + if (defaults != null && defaults.isNotEmpty) { + b.methods.add( + Method( + (final b) => b + ..name = '_defaults' + ..returns = refer('void') + ..static = true + ..lambda = true + ..annotations.add( + refer('BuiltValueHook').call([], { + 'initializeBuilder': literalTrue, + }), + ) + ..requiredParameters.add( + Parameter( + (final b) => b + ..name = 'b' + ..type = refer('${className}Builder'), + ), + ) + ..body = Code( + [ + 'b', + ...defaults, + ].join(), + ), + ), + ); + } + }, + ); + +Method get toJsonMethod => Method( + (final b) => b + ..name = 'toJson' + ..returns = refer('Map') + ..lambda = true + ..body = const Code('_jsonSerializers.serializeWith(serializer, this)! as Map'), + ); + +Method buildSerializer(final String className, {final bool isCustom = false}) => Method((final b) { + b + ..name = 'serializer' + ..returns = refer('Serializer<$className>') + ..lambda = true + ..static = true + ..body = Code( + isCustom ? '_\$${className}Serializer()' : '_\$${toCamelCase(className)}Serializer', + ) + ..type = MethodType.getter; + if (isCustom) { + b.annotations.add(refer('BuiltValueSerializer').call([], {'custom': literalTrue})); + } + }); + +Constructor builtValueConstructor(final String className) => Constructor( + (final b) => b + ..factory = true + ..lambda = true + ..optionalParameters.add( + Parameter( + (final b) => b + ..name = 'b' + ..type = refer('void Function(${className}Builder)?'), + ), + ) + ..redirect = refer('_\$$className'), + ); + +Constructor get hiddenConstructor => Constructor( + (final b) => b + ..name = '_' + ..constant = true, + ); + +Constructor get fromJsonConstructor => Constructor( + (final b) => b + ..factory = true + ..name = 'fromJson' + ..lambda = true + ..requiredParameters.add( + Parameter( + (final b) => b + ..name = 'json' + ..type = refer('Map'), + ), + ) + ..body = const Code('_jsonSerializers.deserializeWith(serializer, json)!'), + ); diff --git a/packages/dynamite/dynamite/lib/src/openapi_builder.dart b/packages/dynamite/dynamite/lib/src/openapi_builder.dart index fe84455f..a893bf80 100644 --- a/packages/dynamite/dynamite/lib/src/openapi_builder.dart +++ b/packages/dynamite/dynamite/lib/src/openapi_builder.dart @@ -39,8 +39,7 @@ class OpenAPIBuilder implements Builder { await buildStep.readAsString(inputId), ) as Map, ); - final classPrefix = toDartName(spec.info.title, uppercaseFirstCharacter: true); - final variablePrefix = toDartName(spec.info.title); + final supportedVersions = ['3.0.3', '3.1.0']; if (!supportedVersions.contains(spec.version)) { throw Exception('Only OpenAPI ${supportedVersions.join(', ')} are supported'); @@ -71,10 +70,7 @@ class OpenAPIBuilder implements Builder { : a.compareTo(b), ); - final state = State( - classPrefix: classPrefix, - variablePrefix: variablePrefix, - ); + final state = State(spec.info.title); final output = [ '// ignore_for_file: camel_case_types', @@ -97,7 +93,7 @@ class OpenAPIBuilder implements Builder { '', Class( (final b) => b - ..name = '${classPrefix}Response' + ..name = '${state.classPrefix}Response' ..types.addAll([ refer('T'), refer('U'), @@ -125,14 +121,14 @@ class OpenAPIBuilder implements Builder { ..annotations.add(refer('override')) ..lambda = true ..body = Code( - "'${classPrefix}Response(data: \$data, headers: \$headers)'", + "'${state.classPrefix}Response(data: \$data, headers: \$headers)'", ), ), ), ).accept(emitter).toString(), Class( (final b) => b - ..name = '${classPrefix}ApiException' + ..name = '${state.classPrefix}ApiException' ..extend = refer('DynamiteApiException') ..constructors.add( Constructor( @@ -152,7 +148,7 @@ class OpenAPIBuilder implements Builder { Method( (final b) => b ..name = 'fromResponse' - ..returns = refer('Future<${classPrefix}ApiException>') + ..returns = refer('Future<${state.classPrefix}ApiException>') ..static = true ..modifier = MethodModifier.async ..requiredParameters.add( @@ -170,7 +166,7 @@ class OpenAPIBuilder implements Builder { const Code("body = 'binary';"), const Code('}'), const Code(''), - Code('return ${classPrefix}ApiException('), + Code('return ${state.classPrefix}ApiException('), const Code('response.statusCode,'), const Code('response.responseHeaders,'), const Code('body,'), @@ -184,7 +180,7 @@ class OpenAPIBuilder implements Builder { ..annotations.add(refer('override')) ..lambda = true ..body = Code( - "'${classPrefix}ApiException(statusCode: \$statusCode, headers: \$headers, body: \$body)'", + "'${state.classPrefix}ApiException(statusCode: \$statusCode, headers: \$headers, body: \$body)'", ), ), ]), @@ -291,7 +287,7 @@ class OpenAPIBuilder implements Builder { Field( (final b) => b ..name = '_rootClient' - ..type = refer('${classPrefix}Client') + ..type = refer('${state.classPrefix}Client') ..modifier = FieldModifier.final$, ), ) @@ -308,7 +304,7 @@ class OpenAPIBuilder implements Builder { ); } b - ..name = '$classPrefix${isRootClient ? 'Client' : clientName(tag)}' + ..name = '${state.classPrefix}${isRootClient ? 'Client' : clientName(tag)}' ..docs.addAll(spec.formattedTagsFor(tag)) ..methods.addAll( [ @@ -320,8 +316,9 @@ class OpenAPIBuilder implements Builder { ..name = toDartName(tag == null ? t : t.substring('$tag/'.length)) ..lambda = true ..type = MethodType.getter - ..returns = refer('$classPrefix${clientName(t)}') - ..body = Code('$classPrefix${clientName(t)}(${isRootClient ? 'this' : '_rootClient'})'), + ..returns = refer('${state.classPrefix}${clientName(t)}') + ..body = + Code('${state.classPrefix}${clientName(t)}(${isRootClient ? 'this' : '_rootClient'})'), ), ], for (final pathEntry in paths.entries) ...[ @@ -389,7 +386,6 @@ class OpenAPIBuilder implements Builder { final result = resolveType( spec, - variablePrefix, state, toDartName( '$operationId-${parameter.name}', @@ -495,13 +491,12 @@ class OpenAPIBuilder implements Builder { final result = resolveType( spec, - variablePrefix, state, toDartName('$operationId-request-$mimeType', uppercaseFirstCharacter: true), mediaType.schema!, nullable: dartParameterNullable, ); - final parameterName = toDartName(result.name.replaceFirst(classPrefix, '')); + final parameterName = toDartName(result.name.replaceFirst(state.classPrefix, '')); switch (mimeType) { case 'application/json': case 'application/x-www-form-urlencoded': @@ -561,7 +556,6 @@ class OpenAPIBuilder implements Builder { '${tag != null ? toDartName(tag, uppercaseFirstCharacter: true) : null}${toDartName(operationId, uppercaseFirstCharacter: true)}Headers'; final result = resolveObject( spec, - variablePrefix, state, identifier, Schema( @@ -591,7 +585,6 @@ class OpenAPIBuilder implements Builder { final result = resolveType( spec, - variablePrefix, state, toDartName( '$operationId-response-$statusCode-$mimeType', @@ -631,9 +624,9 @@ class OpenAPIBuilder implements Builder { } if (headersType != null && dataType != null) { - b.returns = refer('Future<${classPrefix}Response<$dataType, $headersType>>'); + b.returns = refer('Future<${state.classPrefix}Response<$dataType, $headersType>>'); code.write( - 'return ${classPrefix}Response<$dataType, $headersType>(${dataNeedsAwait ?? false ? 'await ' : ''}$dataValue, $headersValue,);', + 'return ${state.classPrefix}Response<$dataType, $headersType>(${dataNeedsAwait ?? false ? 'await ' : ''}$dataValue, $headersValue,);', ); } else if (headersType != null) { b.returns = refer('Future<$headersType>'); @@ -649,7 +642,7 @@ class OpenAPIBuilder implements Builder { code.write('}'); } code.write( - 'throw await ${classPrefix}ApiException.fromResponse(_response); // coverage:ignore-line\n', + 'throw await ${state.classPrefix}ApiException.fromResponse(_response); // coverage:ignore-line\n', ); } else { b.returns = refer('Future'); @@ -674,7 +667,6 @@ class OpenAPIBuilder implements Builder { } else { final result = resolveType( spec, - variablePrefix, state, identifier, schema.value, diff --git a/packages/dynamite/dynamite/pubspec.yaml b/packages/dynamite/dynamite/pubspec.yaml index 09c75580..69406723 100644 --- a/packages/dynamite/dynamite/pubspec.yaml +++ b/packages/dynamite/dynamite/pubspec.yaml @@ -6,6 +6,7 @@ environment: dependencies: build: ^2.4.1 + built_collection: ^5.1.1 built_value: ^8.6.2 code_builder: ^4.6.0 collection: ^1.17.2 @@ -17,7 +18,6 @@ dependencies: dev_dependencies: build_runner: ^2.4.6 - built_collection: ^5.1.1 built_value_generator: ^8.6.2 json_serializable: ^6.7.1 neon_lints: