Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix: #290 supports oneOf polymorphic types with dart_mappable #292

Open
wants to merge 5 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -13,37 +13,83 @@ String dartDartMappableDtoTemplate(
required bool markFileAsGenerated,
}) {
final className = dataClass.name.toPascal;

final parent = dataClass.discriminatorValue?.parentClass;

return '''
${generatedFileComment(markFileAsGenerated: markFileAsGenerated)}
${dartImportDtoTemplate(JsonSerializer.dartMappable)}
${dartImports(imports: dataClass.imports)}
part '${dataClass.name.toSnake}.mapper.dart';

${descriptionComment(dataClass.description)}@MappableClass()
class $className with ${className}Mappable {
${descriptionComment(dataClass.description)}@MappableClass(${() {
if (dataClass.discriminator != null) {
return [
"discriminatorKey: '${dataClass.discriminator!.propertyName}'",
"includeSubClasses: [${dataClass.discriminator!.discriminatorValueToRefMapping.values.join(', ')}]",
].join(", ");
}
if (dataClass.discriminatorValue != null) {
return "discriminatorValue: '${dataClass.discriminatorValue!.propertyValue}'";
}
return "";
}()})
class $className ${parent != null ? "extends $parent " : ""}with ${className}Mappable {

${indentation(2)}const $className(${getParameters(dataClass)});

${getFields(dataClass)}

${getDiscriminatorConvenienceMethods(dataClass)}
${indentation(
2,
)}static $className fromJson(Map<String, dynamic> json) => ${className}Mapper.ensureInitialized().decodeMap<$className>(json);
}
''';
}

String getDiscriminatorConvenienceMethods(UniversalComponentClass dataClass){
if (dataClass.discriminator == null){
return '';
}
return '''
T when<T>({
${dataClass.discriminator!.discriminatorValueToRefMapping.entries.map((e) => 'required T Function(${e.value} ${e.key.toCamel}) ${e.key.toCamel},').join('\n')}
}) {
return maybeWhen(
${dataClass.discriminator!.discriminatorValueToRefMapping.entries.map((e) => '${e.key.toCamel}: ${e.key.toCamel},').join('\n')}
)!;
}
T? maybeWhen<T>({
${dataClass.discriminator!.discriminatorValueToRefMapping.entries.map((e) => 'T Function(${e.value} ${e.key.toCamel})? ${e.key.toCamel},').join('\n')}
}) {
return switch (this) {
${dataClass.discriminator!.discriminatorValueToRefMapping.entries.map((e) => '${e.value} _ => ${e.key.toCamel}?.call(this as ${e.value}),').join('\n')}
_ => throw Exception("Unhandled type: \${this.runtimeType}"),
};
}
''';
}

String getParameters(UniversalComponentClass dataClass) {
if (dataClass.parameters.isNotEmpty) {
return '{\n${_parametersToString(dataClass.parameters)}\n${indentation(2)}}';
// if this class has discriminated values, don't populate the discriminator field
// in the parent class
final parameters = dataClass.parameters
.where((it) => it.name != dataClass.discriminator?.propertyName)
.toList();
if (parameters.isNotEmpty) {
return '{\n${_parametersToString(parameters)}\n${indentation(2)}}';
} else {
return '';
}
}

String getFields(UniversalComponentClass dataClass) {
if (dataClass.parameters.isNotEmpty) {
return '${_fieldsToString(dataClass.parameters)}\n';
// if this class has discriminated values, don't populate the discriminator field
// in the parent class
final parameters = dataClass.parameters
.where((it) => it.name != dataClass.discriminator?.propertyName)
.toList();
if (parameters.isNotEmpty) {
return '${_fieldsToString(parameters)}\n';
} else {
return '';
}
Expand Down
36 changes: 25 additions & 11 deletions swagger_parser/lib/src/parser/model/universal_component_class.dart
Original file line number Diff line number Diff line change
@@ -1,39 +1,53 @@
part of 'universal_data_class.dart';

typedef Discriminator = ({
// The name of the property that is used to discriminate the oneOf variants
String propertyName,

// The mapping of the property value to the ref
Map<String, String> discriminatorValueToRefMapping,

// The list of properties stored for each ref
Map<String, List<UniversalType>> refProperties,
});

typedef DiscriminatorValue = ({
// The name of the property that is used to discriminate the oneOf variants
String propertyValue,
String parentClass,
});

/// Universal template for containing information about component
@immutable
final class UniversalComponentClass extends UniversalDataClass {
/// Constructor for [UniversalComponentClass]
const UniversalComponentClass({
UniversalComponentClass({
Copy link
Author

Choose a reason for hiding this comment

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

I had to remove the const constuctor as the discriminatorValue field needs to be updated after the initial UniversalComponentClass class is constructed.

required super.name,
required this.imports,
required this.parameters,
this.allOf,
this.typeDef = false,
this.discriminator,
this.discriminatorValue,
super.description,
});

/// List of additional references to components
final Set<String> imports;

/// The import of this class
String get import => name.toPascal;

/// List of class fields
final List<UniversalType> parameters;

/// Temp field for containing info about `allOf` for future processing
final ({List<String> refs, List<UniversalType> properties})? allOf;

/// When using a discriminated oneOf, this contains the information about the property name, the mapping of the ref to the property name, and the properties of each of the oneOf variants
final ({
// The name of the property that is used to discriminate the oneOf variants
String propertyName,

// The mapping of the property value to the ref
Map<String, String> discriminatorValueToRefMapping,
final Discriminator? discriminator;

// The list of properties stored for each ref
Map<String, List<UniversalType>> refProperties,
})? discriminator;
/// When using a discriminated oneOf, where this class is one of the discriminated values, this field contains the information about the parent
DiscriminatorValue? discriminatorValue;

/// Whether or not this schema is a basic type
/// "Date": {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import 'package:collection/collection.dart';
import 'package:meta/meta.dart';

import '../utils/case_utils.dart';
import '../utils/type_utils.dart';
import 'universal_type.dart';

Expand Down
54 changes: 37 additions & 17 deletions swagger_parser/lib/src/parser/parser/open_api_parser.dart
Original file line number Diff line number Diff line change
Expand Up @@ -856,13 +856,15 @@ class OpenApiParser {
final allOf =
refs.isNotEmpty ? (refs: refs, properties: parameters) : null;

final discriminator = _parseDiscriminatorInfo(value);
dataClasses.add(
UniversalComponentClass(
name: key,
imports: imports,
parameters: allOf != null ? [] : parameters,
allOf: allOf,
description: value[_descriptionConst]?.toString(),
discriminator: discriminator,
),
);
});
Expand Down Expand Up @@ -916,6 +918,16 @@ class OpenApiParser {
}
discriminator.refProperties[ref] = refedClass.parameters;
discriminatedOneOfClass.imports.addAll(refedClass.imports);
discriminatedOneOfClass.imports.add(refedClass.import);

refedClass.imports.add(discriminatedOneOfClass.import);
refedClass.discriminatorValue = (
propertyValue: discriminatedOneOfClass
.discriminator!.discriminatorValueToRefMapping.entries
.firstWhere((it) => it.value == ref)
.key,
parentClass: discriminatedOneOfClass.name,
);
}
}

Expand Down Expand Up @@ -1153,10 +1165,6 @@ class OpenApiParser {
.containsKey(_propertyNameConst) &&
(map[_discriminatorConst] as Map<String, dynamic>)
.containsKey(_mappingConst)) {
final discriminator = map[_discriminatorConst] as Map<String, dynamic>;
final propertyName = discriminator[_propertyNameConst] as String;
final refMapping = discriminator[_mappingConst] as Map<String, dynamic>;

// Create a base union class for the discriminated types
final baseClassName =
'${additionalName ?? ''} ${name ?? ''} Union'.toPascal;
Expand All @@ -1166,12 +1174,7 @@ class OpenApiParser {
description: map[_descriptionConst]?.toString(),
);

// Cleanup the refMapping to contain only the class name
final cleanedRefMapping = <String, String>{};
for (final key in refMapping.keys) {
final refMap = <String, dynamic>{_refConst: refMapping[key]};
cleanedRefMapping[key] = _formatRef(refMap);
}
final discriminator = _parseDiscriminatorInfo(map);

// Create a sealed class to represent the discriminated union
_objectClasses.add(
Expand All @@ -1181,16 +1184,11 @@ class OpenApiParser {
parameters: [
UniversalType(
type: 'String',
name: propertyName,
name: discriminator?.propertyName,
isRequired: true,
),
],
discriminator: (
propertyName: propertyName,
discriminatorValueToRefMapping: cleanedRefMapping,
// This property is populated by the parser after all the data classes are created
refProperties: <String, List<UniversalType>>{},
),
discriminator: discriminator,
),
);

Expand Down Expand Up @@ -1365,6 +1363,28 @@ class OpenApiParser {
);
}
}

Discriminator? _parseDiscriminatorInfo(Map<String, dynamic> map) {
if (!map.containsKey(_oneOfConst)) {
return null;
}
final discriminator = map[_discriminatorConst] as Map<String, dynamic>;
final propertyName = discriminator[_propertyNameConst] as String;
final refMapping = discriminator[_mappingConst] as Map<String, dynamic>;

// Cleanup the refMapping to contain only the class name
final cleanedRefMapping = <String, String>{};
for (final key in refMapping.keys) {
final refMap = <String, dynamic>{_refConst: refMapping[key]};
cleanedRefMapping[key] = _formatRef(refMap);
}
return (
propertyName: propertyName,
discriminatorValueToRefMapping: cleanedRefMapping,
// This property is populated by the parser after all the data classes are created
refProperties: <String, List<UniversalType>>{},
);
}
}

/// Extension used for [YamlMap]
Expand Down
13 changes: 13 additions & 0 deletions swagger_parser/test/e2e/e2e_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -198,6 +198,19 @@ void main() {
);
});

test('discriminated_one_of.3.0_mappable', () async {
Copy link
Author

Choose a reason for hiding this comment

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

It would be nice to combine this test with the discriminated_one_of.3.0 test above.

I've opened #291 to suggest some changes to the test structure to allow this.

await e2eTest(
'basic/discriminated_one_of.3.0_mappable',
(outputDirectory, schemaPath) => SWPConfig(
outputDirectory: outputDirectory,
schemaPath: schemaPath,
jsonSerializer: JsonSerializer.dartMappable,
putClientsInFolder: true,
),
schemaFileName: 'discriminated_one_of.3.0.json',
);
});

test('empty_class.2.0', () async {
await e2eTest(
'basic/empty_class.2.0',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
import 'package:freezed_annotation/freezed_annotation.dart';

import 'cat_type.dart';
import 'family_members_union.dart';

part 'cat.freezed.dart';
part 'cat.g.dart';
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
import 'package:freezed_annotation/freezed_annotation.dart';

import 'dog_type.dart';
import 'family_members_union.dart';

part 'dog.freezed.dart';
part 'dog.g.dart';
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,11 @@

import 'package:freezed_annotation/freezed_annotation.dart';

import 'cat.dart';
import 'cat_type.dart';
import 'dog.dart';
import 'dog_type.dart';
import 'human.dart';
import 'human_type.dart';

part 'family_members_union.freezed.dart';
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

import 'package:freezed_annotation/freezed_annotation.dart';

import 'family_members_union.dart';
import 'human_type.dart';

part 'human.freezed.dart';
Expand Down
Loading