You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
1555 lines
59 KiB
1555 lines
59 KiB
part of '../dynamite.dart'; |
|
|
|
class OpenAPIBuilder implements Builder { |
|
@override |
|
final buildExtensions = const { |
|
'.openapi.json': ['.openapi.dart'], |
|
}; |
|
|
|
@override |
|
Future<void> build(final BuildStep buildStep) async { |
|
try { |
|
final inputId = buildStep.inputId; |
|
final outputId = inputId.changeExtension('.dart'); |
|
|
|
final emitter = DartEmitter( |
|
orderDirectives: true, |
|
useNullSafetySyntax: true, |
|
); |
|
|
|
final spec = OpenAPI.fromJson( |
|
json.decode( |
|
await buildStep.readAsString(inputId), |
|
) as Map<String, dynamic>, |
|
); |
|
if (spec.version != '3.1.0') { |
|
throw Exception('Only OpenAPI 3.1.0 is supported'); |
|
} |
|
|
|
final tags = <Tag?>[ |
|
null, |
|
if (spec.tags != null) ...[ |
|
...spec.tags!, |
|
], |
|
]; |
|
final hasAnySecurity = spec.security?.isNotEmpty ?? false; |
|
|
|
final state = State(); |
|
final output = <String>[ |
|
"import 'dart:convert';", |
|
"import 'dart:io';", |
|
"import 'dart:typed_data';", |
|
'', |
|
"import 'package:cookie_jar/cookie_jar.dart';", |
|
"import 'package:json_annotation/json_annotation.dart';", |
|
'', |
|
"export 'package:cookie_jar/cookie_jar.dart';", |
|
'', |
|
"part '${p.basename(outputId.changeExtension('.g.dart').path)}';", |
|
'', |
|
Extension( |
|
(final b) => b |
|
..name = 'HttpClientResponseBody' |
|
..on = refer('HttpClientResponse') |
|
..methods.addAll([ |
|
Method( |
|
(final b) => b |
|
..name = 'bodyBytes' |
|
..returns = refer('Future<Uint8List>') |
|
..type = MethodType.getter |
|
..modifier = MethodModifier.async |
|
..body = const Code( |
|
''' |
|
final chunks = await toList(); |
|
if (chunks.isEmpty) { |
|
return Uint8List(0); |
|
} |
|
return Uint8List.fromList(chunks.reduce((final value, final element) => [...value, ...element])); |
|
''', |
|
), |
|
), |
|
Method( |
|
(final b) => b |
|
..name = 'body' |
|
..returns = refer('Future<String>') |
|
..type = MethodType.getter |
|
..modifier = MethodModifier.async |
|
..lambda = true |
|
..body = const Code( |
|
'utf8.decode(await bodyBytes)', |
|
), |
|
), |
|
]), |
|
).accept(emitter).toString(), |
|
Class( |
|
(final b) => b |
|
..name = 'Response' |
|
..types.addAll([ |
|
refer('T'), |
|
refer('U'), |
|
]) |
|
..fields.addAll([ |
|
Field( |
|
(final b) => b |
|
..name = 'data' |
|
..type = refer('T') |
|
..modifier = FieldModifier.final$, |
|
), |
|
Field( |
|
(final b) => b |
|
..name = 'headers' |
|
..type = refer('U') |
|
..modifier = FieldModifier.final$, |
|
), |
|
]) |
|
..constructors.add( |
|
Constructor( |
|
(final b) => b |
|
..requiredParameters.addAll( |
|
['data', 'headers'].map( |
|
(final name) => Parameter( |
|
(final b) => b |
|
..name = name |
|
..toThis = true, |
|
), |
|
), |
|
), |
|
), |
|
) |
|
..methods.add( |
|
Method( |
|
(final b) => b |
|
..name = 'toString' |
|
..returns = refer('String') |
|
..annotations.add(refer('override')) |
|
..lambda = true |
|
..body = const Code( |
|
r"'Response(data: $data, headers: $headers)'", |
|
), |
|
), |
|
), |
|
).accept(emitter).toString(), |
|
Class( |
|
(final b) => b |
|
..name = '_Response' |
|
..fields.addAll([ |
|
Field( |
|
(final b) => b |
|
..name = 'statusCode' |
|
..type = refer('int') |
|
..modifier = FieldModifier.final$, |
|
), |
|
Field( |
|
(final b) => b |
|
..name = 'headers' |
|
..type = refer('Map<String, String>') |
|
..modifier = FieldModifier.final$, |
|
), |
|
Field( |
|
(final b) => b |
|
..name = 'body' |
|
..type = refer('Uint8List') |
|
..modifier = FieldModifier.final$, |
|
), |
|
]) |
|
..constructors.add( |
|
Constructor( |
|
(final b) => b |
|
..requiredParameters.addAll( |
|
['statusCode', 'headers', 'body'].map( |
|
(final name) => Parameter( |
|
(final b) => b |
|
..name = name |
|
..toThis = true, |
|
), |
|
), |
|
), |
|
), |
|
) |
|
..methods.add( |
|
Method( |
|
(final b) => b |
|
..name = 'toString' |
|
..returns = refer('String') |
|
..annotations.add(refer('override')) |
|
..lambda = true |
|
..body = const Code( |
|
r"'_Response(statusCode: $statusCode, headers: $headers, body: ${utf8.decode(body)})'", |
|
), |
|
), |
|
), |
|
).accept(emitter).toString(), |
|
Class( |
|
(final b) => b |
|
..name = 'ApiException' |
|
..extend = refer('_Response') |
|
..implements.add(refer('Exception')) |
|
..constructors.addAll( |
|
[ |
|
Constructor( |
|
(final b) => b |
|
..requiredParameters.addAll( |
|
['statusCode', 'headers', 'body'].map( |
|
(final name) => Parameter( |
|
(final b) => b |
|
..name = name |
|
..toSuper = true, |
|
), |
|
), |
|
), |
|
), |
|
Constructor( |
|
(final b) => b |
|
..name = 'fromResponse' |
|
..factory = true |
|
..lambda = true |
|
..requiredParameters.add( |
|
Parameter( |
|
(final b) => b |
|
..name = 'response' |
|
..type = refer('_Response'), |
|
), |
|
) |
|
..body = const Code('ApiException(response.statusCode, response.headers, response.body,)'), |
|
), |
|
], |
|
) |
|
..methods.add( |
|
Method( |
|
(final b) => b |
|
..name = 'toString' |
|
..returns = refer('String') |
|
..annotations.add(refer('override')) |
|
..lambda = true |
|
..body = const Code( |
|
r"'ApiException(statusCode: ${super.statusCode}, headers: ${super.headers}, body: ${utf8.decode(super.body)})'", |
|
), |
|
), |
|
), |
|
).accept(emitter).toString(), |
|
if (hasAnySecurity) ...[ |
|
Class( |
|
(final b) => b |
|
..name = 'Authentication' |
|
..abstract = true |
|
..methods.add( |
|
Method( |
|
(final b) => b |
|
..name = 'headers' |
|
..type = MethodType.getter |
|
..returns = refer('Map<String, String>'), |
|
), |
|
), |
|
).accept(emitter).toString(), |
|
], |
|
]; |
|
|
|
if (spec.security != null) { |
|
for (final securityRequirement in spec.security!) { |
|
for (final name in securityRequirement.keys) { |
|
final securityScheme = spec.components!.securitySchemes![name]!; |
|
switch (securityScheme.type) { |
|
case 'http': |
|
switch (securityScheme.scheme) { |
|
case 'basic': |
|
output.add( |
|
Class( |
|
(final b) { |
|
final fields = ['username', 'password']; |
|
b |
|
..name = 'HttpBasicAuthentication' |
|
..extend = refer('Authentication') |
|
..constructors.add( |
|
Constructor( |
|
(final b) => b |
|
..optionalParameters.addAll( |
|
fields.map( |
|
(final name) => Parameter( |
|
(final b) => b |
|
..name = name |
|
..toThis = true |
|
..named = true |
|
..required = true, |
|
), |
|
), |
|
), |
|
), |
|
) |
|
..fields.addAll( |
|
fields.map( |
|
(final name) => Field( |
|
(final b) => b |
|
..name = name |
|
..type = refer('String') |
|
..modifier = FieldModifier.final$, |
|
), |
|
), |
|
) |
|
..methods.add( |
|
Method( |
|
(final b) => b |
|
..name = 'headers' |
|
..type = MethodType.getter |
|
..returns = refer('Map<String, String>') |
|
..lambda = true |
|
..body = const Code(r''' |
|
{ |
|
'Authorization': 'Basic ${base64.encode(utf8.encode('$username:$password'))}', |
|
} |
|
'''), |
|
), |
|
); |
|
}, |
|
).accept(emitter).toString(), |
|
); |
|
continue; |
|
} |
|
} |
|
throw Exception('Can not work with security scheme ${securityScheme.toJson()}'); |
|
} |
|
} |
|
} |
|
|
|
for (final tag in tags) { |
|
final isRootClient = tag == null; |
|
final paths = <String, PathItem>{}; |
|
|
|
if (spec.paths != null) { |
|
for (final path in spec.paths!.keys) { |
|
final pathItem = spec.paths![path]!; |
|
for (final method in pathItem.operations.keys) { |
|
final operation = pathItem.operations[method]!; |
|
if ((tag != null && operation.tags != null && operation.tags!.contains(tag.name)) || |
|
(tag == null && (operation.tags == null || operation.tags!.isEmpty))) { |
|
if (paths[path] == null) { |
|
paths[path] = PathItem( |
|
description: pathItem.description, |
|
parameters: pathItem.parameters, |
|
); |
|
} |
|
paths[path] = paths[path]!.copyWithOperations({method: operation}); |
|
} |
|
} |
|
} |
|
} |
|
|
|
if (paths.isEmpty && !isRootClient) { |
|
continue; |
|
} |
|
|
|
output.add( |
|
Class( |
|
(final b) { |
|
if (isRootClient) { |
|
b |
|
..fields.addAll([ |
|
Field( |
|
(final b) => b |
|
..name = 'baseURL' |
|
..type = refer('String') |
|
..modifier = FieldModifier.final$, |
|
), |
|
Field( |
|
(final b) => b |
|
..name = 'baseHeaders' |
|
..type = refer('Map<String, String>') |
|
..modifier = FieldModifier.final$ |
|
..late = true, |
|
), |
|
Field( |
|
(final b) => b |
|
..name = 'httpClient' |
|
..type = refer('HttpClient') |
|
..modifier = FieldModifier.final$ |
|
..late = true, |
|
), |
|
Field( |
|
(final b) => b |
|
..name = 'cookieJar' |
|
..type = refer('CookieJar?') |
|
..modifier = FieldModifier.final$, |
|
), |
|
if (hasAnySecurity) ...[ |
|
Field( |
|
(final b) => b |
|
..name = 'authentication' |
|
..type = refer('Authentication?') |
|
..modifier = FieldModifier.final$, |
|
), |
|
], |
|
]) |
|
..constructors.add( |
|
Constructor( |
|
(final b) => b |
|
..requiredParameters.add( |
|
Parameter( |
|
(final b) => b |
|
..name = 'baseURL' |
|
..toThis = true, |
|
), |
|
) |
|
..optionalParameters.addAll([ |
|
Parameter( |
|
(final b) => b |
|
..name = 'baseHeaders' |
|
..type = refer('Map<String, String>?') |
|
..named = true, |
|
), |
|
Parameter( |
|
(final b) => b |
|
..name = 'httpClient' |
|
..type = refer('HttpClient?') |
|
..named = true, |
|
), |
|
Parameter( |
|
(final b) => b |
|
..name = 'cookieJar' |
|
..toThis = true |
|
..named = true, |
|
), |
|
if (hasAnySecurity) ...[ |
|
Parameter( |
|
(final b) => b |
|
..name = 'authentication' |
|
..toThis = true |
|
..named = true, |
|
), |
|
], |
|
]) |
|
..body = Code(''' |
|
this.baseHeaders = { |
|
if (baseHeaders != null) ...{ |
|
...baseHeaders, |
|
}, |
|
${hasAnySecurity ? ''' |
|
if (authentication != null) ...{ |
|
...authentication!.headers, |
|
}, |
|
''' : ''} |
|
}; |
|
this.httpClient = httpClient ?? HttpClient(); |
|
'''), |
|
), |
|
) |
|
..methods.addAll([ |
|
if (isRootClient) ...[ |
|
for (final tag in tags.where((final tag) => tag != null).toList().cast<Tag>()) ...[ |
|
Method( |
|
(final b) => b |
|
..name = _toDartName(tag.name) |
|
..lambda = true |
|
..type = MethodType.getter |
|
..returns = refer(_clientName(tag)) |
|
..body = Code('${_clientName(tag)}(this)'), |
|
), |
|
], |
|
], |
|
Method( |
|
(final b) => b |
|
..name = 'doRequest' |
|
..returns = refer('Future<_Response>') |
|
..modifier = MethodModifier.async |
|
..requiredParameters.addAll([ |
|
Parameter( |
|
(final b) => b |
|
..name = 'method' |
|
..type = refer('String'), |
|
), |
|
Parameter( |
|
(final b) => b |
|
..name = 'path' |
|
..type = refer('String'), |
|
), |
|
Parameter( |
|
(final b) => b |
|
..name = 'headers' |
|
..type = refer('Map<String, String>'), |
|
), |
|
Parameter( |
|
(final b) => b |
|
..name = 'body' |
|
..type = refer('Uint8List?'), |
|
), |
|
]) |
|
..body = const Code(r''' |
|
final uri = Uri.parse('$baseURL$path'); |
|
final request = await httpClient.openUrl(method, uri); |
|
for (final header in {...baseHeaders, ...headers}.entries) { |
|
request.headers.add(header.key, header.value); |
|
} |
|
if (body != null) { |
|
request.add(body.toList()); |
|
} |
|
if (cookieJar != null) { |
|
request.cookies.addAll(await cookieJar!.loadForRequest(uri)); |
|
} |
|
|
|
final response = await request.close(); |
|
if (cookieJar != null) { |
|
await cookieJar!.saveFromResponse(uri, response.cookies); |
|
} |
|
final responseHeaders = <String, String>{}; |
|
response.headers.forEach((final name, final values) { |
|
responseHeaders[name] = values.last; |
|
}); |
|
return _Response( |
|
response.statusCode, |
|
responseHeaders, |
|
await response.bodyBytes, |
|
); |
|
'''), |
|
), |
|
]); |
|
} else { |
|
b |
|
..fields.add( |
|
Field( |
|
(final b) => b |
|
..name = 'rootClient' |
|
..type = refer('Client') |
|
..modifier = FieldModifier.final$, |
|
), |
|
) |
|
..constructors.add( |
|
Constructor( |
|
(final b) => b.requiredParameters.add( |
|
Parameter( |
|
(final b) => b |
|
..name = 'rootClient' |
|
..toThis = true, |
|
), |
|
), |
|
), |
|
); |
|
} |
|
b |
|
..name = isRootClient ? 'Client' : _clientName(tag) |
|
..methods.addAll( |
|
[ |
|
for (final path in paths.keys) ...[ |
|
for (final httpMethod in paths[path]!.operations.keys) ...[ |
|
Method( |
|
(final b) { |
|
final operation = paths[path]!.operations[httpMethod]!; |
|
final operationId = operation.operationId ?? _toDartName('$httpMethod-$path'); |
|
final pathParameters = <spec_parameter.Parameter>[ |
|
if (paths[path]!.parameters != null) ...paths[path]!.parameters!, |
|
]; |
|
final parameters = <spec_parameter.Parameter>[ |
|
...pathParameters, |
|
if (operation.parameters != null) ...operation.parameters!, |
|
]; |
|
final methodName = _toDartName(operationId); |
|
b |
|
..name = methodName |
|
..modifier = MethodModifier.async; |
|
|
|
final code = StringBuffer(''' |
|
var path = '$path'; |
|
final queryParameters = <String, dynamic>{}; |
|
final headers = <String, String>{}; |
|
Uint8List? body; |
|
'''); |
|
|
|
for (final parameter in parameters) { |
|
final nullable = _isParameterNullable( |
|
parameter.required, |
|
parameter.schema?.default_, |
|
); |
|
|
|
final result = resolveType( |
|
spec, |
|
state, |
|
_toDartName( |
|
parameter.name, |
|
uppercaseFirstCharacter: true, |
|
), |
|
parameter.schema!, |
|
); |
|
|
|
b.optionalParameters.add( |
|
Parameter( |
|
(final b) { |
|
b |
|
..named = true |
|
..name = _toDartName(parameter.name) |
|
..required = parameter.required ?? false; |
|
if (parameter.schema != null) { |
|
if (parameter.schema!.default_ != null) { |
|
final value = parameter.schema!.default_!.toString(); |
|
final result = resolveType( |
|
spec, |
|
state, |
|
parameter.schema!.type!, |
|
parameter.schema!, |
|
); |
|
b.defaultTo = Code(_valueToEscapedValue(result.name, value)); |
|
} |
|
|
|
b.type = refer( |
|
_makeNullable( |
|
result.name, |
|
nullable, |
|
), |
|
); |
|
} |
|
}, |
|
), |
|
); |
|
|
|
if (nullable) { |
|
code.write('if (${_toDartName(parameter.name)} != null) {'); |
|
} |
|
final value = result.encode(result.serialize(_toDartName(parameter.name))); |
|
switch (parameter.in_) { |
|
case 'path': |
|
code.write( |
|
"path = path.replaceAll('{${parameter.name}}', Uri.encodeQueryComponent($value));", |
|
); |
|
break; |
|
case 'query': |
|
code.write( |
|
"queryParameters['${parameter.name}${result is TypeResultList ? '[]' : ''}'] = $value;", |
|
); |
|
break; |
|
case 'header': |
|
code.write( |
|
"headers['${parameter.name}'] = $value;", |
|
); |
|
break; |
|
default: |
|
throw Exception('Can not work with parameter in "${parameter.in_}"'); |
|
} |
|
if (nullable) { |
|
code.write('}'); |
|
} |
|
} |
|
|
|
if (operation.requestBody != null) { |
|
if (operation.requestBody!.content!.length > 1) { |
|
throw Exception('Can not work with multiple mime types right now'); |
|
} |
|
for (final mimeType in operation.requestBody!.content!.keys) { |
|
final mediaType = operation.requestBody!.content![mimeType]!; |
|
|
|
code.write("headers['Content-Type'] = '$mimeType';"); |
|
|
|
final result = resolveType( |
|
spec, |
|
state, |
|
_toDartName(methodName, uppercaseFirstCharacter: true), |
|
mediaType.schema!, |
|
); |
|
switch (mimeType) { |
|
case 'application/json': |
|
b.optionalParameters.add( |
|
Parameter( |
|
(final b) => b |
|
..name = _toDartName(result.name) |
|
..type = refer(result.name) |
|
..named = true |
|
..required = operation.requestBody!.required ?? false, |
|
), |
|
); |
|
final nullable = _isParameterNullable( |
|
operation.requestBody!.required, |
|
mediaType.schema?.default_, |
|
); |
|
if (nullable) { |
|
code.write('if (${_toDartName(result.name)} != null) {'); |
|
} |
|
code.write( |
|
'body = Uint8List.fromList(utf8.encode(${result.encode(result.serialize(_toDartName(result.name)))}));', |
|
); |
|
if (nullable) { |
|
code.write('}'); |
|
} |
|
break; |
|
default: |
|
throw Exception('Can not parse mime type "$mimeType"'); |
|
} |
|
} |
|
} |
|
|
|
code.write( |
|
''' |
|
final response = await ${isRootClient ? '' : 'rootClient.'}doRequest( |
|
'$httpMethod', |
|
Uri(path: path, queryParameters: queryParameters).toString(), |
|
headers, |
|
body, |
|
); |
|
''', |
|
); |
|
|
|
if (operation.responses != null) { |
|
if (operation.responses!.length > 1) { |
|
throw Exception('Can not work with multiple status codes right now'); |
|
} |
|
for (final statusCode in operation.responses!.keys) { |
|
final response = operation.responses![statusCode]!; |
|
code.write('if (response.statusCode == $statusCode) {'); |
|
|
|
String? headersType; |
|
String? headersValue; |
|
if (response.headers != null) { |
|
final identifier = |
|
'${tag != null ? _toDartName(tag.name, uppercaseFirstCharacter: true) : null}${_toDartName(operationId, uppercaseFirstCharacter: true)}Headers'; |
|
final headerParseFunctions = <String, String>{}; |
|
for (final headerName in response.headers!.keys) { |
|
final functionIdentifier = '_${_toDartName('${identifier}Parse$headerName')}'; |
|
headerParseFunctions[headerName] = functionIdentifier; |
|
final result = resolveType( |
|
spec, |
|
state, |
|
identifier, |
|
response.headers![headerName]!.schema!, |
|
); |
|
output.add( |
|
'${result.name} $functionIdentifier(final Map data, final String key) => ${result.deserialize(result.decode('data[key]'))};', |
|
); |
|
} |
|
final result = resolveType( |
|
spec, |
|
state, |
|
identifier, |
|
Schema( |
|
type: 'object', |
|
properties: { |
|
for (final headerName in response.headers!.keys) ...{ |
|
headerName.toLowerCase(): response.headers![headerName]!.schema!, |
|
}, |
|
}, |
|
), |
|
extraJsonSerializableValues: { |
|
'disallowUnrecognizedKeys': 'false', |
|
}, |
|
extraJsonKeyValues: { |
|
for (final headerName in response.headers!.keys) ...{ |
|
headerName.toLowerCase(): { |
|
'readValue': headerParseFunctions[headerName]!, |
|
}, |
|
}, |
|
}, |
|
); |
|
headersType = result.name; |
|
headersValue = result.deserialize('response.headers'); |
|
} |
|
|
|
String? dataType; |
|
String? dataValue; |
|
if (response.content != null) { |
|
if (response.content!.length > 1) { |
|
throw Exception('Can not work with multiple mime types right now'); |
|
} |
|
for (final mimeType in response.content!.keys) { |
|
final mediaType = response.content![mimeType]!; |
|
|
|
final result = resolveType( |
|
spec, |
|
state, |
|
_toDartName(methodName, uppercaseFirstCharacter: true), |
|
mediaType.schema!, |
|
); |
|
switch (mimeType) { |
|
case 'application/json': |
|
dataType = result.name; |
|
dataValue = result.deserialize(result.decode('utf8.decode(response.body)')); |
|
break; |
|
case 'image/png': |
|
dataType = 'Uint8List'; |
|
dataValue = 'response.body'; |
|
break; |
|
default: |
|
throw Exception('Can not parse mime type "$mimeType"'); |
|
} |
|
} |
|
} |
|
|
|
if (headersType != null && dataType != null) { |
|
b.returns = refer('Future<Response<$dataType, $headersType>>'); |
|
code.write('return Response<$dataType, $headersType>($dataValue, $headersValue,);'); |
|
} else if (headersType != null) { |
|
b.returns = refer('Future<$headersType>'); |
|
code.write('return $headersValue;'); |
|
} else if (dataType != null) { |
|
b.returns = refer('Future<$dataType>'); |
|
code.write('return $dataValue;'); |
|
} else { |
|
b.returns = refer('Future'); |
|
code.write('return;'); |
|
} |
|
|
|
code.write('}'); |
|
} |
|
code.write('throw ApiException.fromResponse(response); // coverage:ignore-line\n'); |
|
} else { |
|
b.returns = refer('Future'); |
|
} |
|
b.body = Code(code.toString()); |
|
}, |
|
), |
|
], |
|
], |
|
], |
|
); |
|
}, |
|
).accept(emitter).toString(), |
|
); |
|
} |
|
|
|
if (spec.components?.schemas != null) { |
|
for (final name in spec.components!.schemas!.keys) { |
|
final schema = spec.components!.schemas![name]!; |
|
|
|
final identifier = _toDartName(name, uppercaseFirstCharacter: true); |
|
if (schema.type == null && schema.ref == null && schema.ofs == null) { |
|
output.add('typedef $identifier = dynamic;'); |
|
} else { |
|
final result = resolveType( |
|
spec, |
|
state, |
|
identifier, |
|
schema, |
|
); |
|
if (result is TypeResultBase) { |
|
output.add('typedef $identifier = ${result.name};'); |
|
} |
|
} |
|
} |
|
} |
|
|
|
output.addAll(state.output.map((final e) => e.accept(emitter).toString())); |
|
|
|
if (state.registeredJsonObjects.isNotEmpty) { |
|
output.addAll([ |
|
'// coverage:ignore-start', |
|
'final _deserializers = <Type, dynamic Function(dynamic)>{', |
|
for (final name in state.registeredJsonObjects) ...[ |
|
'$name: (final data) => ${TypeResultObject(name).deserialize('data')},', |
|
'List<$name>: (final data) => ${TypeResultList('List<$name>', TypeResultObject(name)).deserialize('data')},', |
|
], |
|
'};', |
|
'', |
|
'final _serializers = <Type, dynamic Function(dynamic)>{', |
|
for (final name in state.registeredJsonObjects) ...[ |
|
'$name: (final data) => ${TypeResultObject(name).serialize('data')},', |
|
'List<$name>: (final data) => ${TypeResultList('List<$name>', TypeResultObject(name)).serialize('data')},', |
|
], |
|
'};', |
|
'', |
|
'T deserialize<T>(final dynamic data) => _deserializers[T]!(data) as T;', |
|
'', |
|
'dynamic serialize<T>(final T data) => _serializers[T]!(data);', |
|
'// coverage:ignore-end', |
|
]); |
|
} |
|
|
|
final formatter = DartFormatter( |
|
pageWidth: 120, |
|
); |
|
await buildStep.writeAsString( |
|
outputId, |
|
formatter.format( |
|
output |
|
.join('\n') |
|
.replaceAll( |
|
'Map<String, dynamic> toJson()', |
|
' // coverage:ignore-start\nMap<String, dynamic> toJson()', |
|
) |
|
.replaceAll( |
|
'ToJson(this);', |
|
'ToJson(this);\n // coverage:ignore-end\n', |
|
) |
|
.replaceAll( |
|
'dynamic toJson() => _data;', |
|
' // coverage:ignore-start\ndynamic toJson() => _data;\n // coverage:ignore-end\n', |
|
), |
|
), |
|
); |
|
} catch (e, s) { |
|
print(s); |
|
|
|
rethrow; |
|
} |
|
} |
|
} |
|
|
|
String _clientName(final Tag tag) => '${_toDartName(tag.name, uppercaseFirstCharacter: true)}Client'; |
|
|
|
String _toDartName( |
|
final String input, { |
|
final bool uppercaseFirstCharacter = false, |
|
}) { |
|
final result = StringBuffer(); |
|
|
|
final parts = input.split(''); |
|
for (var i = 0; i < parts.length; i++) { |
|
var char = parts[i]; |
|
final prevChar = i > 0 ? parts[i - 1] : null; |
|
if (_isNonAlphaNumericString(char)) { |
|
continue; |
|
} |
|
if (prevChar != null && _isNonAlphaNumericString(prevChar)) { |
|
char = char.toUpperCase(); |
|
} |
|
if (i == 0) { |
|
if (uppercaseFirstCharacter) { |
|
char = char.toUpperCase(); |
|
} else { |
|
char = char.toLowerCase(); |
|
} |
|
} |
|
result.write(char); |
|
} |
|
|
|
final out = result.toString(); |
|
if (_dartKeywords.contains(out) || RegExp(r'^[0-9]+$', multiLine: true).hasMatch(out)) { |
|
return '\$$out'; |
|
} |
|
|
|
return out; |
|
} |
|
|
|
final _dartKeywords = [ |
|
'assert', |
|
'break', |
|
'case', |
|
'catch', |
|
'class', |
|
'const', |
|
'continue', |
|
'default', |
|
'do', |
|
'else', |
|
'enum', |
|
'extends', |
|
'false', |
|
'final', |
|
'finally', |
|
'for', |
|
'if', |
|
'in', |
|
'is', |
|
'new', |
|
'null', |
|
'rethrow', |
|
'return', |
|
'super', |
|
'switch', |
|
'this', |
|
'throw', |
|
'true', |
|
'try', |
|
'var', |
|
'void', |
|
'while', |
|
'with', |
|
'async', |
|
'hide', |
|
'on', |
|
'show', |
|
'sync', |
|
'abstract', |
|
'as', |
|
'covariant', |
|
'deferred', |
|
'dynamic', |
|
'export', |
|
'extension', |
|
'external', |
|
'factory', |
|
'function', |
|
'get', |
|
'implements', |
|
'import', |
|
'interface', |
|
'library', |
|
'mixin', |
|
'operator', |
|
'part', |
|
'set', |
|
'static', |
|
'typedef', |
|
]; |
|
|
|
bool _isNonAlphaNumericString(final String input) => !RegExp(r'^[a-zA-Z0-9]$').hasMatch(input); |
|
|
|
String _makeNullable(final String type, final bool nullable) => nullable && type != 'dynamic' ? '$type?' : type; |
|
|
|
String _toFieldName(final String dartName, final String type) => dartName == type ? '\$$dartName' : dartName; |
|
|
|
bool _isParameterNullable(final bool? required, final dynamic default_) => !(required ?? false) && default_ == null; |
|
|
|
String _valueToEscapedValue(final String type, final dynamic value) => type == 'String' ? "'$value'" : value.toString(); |
|
|
|
class State { |
|
final resolvedTypes = <String>[]; |
|
final registeredJsonObjects = <String>[]; |
|
final output = <Spec>[]; |
|
} |
|
|
|
TypeResult resolveObject( |
|
final OpenAPI spec, |
|
final State state, |
|
final String identifier, |
|
final Schema schema, { |
|
required final Map<String, String>? extraJsonSerializableValues, |
|
required final Map<String, Map<String, String>>? extraJsonKeyValues, |
|
final bool fromJsonString = false, |
|
}) { |
|
if (!state.resolvedTypes.contains(identifier)) { |
|
state.resolvedTypes.add(identifier); |
|
state.registeredJsonObjects.add(identifier); |
|
state.output.add( |
|
Class( |
|
(final b) { |
|
b |
|
..name = identifier |
|
..docs.addAll([ |
|
if (schema.description != null && schema.description!.isNotEmpty) ...[ |
|
'/// ${schema.description!}', |
|
], |
|
]) |
|
..annotations.add( |
|
refer('JsonSerializable').call( |
|
[], |
|
{ |
|
if (schema.additionalProperties ?? false) ...{ |
|
'disallowUnrecognizedKeys': refer('false'), |
|
}, |
|
if (extraJsonSerializableValues != null) ...{ |
|
for (final key in extraJsonSerializableValues.keys) ...{ |
|
key: refer(extraJsonSerializableValues[key]!), |
|
}, |
|
}, |
|
}, |
|
), |
|
) |
|
..constructors.addAll( |
|
[ |
|
Constructor( |
|
(final b) => b |
|
..optionalParameters.addAll( |
|
schema.properties!.keys.map( |
|
(final propertyName) => Parameter( |
|
(final b) { |
|
final propertySchema = schema.properties![propertyName]!; |
|
b |
|
..name = _toDartName(propertyName) |
|
..toThis = true |
|
..named = true |
|
..required = |
|
(schema.required ?? []).contains(propertyName) && propertySchema.default_ == null; |
|
if (propertySchema.default_ != null) { |
|
final value = propertySchema.default_!.toString(); |
|
final result = resolveType( |
|
spec, |
|
state, |
|
propertySchema.type!, |
|
propertySchema, |
|
); |
|
b.defaultTo = Code(_valueToEscapedValue(result.name, value)); |
|
} |
|
}, |
|
), |
|
), |
|
), |
|
), |
|
Constructor( |
|
(final b) => b |
|
..factory = true |
|
..name = 'fromJson' |
|
..lambda = true |
|
..requiredParameters.add( |
|
Parameter( |
|
(final b) => b |
|
..name = 'json' |
|
..type = refer('Map<String, dynamic>'), |
|
), |
|
) |
|
..body = Code('_\$${identifier}FromJson(json)'), |
|
), |
|
Constructor( |
|
(final b) => b |
|
..factory = true |
|
..name = 'fromJsonString' |
|
..lambda = true |
|
..requiredParameters.add( |
|
Parameter( |
|
(final b) => b |
|
..name = 'data' |
|
..type = refer('String'), |
|
), |
|
) |
|
..body = Code('$identifier.fromJson(json.decode(data) as Map<String, dynamic>)'), |
|
), |
|
], |
|
) |
|
..methods.addAll([ |
|
Method( |
|
(final b) => b |
|
..name = 'toJson' |
|
..returns = refer('Map<String, dynamic>') |
|
..lambda = true |
|
..body = Code('_\$${identifier}ToJson(this)'), |
|
), |
|
Method( |
|
(final b) => b |
|
..name = 'toJsonString' |
|
..returns = refer('String') |
|
..lambda = true |
|
..static = true |
|
..requiredParameters.add( |
|
Parameter( |
|
(final b) => b |
|
..name = 'data' |
|
..type = refer(identifier), |
|
), |
|
) |
|
..body = const Code('json.encode(data.toJson())'), |
|
), |
|
]) |
|
..fields.addAll([ |
|
for (final propertyName in schema.properties!.keys) ...[ |
|
Field( |
|
(final b) { |
|
final propertySchema = schema.properties![propertyName]!; |
|
final result = resolveType( |
|
spec, |
|
state, |
|
'${identifier}_${_toDartName(propertyName, uppercaseFirstCharacter: true)}', |
|
propertySchema, |
|
extraJsonSerializableValues: extraJsonSerializableValues, |
|
); |
|
|
|
b |
|
..name = _toDartName(propertyName) |
|
..type = refer( |
|
_makeNullable( |
|
result.name, |
|
!(schema.required ?? []).contains(propertyName), |
|
), |
|
) |
|
..modifier = FieldModifier.final$ |
|
..docs.addAll([ |
|
if (propertySchema.description != null && propertySchema.description!.isNotEmpty) ...[ |
|
'/// ${propertySchema.description!}', |
|
], |
|
]); |
|
if (_toDartName(propertyName) != propertyName || |
|
propertySchema.isJsonString || |
|
extraJsonKeyValues != null) { |
|
b.annotations.add( |
|
refer('JsonKey').call( |
|
[], |
|
{ |
|
if (_toDartName(propertyName) != propertyName) ...{ |
|
'name': refer("'$propertyName'"), |
|
}, |
|
if (propertySchema.isJsonString) ...{ |
|
'fromJson': refer('${result.name}.fromJsonString'), |
|
'toJson': refer('${result.name}.toJsonString'), |
|
}, |
|
if (extraJsonKeyValues != null && extraJsonKeyValues.containsKey(propertyName)) ...{ |
|
for (final key in extraJsonKeyValues[propertyName]!.keys) ...{ |
|
key: refer(extraJsonKeyValues[propertyName]![key]!), |
|
}, |
|
}, |
|
}, |
|
), |
|
); |
|
} |
|
}, |
|
) |
|
], |
|
]); |
|
}, |
|
), |
|
); |
|
} |
|
return TypeResultObject( |
|
identifier, |
|
fromJsonString: fromJsonString, |
|
); |
|
} |
|
|
|
TypeResult resolveType( |
|
final OpenAPI spec, |
|
final State state, |
|
final String identifier, |
|
final Schema schema, { |
|
final Map<String, String>? extraJsonSerializableValues, |
|
final Map<String, Map<String, String>>? extraJsonKeyValues, |
|
final bool ignoreEnum = false, |
|
final bool fromJsonString = false, |
|
}) { |
|
TypeResult? result; |
|
if (schema.ref != null) { |
|
final name = schema.ref!.split('/').last; |
|
result = resolveType( |
|
spec, |
|
state, |
|
name, |
|
spec.components!.schemas![name]!, |
|
extraJsonSerializableValues: extraJsonSerializableValues, |
|
fromJsonString: fromJsonString, |
|
); |
|
} else if (schema.ofs != null) { |
|
if (!state.resolvedTypes.contains(identifier)) { |
|
state.resolvedTypes.add(identifier); |
|
final results = schema.ofs! |
|
.map( |
|
(final s) => resolveType( |
|
spec, |
|
state, |
|
'$identifier${schema.ofs!.indexOf(s)}', |
|
s, |
|
extraJsonSerializableValues: extraJsonSerializableValues, |
|
), |
|
) |
|
.toList(); |
|
state.output.add( |
|
Class( |
|
(final b) { |
|
final fields = <String, String>{}; |
|
for (final result in results) { |
|
final dartName = _toDartName(result.name); |
|
fields[result.name] = _toFieldName(dartName, result.name); |
|
} |
|
|
|
b |
|
..name = identifier |
|
..fields.addAll([ |
|
Field( |
|
(final b) { |
|
b |
|
..name = '_data' |
|
..type = refer('dynamic') |
|
..modifier = FieldModifier.final$; |
|
}, |
|
), |
|
for (final result in results) ...[ |
|
Field( |
|
(final b) { |
|
final s = schema.ofs![results.indexOf(result)]; |
|
b |
|
..name = fields[result.name]! |
|
..type = refer(_makeNullable(result.name, true)) |
|
..modifier = FieldModifier.final$ |
|
..docs.addAll([ |
|
if (s.description != null && s.description!.isNotEmpty) ...[ |
|
'/// ${s.description!}', |
|
], |
|
]); |
|
}, |
|
), |
|
], |
|
]) |
|
..constructors.addAll([ |
|
Constructor( |
|
(final b) => b |
|
..requiredParameters.add( |
|
Parameter( |
|
(final b) => b |
|
..name = '_data' |
|
..toThis = true, |
|
), |
|
) |
|
..optionalParameters.addAll([ |
|
for (final result in results) ...[ |
|
Parameter( |
|
(final b) => b |
|
..name = fields[result.name]! |
|
..toThis = true |
|
..named = true, |
|
), |
|
], |
|
]), |
|
), |
|
Constructor( |
|
(final b) { |
|
b |
|
..factory = true |
|
..name = 'fromJson' |
|
..requiredParameters.add( |
|
Parameter( |
|
(final b) => b |
|
..name = 'data' |
|
..type = refer('dynamic'), |
|
), |
|
) |
|
..body = Code( |
|
<String>[ |
|
for (final result in results) ...[ |
|
'${result.name}? ${fields[result.name]!};', |
|
], |
|
for (final result in results) ...[ |
|
if (schema.discriminator != null) ...[ |
|
"if (data['${schema.discriminator!.propertyName}'] == '${result.name}'", |
|
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}')) |
|
.map((final entry) => entry.key)) ...[ |
|
" || data['${schema.discriminator!.propertyName}'] == '$key'", |
|
], |
|
], |
|
') {', |
|
], |
|
'try {', |
|
'${fields[result.name]!} = ${result.deserialize('data')};', |
|
'} catch (_) {', |
|
if (schema.discriminator != null) ...[ |
|
'rethrow;', |
|
], |
|
'}', |
|
if (schema.discriminator != null) ...[ |
|
'}', |
|
], |
|
], |
|
if (schema.oneOf != null) ...[ |
|
"assert([${fields.values.join(',')}].where((final x) => x != null).length == 1, 'Need oneOf for \$data');", |
|
], |
|
if (schema.allOf != null) ...[ |
|
"assert([${fields.values.join(',')}].where((final x) => x != null).length == ${fields.length}, 'Need allOf for \$data');", |
|
], |
|
'return $identifier(', |
|
'data,', |
|
for (final result in results) ...[ |
|
'${fields[result.name]!}: ${fields[result.name]!},', |
|
], |
|
');', |
|
].join(), |
|
); |
|
}, |
|
), |
|
Constructor( |
|
(final b) { |
|
b |
|
..factory = true |
|
..lambda = true |
|
..name = 'fromJsonString' |
|
..requiredParameters.add( |
|
Parameter( |
|
(final b) => b |
|
..name = 'data' |
|
..type = refer('String'), |
|
), |
|
) |
|
..body = Code('$identifier.fromJson(json.decode(data))'); |
|
}, |
|
), |
|
]) |
|
..methods.addAll([ |
|
Method( |
|
(final b) => b |
|
..name = 'toJson' |
|
..returns = refer('dynamic') |
|
..lambda = true |
|
..body = const Code('_data'), |
|
), |
|
Method( |
|
(final b) => b |
|
..name = 'toJsonString' |
|
..returns = refer('String') |
|
..lambda = true |
|
..static = true |
|
..requiredParameters.add( |
|
Parameter( |
|
(final b) => b |
|
..name = 'data' |
|
..type = refer('dynamic'), |
|
), |
|
) |
|
..body = const Code('json.encode(data)'), |
|
), |
|
]); |
|
}, |
|
), |
|
); |
|
} |
|
|
|
result = TypeResultObject(identifier); |
|
} else { |
|
switch (schema.type) { |
|
case 'boolean': |
|
result = TypeResultBase('bool'); |
|
break; |
|
case 'integer': |
|
result = TypeResultBase('int'); |
|
break; |
|
case 'number': |
|
result = TypeResultBase('num'); |
|
break; |
|
case 'string': |
|
switch (schema.format) { |
|
case 'binary': |
|
result = TypeResultBase('Uint8List'); |
|
break; |
|
} |
|
|
|
if (schema.isJsonString) { |
|
result = resolveType( |
|
spec, |
|
state, |
|
identifier, |
|
schema.contentSchema!, |
|
extraJsonSerializableValues: extraJsonSerializableValues, |
|
fromJsonString: true, |
|
); |
|
break; |
|
} |
|
|
|
result = TypeResultBase( |
|
'String', |
|
); |
|
break; |
|
case 'array': |
|
if (schema.items != null) { |
|
final subResult = resolveType( |
|
spec, |
|
state, |
|
identifier, |
|
schema.items!, |
|
extraJsonSerializableValues: extraJsonSerializableValues, |
|
); |
|
result = TypeResultList( |
|
'List<${subResult.name}>', |
|
subResult, |
|
); |
|
} else { |
|
result = TypeResultList( |
|
'List', |
|
TypeResultBase('dynamic'), |
|
); |
|
} |
|
break; |
|
case 'object': |
|
if (schema.properties == null) { |
|
result = TypeResultBase('dynamic'); |
|
break; |
|
} |
|
if (schema.properties!.isEmpty) { |
|
result = TypeResultMap( |
|
'Map<String, dynamic>', |
|
TypeResultBase('dynamic'), |
|
); |
|
break; |
|
} |
|
|
|
result = resolveObject( |
|
spec, |
|
state, |
|
identifier, |
|
schema, |
|
extraJsonSerializableValues: extraJsonSerializableValues, |
|
extraJsonKeyValues: extraJsonKeyValues, |
|
fromJsonString: fromJsonString, |
|
); |
|
break; |
|
} |
|
} |
|
|
|
if (result != null) { |
|
if (!ignoreEnum && schema.enum_ != null) { |
|
if (!state.resolvedTypes.contains(identifier)) { |
|
state.resolvedTypes.add(identifier); |
|
state.output.add( |
|
Enum( |
|
(final b) => b |
|
..name = identifier |
|
..constructors.add( |
|
Constructor( |
|
(final b) => b |
|
..constant = true |
|
..requiredParameters.add( |
|
Parameter( |
|
(final b) => b |
|
..name = 'value' |
|
..toThis = true, |
|
), |
|
), |
|
), |
|
) |
|
..fields.add( |
|
Field( |
|
(final b) => b |
|
..name = 'value' |
|
..type = refer(result!.name) |
|
..modifier = FieldModifier.final$, |
|
), |
|
) |
|
..values.addAll( |
|
schema.enum_!.map( |
|
(final value) => EnumValue( |
|
(final b) { |
|
final result = resolveType( |
|
spec, |
|
state, |
|
'$identifier${_toDartName(value.toString(), uppercaseFirstCharacter: true)}', |
|
schema, |
|
ignoreEnum: true, |
|
extraJsonSerializableValues: extraJsonSerializableValues, |
|
); |
|
b |
|
..name = _toDartName(value.toString()) |
|
..arguments.add( |
|
refer(_valueToEscapedValue(result.name, value)), |
|
); |
|
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 $identifier.', |
|
); |
|
} |
|
b.annotations.add( |
|
refer('JsonValue').call([ |
|
refer(_valueToEscapedValue(result.name, value.toString())), |
|
]), |
|
); |
|
} |
|
}, |
|
), |
|
), |
|
) |
|
..methods.add( |
|
Method( |
|
(final b) => b |
|
..name = 'fromValue' |
|
..static = true |
|
..returns = refer(identifier) |
|
..requiredParameters.add( |
|
Parameter( |
|
(final b) => b |
|
..name = 'value' |
|
..type = refer(result!.name), |
|
), |
|
) |
|
..body = Code( |
|
[ |
|
'switch (value) {', |
|
for (final value in schema.enum_!) ...[ |
|
'case ${_valueToEscapedValue(result!.name, value)}:', |
|
'return $identifier.${_toDartName(value.toString())};', |
|
], |
|
'default:', |
|
'throw Exception(\'Can not parse UserStatusClearAtTime0 from "\$value"\');', |
|
'}', |
|
].join(), |
|
), |
|
), |
|
), |
|
), |
|
); |
|
} |
|
result = TypeResultEnum(identifier, result); |
|
} |
|
|
|
return result; |
|
} |
|
|
|
throw Exception('Can not convert OpenAPI type "${schema.toJson()}" to a Dart type'); |
|
}
|
|
|