Skip to content

Commit 0a8333b

Browse files
committed
Merge branch 'nim-working-version-fastcomments' into devon-fixes-all
2 parents 6ba31aa + f8c3172 commit 0a8333b

29 files changed

Lines changed: 599 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

modules/openapi-generator/src/test/java/org/openapitools/codegen/nim/NimClientCodegenTest.java

Lines changed: 120 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
package org.openapitools.codegen.nim;
22

3+
import io.swagger.v3.oas.models.media.ObjectSchema;
34
import org.openapitools.codegen.CodegenConstants;
45
import org.openapitools.codegen.languages.NimClientCodegen;
56
import org.testng.Assert;
@@ -35,4 +36,123 @@ public void testAdditionalPropertiesPutForConfigValues() throws Exception {
3536
Assert.assertEquals(codegen.additionalProperties().get(CodegenConstants.HIDE_GENERATION_TIMESTAMP), Boolean.FALSE);
3637
Assert.assertEquals(codegen.isHideGenerationTimestamp(), false);
3738
}
39+
40+
@Test
41+
public void testUnderscoresEdgeCases() throws Exception {
42+
final NimClientCodegen codegen = new NimClientCodegen();
43+
44+
// Test model filename with trailing underscores
45+
String result = codegen.toModelFilename("Record_string__before_string_or_null__after_string_or_null___value");
46+
Assert.assertFalse(result.endsWith("_"), "Model filename should not end with underscore: " + result);
47+
48+
// Test model filename with multiple consecutive underscores
49+
result = codegen.toModelFilename("Record_string_string_or_number__value");
50+
Assert.assertFalse(result.endsWith("_"), "Model filename should not end with underscore: " + result);
51+
52+
// Verify no consecutive underscores remain (except the required prefix)
53+
Assert.assertFalse(result.contains("__"), "Model filename should not contain consecutive underscores: " + result);
54+
55+
// Test model import with trailing underscores
56+
result = codegen.toModelImport("Record_string__before_string_or_null__after_string_or_null___value");
57+
Assert.assertFalse(result.endsWith("_"), "Model import should not end with underscore: " + result);
58+
59+
// Test model import with multiple consecutive underscores
60+
result = codegen.toModelImport("Record_string_string_or_number__value");
61+
Assert.assertFalse(result.endsWith("_"), "Model import should not end with underscore: " + result);
62+
63+
// Test API filename with trailing underscores
64+
result = codegen.toApiFilename("SomeApi_");
65+
Assert.assertFalse(result.endsWith("_"), "API filename should not end with underscore: " + result);
66+
67+
// Test API import with trailing underscores
68+
result = codegen.toApiImport("SomeApi_");
69+
Assert.assertFalse(result.endsWith("_"), "API import should not end with underscore: " + result);
70+
}
71+
72+
@Test
73+
public void testSanitizationPreservesNormalNames() throws Exception {
74+
final NimClientCodegen codegen = new NimClientCodegen();
75+
76+
// Verify that normal names without trailing underscores are not changed
77+
String result = codegen.toModelFilename("UserData");
78+
Assert.assertTrue(result.startsWith("model_"), "Model filename should start with model_");
79+
Assert.assertFalse(result.endsWith("_"), "Model filename should not end with underscore");
80+
81+
result = codegen.toApiFilename("DefaultApi");
82+
Assert.assertTrue(result.startsWith("api_"), "API filename should start with api_");
83+
Assert.assertFalse(result.endsWith("_"), "API filename should not end with underscore");
84+
}
85+
86+
@Test
87+
public void testObjectTypeMapping() throws Exception {
88+
final NimClientCodegen codegen = new NimClientCodegen();
89+
90+
// Test that object type is mapped to JsonNode to avoid Nim keyword conflict
91+
ObjectSchema objectSchema = new ObjectSchema();
92+
String result = codegen.getTypeDeclaration(objectSchema);
93+
94+
// object types without properties should map to JsonNode
95+
Assert.assertEquals(result, "JsonNode",
96+
"Free-form object type should map to JsonNode to avoid Nim 'object' keyword conflict");
97+
}
98+
99+
@Test
100+
public void testTypeNameConsistency() throws Exception {
101+
final NimClientCodegen codegen = new NimClientCodegen();
102+
103+
// Test that response type names don't have underscores between number and text
104+
String result = codegen.toModelName("GetComments_200_response");
105+
Assert.assertFalse(result.contains("_200_"), "Type name should not contain _200_: " + result);
106+
Assert.assertTrue(result.contains("200"), "Type name should contain 200: " + result);
107+
108+
// The filename should also be consistent
109+
String filename = codegen.toModelFilename("GetComments_200_response");
110+
String importName = codegen.toModelImport("GetComments_200_response");
111+
112+
// Extract the type name from the filename and import
113+
String filenameTypePart = filename.replace("model_", "");
114+
String importTypePart = importName.replace("model_", "");
115+
116+
Assert.assertEquals(filenameTypePart, importTypePart,
117+
"Filename and import should reference the same type");
118+
}
119+
120+
@Test
121+
public void testImportConsistencyAfterProcessing() throws Exception {
122+
final NimClientCodegen codegen = new NimClientCodegen();
123+
124+
// Simulate what happens during model processing:
125+
// 1. Model is created with original schema name
126+
String schemaName = "AddDomainConfig_200_response_anyOf";
127+
String typeName = codegen.toModelName(schemaName); // Camelizes to AddDomainConfig200ResponseAnyOf
128+
String filename = codegen.toModelFilename(schemaName); // Creates model_add_domain_config_200_response_any_of
129+
130+
// 2. When another model imports this type, it uses the camelized type name
131+
String importFromTypeName = codegen.toModelImport(typeName);
132+
133+
// The import should match the filename (or at least be loadable)
134+
Assert.assertEquals(filename, importFromTypeName,
135+
"Import generated from type name should match filename generated from schema name");
136+
}
137+
138+
@Test
139+
public void testNormalizeSchemaName() throws Exception {
140+
final NimClientCodegen codegen = new NimClientCodegen();
141+
142+
// Test that schema names with _200_ are normalized
143+
String result = codegen.toModelName("GetComments_200_response");
144+
Assert.assertEquals(result, "GetComments200response",
145+
"Should normalize _200_ to 200: " + result);
146+
147+
// Test that schema names with trailing _1 are normalized
148+
result = codegen.toModelName("Config_anyOf_1");
149+
Assert.assertEquals(result, "ConfigAnyOf1",
150+
"Should normalize _1 to 1: " + result);
151+
152+
// Verify consistency between filename and import
153+
String filename = codegen.toModelFilename("GetComments_200_response");
154+
String importPath = codegen.toModelImport("GetComments200response");
155+
Assert.assertEquals(filename, importPath,
156+
"Filename and import should match after normalization");
157+
}
38158
}

0 commit comments

Comments
 (0)