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

Closes #13 Support anyOf with primitive types and arrays #21

Merged
merged 1 commit into from
Oct 30, 2023
Merged
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
172 changes: 170 additions & 2 deletions lib/src/generators/schema.dart
Original file line number Diff line number Diff line change
Expand Up @@ -125,7 +125,17 @@ class SchemaGenerator extends BaseGenerator {
for (final extra in spec.extraSchemaMapping[s]!) {
schemas[extra]?.mapOrNull(
object: (extraSchema) {
_writeObject(name: extra, schema: extraSchema);
if (extraSchema.anyOf?.isEmpty ?? true) {
_writeObject(name: extra, schema: extraSchema);
} else {
final isPrimitive = extraSchema.anyOf!
.map((e) => e.maybeMap(
object: (_) => false, orElse: () => true))
.any((e) => e);
if (isPrimitive) {
_writePrimitiveUnion(schema: extraSchema);
}
}
},
enumeration: (extraSchema) {
_writeEnumeration(name: extra, schema: extraSchema);
Expand Down Expand Up @@ -307,6 +317,113 @@ class SchemaGenerator extends BaseGenerator {
""", mode: FileMode.append);
}

// ------------------------------------------
// METHOD: _writePrimitiveUnion
// ------------------------------------------

void _writePrimitiveUnion({
required Schema schema,
}) {
final union = schema.title;

// Union header
file.writeAsStringSync("""
// ==========================================
// CLASS: $union
// ==========================================

/// ${schema.description?.trim().replaceAll('\n', '\n/// ') ?? 'No Description'}
@freezed
sealed class $union with _\$$union {
const $union._();\n

""", mode: FileMode.append);

// Keep track of the required converter logic
final List<String> converters = [];

schema.mapOrNull(
object: (s) {
for (final a in (s.anyOf ?? <Schema>[])) {
a.mapOrNull(
string: (o) {
final uName = '${union}String';
converters.add('$uName(value: final v) => v,');
file.writeAsStringSync(
'const factory $union.string(${o.toDartType()} value,) = $uName;\n\n',
mode: FileMode.append,
);
},
number: (o) {
final uName = '${union}Number';
converters.add('$uName(value: final v) => v,');
file.writeAsStringSync(
'const factory $union.number(${o.toDartType()} value,) = $uName;\n\n',
mode: FileMode.append,
);
},
integer: (o) {
final uName = '${union}Integer';
converters.add('$uName(value: final v) => v,');
file.writeAsStringSync(
'const factory $union.integer(${o.toDartType()} value,) = $uName;\n\n',
mode: FileMode.append,
);
},
enumeration: (o) {
final uName = '${union}Enum';
converters.add(
'$uName(value: final v) => _\$${o.title}EnumMap[v]!,',
);
file.writeAsStringSync(
'const factory $union.enumeration(${o.title} value,) = $uName;\n\n',
mode: FileMode.append,
);
},
array: (o) {
final factoryName = 'array${o.title?.split('Array').last}';
final uName = '$union${factoryName.pascalCase}';
converters.add('$uName(value: final v) => v,');
file.writeAsStringSync(
'const factory $union.$factoryName(${o.toDartType()} value,) = $uName;\n\n',
mode: FileMode.append,
);
},
);
}
},
);

// Union footer
file.writeAsStringSync("""
/// Object construction from a JSON representation
factory $union.fromJson(Map<String, dynamic> json) => _\$${union}FromJson(json);

}\n
""", mode: FileMode.append);

// Write converter
file.writeAsStringSync("""
/// Custom JSON converter for [$union]
class _${union}Converter
implements JsonConverter<$union, Object?> {
const _${union}Converter();

@override
$union fromJson(Object? json) {
throw UnimplementedError();
}

@override
Object? toJson($union data) {
return switch (data) {
${converters.join('\n')}
};
}
}
""", mode: FileMode.append);
}

// ------------------------------------------
// METHOD: _writeObject
// ------------------------------------------
Expand Down Expand Up @@ -461,8 +578,59 @@ class SchemaGenerator extends BaseGenerator {
orElse: () => p,
);
bool hasDefault = p.defaultValue != null;

String customConverter = '';

// Prefix with expected custom converter
if ((p.anyOf?.isNotEmpty ?? false) &&
(p.title?.toLowerCase().startsWith('union') ?? false)) {
customConverter =
'@_${p.toDartType().replaceAll('?', '')}Converter()';
}

// Handle union defaults
if (hasDefault && (p.anyOf?.isNotEmpty ?? false)) {
final aTypes = p.anyOf!.map((e) => e.type);
if (p.defaultValue is String &&
(aTypes.contains(SchemaType.string))) {
p = p.copyWith(
defaultValue: "${p.title}.string('${p.defaultValue}'),",
);
} else if (p.defaultValue is String &&
(aTypes.contains(SchemaType.enumeration))) {
final a = p.anyOf!.firstWhereOrNull(
(e) => e.type == SchemaType.enumeration,
);
p = p.copyWith(
defaultValue:
"${p.title}.enumeration(${a?.title}.${p.defaultValue.toString().camelCase}),",
);
} else if (p.defaultValue is bool &&
(aTypes.contains(SchemaType.boolean))) {
p = p.copyWith(
defaultValue: "${p.title}.boolean(${p.defaultValue}),",
);
} else if (p.defaultValue is int &&
(aTypes.contains(SchemaType.integer))) {
p = p.copyWith(
defaultValue: "${p.title}.integer(${p.defaultValue}),",
);
} else if (p.defaultValue is num &&
(aTypes.contains(SchemaType.number))) {
p = p.copyWith(
defaultValue: "${p.title}.number(${p.defaultValue}),",
);
} else {
// All else fails, cannot ensure a default value is valid
// Force the field to be required
hasDefault = false;
required = true;
}
}

bool nullable = !hasDefault && !required || p.nullable == true;
String c = formatDescription(p.description);
c += '$customConverter ';

List<String> unionSchemas = [];
if (p.anyOf != null) {
Expand All @@ -478,7 +646,7 @@ class SchemaGenerator extends BaseGenerator {

c += getJsonKey(nullable: nullable);

if (hasDefault) {
if (hasDefault & !required) {
c += "@Default(${p.defaultValue}) ";
}

Expand Down
45 changes: 18 additions & 27 deletions lib/src/open_api/index.freezed.dart
Original file line number Diff line number Diff line change
Expand Up @@ -8540,7 +8540,7 @@ mixin _$Schema {
String? get description => throw _privateConstructorUsedError;

/// The default value code to place into `@Default()`
Object? get defaultValue => throw _privateConstructorUsedError;
dynamic get defaultValue => throw _privateConstructorUsedError;

/// Reference to a schema definition
@JsonKey(name: '\$ref')
Expand Down Expand Up @@ -8654,7 +8654,7 @@ abstract class _$$SchemaObjectImplCopyWith<$Res>
$Res call(
{String? title,
String? description,
String? defaultValue,
dynamic defaultValue,
@JsonKey(name: '\$ref') @_SchemaRefConverter() String? ref,
@_SchemaListConverter() List<Schema>? allOf,
@_SchemaListConverter() List<Schema>? anyOf,
Expand Down Expand Up @@ -8706,7 +8706,7 @@ class __$$SchemaObjectImplCopyWithImpl<$Res>
defaultValue: freezed == defaultValue
? _value.defaultValue
: defaultValue // ignore: cast_nullable_to_non_nullable
as String?,
as dynamic,
ref: freezed == ref
? _value.ref
: ref // ignore: cast_nullable_to_non_nullable
Expand Down Expand Up @@ -8820,7 +8820,7 @@ class _$SchemaObjectImpl extends _SchemaObject {

/// The default value code to place into `@Default()`
@override
final String? defaultValue;
final dynamic defaultValue;

/// Reference to a schema definition
@override
Expand Down Expand Up @@ -8918,8 +8918,8 @@ class _$SchemaObjectImpl extends _SchemaObject {
(identical(other.title, title) || other.title == title) &&
(identical(other.description, description) ||
other.description == description) &&
(identical(other.defaultValue, defaultValue) ||
other.defaultValue == defaultValue) &&
const DeepCollectionEquality()
.equals(other.defaultValue, defaultValue) &&
(identical(other.ref, ref) || other.ref == ref) &&
const DeepCollectionEquality().equals(other._allOf, _allOf) &&
const DeepCollectionEquality().equals(other._anyOf, _anyOf) &&
Expand All @@ -8941,7 +8941,7 @@ class _$SchemaObjectImpl extends _SchemaObject {
runtimeType,
title,
description,
defaultValue,
const DeepCollectionEquality().hash(defaultValue),
ref,
const DeepCollectionEquality().hash(_allOf),
const DeepCollectionEquality().hash(_anyOf),
Expand Down Expand Up @@ -9019,7 +9019,7 @@ abstract class _SchemaObject extends Schema {
const factory _SchemaObject(
{final String? title,
final String? description,
final String? defaultValue,
final dynamic defaultValue,
@JsonKey(name: '\$ref') @_SchemaRefConverter() final String? ref,
@_SchemaListConverter() final List<Schema>? allOf,
@_SchemaListConverter() final List<Schema>? anyOf,
Expand All @@ -9045,7 +9045,7 @@ abstract class _SchemaObject extends Schema {
@override

/// The default value code to place into `@Default()`
String? get defaultValue;
dynamic get defaultValue;
@override

/// Reference to a schema definition
Expand Down Expand Up @@ -10669,7 +10669,7 @@ abstract class _$$SchemaArrayImplCopyWith<$Res>
String? description,
@JsonKey(name: 'default') List<dynamic>? defaultValue,
bool? nullable,
List<dynamic>? example,
dynamic example,
@JsonKey(fromJson: _fromJsonInt) int? minItems,
@JsonKey(fromJson: _fromJsonInt) int? maxItems,
Schema items,
Expand Down Expand Up @@ -10723,9 +10723,9 @@ class __$$SchemaArrayImplCopyWithImpl<$Res>
: nullable // ignore: cast_nullable_to_non_nullable
as bool?,
example: freezed == example
? _value._example
? _value.example
: example // ignore: cast_nullable_to_non_nullable
as List<dynamic>?,
as dynamic,
minItems: freezed == minItems
? _value.minItems
: minItems // ignore: cast_nullable_to_non_nullable
Expand Down Expand Up @@ -10775,14 +10775,13 @@ class _$SchemaArrayImpl extends _SchemaArray {
this.description,
@JsonKey(name: 'default') final List<dynamic>? defaultValue,
this.nullable,
final List<dynamic>? example,
this.example,
@JsonKey(fromJson: _fromJsonInt) this.minItems,
@JsonKey(fromJson: _fromJsonInt) this.maxItems,
required this.items,
@JsonKey(name: '\$ref') @_SchemaRefConverter() this.ref,
final String? $type})
: _defaultValue = defaultValue,
_example = example,
$type = $type ?? 'array',
super._();

Expand All @@ -10808,16 +10807,8 @@ class _$SchemaArrayImpl extends _SchemaArray {

@override
final bool? nullable;
final List<dynamic>? _example;
@override
List<dynamic>? get example {
final value = _example;
if (value == null) return null;
if (_example is EqualUnmodifiableListView) return _example;
// ignore: implicit_dynamic_type
return EqualUnmodifiableListView(value);
}

final dynamic example;
@override
@JsonKey(fromJson: _fromJsonInt)
final int? minItems;
Expand Down Expand Up @@ -10852,7 +10843,7 @@ class _$SchemaArrayImpl extends _SchemaArray {
.equals(other._defaultValue, _defaultValue) &&
(identical(other.nullable, nullable) ||
other.nullable == nullable) &&
const DeepCollectionEquality().equals(other._example, _example) &&
const DeepCollectionEquality().equals(other.example, example) &&
(identical(other.minItems, minItems) ||
other.minItems == minItems) &&
(identical(other.maxItems, maxItems) ||
Expand All @@ -10870,7 +10861,7 @@ class _$SchemaArrayImpl extends _SchemaArray {
description,
const DeepCollectionEquality().hash(_defaultValue),
nullable,
const DeepCollectionEquality().hash(_example),
const DeepCollectionEquality().hash(example),
minItems,
maxItems,
items,
Expand Down Expand Up @@ -10946,7 +10937,7 @@ abstract class _SchemaArray extends Schema {
final String? description,
@JsonKey(name: 'default') final List<dynamic>? defaultValue,
final bool? nullable,
final List<dynamic>? example,
final dynamic example,
@JsonKey(fromJson: _fromJsonInt) final int? minItems,
@JsonKey(fromJson: _fromJsonInt) final int? maxItems,
required final Schema items,
Expand All @@ -10967,7 +10958,7 @@ abstract class _SchemaArray extends Schema {
List<dynamic>? get defaultValue;
@override
bool? get nullable;
List<dynamic>? get example;
dynamic get example;
@JsonKey(fromJson: _fromJsonInt)
int? get minItems;
@JsonKey(fromJson: _fromJsonInt)
Expand Down
4 changes: 2 additions & 2 deletions lib/src/open_api/index.g.dart

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading