generateApiTypes function

String generateApiTypes(
  1. Map<String, dynamic> swagger,
  2. String prefix
)

Implementation

String generateApiTypes(Map<String, dynamic> swagger, String prefix) {
  final defs = (swagger['definitions'] as Map).cast<String, dynamic>();
  final latest = extractLatestVersions(defs);
  final paths = (swagger['paths'] as Map).cast<String, dynamic>();
  final tags = swagger['tags'];
  final namespace = (tags is List && tags.isNotEmpty && tags.first is Map)
      ? (tags.first['name'] as String?)
      : null;

  final b = StringBuffer();

  for (final entry in paths.entries) {
    final methods = (entry.value as Map).cast<String, dynamic>();
    final op = (methods['post'] as Map?)?.cast<String, dynamic>();
    if (op == null) continue;

    final operationId = op['operationId'] as String?;
    if (operationId == null) continue;

    final opNameNoNs = operationId.replaceFirst('${namespace}_', 'T');
    final methodName = opNameNoNs[0].toLowerCase() + opNameNoNs.substring(1);
    final mType = methodTypeFromMethodName(methodName, prefix);

    // response $ref
    String? responseRef;
    final responses = (op['responses'] as Map?)?.cast<String, dynamic>();
    if (responses != null) {
      final ok = (responses['200'] as Map?)?.cast<String, dynamic>();
      final schema = (ok?['schema'] as Map?)?.cast<String, dynamic>();
      if (schema != null && schema[r'$ref'] is String) {
        responseRef = schema[r'$ref'] as String?;
      }
    }
    final responseTypeName =
        responseRef != null ? refToName(responseRef) : null;

    final apiTypeName = '$prefix${_ucFirst(opNameNoNs)}Response';
    final apiBodyTypeName = '$prefix${_ucFirst(opNameNoNs)}Body';
    final apiInputTypeName = '$prefix${_ucFirst(opNameNoNs)}Input';

    // RESPONSE
    if (mType == 'command') {
      // Find corresponding Result type using request 'type' enum mapping (like JS)
      String? resultTypeName;
      String? versionSuffix;

      final params = (op['parameters'] as List?)?.cast<dynamic>() ?? [];
      for (final p in params) {
        final param = (p as Map).cast<String, dynamic>();
        if (param['in'] == 'body' &&
            param['schema'] is Map &&
            (param['schema'][r'$ref'] is String) &&
            defs.containsKey(refToName(param['schema'][r'$ref'] as String))) {
          final reqDef = defs[refToName(param['schema'][r'$ref'] as String)]
              as Map<String, dynamic>;
          final reqProps =
              (reqDef['properties'] as Map?)?.cast<String, dynamic>();
          if (reqProps != null &&
              reqProps['type'] is Map &&
              (reqProps['type']['enum'] is List) &&
              (reqProps['type']['enum'] as List).isNotEmpty) {
            final reqTypeName = refToName(param['schema'][r'$ref'] as String);
            final baseActivity = reqTypeName
                .replaceFirst(RegExp(r'^v\d+'), '')
                .replaceFirst(RegExp(r'Request(V\d+)?$'), '');

            final activityTypeKey =
                (reqProps['type']['enum'] as List).first as String?;
            final mapped = activityTypeKey != null
                ? VERSIONED_ACTIVITY_TYPES[activityTypeKey]
                : null;
            if (mapped != null) {
              final m = RegExp(r'(V\d+)$').firstMatch(mapped);
              versionSuffix = m?.group(1);
            }

            final resultBase = '${baseActivity}Result';
            String? candidate;
            if (versionSuffix != null) {
              candidate = defs.keys.firstWhere(
                (k) =>
                    k.startsWith('v1${baseActivity}Result') &&
                    k.endsWith(versionSuffix!),
                orElse: () => '',
              );
            }
            resultTypeName = (candidate != null && candidate.isNotEmpty)
                ? candidate
                : latest[resultBase]?.fullName;
          }
        }
      }

      // Build a synthetic response class:
      b.writeln('class $apiTypeName {');
      b.writeln('  final v1Activity activity;');
      if (resultTypeName != null && defs[resultTypeName] is Map) {
        // Inline properties: we’ll just include a nested field called "result"
        b.writeln('  final $resultTypeName? result;');
      }
      b.writeln(
          '  const $apiTypeName({required this.activity, ${resultTypeName != null ? 'this.result,' : ''}});');
      b.writeln('  factory $apiTypeName.fromJson(Map<String, dynamic> json) {');
      b.writeln('    return $apiTypeName(');
      b.writeln(
          '      activity: v1Activity.fromJson(json[\'activity\'] as Map<String, dynamic>),');
      if (resultTypeName != null) {
        b.writeln(
            '      result: json.containsKey(\'result\') && json[\'result\'] != null'
            ' ? ${resultTypeName}.fromJson(json[\'result\'] as Map<String, dynamic>) : null,');
      }
      b.writeln('    );');
      b.writeln('  }');
      b.writeln('  Map<String, dynamic> toJson() => {');
      b.writeln('    \'activity\': activity.toJson(),');
      if (resultTypeName != null) {
        b.writeln('    if (result != null) \'result\': result!.toJson(),');
      }
      b.writeln('  };');
      b.writeln('}\n');
    } else if (mType == 'query' || mType == 'noop' || mType == 'proxy') {
      // Similar to JS, we emit the response shape fields.
      final respDef = responseTypeName != null ? defs[responseTypeName] : null;
      if (respDef is Map && respDef['properties'] is Map) {
        // Inline as a class with all properties
        final fakeDef = <String, dynamic>{
          'type': 'object',
          'properties': respDef['properties'],
          'required': respDef['required'],
        };
        // emit as class
        // Name collision safe: we can just generate a class with those fields
        // (no additionalProperties capture)
        // Reuse definition generator:
        // But we want the class name $apiTypeName
        b.write(generateClass(apiTypeName, fakeDef, defs));
      } else {
        // empty
        b.writeln('class $apiTypeName {');
        b.writeln('  const $apiTypeName();');
        b.writeln(
            '  factory $apiTypeName.fromJson(Map<String, dynamic> json) => const $apiTypeName();');
        b.writeln('  Map<String, dynamic> toJson() => {};');
        b.writeln('}\n');
      }
    } else if (mType == 'activityDecision') {
      final activityType =
          responseTypeName != null ? defs[responseTypeName] : null;
      if (activityType is Map && activityType['properties'] is Map) {
        final fakeDef = <String, dynamic>{
          'type': 'object',
          'properties': activityType['properties'],
          'required': activityType['required'],
        };
        b.write(generateClass(apiTypeName, fakeDef, defs));
      }
    }

    // REQUEST BODY
    // Find request $ref
    Map<String, dynamic>? requestTypeDef;
    final params = (op['parameters'] as List?)?.cast<dynamic>() ?? [];
    for (final p in params) {
      final pm = (p as Map).cast<String, dynamic>();
      if (pm['in'] == 'body' &&
          pm['schema'] is Map &&
          (pm['schema'][r'$ref'] is String)) {
        final rname = refToName(pm['schema'][r'$ref'] as String);
        if (defs.containsKey(rname)) {
          requestTypeDef = (defs[rname] as Map).cast<String, dynamic>();
        }
      }
    }

    if (requestTypeDef == null) {
      if (mType == 'noop') {
        // Empty Body
        b.writeln('class $apiBodyTypeName {');
        b.writeln('  const $apiBodyTypeName();');
        b.writeln(
            '  factory $apiBodyTypeName.fromJson(Map<String, dynamic> json) => const $apiBodyTypeName();');
        b.writeln('  Map<String, dynamic> toJson() => {};');
        b.writeln('}\n');

        // Input wrapper
        b.writeln('class $apiInputTypeName {');
        b.writeln('  final $apiBodyTypeName body;');
        b.writeln('  const $apiInputTypeName({required this.body});');
        b.writeln(
            '  factory $apiInputTypeName.fromJson(Map<String, dynamic> json) => '
            '$apiInputTypeName(body: json[\'body\'] is Map<String, dynamic> '
            '? $apiBodyTypeName.fromJson(json[\'body\'] as Map<String, dynamic>) '
            ': const $apiBodyTypeName());');
        b.writeln(
            '  Map<String, dynamic> toJson() => {\'body\': body.toJson()};');
        b.writeln('}\n');
        continue;
      }
      continue;
    }

    final bodyBuf = StringBuffer();
    bodyBuf.writeln('class $apiBodyTypeName {');
    // Fields (timestampMs, organizationId + intent params OR request props depending on methodType)
    final fields = <FieldSpec>[];

    if (mType == 'command' || mType == 'activityDecision') {
      fields.addAll([
        FieldSpec(
            fieldName: 'timestampMs',
            jsonKey: 'timestampMs',
            type: 'String?',
            nullable: true,
            description: null),
        FieldSpec(
            fieldName: 'organizationId',
            jsonKey: 'organizationId',
            type: 'String?',
            nullable: true,
            description: null),
      ]);
      final props =
          (requestTypeDef['properties'] as Map?)?.cast<String, dynamic>();
      final ref = props?['parameters'] is Map
          ? (props?['parameters'][r'$ref'] as String?)
          : null;
      if (ref != null) {
        final intentTypeName = refToName(ref);
        final intentDef = (defs[intentTypeName] as Map).cast<String, dynamic>();
        final isAllOptional =
            METHODS_WITH_ONLY_OPTIONAL_PARAMETERS.contains(methodName);
        final reqd = (intentDef['required'] is List)
            ? (intentDef['required'] as List).cast<String>()
            : <String>[];
        final iprops =
            (intentDef['properties'] as Map?)?.cast<String, dynamic>() ?? {};
        for (final e in iprops.entries) {
          final key = e.key;
          final s = (e.value as Map).cast<String, dynamic>();
          String t = 'dynamic';
          if (s[r'$ref'] is String)
            t = refToName(s[r'$ref'] as String);
          else if (s['type'] is String) t = typeToDart(s['type'] as String, s);
          final isReq = isAllOptional ? false : reqd.contains(key);
          fields.add(FieldSpec(
            fieldName: sanitizeFieldName(key),
            jsonKey: key,
            type: isReq ? t : '$t?',
            nullable: !isReq,
            description: s['description'] as String?,
          ));
        }
      }
    } else if (mType == 'query' || mType == 'noop') {
      fields.add(FieldSpec(
          fieldName: 'organizationId',
          jsonKey: 'organizationId',
          type: 'String?',
          nullable: true,
          description: null));
      final reqProps =
          (requestTypeDef['properties'] as Map?)?.cast<String, dynamic>() ?? {};
      final reqd = (requestTypeDef['required'] is List)
          ? (requestTypeDef['required'] as List).cast<String>()
          : <String>[];
      final isAllOptional =
          METHODS_WITH_ONLY_OPTIONAL_PARAMETERS.contains(methodName);
      for (final e in reqProps.entries) {
        if (e.key == 'organizationId') continue;
        final s = (e.value as Map).cast<String, dynamic>();
        String t = 'dynamic';
        if (s[r'$ref'] is String)
          t = refToName(s[r'$ref'] as String);
        else if (s['type'] is String) t = typeToDart(s['type'] as String, s);
        final isReq = isAllOptional ? false : reqd.contains(e.key);
        fields.add(FieldSpec(
          fieldName: sanitizeFieldName(e.key),
          jsonKey: e.key,
          type: isReq ? t : '$t?',
          nullable: !isReq,
          description: s['description'] as String?,
        ));
      }
    } else if (mType == 'proxy') {
      final reqProps =
          (requestTypeDef['properties'] as Map?)?.cast<String, dynamic>() ?? {};
      final reqd = (requestTypeDef['required'] is List)
          ? (requestTypeDef['required'] as List).cast<String>()
          : <String>[];
      final isAllOptional =
          METHODS_WITH_ONLY_OPTIONAL_PARAMETERS.contains(methodName);
      for (final e in reqProps.entries) {
        final s = (e.value as Map).cast<String, dynamic>();
        String t = 'dynamic';
        if (s[r'$ref'] is String)
          t = refToName(s[r'$ref'] as String);
        else if (s['type'] is String) t = typeToDart(s['type'] as String, s);
        final isReq = isAllOptional ? false : reqd.contains(e.key);
        fields.add(FieldSpec(
          fieldName: sanitizeFieldName(e.key),
          jsonKey: e.key,
          type: isReq ? t : '$t?',
          nullable: !isReq,
          description: s['description'] as String?,
        ));
      }
    }

    // Emit fields
    for (final f in fields) {
      if ((f.description ?? '').isNotEmpty) {
        bodyBuf.writeln('  /// ${f.description}');
      }
      bodyBuf.writeln('  final ${f.type} ${f.fieldName};');
    }
    bodyBuf.writeln();
    // ctor
    bodyBuf.writeln('  const $apiBodyTypeName(${fields.isEmpty ? '' : '{'}');
    for (final f in fields) {
      final req = f.nullable ? '' : 'required ';
      bodyBuf.writeln('    $req this.${f.fieldName},');
    }
    bodyBuf.writeln('  ${fields.isEmpty ? '' : '}'});');
    // fromJson
    bodyBuf.writeln(
        '  factory $apiBodyTypeName.fromJson(Map<String, dynamic> json) {');
    for (final f in fields) {
      final readExpr = _jsonReadExpr(
        f.type.endsWith('?') ? f.type.substring(0, f.type.length - 1) : f.type,
        "json['${f.jsonKey}']",
        defs,
        f.type.endsWith('?') ? false : true,
      );
      bodyBuf.writeln('    final _${f.fieldName} = $readExpr;');
    }
    bodyBuf.writeln('    return $apiBodyTypeName(');
    for (final f in fields) {
      bodyBuf.writeln('      ${f.fieldName}: _${f.fieldName},');
    }
    bodyBuf.writeln('    );');
    bodyBuf.writeln('  }');
    // toJson
    bodyBuf.writeln('  Map<String, dynamic> toJson() {');
    bodyBuf.writeln('    final _json = <String, dynamic>{};');
    for (final f in fields) {
      final write = _jsonWriteExpr(f.type, f.fieldName, defs);
      if (f.nullable) {
        bodyBuf.writeln("    if (${f.fieldName} != null) {");
        bodyBuf.writeln("      _json['${f.jsonKey}'] = $write;");
        bodyBuf.writeln('    }');
      } else {
        bodyBuf.writeln("    _json['${f.jsonKey}'] = $write;");
      }
    }
    bodyBuf.writeln('    return _json;');
    bodyBuf.writeln('  }');
    bodyBuf.writeln('}\n');

    b.write(bodyBuf.toString());

    // INPUT WRAPPER
    b.writeln('class $apiInputTypeName {');
    b.writeln('  final $apiBodyTypeName body;');
    b.writeln('  const $apiInputTypeName({required this.body});');
    b.writeln(
        '  factory $apiInputTypeName.fromJson(Map<String, dynamic> json) => '
        '$apiInputTypeName(body: $apiBodyTypeName.fromJson(json[\'body\'] as Map<String, dynamic>));');
    b.writeln('  Map<String, dynamic> toJson() => {\'body\': body.toJson()};');
    b.writeln('}\n');
  }

  return b.toString();
}