parseRestClients method
Parses rest clients from paths section of definition file
and return list of UniversalRestClient
Implementation
List<UniversalRestClient> parseRestClients() {
final restClients = <UniversalRestClient>[];
final imports = SplayTreeSet<String>();
var resultContentType = config.defaultContentType;
/// Parses return type for client query for OpenApi v3
UniversalType? returnTypeV3(
Map<String, dynamic> map,
String additionalName,
) {
final code2xx = code2xxMap(map);
if (code2xx == null || !code2xx.containsKey(_contentConst)) {
return null;
}
final contentTypeMap = code2xx[_contentConst] as Map<String, dynamic>?;
final contentType = contentTypeMap?.entries.firstOrNull;
if (contentType == null) {
return null;
}
final contentTypeValue = contentType.value as Map<String, dynamic>;
if (contentTypeValue.isEmpty ||
!contentTypeValue.containsKey(_schemaConst) ||
(contentTypeValue[_schemaConst] as Map<String, dynamic>).isEmpty) {
return null;
}
final schemaMap = contentTypeValue[_schemaConst] as Map<String, dynamic>;
// Track schema references for filtering
_extractSchemaRefs(schemaMap, null);
final typeWithImport = _findType(
schemaMap,
additionalName: additionalName,
// Return type is most often required in any case
isRequired: true,
);
if (typeWithImport.import != null) {
imports.add(typeWithImport.import!);
}
// List<dynamic> is not supported by Retrofit, use dynamic instead
if (typeWithImport.type.type == _objectConst) {
return typeWithImport.type.copyWith(wrappingCollections: const []);
}
return typeWithImport.type;
}
/// Parses query parameters (parameters and requestBody)
/// into universal models for OpenApi v3
List<UniversalRequestType> parametersV3(
Map<String, dynamic> map, {
String? requestBodyAdditionalName,
}) {
if (!map.containsKey(_parametersConst) &&
!map.containsKey(_requestBodyConst)) {
return [];
}
final types = <UniversalRequestType>[];
if (map.containsKey(_parametersConst)) {
for (final rawParameter in map[_parametersConst] as List<dynamic>) {
final isRefParameter = (rawParameter as Map<String, dynamic>)
.containsKey(_refConst);
var parameter = rawParameter;
if (isRefParameter) {
// find ref parameter
final refParameterName = _formatRef(parameter);
final isRefParameterExist =
_definitionFileContent.containsKey(_componentsConst) &&
(_definitionFileContent[_componentsConst]
as Map<String, dynamic>)
.containsKey(_parametersConst) &&
((_definitionFileContent[_componentsConst]
as Map<String, dynamic>)[_parametersConst]
as Map<String, dynamic>)
.containsKey(refParameterName);
if (!isRefParameterExist) {
throw OpenApiParserException(
'${parameter[_refConst]} does not exist in schema',
);
}
parameter =
((_definitionFileContent[_componentsConst]
as Map<String, dynamic>)[_parametersConst]
as Map<String, dynamic>)[refParameterName]!
as Map<String, dynamic>;
}
final paramName = parameter[_nameConst];
if (config.skippedParameters.contains(paramName)) {
continue;
}
final isRequired =
parameter[_requiredConst]?.toString().toBool() ?? false;
final typeWithImport = _findType(
parameter[_schemaConst] != null
? parameter[_schemaConst] as Map<String, dynamic>
: parameter,
name: paramName?.toString(),
isRequired: isRequired,
);
if (typeWithImport.import != null) {
imports.add(typeWithImport.import!);
}
final parameterType = HttpParameterType.values.firstWhereOrNull(
(e) => e.name == (parameter[_inConst].toString()),
);
if (parameterType == null) {
stdout.writeln(
'Warning:\nparameterType ${parameter[_inConst]} not supported',
);
} else {
if (!parameterType.isBody &&
(paramName == null || paramName.toString().isEmpty)) {
throw OpenApiParserException(
'Parameter with "in: ${parameter[_inConst]}" must have a "name" field',
);
}
types.add(
UniversalRequestType(
parameterType: parameterType,
type: typeWithImport.type,
description: parameter[_descriptionConst]?.toString(),
name: parameterType.isBody && paramName == _bodyConst
? null
: paramName?.toString(),
deprecated:
parameter[_deprecatedConst].toString().toBool() ?? false,
),
);
}
}
}
if (map.containsKey(_requestBodyConst)) {
var requestBody = map[_requestBodyConst] as Map<String, dynamic>;
final isRefBody = requestBody.containsKey(_refConst);
if (isRefBody) {
final refBodyName = _formatRef(requestBody);
final isRefBodyExist =
_definitionFileContent.containsKey(_componentsConst) &&
(_definitionFileContent[_componentsConst] as Map<String, dynamic>)
.containsKey(_requestBodiesConst) &&
((_definitionFileContent[_componentsConst]
as Map<String, dynamic>)[_requestBodiesConst]
as Map<String, dynamic>)
.containsKey(refBodyName);
if (!isRefBodyExist) {
throw OpenApiParserException(
'${requestBody[_refConst]} does not exist in schema',
);
}
requestBody =
((_definitionFileContent[_componentsConst]
as Map<String, dynamic>)[_requestBodiesConst]
as Map<String, dynamic>)[refBodyName]
as Map<String, dynamic>;
}
if (!requestBody.containsKey(_contentConst)) {
throw const OpenApiParserException(
'Request body must always have content.',
);
}
final contentTypes = requestBody[_contentConst] as Map<String, dynamic>;
Map<String, dynamic>? contentType;
if (contentTypes.containsKey(_multipartFormDataConst)) {
contentType =
contentTypes[_multipartFormDataConst] as Map<String, dynamic>;
resultContentType = _multipartFormDataConst;
} else if (contentTypes.containsKey(_formUrlEncodedConst)) {
contentType =
contentTypes[_formUrlEncodedConst] as Map<String, dynamic>;
resultContentType = _formUrlEncodedConst;
} else {
final content = contentTypes.containsKey(config.defaultContentType)
? contentTypes[config.defaultContentType]
: contentTypes.entries.firstOrNull?.value;
contentType = content == null
? null
: content as Map<String, dynamic>;
}
if (contentType == null) {
throw const OpenApiParserException(
'Response must always have a content type.',
);
}
if (resultContentType == _multipartFormDataConst) {
final schemaContent =
contentType[_schemaConst] as Map<String, dynamic>;
final Map<String, dynamic> properties;
final List<String> requiredParameters;
if ((contentType[_schemaConst] as Map<String, dynamic>).containsKey(
_refConst,
)) {
final isRequired =
requestBody[_requiredConst]?.toString().toBool() ?? false;
// Track schema references for filtering
_extractSchemaRefs(
contentType[_schemaConst] as Map<String, dynamic>,
null,
);
final typeWithImport = _findType(
contentType[_schemaConst] as Map<String, dynamic>,
additionalName: requestBodyAdditionalName,
isRequired: isRequired,
);
final type = typeWithImport.type.type;
_skipDataClasses.add(type);
final components =
_definitionFileContent[_componentsConst]
as Map<String, dynamic>;
final schemes = components[_schemasConst] as Map<String, dynamic>;
final dataClass = schemes[type] as Map<String, dynamic>?;
if (dataClass != null &&
dataClass[_propertiesConst] is Map<String, dynamic>) {
properties = dataClass[_propertiesConst] as Map<String, dynamic>;
requiredParameters =
(dataClass[_requiredConst] as List<dynamic>?)
?.map((e) => e.toString())
.toList() ??
[];
} else {
properties = {};
requiredParameters = [];
}
} else {
_extractSchemaRefs(schemaContent, null);
if (schemaContent[_propertiesConst] is Map<String, dynamic>) {
properties =
schemaContent[_propertiesConst] as Map<String, dynamic>;
} else {
properties = {};
}
requiredParameters =
(schemaContent[_requiredConst] as List<dynamic>?)
?.map((e) => e.toString())
.toList() ??
[];
}
for (final propName in properties.keys) {
final propValue = properties[propName] as Map<String, dynamic>;
final isRequired = requiredParameters.contains(propName);
final typeWithImport = _findType(
propValue,
name: null,
isRequired: isRequired,
);
final currentType = typeWithImport.type;
if (typeWithImport.import != null) {
imports.add(typeWithImport.import!);
}
types.add(
UniversalRequestType(
parameterType: HttpParameterType.part,
name: propName,
description: currentType.description,
type: UniversalType(
type: currentType.type,
name: propName.toCamel,
description: currentType.description,
format: currentType.format,
defaultValue: currentType.defaultValue,
isRequired: currentType.isRequired,
nullable: currentType.nullable,
wrappingCollections: currentType.wrappingCollections,
deprecated: currentType.deprecated,
),
),
);
}
} else {
final isRequired =
requestBody[_requiredConst]?.toString().toBool() ?? false;
// Track schema references for filtering
_extractSchemaRefs(
contentType[_schemaConst] as Map<String, dynamic>,
null,
);
final typeWithImport = _findType(
contentType[_schemaConst] as Map<String, dynamic>,
additionalName: requestBodyAdditionalName,
isRequired: isRequired,
);
final currentType = typeWithImport.type;
if (typeWithImport.import != null) {
imports.add(typeWithImport.import!);
}
types.add(
UniversalRequestType(
parameterType: HttpParameterType.body,
description: currentType.description,
type: UniversalType(
type: currentType.type,
name: _bodyConst,
description: currentType.description,
format: currentType.format,
defaultValue: currentType.defaultValue,
isRequired: currentType.isRequired,
nullable: currentType.nullable,
wrappingCollections: currentType.wrappingCollections,
deprecated: currentType.deprecated,
),
),
);
}
}
return types;
}
/// Parses return type for client query for OpenApi v2
/// Returns a record with the return type (streaming is detected separately)
UniversalType? returnTypeV2(
Map<String, dynamic> map,
String additionalName,
) {
final code2xx = code2xxMap(map);
if (code2xx == null || !code2xx.containsKey(_schemaConst)) {
return null;
}
final schemaMap = code2xx[_schemaConst] as Map<String, dynamic>? ?? {};
// Track schema references for filtering
_extractSchemaRefs(schemaMap, null);
final typeWithImport = _findType(
schemaMap,
additionalName: additionalName,
// Return type is most often required in any case
isRequired: true,
);
if (typeWithImport.import != null) {
imports.add(typeWithImport.import!);
}
final type = typeWithImport.type;
return UniversalType(
type: type.type,
wrappingCollections:
// List<dynamic> is not supported by Retrofit, use dynamic instead
type.type == _objectConst ? const [] : type.wrappingCollections,
isRequired: typeWithImport.type.isRequired,
deprecated: type.deprecated,
);
}
/// Parses query parameters (parameters and requestBody)
/// into universal models for OpenApi v2
List<UniversalRequestType> parametersV2(Map<String, dynamic> map) {
final types = <UniversalRequestType>[];
if (!map.containsKey(_parametersConst)) {
return types;
}
if (map.containsKey(_consumesConst) &&
map[_consumesConst] is List<dynamic>) {
final consumes = map[_consumesConst] as List<dynamic>;
if (consumes.contains(_multipartFormDataConst)) {
resultContentType = _multipartFormDataConst;
} else if (consumes.contains(_formUrlEncodedConst)) {
resultContentType = _formUrlEncodedConst;
} else if (consumes.isNotEmpty && consumes.first != null) {
resultContentType = consumes.first as String;
}
}
for (final rawParameter in map[_parametersConst] as List<dynamic>) {
final isRequired =
(rawParameter as Map<String, dynamic>)[_requiredConst]
?.toString()
.toBool() ??
false;
final isRefParameter = rawParameter.containsKey(_refConst);
var parameter = Map<String, dynamic>.of(rawParameter);
if (isRefParameter) {
// find ref parameter
final refParameterName = _formatRef(rawParameter);
final isRefParameterExist =
_definitionFileContent.containsKey(_parametersConst) &&
(_definitionFileContent[_parametersConst] as Map<String, dynamic>)
.containsKey(refParameterName);
if (!isRefParameterExist) {
throw OpenApiParserException(
'${rawParameter[_refConst]} does not exist in schema',
);
}
parameter =
(_definitionFileContent[_parametersConst]
as Map<String, dynamic>)[refParameterName]!
as Map<String, dynamic>;
}
final parameterName = parameter[_nameConst]?.toString();
final typeWithImport = _findType(
parameter[_schemaConst] != null
? parameter[_schemaConst] as Map<String, dynamic>
: parameter,
name: parameterName,
additionalName: null,
isRequired: isRequired,
);
if (config.skippedParameters.contains(parameter[_nameConst])) {
continue;
}
if (typeWithImport.import != null) {
imports.add(typeWithImport.import!);
}
final parameterType = HttpParameterType.values.firstWhereOrNull(
(e) => e.name == (parameter[_inConst].toString()),
);
if (parameterType == null) {
// ignore: avoid_print
stdout.writeln(
'Warning:\nparameterType ${parameter[_inConst]} not supported',
);
} else {
final nameValue = parameter[_nameConst];
if (!parameterType.isBody &&
(nameValue == null || nameValue.toString().isEmpty)) {
throw OpenApiParserException(
'Parameter with "in: ${parameter[_inConst]}" must have a "name" field',
);
}
types.add(
UniversalRequestType(
parameterType: parameterType,
type: typeWithImport.type,
description: parameter[_descriptionConst]?.toString(),
name: parameterType.isBody && parameter[_nameConst] == _bodyConst
? null
: parameter[_nameConst].toString(),
deprecated:
parameter[_deprecatedConst].toString().toBool() ?? false,
),
);
}
}
return types;
}
if (!_definitionFileContent.containsKey(_pathsConst)) {
return [];
}
(_definitionFileContent[_pathsConst] as Map<String, dynamic>).forEach((
path,
pathValue,
) {
final pathValueMap = pathValue as Map<String, dynamic>;
// global parameters are defined at the path level (i.e. /users/{id})
final globalParameters = <UniversalRequestType>[];
if (pathValueMap.containsKey(_parametersConst)) {
final params = _apiInfo.schemaVersion == OAS.v2
? parametersV2(pathValue)
: parametersV3(pathValue);
globalParameters.addAll(params);
}
pathValue.forEach((key, requestPath) {
// Process this path/method within its context
_contextStack.withContext('path:$path:$key', () {
// `servers` and `parameters` contain List<dynamic>, skip them
if (key == _serversConst ||
key == _parametersConst ||
key.startsWith('x-')) {
return;
}
// check if this requestPath has any tags that
// define wether the requestPath should be included
if (!_isPathIncluded(requestPath as Map<String, dynamic>)) {
return;
}
_anchorRegistry.markContextAsIncluded(_contextStack.current!);
final requestPathResponses =
requestPath[_responsesConst] as Map<String, dynamic>;
final operationId = requestPath[_operationIdConst]?.toString();
final rawTag = _getTag(requestPath) ?? config.defaultClient;
final (protectedTag, _) = protectName(rawTag, uniqueIfNull: true);
final currentTag = protectedTag!.toSnake;
final rawBaseName = operationId?.isNotEmpty == true
? operationId!
: path;
final (protectedBaseName, _) = protectName(
rawBaseName,
uniqueIfNull: true,
);
final baseName = protectedBaseName!.toPascal;
var indexedBaseName = baseName;
if (operationId?.isNotEmpty == true) {
_operationIdUsagePerTag.putIfAbsent(currentTag, () => {});
final usageCount = _operationIdUsagePerTag[currentTag]!;
if (usageCount.containsKey(baseName)) {
usageCount[baseName] = usageCount[baseName]! + 1;
indexedBaseName = '$baseName${usageCount[baseName]}';
} else {
usageCount[baseName] = 1;
}
}
final responseAdditionalName = '${indexedBaseName}Response';
final requestBodyAdditionalName = '${indexedBaseName}Request';
final returnType = _apiInfo.schemaVersion == OAS.v2
? returnTypeV2(requestPathResponses, responseAdditionalName)
: returnTypeV3(requestPathResponses, responseAdditionalName);
final parameters = _apiInfo.schemaVersion == OAS.v2
? parametersV2(requestPath)
: parametersV3(
requestPath,
requestBodyAdditionalName: requestBodyAdditionalName,
);
// Add global parameters that have not been overridden by local parameters
// defined at the request level.
parameters.addAll(
globalParameters.where(
(e) =>
parameters.every((p) => p.name != e.name && p.type != e.type),
),
);
// Build full description
final summary = requestPath[_summaryConst]?.toString().trim();
var description = requestPath[_descriptionConst]?.toString().trim();
description = switch ((summary, description)) {
(null, null) || ('', '') => null,
(_, null) || (_, '') => summary,
(null, _) || ('', _) => description,
(_, _) => '$summary\n\n$description',
};
final parametersDescription = parameters
.where((e) => e.description != null)
.map((e) => '[${e.name?.toCamel ?? 'body'}] - ${e.description}')
.join('\n\n')
.trim();
description = switch ((description, parametersDescription)) {
(null, '') || ('', '') => null,
(_, '') => description,
(null, _) || ('', _) => parametersDescription,
(_, _) => '$description\n\n$parametersDescription',
};
// End build full description
String requestName;
if (config.pathMethodName) {
requestName = (key + path).toCamel;
} else {
final operationIdName = requestPath[_operationIdConst]
?.toString()
.toCamel;
final (_, nameDescription) = protectName(operationIdName);
if (nameDescription != null) {
description = '$description\n\n$nameDescription';
requestName = (key + path).toCamel;
} else {
requestName = operationIdName ?? (key + path).toCamel;
}
}
final request = UniversalRequest(
name: requestName,
description: description,
requestType: HttpRequestType.fromString(key)!,
route: path,
contentType: resultContentType,
returnType: returnType,
parameters: parameters,
isDeprecated:
requestPath[_deprecatedConst].toString().toBool() ?? false,
);
final sameTagIndex = restClients.indexWhere(
(e) => e.name == currentTag,
);
if (sameTagIndex == -1) {
restClients.add(
UniversalRestClient(
name: currentTag,
requests: [request],
imports: SplayTreeSet<String>.of(imports),
),
);
} else {
final existingRequests = restClients[sameTagIndex].requests;
var uniqueName = requestName;
var index = 2;
while (existingRequests.any((r) => r.name == uniqueName)) {
uniqueName = '$requestName$index';
index++;
}
if (uniqueName != requestName) {
final updatedRequest = UniversalRequest(
name: uniqueName,
description: request.description,
requestType: request.requestType,
route: request.route,
contentType: request.contentType,
returnType: request.returnType,
parameters: request.parameters,
isDeprecated: request.isDeprecated,
);
restClients[sameTagIndex].requests.add(updatedRequest);
} else {
restClients[sameTagIndex].requests.add(request);
}
restClients[sameTagIndex].imports.addAll(imports);
}
resultContentType = config.defaultContentType;
imports.clear();
});
});
});
return restClients;
}