Compare commits
No commits in common. "develop" and "master" have entirely different histories.
|
|
@ -558,7 +558,7 @@ packages:
|
||||||
path: ".."
|
path: ".."
|
||||||
relative: true
|
relative: true
|
||||||
source: path
|
source: path
|
||||||
version: "3.3.0"
|
version: "3.2.1"
|
||||||
term_glyph:
|
term_glyph:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
|
|
|
||||||
|
|
@ -396,7 +396,7 @@ class ApiModel {
|
||||||
.where((entry) {
|
.where((entry) {
|
||||||
final value = entry.value;
|
final value = entry.value;
|
||||||
if (value is Map<String, dynamic>) {
|
if (value is Map<String, dynamic>) {
|
||||||
return !_isNullableSchemaDefinition(value);
|
return !(value['nullable'] as bool? ?? false);
|
||||||
}
|
}
|
||||||
return true;
|
return true;
|
||||||
})
|
})
|
||||||
|
|
@ -528,27 +528,6 @@ class ApiModel {
|
||||||
type: type,
|
type: type,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
static bool _isNullableSchemaDefinition(Map<String, dynamic> schema) {
|
|
||||||
if (schema['nullable'] as bool? ?? false) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
for (final unionKey in const ['anyOf', 'oneOf']) {
|
|
||||||
final unionList = schema[unionKey] as List<dynamic>?;
|
|
||||||
if (unionList == null) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
for (final branch in unionList) {
|
|
||||||
if (branch is Map<String, dynamic> && branch['type'] == 'null') {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// API属性信息
|
/// API属性信息
|
||||||
|
|
@ -587,20 +566,8 @@ class ApiProperty {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
final resolvedNullableUnion = _resolveNullableUnionProperty(
|
final type = PropertyType.fromString(json['type'] as String? ?? 'string');
|
||||||
name,
|
|
||||||
json,
|
|
||||||
requiredFields,
|
|
||||||
maxDepth: maxDepth,
|
|
||||||
currentDepth: currentDepth,
|
|
||||||
);
|
|
||||||
if (resolvedNullableUnion != null) {
|
|
||||||
return resolvedNullableUnion;
|
|
||||||
}
|
|
||||||
|
|
||||||
var type = PropertyType.fromString(json['type'] as String?);
|
|
||||||
String? reference;
|
String? reference;
|
||||||
final nullable = json['nullable'] as bool? ?? false;
|
|
||||||
ApiModel? items;
|
ApiModel? items;
|
||||||
final nestedProperties = <String, ApiProperty>{};
|
final nestedProperties = <String, ApiProperty>{};
|
||||||
var nestedRequired = <String>[];
|
var nestedRequired = <String>[];
|
||||||
|
|
@ -612,14 +579,6 @@ class ApiProperty {
|
||||||
reference = ref.split('/').last;
|
reference = ref.split('/').last;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (type == PropertyType.unknown) {
|
|
||||||
if (json['properties'] != null) {
|
|
||||||
type = PropertyType.object;
|
|
||||||
} else if (json['items'] != null) {
|
|
||||||
type = PropertyType.array;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 处理复杂 schema(组合模式等)
|
// 处理复杂 schema(组合模式等)
|
||||||
if (json['allOf'] != null ||
|
if (json['allOf'] != null ||
|
||||||
json['oneOf'] != null ||
|
json['oneOf'] != null ||
|
||||||
|
|
@ -735,7 +694,7 @@ class ApiProperty {
|
||||||
format: json['format'] as String?,
|
format: json['format'] as String?,
|
||||||
description: json['description'] as String? ?? '',
|
description: json['description'] as String? ?? '',
|
||||||
required: requiredFields.contains(name),
|
required: requiredFields.contains(name),
|
||||||
nullable: nullable,
|
nullable: json['nullable'] as bool? ?? false,
|
||||||
example: json['example'],
|
example: json['example'],
|
||||||
defaultValue: json['default'],
|
defaultValue: json['default'],
|
||||||
reference: reference,
|
reference: reference,
|
||||||
|
|
@ -745,110 +704,6 @@ class ApiProperty {
|
||||||
schema: schema,
|
schema: schema,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
static ApiProperty? _resolveNullableUnionProperty(
|
|
||||||
String name,
|
|
||||||
Map<String, dynamic> json,
|
|
||||||
List<String> requiredFields, {
|
|
||||||
required int maxDepth,
|
|
||||||
required int currentDepth,
|
|
||||||
}) {
|
|
||||||
for (final unionKey in const ['anyOf', 'oneOf']) {
|
|
||||||
final unionList = json[unionKey] as List<dynamic>?;
|
|
||||||
if (unionList == null) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
final nonNullBranches = <Map<String, dynamic>>[];
|
|
||||||
var hasNullBranch = false;
|
|
||||||
|
|
||||||
for (final item in unionList) {
|
|
||||||
if (item is! Map<String, dynamic>) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
if (item['type'] == 'null') {
|
|
||||||
hasNullBranch = true;
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
nonNullBranches.add(item);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!hasNullBranch) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
final required = requiredFields.contains(name);
|
|
||||||
final description = json['description'] as String? ?? '';
|
|
||||||
final schema = ApiSchema.fromJson(json);
|
|
||||||
|
|
||||||
if (nonNullBranches.length != 1) {
|
|
||||||
return ApiProperty(
|
|
||||||
name: name,
|
|
||||||
type: PropertyType.unknown,
|
|
||||||
format: json['format'] as String?,
|
|
||||||
description: description,
|
|
||||||
required: required,
|
|
||||||
nullable: true,
|
|
||||||
example: json['example'],
|
|
||||||
defaultValue: json['default'],
|
|
||||||
schema: schema,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
final resolvedBranch = ApiProperty.fromJson(
|
|
||||||
name,
|
|
||||||
_mergeNullableUnionBranch(json, nonNullBranches.single),
|
|
||||||
requiredFields,
|
|
||||||
maxDepth: maxDepth,
|
|
||||||
currentDepth: currentDepth,
|
|
||||||
);
|
|
||||||
|
|
||||||
return ApiProperty(
|
|
||||||
name: name,
|
|
||||||
type: resolvedBranch.type,
|
|
||||||
format: resolvedBranch.format ?? json['format'] as String?,
|
|
||||||
description: resolvedBranch.description.isNotEmpty
|
|
||||||
? resolvedBranch.description
|
|
||||||
: description,
|
|
||||||
required: required,
|
|
||||||
nullable: true,
|
|
||||||
example: json.containsKey('example')
|
|
||||||
? json['example']
|
|
||||||
: resolvedBranch.example,
|
|
||||||
defaultValue: json.containsKey('default')
|
|
||||||
? json['default']
|
|
||||||
: resolvedBranch.defaultValue,
|
|
||||||
reference: resolvedBranch.reference,
|
|
||||||
items: resolvedBranch.items,
|
|
||||||
nestedProperties: resolvedBranch.nestedProperties,
|
|
||||||
nestedRequired: resolvedBranch.nestedRequired,
|
|
||||||
schema: schema,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
static Map<String, dynamic> _mergeNullableUnionBranch(
|
|
||||||
Map<String, dynamic> parent,
|
|
||||||
Map<String, dynamic> branch,
|
|
||||||
) {
|
|
||||||
final merged = Map<String, dynamic>.from(branch);
|
|
||||||
|
|
||||||
for (final key in const ['description', 'format', 'example', 'default']) {
|
|
||||||
if (!merged.containsKey(key) && parent.containsKey(key)) {
|
|
||||||
merged[key] = parent[key];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if ((parent['nullable'] as bool? ?? false) ||
|
|
||||||
(branch['nullable'] as bool? ?? false)) {
|
|
||||||
merged['nullable'] = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
return merged;
|
|
||||||
}
|
|
||||||
|
|
||||||
final String name;
|
final String name;
|
||||||
final PropertyType type;
|
final PropertyType type;
|
||||||
final String? format;
|
final String? format;
|
||||||
|
|
|
||||||
|
|
@ -1055,43 +1055,6 @@ void main() {
|
||||||
expect(model.properties['name']!.required, true);
|
expect(model.properties['name']!.required, true);
|
||||||
expect(model.required, ['id', 'name']);
|
expect(model.required, ['id', 'name']);
|
||||||
});
|
});
|
||||||
|
|
||||||
test(
|
|
||||||
'treats anyOf/oneOf nullable properties as optional when required is omitted',
|
|
||||||
() {
|
|
||||||
final json = {
|
|
||||||
'description': 'Envelope model',
|
|
||||||
'properties': {
|
|
||||||
'data': {
|
|
||||||
'anyOf': [
|
|
||||||
{
|
|
||||||
'type': 'array',
|
|
||||||
'items': {r'$ref': '#/components/schemas/StoreResponse'},
|
|
||||||
},
|
|
||||||
{'type': 'null'},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
'meta': {
|
|
||||||
'oneOf': [
|
|
||||||
{r'$ref': '#/components/schemas/MetaResponse'},
|
|
||||||
{'type': 'null'},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
'code': {
|
|
||||||
'type': 'integer',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
final model = ApiModel.fromJson('Envelope', json);
|
|
||||||
|
|
||||||
expect(model.required, ['code']);
|
|
||||||
expect(model.properties['data']!.nullable, isTrue);
|
|
||||||
expect(model.properties['data']!.required, isFalse);
|
|
||||||
expect(model.properties['meta']!.nullable, isTrue);
|
|
||||||
expect(model.properties['meta']!.required, isFalse);
|
|
||||||
expect(model.properties['code']!.required, isTrue);
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|
||||||
group('ApiProperty complex types', () {
|
group('ApiProperty complex types', () {
|
||||||
|
|
@ -1787,81 +1750,4 @@ void main() {
|
||||||
expect(schema.dependencies.length, 1);
|
expect(schema.dependencies.length, 1);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
group('ApiProperty nullable union', () {
|
|
||||||
test('parses anyOf ref + null as nullable reference property', () {
|
|
||||||
final property = ApiProperty.fromJson(
|
|
||||||
'data',
|
|
||||||
{
|
|
||||||
'anyOf': [
|
|
||||||
{r'$ref': '#/components/schemas/StoreResponse'},
|
|
||||||
{'type': 'null'},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
const [],
|
|
||||||
);
|
|
||||||
|
|
||||||
expect(property.type, PropertyType.reference);
|
|
||||||
expect(property.reference, 'StoreResponse');
|
|
||||||
expect(property.nullable, isTrue);
|
|
||||||
});
|
|
||||||
|
|
||||||
test('parses oneOf ref + null as nullable reference property', () {
|
|
||||||
final property = ApiProperty.fromJson(
|
|
||||||
'data',
|
|
||||||
{
|
|
||||||
'oneOf': [
|
|
||||||
{r'$ref': '#/components/schemas/StoreResponse'},
|
|
||||||
{'type': 'null'},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
const [],
|
|
||||||
);
|
|
||||||
|
|
||||||
expect(property.type, PropertyType.reference);
|
|
||||||
expect(property.reference, 'StoreResponse');
|
|
||||||
expect(property.nullable, isTrue);
|
|
||||||
});
|
|
||||||
|
|
||||||
test('parses anyOf array + null without losing item type', () {
|
|
||||||
final property = ApiProperty.fromJson(
|
|
||||||
'data',
|
|
||||||
{
|
|
||||||
'anyOf': [
|
|
||||||
{
|
|
||||||
'type': 'array',
|
|
||||||
'items': {r'$ref': '#/components/schemas/StoreResponse'},
|
|
||||||
},
|
|
||||||
{'type': 'null'},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
const [],
|
|
||||||
);
|
|
||||||
|
|
||||||
expect(property.type, PropertyType.array);
|
|
||||||
expect(property.items, isNotNull);
|
|
||||||
expect(property.items?.name, 'StoreResponse');
|
|
||||||
expect(property.nullable, isTrue);
|
|
||||||
});
|
|
||||||
|
|
||||||
test('downgrades nullable multi-branch union to unknown instead of string',
|
|
||||||
() {
|
|
||||||
final property = ApiProperty.fromJson(
|
|
||||||
'data',
|
|
||||||
{
|
|
||||||
'anyOf': [
|
|
||||||
{r'$ref': '#/components/schemas/StoreResponse'},
|
|
||||||
{r'$ref': '#/components/schemas/DeviceResponse'},
|
|
||||||
{'type': 'null'},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
const [],
|
|
||||||
);
|
|
||||||
|
|
||||||
expect(property.type, PropertyType.unknown);
|
|
||||||
expect(property.reference, isNull);
|
|
||||||
expect(property.nullable, isTrue);
|
|
||||||
expect(property.schema, isNotNull);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -343,52 +343,6 @@ void main() {
|
||||||
contains('factory User.fromJson(Map<String, dynamic> json)'),
|
contains('factory User.fromJson(Map<String, dynamic> json)'),
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('preserves nullable anyOf array item type in generated models', () {
|
|
||||||
final document = SwaggerDocument.fromJson(<String, dynamic>{
|
|
||||||
'openapi': '3.1.0',
|
|
||||||
'info': {
|
|
||||||
'title': 'Nullable union test',
|
|
||||||
'version': '1.0.0',
|
|
||||||
},
|
|
||||||
'paths': <String, dynamic>{},
|
|
||||||
'components': {
|
|
||||||
'schemas': {
|
|
||||||
'StoreResponse': {
|
|
||||||
'type': 'object',
|
|
||||||
'properties': {
|
|
||||||
'id': {'type': 'string'},
|
|
||||||
},
|
|
||||||
'required': ['id'],
|
|
||||||
},
|
|
||||||
'StoreListEnvelope': {
|
|
||||||
'type': 'object',
|
|
||||||
'properties': {
|
|
||||||
'data': {
|
|
||||||
'anyOf': [
|
|
||||||
{
|
|
||||||
'type': 'array',
|
|
||||||
'items': {
|
|
||||||
r'$ref': '#/components/schemas/StoreResponse',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{'type': 'null'},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
final generator = ModelCodeGenerator(document);
|
|
||||||
final result = generator
|
|
||||||
.generateSingleModelFile(document.models['StoreListEnvelope']!);
|
|
||||||
|
|
||||||
expect(result, contains('List<StoreResponse>? data'));
|
|
||||||
expect(result, isNot(contains('List<dynamic> data')));
|
|
||||||
expect(result, isNot(contains('String data')));
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue