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