Skip to content

Commit c84768b

Browse files
committed
Nim Generator Fixes
1 parent e86daf9 commit c84768b

29 files changed

Lines changed: 619 additions & 35 deletions

modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/NimClientCodegen.java

Lines changed: 120 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@
3434

3535
import java.io.File;
3636
import java.util.*;
37+
import java.util.regex.Pattern;
3738

3839
import static org.openapitools.codegen.utils.CamelizeOption.LOWERCASE_FIRST_LETTER;
3940
import static org.openapitools.codegen.utils.StringUtils.camelize;
@@ -167,11 +168,63 @@ public NimClientCodegen() {
167168
typeMapping.put("DateTime", "string");
168169
typeMapping.put("password", "string");
169170
typeMapping.put("file", "string");
171+
typeMapping.put("object", "JsonNode");
172+
typeMapping.put("AnyType", "JsonNode");
170173
}
171174

172175
@Override
173176
public ModelsMap postProcessModels(ModelsMap objs) {
174-
return postProcessModelsEnum(objs);
177+
objs = postProcessModelsEnum(objs);
178+
179+
// Mark top-level string enums for proper enum generation in template
180+
for (ModelMap mo : objs.getModels()) {
181+
CodegenModel cm = mo.getModel();
182+
if (cm.isEnum && cm.allowableValues != null && cm.allowableValues.containsKey("enumVars")) {
183+
cm.vendorExtensions.put("x-is-top-level-enum", true);
184+
}
185+
186+
// Fix dataType fields that contain underscored type names
187+
// This handles cases like Table[string, Record_string__foo__value]
188+
for (CodegenProperty var : cm.vars) {
189+
if (var.dataType != null && var.dataType.contains("Record_")) {
190+
var.dataType = fixRecordTypeReferences(var.dataType);
191+
}
192+
if (var.datatypeWithEnum != null && var.datatypeWithEnum.contains("Record_")) {
193+
var.datatypeWithEnum = fixRecordTypeReferences(var.datatypeWithEnum);
194+
}
195+
}
196+
}
197+
198+
return objs;
199+
}
200+
201+
/**
202+
* Fix underscored Record type references in dataType strings.
203+
* Converts Record_string__foo___value to RecordStringFooValue.
204+
*/
205+
private String fixRecordTypeReferences(String typeString) {
206+
if (typeString == null || !typeString.contains("Record_")) {
207+
return typeString;
208+
}
209+
210+
// Pattern to match Record_string_... type names with underscores
211+
// These are embedded in strings like: Table[string, Record_string__foo__value]
212+
String result = typeString;
213+
214+
// Match Record_ followed by any characters until end or comma/bracket
215+
Pattern pattern = Pattern.compile("Record_[a-z_]+");
216+
java.util.regex.Matcher matcher = pattern.matcher(result);
217+
218+
StringBuffer sb = new StringBuffer();
219+
while (matcher.find()) {
220+
String matched = matcher.group();
221+
// Camelize the matched Record type name
222+
String camelized = camelize(matched);
223+
matcher.appendReplacement(sb, camelized);
224+
}
225+
matcher.appendTail(sb);
226+
227+
return sb.toString();
175228
}
176229

177230
@Override
@@ -192,6 +245,8 @@ public void processOpts() {
192245
apiPackage = File.separator + packageName + File.separator + "apis";
193246
modelPackage = File.separator + packageName + File.separator + "models";
194247
supportingFiles.add(new SupportingFile("lib.mustache", "", packageName + ".nim"));
248+
supportingFiles.add(new SupportingFile("model_any_type.mustache", packageName + File.separator + "models", "model_any_type.nim"));
249+
supportingFiles.add(new SupportingFile("model_object.mustache", packageName + File.separator + "models", "model_object.nim"));
195250
}
196251

