parseRestClients method

List<UniversalRestClient> parseRestClients()

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;
}