generateClientFromSwagger function

Future<void> generateClientFromSwagger({
  1. required List<TFileInfo> fileList,
  2. required String targetPath,
})

Generates a Dart HTTP client class from a Swagger specification.

Parameters:

  • spec: The SwaggerRoot containing the parsed Swagger specification.
  • targetPath: The String file path where the generated client code will be written.

This function performs the following:

  • Extracts the namespace from the Swagger tags.
  • Validates namespace and operation IDs for compliance.
  • Generates an HTTP Client class with methods to interact with API endpoints.
  • Adds methods for stamping and sending POST requests, as well as generating signed requests.
  • Organizes and writes the generated code into a Dart file at the specified targetPath.

Returns:

  • A Future that resolves when the client generation process is complete.

Implementation

Future<void> generateClientFromSwagger({
  required List<TFileInfo> fileList,
  required String targetPath,
}) async {

   if (fileList.length != 1) {
      throw Exception(
          'Expected 1 spec in public API folder. Got ${fileList.length}');
    }

  final SwaggerRoot spec =
        SwaggerRoot.fromJson(fileList[0].parsedData);


  final namespace = spec.tags.map((tag) => tag.name).firstWhere(
        (name) => name.isNotEmpty,
        orElse: () => throw Exception(
          'Invalid namespace in spec, cannot generate HTTP client',
        ),
      );

  if (namespace.isEmpty) {
    throw Exception(
        'Invalid namespace "$namespace" in spec, cannot generate HTTP client');
  }

  final importStatementSet = <String>[
    'import "public_api.types.dart";',
    'import "../../../../base.dart";',
    'import "../../../../version.dart";'
        'import "dart:convert";',
    'import "dart:async";',
    'import "dart:io";',
  ];

  final List<String> codeBuffer = [];

  codeBuffer.add('''
      /// HTTP Client for interacting with Turnkey API
      class TurnkeyClient {
        final THttpConfig config;
        final TStamper stamper;

        TurnkeyClient({required this.config, required this.stamper}) {
          if (config.baseUrl.isEmpty) {
            throw Exception('Missing base URL. Please verify environment variables.');
          }
        }

        Future<TResponseType> request<TBodyType, TResponseType>(
          String url,
          TBodyType body,
          TResponseType Function(Map<String, dynamic>) fromJson,

        ) async {
          final fullUrl = '\${config.baseUrl}\$url';
          final stringifiedBody = jsonEncode(body);
          final stamp = await stamper.stamp(stringifiedBody);

          final client = HttpClient();
          try {
            final request = await client.postUrl(Uri.parse(fullUrl));
            request.headers.set(stamp.stampHeaderName, stamp.stampHeaderValue);
            request.headers.set('X-Client-Version', VERSION);
            request.headers.contentType = ContentType.json;
            request.write(stringifiedBody);

            final response = await request.close();

            if (response.statusCode != 200) {
              final errorBody = await response.transform(utf8.decoder).join();
              throw TurnkeyRequestError(
                GrpcStatus.fromJson(jsonDecode(errorBody)),
              );
            }

            final responseBody = await response.transform(utf8.decoder).join();
            final decodedJson = jsonDecode(responseBody) as Map<String, dynamic>;

            return fromJson(decodedJson);
          } finally {
            client.close();
          }
        }

    ''');

  for (final endpointEntry in spec.paths.entries) {
    final String endpointPath = endpointEntry.key;
    final methodMap = endpointEntry.value.requests;
    if (methodMap['post'] == null) {
      throw Exception(
        'Found a non-POST endpoint in public API swagger!',
      );
    }

    if (methodMap['post']!.operationId.isEmpty) {
      throw Exception(
        'Invalid operationId in path ${endpointPath}',
      );
    }

    final operation = methodMap['post']!;
    final operationId = operation.operationId;

    final bool isEndpointDeprecated = operation.deprecated;

    final String methodName = operationId[0].toLowerCase() +
        operationId.substring(1);

    final inputType = 'T${operationId}Body';
    final responseType = 'T${operationId}Response';

    codeBuffer.add(assembleDartDocComment([
      operation.description,
      'Sign the provided `$inputType` with the client\'s `stamp` function and submit the request (POST $endpointPath).',
      'See also: `stamp$operationId`.',
      if (isEndpointDeprecated) '@deprecated',
    ]));

    codeBuffer.add('''
      Future<$responseType> $methodName({
        required $inputType input,
      }) async {
        return await request<$inputType, $responseType>(
          "$endpointPath",
          input,
          (json) => $responseType.fromJson(json)
        );
      }
    ''');

    codeBuffer.add(assembleDartDocComment([
      'Produce a `SignedRequest` from `$inputType` by using the client\'s `stamp` function.',
      'See also: `$operationId`.',
      if (isEndpointDeprecated) '@deprecated',
      '\n',
    ]));

    codeBuffer.add('''
      Future<TSignedRequest> stamp$operationId({
        required $inputType input,
        }) async {
          final fullUrl = '\${config.baseUrl}$endpointPath';
          final body = jsonEncode(input);
          final stamp = await stamper.stamp(body);

          return TSignedRequest(
            body: body,
            stamp: stamp,
            url: fullUrl,
          );
        }
    ''');
  }

  // End of the TurnkeyClient class definition
  codeBuffer.add('}');

  final imports = importStatementSet
      .where((importStatement) => importStatement.isNotEmpty)
      .join("\n");

// Combine the comment header, imports, and code buffer
  final output = [
    COMMENT_HEADER,
    imports,
    ...codeBuffer,
  ].join("\n\n");

  await safeWriteFileAsync("$targetPath/public_api.client.dart", output);
  await formatDocument(targetPath);
}