Skip to content

Commit 29619cb

Browse files
authored
Add add_openapi_metadata option for convenience interceptors logging (#404)
1 parent 62fdd93 commit 29619cb

File tree

13 files changed

+362
-25
lines changed

13 files changed

+362
-25
lines changed

swagger_parser/CHANGELOG.md

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,33 @@
1+
## 1.36.0
2+
- Add `add_openapi_metadata` (default `false`) to generate OpenAPI `tags`, `operationId`, and `externalDocsUrl` constants for each endpoint; when `extras_parameter_by_default` is `true`, the metadata is also prefilled into Dio `extras`—handy for interceptors and logging without overwriting user-supplied extras
3+
- Use fully-qualified default extras values (e.g. `BannerApi.findAllBannersOpenapiExtras`) so generated implementations can access the static metadata constants
4+
```
5+
swagger_parser:
6+
extras_parameter_by_default: true
7+
add_openapi_metadata: true
8+
9+
abstract class PetsClient {
10+
static const Map<String, dynamic> listPetsOpenapiExtras =
11+
<String, dynamic>{
12+
'openapi': <String, dynamic>{
13+
'tags': <String>['pets'],
14+
'operationId': 'listPets',
15+
'externalDocsUrl': 'https://docs.example.com/pets',
16+
},
17+
};
18+
19+
@GET('/pets')
20+
Future<void> listPets({
21+
// defaults to the OpenAPI metadata; merge with your own extras if needed
22+
@Extras() Map<String, dynamic>? extras =
23+
PetsClient.listPetsOpenapiExtras,
24+
@DioOptions() RequestOptions? options,
25+
});
26+
}
27+
28+
# https://openapi.sepc/pets/listPets
29+
```
30+
131
## 1.35.2
232
- Fix enum name values being a int returned in a toString
333

swagger_parser/README.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -85,6 +85,12 @@ swagger_parser:
8585
# If the value is 'true', then the annotation will be added to all requests.
8686
extras_parameter_by_default: false
8787
88+
# Optional (dart only).
89+
# Generate static OpenAPI metadata (tags, operationId, externalDocsUrl) for each request.
90+
# If extras_parameter_by_default is true, this metadata is also used as the default `extras` value.
91+
# Disabled by default.
92+
add_openapi_metadata: false
93+
8894
# Optional (dart only).
8995
# Support @DioOptions annotation for interceptors.
9096
# If the value is 'true', then the annotation will be added to all requests.

swagger_parser/example/swagger_parser.yaml

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,12 @@ swagger_parser:
2727
# If the value is 'true', then the annotation will be added to all requests.
2828
extras_parameter_by_default: false
2929

30+
# Optional (dart only).
31+
# Generate static OpenAPI metadata (tags, operationId, externalDocsUrl) for each request.
32+
# If extras_parameter_by_default is true, this metadata is also used as the default `extras` value.
33+
# Disabled by default.
34+
add_openapi_metadata: false
35+
3036
# Optional (dart only). Set 'true' to generate root client
3137
# with interface and all clients instances.
3238
root_client: true

swagger_parser/lib/src/config/swp_config.dart

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ class SWPConfig {
2929
this.defaultContentType = 'application/json',
3030
this.extrasParameterByDefault = false,
3131
this.dioOptionsParameterByDefault = false,
32+
this.addOpenApiMetadata = false,
3233
this.pathMethodName = false,
3334
this.mergeClients = false,
3435
this.enumsParentPrefix = true,
@@ -68,6 +69,7 @@ class SWPConfig {
6869
required this.defaultContentType,
6970
required this.extrasParameterByDefault,
7071
required this.dioOptionsParameterByDefault,
72+
required this.addOpenApiMetadata,
7173
required this.pathMethodName,
7274
required this.mergeClients,
7375
required this.enumsParentPrefix,
@@ -144,6 +146,8 @@ class SWPConfig {
144146
final dioOptionsParameterByDefault =
145147
yamlMap['dio_options_parameter_by_default'] as bool? ??
146148
rootConfig?.dioOptionsParameterByDefault;
149+
final addOpenApiMetadata = yamlMap['add_openapi_metadata'] as bool? ??
150+
rootConfig?.addOpenApiMetadata;
147151
final pathMethodName =
148152
yamlMap['path_method_name'] as bool? ?? rootConfig?.pathMethodName;
149153
final mergeClients =
@@ -302,6 +306,7 @@ class SWPConfig {
302306
extrasParameterByDefault ?? dc.extrasParameterByDefault,
303307
dioOptionsParameterByDefault:
304308
dioOptionsParameterByDefault ?? dc.dioOptionsParameterByDefault,
309+
addOpenApiMetadata: addOpenApiMetadata ?? dc.addOpenApiMetadata,
305310
mergeClients: mergeClients ?? dc.mergeClients,
306311
enumsParentPrefix: enumsParentPrefix ?? dc.enumsParentPrefix,
307312
skippedParameters: skippedParameters ?? dc.skippedParameters,
@@ -447,6 +452,10 @@ class SWPConfig {
447452
/// ```
448453
final bool dioOptionsParameterByDefault;
449454

455+
/// DART ONLY
456+
/// Add static OpenAPI metadata into extras by default when extras are enabled.
457+
final bool addOpenApiMetadata;
458+
450459
/// If `true`, use the endpoint path for the method name.
451460
/// if `false`, use `operationId`.
452461
final bool pathMethodName;
@@ -530,6 +539,7 @@ class SWPConfig {
530539
defaultContentType: defaultContentType,
531540
extrasParameterByDefault: extrasParameterByDefault,
532541
dioOptionsParameterByDefault: dioOptionsParameterByDefault,
542+
addOpenApiMetadata: addOpenApiMetadata,
533543
rootClient: rootClient,
534544
rootClientName: rootClientName,
535545
clientPostfix: clientPostfix,

swagger_parser/lib/src/generator/config/generator_config.dart

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ class GeneratorConfig {
1414
this.rootClient = true,
1515
this.extrasParameterByDefault = false,
1616
this.dioOptionsParameterByDefault = false,
17+
this.addOpenApiMetadata = false,
1718
this.rootClientName = 'RestClient',
1819
this.clientPostfix,
1920
this.exportFile = true,
@@ -109,6 +110,10 @@ class GeneratorConfig {
109110
/// ```
110111
final bool dioOptionsParameterByDefault;
111112

113+
/// DART ONLY
114+
/// Add static OpenAPI metadata into extras by default when extras are enabled.
115+
final bool addOpenApiMetadata;
116+
112117
/// Optional. Set regex replacement rules for the names of the generated classes/enums.
113118
/// All rules are applied in order.
114119
final List<ReplacementRule> replacementRules;

swagger_parser/lib/src/generator/generator/fill_controller.dart

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -91,6 +91,7 @@ final class FillController {
9191
defaultContentType: config.defaultContentType,
9292
extrasParameterByDefault: config.extrasParameterByDefault,
9393
dioOptionsParameterByDefault: config.dioOptionsParameterByDefault,
94+
addOpenApiMetadata: config.addOpenApiMetadata,
9495
originalHttpResponse: config.originalHttpResponse,
9596
useMultipartFile: config.useMultipartFile,
9697
fileName: fileName,

swagger_parser/lib/src/generator/model/programming_language.dart

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -115,6 +115,7 @@ enum ProgrammingLanguage {
115115
required bool useMultipartFile,
116116
bool extrasParameterByDefault = false,
117117
bool dioOptionsParameterByDefault = false,
118+
bool addOpenApiMetadata = false,
118119
bool originalHttpResponse = false,
119120
String? fileName,
120121
}) =>
@@ -125,6 +126,7 @@ enum ProgrammingLanguage {
125126
defaultContentType: defaultContentType,
126127
extrasParameterByDefault: extrasParameterByDefault,
127128
dioOptionsParameterByDefault: dioOptionsParameterByDefault,
129+
addOpenApiMetadata: addOpenApiMetadata,
128130
originalHttpResponse: originalHttpResponse,
129131
useMultipartFile: useMultipartFile,
130132
fileName: fileName,

swagger_parser/lib/src/generator/templates/dart_retrofit_client_template.dart

Lines changed: 91 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -13,12 +13,15 @@ String dartRetrofitClientTemplate({
1313
required bool useMultipartFile,
1414
bool extrasParameterByDefault = false,
1515
bool dioOptionsParameterByDefault = false,
16+
bool addOpenApiMetadata = false,
1617
bool originalHttpResponse = false,
1718
String? fileName,
1819
}) {
1920
final parameterTypes = restClient.requests
2021
.expand((r) => r.parameters.map((p) => p.type))
2122
.toSet();
23+
final includeExtras = extrasParameterByDefault;
24+
final includeMetadata = addOpenApiMetadata;
2225
final sb = StringBuffer('''
2326
${_convertImport(restClient)}${ioImport(parameterTypes, useMultipartFile: useMultipartFile)}import 'package:dio/dio.dart'${_hideHeaders(restClient, defaultContentType)};
2427
import 'package:retrofit/retrofit.dart';
@@ -29,26 +32,47 @@ part '${fileName ?? name.toSnake}.g.dart';
2932
abstract class $name {
3033
factory $name(Dio dio, {String? baseUrl}) = _$name;
3134
''');
35+
36+
if (includeMetadata && restClient.requests.isNotEmpty) {
37+
sb.write('\n');
38+
for (final request in restClient.requests) {
39+
sb.write(_openApiExtrasConst(request));
40+
}
41+
}
42+
3243
for (final request in restClient.requests) {
44+
final openApiExtrasConstName =
45+
includeMetadata ? _openApiConstName(request) : null;
46+
sb.write('\n');
3347
sb.write(
34-
_toClientRequest(request, defaultContentType,
35-
originalHttpResponse: originalHttpResponse,
36-
extrasParameterByDefault: extrasParameterByDefault,
37-
dioOptionsParameterByDefault: dioOptionsParameterByDefault,
38-
useMultipartFile: useMultipartFile),
48+
_toClientRequest(
49+
request,
50+
defaultContentType,
51+
className: name,
52+
originalHttpResponse: originalHttpResponse,
53+
addExtrasParameter: includeExtras,
54+
addDioOptionsParameter: dioOptionsParameterByDefault,
55+
includeMetadata: includeMetadata,
56+
useMultipartFile: useMultipartFile,
57+
openApiExtrasConstName: openApiExtrasConstName,
58+
),
3959
);
4060
}
61+
4162
sb.write('}\n');
4263
return sb.toString();
4364
}
4465

4566
String _toClientRequest(
4667
UniversalRequest request,
4768
String defaultContentType, {
69+
required String className,
4870
required bool originalHttpResponse,
49-
required bool extrasParameterByDefault,
50-
required bool dioOptionsParameterByDefault,
71+
required bool addExtrasParameter,
72+
required bool addDioOptionsParameter,
73+
required bool includeMetadata,
5174
required bool useMultipartFile,
75+
String? openApiExtrasConstName,
5276
}) {
5377
final responseType = request.returnType == null
5478
? 'void'
@@ -71,32 +95,41 @@ String _toClientRequest(
7195
final dioResponseTypeAnnotation =
7296
isBinaryResponse ? '\n @DioResponseType(ResponseType.bytes)' : '';
7397

74-
final sb = StringBuffer(
75-
'''
98+
final defaultExtras = includeMetadata && addExtrasParameter
99+
? _openApiExtrasReference(
100+
openApiExtrasConstName,
101+
request,
102+
className: className,
103+
)
104+
: null;
105+
106+
final sb = StringBuffer()
107+
..write(
108+
" ${descriptionComment(request.description, tabForFirstLine: false, tab: ' ', end: ' ')}${request.isDeprecated ? "@Deprecated('This method is marked as deprecated')\n " : ''}${_contentTypeHeader(request, defaultContentType)}@${request.requestType.name.toUpperCase()}('${request.route}')$dioResponseTypeAnnotation\n Future<$finalResponseType> ${request.name}(",
109+
);
76110

77-
${descriptionComment(request.description, tabForFirstLine: false, tab: ' ', end: ' ')}${request.isDeprecated ? "@Deprecated('This method is marked as deprecated')\n " : ''}${_contentTypeHeader(request, defaultContentType)}@${request.requestType.name.toUpperCase()}('${request.route}')$dioResponseTypeAnnotation
78-
Future<$finalResponseType> ${request.name}(''',
79-
);
80111
if (request.parameters.isNotEmpty ||
81-
extrasParameterByDefault ||
82-
dioOptionsParameterByDefault) {
112+
addExtrasParameter ||
113+
addDioOptionsParameter) {
83114
sb.write('{\n');
84115
}
116+
85117
final sortedByRequired = List<UniversalRequestType>.from(
86118
request.parameters.sorted((a, b) => a.type.compareTo(b.type)),
87119
);
88120
for (final parameter in sortedByRequired) {
89121
sb.write('${_toParameter(parameter, useMultipartFile)}\n');
90122
}
91-
if (extrasParameterByDefault) {
92-
sb.write(_addExtraParameter());
123+
if (addExtrasParameter) {
124+
sb.write(_addExtraParameter(defaultExtras));
93125
}
94-
if (dioOptionsParameterByDefault) {
126+
if (addDioOptionsParameter) {
95127
sb.write(_addDioOptionsParameter());
96128
}
129+
97130
if (request.parameters.isNotEmpty ||
98-
extrasParameterByDefault ||
99-
dioOptionsParameterByDefault) {
131+
addExtrasParameter ||
132+
addDioOptionsParameter) {
100133
sb.write(' });\n');
101134
} else {
102135
sb.write(');\n');
@@ -111,7 +144,43 @@ String _convertImport(UniversalRestClient restClient) =>
111144
? "import 'dart:convert';\n"
112145
: '';
113146

114-
String _addExtraParameter() => ' @Extras() Map<String, dynamic>? extras,\n';
147+
String _addExtraParameter(String? defaultExtras) =>
148+
' @Extras() Map<String, dynamic>? extras${defaultExtras != null ? ' =\n $defaultExtras' : ''},\n';
149+
150+
String _openApiExtrasReference(
151+
String? openApiExtrasConstName,
152+
UniversalRequest request, {
153+
required String className,
154+
}) {
155+
return openApiExtrasConstName != null
156+
? '$className.$openApiExtrasConstName'
157+
: _openApiExtrasLiteral(request);
158+
}
159+
160+
String _openApiExtrasConst(UniversalRequest request) =>
161+
' static const Map<String, dynamic> ${_openApiConstName(request)} =\n'
162+
' ${_openApiExtrasLiteral(request)};\n';
163+
164+
String _openApiConstName(UniversalRequest request) =>
165+
'${request.name}OpenapiExtras';
166+
167+
String _openApiExtrasLiteral(UniversalRequest request) {
168+
final tags = request.tags.map(_quoteJson).join(', ');
169+
final operationId = _quoteJson(request.operationId ?? request.name);
170+
final externalDocsUrl = request.externalDocsUrl != null
171+
? _quoteJson(request.externalDocsUrl!)
172+
: 'null';
173+
return '''<String, dynamic>{
174+
'openapi': <String, dynamic>{
175+
'tags': <String>[$tags],
176+
'operationId': $operationId,
177+
'externalDocsUrl': $externalDocsUrl,
178+
},
179+
}''';
180+
}
181+
182+
String _quoteJson(String value) =>
183+
'"${value.replaceAll(r'\\', r'\\\\').replaceAll('"', r'\\"')}"';
115184

116185
String _addDioOptionsParameter() =>
117186
' @DioOptions() RequestOptions? options,\n';
@@ -140,7 +209,7 @@ String _toParameter(UniversalRequestType parameter, bool useMultipartFile) {
140209
: '';
141210

142211
return '$deprecatedAnnotation @${parameter.parameterType.type}'
143-
"(${parameter.name != null && !parameter.parameterType.isBody ? "${parameter.parameterType.isPart ? 'name: ' : ''}${_startWith$(parameter.name!) ? 'r' : ''}'${parameter.name}'" : ''}) "
212+
"(${parameter.name != null && !parameter.parameterType.isBody ? "${parameter.parameterType.isPart ? 'name: ' : ''}${_startsWithDollar(parameter.name!) ? 'r' : ''}'${parameter.name}'" : ''}) "
144213
'${_required(parameter.type)}'
145214
'$parameterType '
146215
'$keywordArguments${_defaultValue(parameter.type)},';
@@ -182,4 +251,4 @@ String _defaultValue(UniversalType t) => !t.isRequired && t.defaultValue != null
182251
'${t.enumType != null ? '${t.type}.${protectDefaultEnum(t.defaultValue?.toCamel)?.toCamel}' : protectDefaultValue(t.defaultValue, type: t.type)}'
183252
: '';
184253

185-
bool _startWith$(String name) => name.isNotEmpty && name.startsWith(r'$');
254+
bool _startsWithDollar(String name) => name.isNotEmpty && name.startsWith(r'$');

swagger_parser/lib/src/parser/model/universal_request.dart

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,9 @@ final class UniversalRequest {
1313
required this.route,
1414
required this.returnType,
1515
required this.parameters,
16+
this.tags = const [],
17+
this.operationId,
18+
this.externalDocsUrl,
1619
this.contentType = 'application/json',
1720
this.description,
1821
this.isDeprecated = false,
@@ -24,6 +27,15 @@ final class UniversalRequest {
2427
/// Request description
2528
final String? description;
2629

30+
/// Original OpenAPI tags
31+
final List<String> tags;
32+
33+
/// Original OpenAPI operationId
34+
final String? operationId;
35+
36+
/// Optional OpenAPI externalDocs url
37+
final String? externalDocsUrl;
38+
2739
/// HTTP type of request
2840
final HttpRequestType requestType;
2941

@@ -59,6 +71,9 @@ final class UniversalRequest {
5971
contentType == other.contentType &&
6072
route == other.route &&
6173
returnType == other.returnType &&
74+
const DeepCollectionEquality().equals(tags, other.tags) &&
75+
operationId == other.operationId &&
76+
externalDocsUrl == other.externalDocsUrl &&
6277
const DeepCollectionEquality().equals(parameters, other.parameters) &&
6378
isMultiPart == other.isMultiPart &&
6479
isFormUrlEncoded == other.isFormUrlEncoded;
@@ -69,13 +84,19 @@ final class UniversalRequest {
6984
requestType.hashCode ^
7085
route.hashCode ^
7186
returnType.hashCode ^
87+
tags.hashCode ^
88+
operationId.hashCode ^
89+
externalDocsUrl.hashCode ^
7290
contentType.hashCode ^
7391
parameters.hashCode ^
7492
isMultiPart.hashCode ^
7593
isFormUrlEncoded.hashCode;
7694

7795
@override
7896
String toString() => 'UniversalRequest(name: $name, '
97+
'tags: $tags, '
98+
'operationId: $operationId, '
99+
'externalDocsUrl: $externalDocsUrl, '
79100
'requestType: $requestType, '
80101
'route: $route, '
81102
'parameters: $parameters, '

0 commit comments

Comments
 (0)