From 3d0508edc0038312a4f74734ce8a5562420ddbbf Mon Sep 17 00:00:00 2001 From: Nikolas Rimikis Date: Sun, 3 Sep 2023 17:45:09 +0200 Subject: [PATCH] feat(dynamite): use inheritance for allOf Signed-off-by: Nikolas Rimikis --- .../dynamite/lib/src/builder/ofs_builder.dart | 168 ++++++++++++------ .../lib/src/builder/resolve_interface.dart | 91 ++++++++++ .../lib/src/builder/resolve_object.dart | 56 ++---- .../lib/src/builder/resolve_type.dart | 8 + .../dynamite/lib/src/builder/state.dart | 1 + .../dynamite/lib/src/helpers/built_value.dart | 114 ++++++------ 6 files changed, 284 insertions(+), 154 deletions(-) create mode 100644 packages/dynamite/dynamite/lib/src/builder/resolve_interface.dart diff --git a/packages/dynamite/dynamite/lib/src/builder/ofs_builder.dart b/packages/dynamite/dynamite/lib/src/builder/ofs_builder.dart index 1b9d7bc9..04583373 100644 --- a/packages/dynamite/dynamite/lib/src/builder/ofs_builder.dart +++ b/packages/dynamite/dynamite/lib/src/builder/ofs_builder.dart @@ -1,5 +1,6 @@ import 'package:built_collection/built_collection.dart'; import 'package:code_builder/code_builder.dart'; +import 'package:dynamite/src/builder/resolve_interface.dart'; import 'package:dynamite/src/builder/resolve_type.dart'; import 'package:dynamite/src/builder/state.dart'; import 'package:dynamite/src/helpers/built_value.dart'; @@ -8,6 +9,61 @@ 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 resolveAllOf( + 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 interfaces = {}; + + for (final s in schema.allOf!) { + final TypeResultObject interfaceClass; + if (s.ref != null) { + final object = resolveType( + spec, + state, + identifier, + s, + nullable: nullable, + ); + + if (object is! TypeResultObject) { + throw StateError('allOf does only allow objects. Please change $identifier'); + } + + interfaceClass = object; + } else { + final interfaceName = schema.ofs!.length == 1 ? identifier : '${identifier}_${schema.allOf!.indexOf(s)}'; + + interfaceClass = resolveInterface( + spec, + state, + interfaceName, + s, + ); + } + + interfaces.add(interfaceClass); + } + + state.output.add( + buildBuiltClass( + '${state.classPrefix}$identifier', + interfaces: interfaces, + ), + ); + } + return result; +} + TypeResult resolveOfs( final OpenAPI spec, final State state, @@ -15,6 +71,10 @@ TypeResult resolveOfs( final Schema schema, { final bool nullable = false, }) { + if (schema.allOf != null) { + throw StateError('allOf should be handled with inheritance'); + } + final result = TypeResultObject( '${state.classPrefix}$identifier', nullable: nullable, @@ -28,7 +88,7 @@ TypeResult resolveOfs( state, '$identifier${schema.ofs!.indexOf(s)}', s, - nullable: !(schema.allOf?.contains(s) ?? false), + nullable: true, ), ) .toList(); @@ -42,7 +102,7 @@ TypeResult resolveOfs( state.output.addAll([ buildBuiltClass( '${state.classPrefix}$identifier', - BuiltList.build((final b) { + methods: BuiltList.build((final b) { b.add( Method( (final b) { @@ -152,69 +212,59 @@ TypeResult resolveOfs( [ '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 (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'", - ], - ') {', + "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}');", + 'try {', + if (result is TypeResultBase || result is TypeResultEnum) ...[ + 'result._${fields[result.name]!} = ${result.deserialize('data')};', + ] else ...[ + 'result._${fields[result.name]!} = ${result.deserialize('data')}.toBuilder();', ], - 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}');", + '} 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(), diff --git a/packages/dynamite/dynamite/lib/src/builder/resolve_interface.dart b/packages/dynamite/dynamite/lib/src/builder/resolve_interface.dart new file mode 100644 index 00000000..d20f472f --- /dev/null +++ b/packages/dynamite/dynamite/lib/src/builder/resolve_interface.dart @@ -0,0 +1,91 @@ +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/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'; + +TypeResultObject resolveInterface( + final OpenAPI spec, + final State state, + final String identifier, + final Schema schema, +) { + final result = TypeResultObject( + '${state.classPrefix}$identifier', + ); + + if (state.resolvedInterfaces.add(result)) { + final className = '${state.classPrefix}$identifier$interfaceSuffix'; + + state.output.add( + Class((final b) { + b + ..abstract = true + ..modifier = ClassModifier.interface + ..name = className + ..annotations.add(refer('BuiltValue').call([], {'instantiable': literalFalse})); + + for (final property in schema.properties!.entries) { + b.methods.add( + Method( + (final b) { + 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, + ), + ); + + 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), + }), + ); + } + }, + ), + ); + } + + b.methods.addAll([ + Method( + (final b) => b + ..returns = refer(className) + ..name = 'rebuild' + ..requiredParameters.add( + Parameter( + (final b) => b + ..name = 'updates' + ..type = refer('void Function(${className}Builder)'), + ), + ), + ), + Method( + (final b) => b + ..returns = refer('${className}Builder') + ..name = 'toBuilder', + ), + ]); + }), + ); + } + + return result; +} diff --git a/packages/dynamite/dynamite/lib/src/builder/resolve_object.dart b/packages/dynamite/dynamite/lib/src/builder/resolve_object.dart index 5bf5ab64..d0300f1e 100644 --- a/packages/dynamite/dynamite/lib/src/builder/resolve_object.dart +++ b/packages/dynamite/dynamite/lib/src/builder/resolve_object.dart @@ -1,17 +1,15 @@ -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_interface.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'; 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 resolveObject( +TypeResultObject resolveObject( final OpenAPI spec, final State state, final String identifier, @@ -39,49 +37,23 @@ TypeResult resolveObject( } } - state.output.add( + final interfaceClass = resolveInterface( + spec, + state, + identifier, + schema, + ); + + state.output.addAll([ buildBuiltClass( '${state.classPrefix}$identifier', - BuiltList.build((final b) { - for (final property in schema.properties!.entries) { - b.add( - Method( - (final b) { - 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, - ), - ); - - 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), - }), - ); - } - }, - ), - ); - } - }), defaults: defaults, customSerializer: isHeader, + interfaces: [ + interfaceClass, + ], ), - ); + ]); if (isHeader) { state.output.add(buildHeaderSerializer(state, identifier, spec, schema)); } diff --git a/packages/dynamite/dynamite/lib/src/builder/resolve_type.dart b/packages/dynamite/dynamite/lib/src/builder/resolve_type.dart index a69d4999..b6bab840 100644 --- a/packages/dynamite/dynamite/lib/src/builder/resolve_type.dart +++ b/packages/dynamite/dynamite/lib/src/builder/resolve_type.dart @@ -30,6 +30,14 @@ TypeResult resolveType( spec.components!.schemas![name]!, nullable: nullable, ); + } else if (schema.allOf != null) { + result = resolveAllOf( + spec, + state, + identifier, + schema, + nullable: nullable, + ); } else if (schema.ofs != null) { result = resolveOfs( spec, diff --git a/packages/dynamite/dynamite/lib/src/builder/state.dart b/packages/dynamite/dynamite/lib/src/builder/state.dart index 75c9abe6..c9d66d20 100644 --- a/packages/dynamite/dynamite/lib/src/builder/state.dart +++ b/packages/dynamite/dynamite/lib/src/builder/state.dart @@ -12,4 +12,5 @@ class State { final output = []; final resolvedTypes = {}; + final resolvedInterfaces = {}; } diff --git a/packages/dynamite/dynamite/lib/src/helpers/built_value.dart b/packages/dynamite/dynamite/lib/src/helpers/built_value.dart index 7ac0f247..bf58c9c0 100644 --- a/packages/dynamite/dynamite/lib/src/helpers/built_value.dart +++ b/packages/dynamite/dynamite/lib/src/helpers/built_value.dart @@ -1,65 +1,73 @@ import 'package:built_collection/built_collection.dart'; import 'package:code_builder/code_builder.dart'; import 'package:dynamite/src/helpers/dart_helpers.dart'; +import 'package:dynamite/src/type_result/type_result.dart'; + +const interfaceSuffix = 'Interface'; Spec buildBuiltClass( - final String className, - final BuiltList methods, { + final String className, { + final BuiltList? methods, final Iterable? defaults, + final Iterable? interfaces, 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), - ]); +}) { + assert((interfaces == null) != (methods == null), 'Either provide an interface or methods.'); + + return Class( + (final b) { + b + ..name = className + ..abstract = true + ..implements.addAll([ + ...?interfaces?.map((final i) => refer('${i.name}$interfaceSuffix')), + 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(), + 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