197252
@Override
@@ -215,34 +270,68 @@ public String escapeUnsafeCharacters(String input) {
215270

216271
@Override
217272
public String toModelImport(String name) {
273+
name = normalizeSchemaName(name);
218274
name = name.replaceAll("-", "_");
275+
219276
if (importMapping.containsKey(name)) {
220-
return "model_" + StringUtils.underscore(importMapping.get(name));
277+
return sanitizeNimIdentifier("model_" + StringUtils.underscore(importMapping.get(name)));
221278
} else {
222-
return "model_" + StringUtils.underscore(name);
279+
return sanitizeNimIdentifier("model_" + StringUtils.underscore(name));
223280
}
224281
}
225282

226283
@Override
227284
public String toApiImport(String name) {
228285
name = name.replaceAll("-", "_");
229286
if (importMapping.containsKey(name)) {
230-
return "api_" + StringUtils.underscore(importMapping.get(name));
287+
return sanitizeNimIdentifier("api_" + StringUtils.underscore(importMapping.get(name)));
231288
} else {
232-
return "api_" + StringUtils.underscore(name);
289+
return sanitizeNimIdentifier("api_" + StringUtils.underscore(name));
233290
}
234291
}
235292

293+
/**
294+
* Normalize schema names to ensure consistency across filename, import, and type name generation.
295+
* This is called early in the pipeline so downstream methods work with consistent names.
296+
*/
297+
private String normalizeSchemaName(String name) {
298+
if (name == null) {
299+
return null;
300+
}
301+
// Remove underscores around and before digits (HTTP status codes, version numbers, etc.)
302+
// e.g., "GetComments_200_response" -> "GetComments200response"
303+
// e.g., "Config_anyOf_1" -> "ConfiganyOf1"
304+
// This ensures consistent handling whether the name comes with or without underscores
305+
name = name.replaceAll("_(\\d+)_", "$1"); // Underscores on both sides
306+
name = name.replaceAll("_(\\d+)$", "$1"); // Trailing underscore before digits
307+
return name;
308+
}
309+
310+
@Override
311+
public CodegenModel fromModel(String name, Schema schema) {
312+
// Normalize the schema name before any processing
313+
name = normalizeSchemaName(name);
314+
return super.fromModel(name, schema);
315+
}
316+
317+
@Override
318+
public String toModelName(String name) {
319+
// Name should be normalized by fromModel, but normalize again for safety
320+
name = normalizeSchemaName(name);
321+
return camelize(sanitizeName(name));
322+
}
323+
236324
@Override
237325
public String toModelFilename(String name) {
326+
name = normalizeSchemaName(name);
238327
name = name.replaceAll("-", "_");
239-
return "model_" + StringUtils.underscore(name);
328+
return sanitizeNimIdentifier("model_" + StringUtils.underscore(name));
240329
}
241330

242331
@Override
243332
public String toApiFilename(String name) {
244333
name = name.replaceAll("-", "_");
245-
return "api_" + StringUtils.underscore(name);
334+
return sanitizeNimIdentifier("api_" + StringUtils.underscore(name));
246335
}
247336

248337
@Override
@@ -262,6 +351,12 @@ public OperationsMap postProcessOperationsWithModels(OperationsMap objs, List<Mo
262351
List<CodegenOperation> operations = objectMap.getOperation();
263352
for (CodegenOperation operation : operations) {
264353
operation.httpMethod = operation.httpMethod.toLowerCase(Locale.ROOT);
354+
355+
// Set custom flag for DELETE operations with body to use different template logic
356+
// Nim's httpClient.delete() doesn't support a body parameter
357+
if ("delete".equals(operation.httpMethod) && operation.getHasBodyParam()) {
358+
operation.vendorExtensions.put("x-nim-delete-with-body", true);
359+
}
265360
}
266361

267362
return objs;
@@ -360,6 +455,24 @@ private boolean isValidIdentifier(String identifier) {
360455
return identifier.matches("^(?:[A-Z]|[a-z]|[\\x80-\\xff])(_?(?:[A-Z]|[a-z]|[\\x80-\\xff]|[0-9]))*$");
361456
}
362457

458+
/**
459+
* Sanitize a Nim identifier by removing trailing underscores and collapsing multiple underscores.
460+
* Nim does not allow identifiers to end with underscores.
461+
*
462+
* @param name the identifier to sanitize
463+
* @return the sanitized identifier
464+
*/
465+
private String sanitizeNimIdentifier(String name) {
466+
if (name == null || name.isEmpty()) {
467+
return name;
468+
}
469+
// Remove trailing underscores (Nim identifiers cannot end with underscore)
470+
name = name.replaceAll("_+$", "");
471+
// Collapse multiple consecutive underscores to single underscore
472+
name = name.replaceAll("_+", "_");
473+
return name;
474+
}
475+
363476
@Override
364477
public String toEnumVarName(String name, String datatype) {
365478
name = name.replace(" ", "_");

modules/openapi-generator/src/main/resources/nim-client/api.mustache

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -47,9 +47,15 @@ proc {{{operationId}}}*(httpClient: HttpClient{{#allParams}}, {{{paramName}}}: {
4747
{{#formParams}} "{{{baseName}}}": ${{{paramName}}}{{#isArray}}.join(","){{/isArray}}, # {{{description}}}
4848
{{/formParams}}
4949
}){{/isMultipart}}{{/hasFormParams}}{{#returnType}}
50-
50+
{{#vendorExtensions.x-nim-delete-with-body}}
51+
let response = httpClient.request(basepath & {{^pathParams}}"{{{path}}}"{{/pathParams}}{{#hasPathParams}}fmt"{{{path}}}"{{/hasPathParams}}{{#hasQueryParams}} & "?" & url_encoded_query_params{{/hasQueryParams}}, httpMethod = HttpDelete{{#bodyParams}}, body = $(%{{{paramName}}}){{/bodyParams}})
52+
{{/vendorExtensions.x-nim-delete-with-body}}{{^vendorExtensions.x-nim-delete-with-body}}
5153
let response = httpClient.{{{httpMethod}}}(basepath & {{^pathParams}}"{{{path}}}"{{/pathParams}}{{#hasPathParams}}fmt"{{{path}}}"{{/hasPathParams}}{{#hasQueryParams}} & "?" & url_encoded_query_params{{/hasQueryParams}}{{#hasBodyParam}}{{#bodyParams}}, $(%{{{paramName}}}){{/bodyParams}}{{/hasBodyParam}}{{#hasFormParams}}, {{^isMultipart}}$form_data{{/isMultipart}}{{#isMultipart}}multipart=multipart_data{{/isMultipart}}{{/hasFormParams}})
52-
constructResult[{{{returnType}}}](response){{/returnType}}{{^returnType}}
53-
httpClient.{{{httpMethod}}}(basepath & {{^pathParams}}"{{{path}}}"{{/pathParams}}{{#hasPathParams}}fmt"{{{path}}}"{{/hasPathParams}}{{#hasQueryParams}} & "?" & url_encoded_query_params{{/hasQueryParams}}{{#hasBodyParam}}{{#bodyParams}}, $(%{{{paramName}}}){{/bodyParams}}{{/hasBodyParam}}{{#hasFormParams}}, {{^isMultipart}}$form_data{{/isMultipart}}{{#isMultipart}}multipart=multipart_data{{/isMultipart}}{{/hasFormParams}}){{/returnType}}
54+
{{/vendorExtensions.x-nim-delete-with-body}}
55+
constructResult[{{{returnType}}}](response){{/returnType}}{{^returnType}}{{#vendorExtensions.x-nim-delete-with-body}}
56+
httpClient.request(basepath & {{^pathParams}}"{{{path}}}"{{/pathParams}}{{#hasPathParams}}fmt"{{{path}}}"{{/hasPathParams}}{{#hasQueryParams}} & "?" & url_encoded_query_params{{/hasQueryParams}}, httpMethod = HttpDelete{{#bodyParams}}, body = $(%{{{paramName}}}){{/bodyParams}})
57+
{{/vendorExtensions.x-nim-delete-with-body}}{{^vendorExtensions.x-nim-delete-with-body}}
58+
httpClient.{{{httpMethod}}}(basepath & {{^pathParams}}"{{{path}}}"{{/pathParams}}{{#hasPathParams}}fmt"{{{path}}}"{{/hasPathParams}}{{#hasQueryParams}} & "?" & url_encoded_query_params{{/hasQueryParams}}{{#hasBodyParam}}{{#bodyParams}}, $(%{{{paramName}}}){{/bodyParams}}{{/hasBodyParam}}{{#hasFormParams}}, {{^isMultipart}}$form_data{{/isMultipart}}{{#isMultipart}}multipart=multipart_data{{/isMultipart}}{{/hasFormParams}})
59+
{{/vendorExtensions.x-nim-delete-with-body}}{{/returnType}}
5460

5561
{{/operation}}{{/operations}}

modules/openapi-generator/src/main/resources/nim-client/model.mustache

Lines changed: 16 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,18 @@ import json
33
import tables
44

55
{{#imports}}import {{import}}
6-
{{/imports}}{{#models}}{{#model}}{{#vars}}{{#isEnum}}
6+
{{/imports}}{{#models}}{{#model}}{{#isEnum}}
7+
type {{{classname}}}* {.pure.} = enum{{#allowableValues}}{{#enumVars}}
8+
{{{name}}}{{/enumVars}}{{/allowableValues}}
9+
10+
func `%`*(v: {{{classname}}}): JsonNode =
11+
result = case v:{{#allowableValues}}{{#enumVars}}
12+
of {{{classname}}}.{{{name}}}: %{{{value}}}{{/enumVars}}{{/allowableValues}}
13+
14+
func `$`*(v: {{{classname}}}): string =
15+
result = case v:{{#allowableValues}}{{#enumVars}}
16+
of {{{classname}}}.{{{name}}}: $({{{value}}}){{/enumVars}}{{/allowableValues}}
17+
{{/isEnum}}{{^isEnum}}{{#vars}}{{#isEnum}}
718
type {{{enumName}}}* {.pure.} = enum{{#allowableValues}}{{#enumVars}}
819
{{{name}}}{{/enumVars}}{{/allowableValues}}
920
{{/isEnum}}{{/vars}}
@@ -12,12 +23,10 @@ type {{{classname}}}* = object
1223
{{{name}}}*: {{#isEnum}}{{{enumName}}}{{/isEnum}}{{^isEnum}}{{{dataType}}}{{/isEnum}}{{#description}} ## {{{.}}}{{/description}}{{/vars}}
1324
{{#vars}}{{#isEnum}}
1425
func `%`*(v: {{{enumName}}}): JsonNode =
15-
let str = case v:{{#allowableValues}}{{#enumVars}}
16-
of {{{enumName}}}.{{{name}}}: {{{value}}}{{/enumVars}}{{/allowableValues}}
17-
18-
JsonNode(kind: JString, str: str)
26+
result = case v:{{#allowableValues}}{{#enumVars}}
27+
of {{{enumName}}}.{{{name}}}: %{{{value}}}{{/enumVars}}{{/allowableValues}}
1928

2029
func `$`*(v: {{{enumName}}}): string =
2130
result = case v:{{#allowableValues}}{{#enumVars}}
22-
of {{{enumName}}}.{{{name}}}: {{{value}}}{{/enumVars}}{{/allowableValues}}
23-
{{/isEnum}}{{/vars}}{{/model}}{{/models}}
31+
of {{{enumName}}}.{{{name}}}: $({{{value}}}){{/enumVars}}{{/allowableValues}}
32+
{{/isEnum}}{{/vars}}{{/isEnum}}{{/model}}{{/models}}
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
{{>header}}
2+
import json
3+
4+
# AnyType represents any JSON value
5+
# This is used for fields that can contain arbitrary JSON data
6+
type AnyType* = JsonNode
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
{{>header}}
2+
import json
3+
import tables
4+
5+
# Object represents an arbitrary JSON object
6+
# Using JsonNode instead of the 'object' keyword to avoid Nim keyword conflicts
7+
type Object* = JsonNode

0 commit comments

Comments
 (0)