Nikolas Rimikis
1 year ago
3 changed files with 700 additions and 644 deletions
@ -0,0 +1,651 @@
|
||||
import 'package:code_builder/code_builder.dart'; |
||||
import 'package:collection/collection.dart'; |
||||
import 'package:dynamite/src/builder/resolve_object.dart'; |
||||
import 'package:dynamite/src/builder/resolve_type.dart'; |
||||
import 'package:dynamite/src/builder/state.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/path_item.dart'; |
||||
import 'package:dynamite/src/models/schema.dart'; |
||||
import 'package:dynamite/src/type_result/type_result.dart'; |
||||
|
||||
List<Spec> generateDynamiteOverrides(final State state) => [ |
||||
Class( |
||||
(final b) => b |
||||
..name = '${state.classPrefix}Response' |
||||
..types.addAll([ |
||||
refer('T'), |
||||
refer('U'), |
||||
]) |
||||
..extend = refer('DynamiteResponse<T, U>') |
||||
..constructors.add( |
||||
Constructor( |
||||
(final b) => b |
||||
..requiredParameters.addAll( |
||||
['data', 'headers'].map( |
||||
(final name) => Parameter( |
||||
(final b) => b |
||||
..name = name |
||||
..toSuper = true, |
||||
), |
||||
), |
||||
), |
||||
), |
||||
) |
||||
..methods.add( |
||||
Method( |
||||
(final b) => b |
||||
..name = 'toString' |
||||
..returns = refer('String') |
||||
..annotations.add(refer('override')) |
||||
..lambda = true |
||||
..body = Code( |
||||
"'${state.classPrefix}Response(data: \$data, headers: \$headers)'", |
||||
), |
||||
), |
||||
), |
||||
), |
||||
Class( |
||||
(final b) => b |
||||
..name = '${state.classPrefix}ApiException' |
||||
..extend = refer('DynamiteApiException') |
||||
..constructors.add( |
||||
Constructor( |
||||
(final b) => b |
||||
..requiredParameters.addAll( |
||||
['statusCode', 'headers', 'body'].map( |
||||
(final name) => Parameter( |
||||
(final b) => b |
||||
..name = name |
||||
..toSuper = true, |
||||
), |
||||
), |
||||
), |
||||
), |
||||
) |
||||
..methods.addAll([ |
||||
Method( |
||||
(final b) => b |
||||
..name = 'fromResponse' |
||||
..returns = refer('Future<${state.classPrefix}ApiException>') |
||||
..static = true |
||||
..modifier = MethodModifier.async |
||||
..requiredParameters.add( |
||||
Parameter( |
||||
(final b) => b |
||||
..name = 'response' |
||||
..type = refer('HttpClientResponse'), |
||||
), |
||||
) |
||||
..body = Block.of([ |
||||
const Code('String body;'), |
||||
const Code('try {'), |
||||
const Code('body = await response.body;'), |
||||
const Code('} on FormatException {'), |
||||
const Code("body = 'binary';"), |
||||
const Code('}'), |
||||
const Code(''), |
||||
Code('return ${state.classPrefix}ApiException('), |
||||
const Code('response.statusCode,'), |
||||
const Code('response.responseHeaders,'), |
||||
const Code('body,'), |
||||
const Code(');'), |
||||
]), |
||||
), |
||||
Method( |
||||
(final b) => b |
||||
..name = 'toString' |
||||
..returns = refer('String') |
||||
..annotations.add(refer('override')) |
||||
..lambda = true |
||||
..body = Code( |
||||
"'${state.classPrefix}ApiException(statusCode: \$statusCode, headers: \$headers, body: \$body)'", |
||||
), |
||||
), |
||||
]), |
||||
), |
||||
]; |
||||
|
||||
Iterable<Class> generateClients( |
||||
final OpenAPI spec, |
||||
final State state, |
||||
) sync* { |
||||
final tags = generateTags(spec); |
||||
yield buildRootClient(spec, state, tags); |
||||
|
||||
for (final tag in tags) { |
||||
yield buildClient(spec, state, tags, tag); |
||||
} |
||||
} |
||||
|
||||
Class buildRootClient( |
||||
final OpenAPI spec, |
||||
final State state, |
||||
final List<String> tags, |
||||
) => |
||||
Class( |
||||
(final b) { |
||||
b |
||||
..extend = refer('DynamiteClient') |
||||
..name = '${state.classPrefix}Client' |
||||
..docs.addAll(spec.formattedTagsFor(null)) |
||||
..constructors.addAll([ |
||||
Constructor( |
||||
(final b) => b |
||||
..requiredParameters.add( |
||||
Parameter( |
||||
(final b) => b |
||||
..name = 'baseURL' |
||||
..toSuper = true, |
||||
), |
||||
) |
||||
..optionalParameters.addAll([ |
||||
Parameter( |
||||
(final b) => b |
||||
..name = 'baseHeaders' |
||||
..toSuper = true |
||||
..named = true, |
||||
), |
||||
Parameter( |
||||
(final b) => b |
||||
..name = 'userAgent' |
||||
..toSuper = true |
||||
..named = true, |
||||
), |
||||
Parameter( |
||||
(final b) => b |
||||
..name = 'httpClient' |
||||
..toSuper = true |
||||
..named = true, |
||||
), |
||||
Parameter( |
||||
(final b) => b |
||||
..name = 'cookieJar' |
||||
..toSuper = true |
||||
..named = true, |
||||
), |
||||
if (spec.hasAnySecurity) ...[ |
||||
Parameter( |
||||
(final b) => b |
||||
..name = 'authentications' |
||||
..toSuper = true |
||||
..named = true, |
||||
), |
||||
], |
||||
]), |
||||
), |
||||
Constructor( |
||||
(final b) => b |
||||
..name = 'fromClient' |
||||
..requiredParameters.add( |
||||
Parameter( |
||||
(final b) => b |
||||
..name = 'client' |
||||
..type = refer('DynamiteClient'), |
||||
), |
||||
) |
||||
..initializers.add( |
||||
const Code(''' |
||||
super( |
||||
client.baseURL, |
||||
baseHeaders: client.baseHeaders, |
||||
httpClient: client.httpClient, |
||||
cookieJar: client.cookieJar, |
||||
authentications: client.authentications, |
||||
) |
||||
'''), |
||||
), |
||||
), |
||||
]); |
||||
|
||||
for (final tag in tags.where((final t) => !t.contains('/'))) { |
||||
final client = '${state.classPrefix}${clientName(tag)}'; |
||||
|
||||
b.methods.add( |
||||
Method( |
||||
(final b) => b |
||||
..name = toDartName(tag) |
||||
..lambda = true |
||||
..type = MethodType.getter |
||||
..returns = refer(client) |
||||
..body = Code('$client(this)'), |
||||
), |
||||
); |
||||
} |
||||
|
||||
b.methods.addAll(buildTags(spec, state, tags, null)); |
||||
}, |
||||
); |
||||
|
||||
Class buildClient( |
||||
final OpenAPI spec, |
||||
final State state, |
||||
final List<String> tags, |
||||
final String tag, |
||||
) => |
||||
Class( |
||||
(final b) { |
||||
b |
||||
..name = '${state.classPrefix}${clientName(tag)}' |
||||
..docs.addAll(spec.formattedTagsFor(tag)) |
||||
..constructors.add( |
||||
Constructor( |
||||
(final b) => b.requiredParameters.add( |
||||
Parameter( |
||||
(final b) => b |
||||
..name = '_rootClient' |
||||
..toThis = true, |
||||
), |
||||
), |
||||
), |
||||
) |
||||
..fields.add( |
||||
Field( |
||||
(final b) => b |
||||
..name = '_rootClient' |
||||
..type = refer('${state.classPrefix}Client') |
||||
..modifier = FieldModifier.final$, |
||||
), |
||||
); |
||||
|
||||
for (final t in tags.where((final t) => t.startsWith('$tag/'))) { |
||||
b.methods.add( |
||||
Method( |
||||
(final b) => b |
||||
..name = toDartName(t.substring('$tag/'.length)) |
||||
..lambda = true |
||||
..type = MethodType.getter |
||||
..returns = refer('${state.classPrefix}${clientName(t)}') |
||||
..body = Code('${state.classPrefix}${clientName(t)}(_rootClient)'), |
||||
), |
||||
); |
||||
} |
||||
|
||||
b.methods.addAll(buildTags(spec, state, tags, tag)); |
||||
}, |
||||
); |
||||
|
||||
Iterable<Method> buildTags( |
||||
final OpenAPI spec, |
||||
final State state, |
||||
final List<String> tags, |
||||
final String? tag, |
||||
) sync* { |
||||
final isRootClient = tag == null; |
||||
final paths = generatePaths(spec, tag); |
||||
|
||||
for (final pathEntry in paths.entries) { |
||||
for (final operationEntry in pathEntry.value.operations.entries) { |
||||
yield Method( |
||||
(final b) { |
||||
final httpMethod = operationEntry.key; |
||||
final operation = operationEntry.value; |
||||
final operationId = operation.operationId ?? toDartName('$httpMethod-${pathEntry.key}'); |
||||
final parameters = [ |
||||
...?pathEntry.value.parameters, |
||||
...?operation.parameters, |
||||
]..sort(sortRequiredParameters); |
||||
b |
||||
..name = toDartName(filterMethodName(operationId, tag ?? '')) |
||||
..modifier = MethodModifier.async |
||||
..docs.addAll(operation.formattedDescription); |
||||
if (operation.deprecated ?? false) { |
||||
b.annotations.add(refer('Deprecated').call([refer("''")])); |
||||
} |
||||
|
||||
final acceptHeader = operation.responses?.values |
||||
.map((final response) => response.content?.keys) |
||||
.whereNotNull() |
||||
.expand((final element) => element) |
||||
.toSet() |
||||
.join(',') ?? |
||||
''; |
||||
final code = StringBuffer(''' |
||||
var _path = '${pathEntry.key}'; |
||||
final _queryParameters = <String, dynamic>{}; |
||||
final _headers = <String, String>{${acceptHeader.isNotEmpty ? "'Accept': '$acceptHeader'," : ''}}; |
||||
Uint8List? _body; |
||||
'''); |
||||
|
||||
final security = operation.security ?? spec.security ?? []; |
||||
final securityRequirements = security.where((final requirement) => requirement.isNotEmpty); |
||||
final isOptionalSecurity = securityRequirements.length != security.length; |
||||
code.write(' // coverage:ignore-start\n'); |
||||
for (final requirement in securityRequirements) { |
||||
final securityScheme = spec.components!.securitySchemes![requirement.keys.single]!; |
||||
code.write(''' |
||||
if (${isRootClient ? 'this' : '_rootClient'}.authentications.where((final a) => a.type == '${securityScheme.type}' && a.scheme == '${securityScheme.scheme}').isNotEmpty) { |
||||
_headers.addAll(${isRootClient ? 'this' : '_rootClient'}.authentications.singleWhere((final a) => a.type == '${securityScheme.type}' && a.scheme == '${securityScheme.scheme}').headers); |
||||
} |
||||
'''); |
||||
if (securityRequirements.last != requirement) { |
||||
code.write('else'); |
||||
} |
||||
} |
||||
if (securityRequirements.isNotEmpty && !isOptionalSecurity) { |
||||
code.write(''' |
||||
else { |
||||
throw Exception('Missing authentication for ${securityRequirements.map((final r) => r.keys.single).join(' or ')}'); |
||||
} |
||||
'''); |
||||
} |
||||
code.write(' // coverage:ignore-end\n'); |
||||
|
||||
for (final parameter in parameters) { |
||||
final dartParameterNullable = isDartParameterNullable( |
||||
parameter.required, |
||||
parameter.schema, |
||||
); |
||||
|
||||
final result = resolveType( |
||||
spec, |
||||
state, |
||||
toDartName( |
||||
'$operationId-${parameter.name}', |
||||
uppercaseFirstCharacter: true, |
||||
), |
||||
parameter.schema!, |
||||
nullable: dartParameterNullable, |
||||
).dartType; |
||||
|
||||
if (result.name == 'String') { |
||||
if (parameter.schema?.pattern != null) { |
||||
code.write(''' |
||||
if (!RegExp(r'${parameter.schema!.pattern!}').hasMatch(${toDartName(parameter.name)})) { |
||||
throw Exception('Invalid value "\$${toDartName(parameter.name)}" for parameter "${toDartName(parameter.name)}" with pattern "\${r'${parameter.schema!.pattern!}'}"'); // coverage:ignore-line |
||||
} |
||||
'''); |
||||
} |
||||
if (parameter.schema?.minLength != null) { |
||||
code.write(''' |
||||
if (${toDartName(parameter.name)}.length < ${parameter.schema!.minLength!}) { |
||||
throw Exception('Parameter "${toDartName(parameter.name)}" has to be at least ${parameter.schema!.minLength!} characters long'); // coverage:ignore-line |
||||
} |
||||
'''); |
||||
} |
||||
if (parameter.schema?.maxLength != null) { |
||||
code.write(''' |
||||
if (${toDartName(parameter.name)}.length > ${parameter.schema!.maxLength!}) { |
||||
throw Exception('Parameter "${toDartName(parameter.name)}" has to be at most ${parameter.schema!.maxLength!} characters long'); // coverage:ignore-line |
||||
} |
||||
'''); |
||||
} |
||||
} |
||||
|
||||
final defaultValueCode = parameter.schema?.default_ != null |
||||
? valueToEscapedValue(result, parameter.schema!.default_.toString()) |
||||
: null; |
||||
|
||||
b.optionalParameters.add( |
||||
Parameter( |
||||
(final b) { |
||||
b |
||||
..named = true |
||||
..name = toDartName(parameter.name) |
||||
..required = parameter.isDartRequired; |
||||
if (parameter.schema != null) { |
||||
b.type = refer(result.nullableName); |
||||
} |
||||
if (defaultValueCode != null) { |
||||
b.defaultTo = Code(defaultValueCode); |
||||
} |
||||
}, |
||||
), |
||||
); |
||||
|
||||
if (dartParameterNullable) { |
||||
code.write('if (${toDartName(parameter.name)} != null) {'); |
||||
} |
||||
final value = result.encode( |
||||
toDartName(parameter.name), |
||||
onlyChildren: result is TypeResultList && parameter.in_ == 'query', |
||||
); |
||||
if (defaultValueCode != null && parameter.in_ == 'query') { |
||||
code.write('if (${toDartName(parameter.name)} != $defaultValueCode) {'); |
||||
} |
||||
switch (parameter.in_) { |
||||
case 'path': |
||||
code.write( |
||||
"_path = _path.replaceAll('{${parameter.name}}', Uri.encodeQueryComponent($value));", |
||||
); |
||||
case 'query': |
||||
code.write( |
||||
"_queryParameters['${parameter.name}'] = $value;", |
||||
); |
||||
case 'header': |
||||
code.write( |
||||
"_headers['${parameter.name}'] = $value;", |
||||
); |
||||
default: |
||||
throw Exception('Can not work with parameter in "${parameter.in_}"'); |
||||
} |
||||
if (defaultValueCode != null && parameter.in_ == 'query') { |
||||
code.write('}'); |
||||
} |
||||
if (dartParameterNullable) { |
||||
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 content in operation.requestBody!.content!.entries) { |
||||
final mimeType = content.key; |
||||
final mediaType = content.value; |
||||
|
||||
code.write("_headers['Content-Type'] = '$mimeType';"); |
||||
|
||||
final dartParameterNullable = isDartParameterNullable( |
||||
operation.requestBody!.required, |
||||
mediaType.schema, |
||||
); |
||||
|
||||
final result = resolveType( |
||||
spec, |
||||
state, |
||||
toDartName('$operationId-request-$mimeType', uppercaseFirstCharacter: true), |
||||
mediaType.schema!, |
||||
nullable: dartParameterNullable, |
||||
); |
||||
final parameterName = toDartName(result.name.replaceFirst(state.classPrefix, '')); |
||||
switch (mimeType) { |
||||
case 'application/json': |
||||
case 'application/x-www-form-urlencoded': |
||||
final dartParameterRequired = isRequired( |
||||
operation.requestBody!.required, |
||||
mediaType.schema?.default_, |
||||
); |
||||
b.optionalParameters.add( |
||||
Parameter( |
||||
(final b) => b |
||||
..name = parameterName |
||||
..type = refer(result.nullableName) |
||||
..named = true |
||||
..required = dartParameterRequired, |
||||
), |
||||
); |
||||
|
||||
if (dartParameterNullable) { |
||||
code.write('if ($parameterName != null) {'); |
||||
} |
||||
code.write( |
||||
'_body = utf8.encode(${result.encode(parameterName, mimeType: mimeType)}) as Uint8List;', |
||||
); |
||||
if (dartParameterNullable) { |
||||
code.write('}'); |
||||
} |
||||
default: |
||||
throw Exception('Can not parse mime type "$mimeType"'); |
||||
} |
||||
} |
||||
} |
||||
|
||||
code.write( |
||||
''' |
||||
final _response = await ${isRootClient ? 'this' : '_rootClient'}.doRequest( |
||||
'$httpMethod', |
||||
Uri(path: _path, queryParameters: _queryParameters.isNotEmpty ? _queryParameters : null), |
||||
_headers, |
||||
_body, |
||||
); |
||||
''', |
||||
); |
||||
|
||||
if (operation.responses != null) { |
||||
if (operation.responses!.length > 1) { |
||||
throw Exception('Can not work with multiple status codes right now'); |
||||
} |
||||
for (final responseEntry in operation.responses!.entries) { |
||||
final statusCode = responseEntry.key; |
||||
final response = responseEntry.value; |
||||
code.write('if (_response.statusCode == $statusCode) {'); |
||||
|
||||
String? headersType; |
||||
String? headersValue; |
||||
if (response.headers != null) { |
||||
final identifier = |
||||
'${tag != null ? toDartName(tag, uppercaseFirstCharacter: true) : null}${toDartName(operationId, uppercaseFirstCharacter: true)}Headers'; |
||||
final result = resolveObject( |
||||
spec, |
||||
state, |
||||
identifier, |
||||
Schema( |
||||
properties: response.headers!.map( |
||||
(final headerName, final value) => MapEntry( |
||||
headerName.toLowerCase(), |
||||
value.schema!, |
||||
), |
||||
), |
||||
), |
||||
isHeader: true, |
||||
); |
||||
headersType = result.name; |
||||
headersValue = result.deserialize('_response.responseHeaders'); |
||||
} |
||||
|
||||
String? dataType; |
||||
String? dataValue; |
||||
bool? dataNeedsAwait; |
||||
if (response.content != null) { |
||||
if (response.content!.length > 1) { |
||||
throw Exception('Can not work with multiple mime types right now'); |
||||
} |
||||
for (final content in response.content!.entries) { |
||||
final mimeType = content.key; |
||||
final mediaType = content.value; |
||||
|
||||
final result = resolveType( |
||||
spec, |
||||
state, |
||||
toDartName( |
||||
'$operationId-response-$statusCode-$mimeType', |
||||
uppercaseFirstCharacter: true, |
||||
), |
||||
mediaType.schema!, |
||||
); |
||||
|
||||
if (mimeType == '*/*' || mimeType == 'application/octet-stream' || mimeType.startsWith('image/')) { |
||||
dataType = 'Uint8List'; |
||||
dataValue = '_response.bodyBytes'; |
||||
dataNeedsAwait = true; |
||||
} else if (mimeType.startsWith('text/') || mimeType == 'application/javascript') { |
||||
dataType = 'String'; |
||||
dataValue = '_response.body'; |
||||
dataNeedsAwait = true; |
||||
} else if (mimeType == 'application/json') { |
||||
dataType = result.name; |
||||
if (result.name == 'dynamic') { |
||||
dataValue = ''; |
||||
} else if (result.name == 'String') { |
||||
dataValue = '_response.body'; |
||||
dataNeedsAwait = true; |
||||
} else if (result is TypeResultEnum || result is TypeResultBase) { |
||||
dataValue = result.deserialize(result.decode('await _response.body')); |
||||
dataNeedsAwait = false; |
||||
} else { |
||||
dataValue = result.deserialize('await _response.jsonBody'); |
||||
dataNeedsAwait = false; |
||||
} |
||||
} else { |
||||
throw Exception('Can not parse mime type "$mimeType"'); |
||||
} |
||||
} |
||||
} |
||||
|
||||
if (headersType != null && dataType != null) { |
||||
b.returns = refer('Future<${state.classPrefix}Response<$dataType, $headersType>>'); |
||||
code.write( |
||||
'return ${state.classPrefix}Response<$dataType, $headersType>(${dataNeedsAwait ?? false ? 'await ' : ''}$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 await ${state.classPrefix}ApiException.fromResponse(_response); // coverage:ignore-line\n', |
||||
); |
||||
} else { |
||||
b.returns = refer('Future'); |
||||
} |
||||
b.body = Code(code.toString()); |
||||
}, |
||||
); |
||||
} |
||||
} |
||||
} |
||||
|
||||
Map<String, PathItem> generatePaths(final OpenAPI spec, final String? tag) { |
||||
final paths = <String, PathItem>{}; |
||||
|
||||
if (spec.paths != null) { |
||||
for (final path in spec.paths!.entries) { |
||||
for (final operationEntry in path.value.operations.entries) { |
||||
final operation = operationEntry.value; |
||||
if ((operation.tags != null && operation.tags!.contains(tag)) || |
||||
(tag == null && (operation.tags == null || operation.tags!.isEmpty))) { |
||||
paths[path.key] ??= PathItem( |
||||
description: path.value.description, |
||||
parameters: path.value.parameters, |
||||
); |
||||
paths[path.key] = paths[path.key]!.copyWithOperations({operationEntry.key: operation}); |
||||
} |
||||
} |
||||
} |
||||
} |
||||
|
||||
return paths; |
||||
} |
||||
|
||||
List<String> generateTags(final OpenAPI spec) { |
||||
final tags = <String>[]; |
||||
|
||||
if (spec.paths != null) { |
||||
for (final pathItem in spec.paths!.values) { |
||||
for (final operation in pathItem.operations.values) { |
||||
if (operation.tags != null) { |
||||
for (final tag in operation.tags!) { |
||||
final tagPart = tag.split('/').first; |
||||
if (!tags.contains(tagPart)) { |
||||
tags.add(tagPart); |
||||
} |
||||
} |
||||
} |
||||
} |
||||
} |
||||
} |
||||
|
||||
return tags..sort((final a, final b) => a.compareTo(b)); |
||||
} |
Loading…
Reference in new issue