Skip to content

Commit d6f24ed

Browse files
committed
Merge branch 'nim-working-version-fastcomments' into devon-fixes-all
2 parents 29b07bb + 16051dd commit d6f24ed

33 files changed

Lines changed: 1224 additions & 81 deletions

docs/generators/nim.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -226,8 +226,8 @@ These options may be applied as additional-properties (cli) or configOptions (pl
226226
|Polymorphism|✗|OAS2,OAS3
227227
|Union|✗|OAS3
228228
|allOf|✗|OAS2,OAS3
229-
|anyOf||OAS3
230-
|oneOf||OAS3
229+
|anyOf||OAS3
230+
|oneOf||OAS3
231231
|not|✗|OAS3
232232

233233
### Security Feature

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

Lines changed: 247 additions & 3 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.Matcher;
3738
import java.util.regex.Pattern;
3839

3940
import static org.openapitools.codegen.utils.CamelizeOption.LOWERCASE_FIRST_LETTER;
@@ -78,6 +79,10 @@ public NimClientCodegen() {
7879
.excludeSchemaSupportFeatures(
7980
SchemaSupportFeature.Polymorphism
8081
)
82+
.includeSchemaSupportFeatures(
83+
SchemaSupportFeature.oneOf,
84+
SchemaSupportFeature.anyOf
85+
)
8186
.excludeParameterFeatures(
8287
ParameterFeature.Cookie
8388
)
@@ -172,26 +177,166 @@ public NimClientCodegen() {
172177
typeMapping.put("AnyType", "JsonNode");
173178
}
174179

180+
181+
@Override
182+
public Map<String, ModelsMap> postProcessAllModels(Map<String, ModelsMap> allModels) {
183+
allModels = super.postProcessAllModels(allModels);
184+
185+
// First pass: identify all models that have fields with custom JSON names
186+
Set<String> modelsWithCustomJson = new HashSet<>();
187+
188+
for (Map.Entry<String, ModelsMap> entry : allModels.entrySet()) {
189+
ModelsMap modelsMap = entry.getValue();
190+
for (ModelMap mo : modelsMap.getModels()) {
191+
CodegenModel cm = mo.getModel();
192+
193+
// Check if this model has fields with custom JSON names
194+
for (CodegenProperty var : cm.vars) {
195+
if (var.vendorExtensions.containsKey("x-json-name")) {
196+
modelsWithCustomJson.add(cm.classname);
197+
break;
198+
}
199+
}
200+
}
201+
}
202+
203+
// Second pass: cascade custom JSON handling to parent models and mark array fields
204+
// We need multiple passes to handle transitive dependencies
205+
boolean changed = true;
206+
while (changed) {
207+
changed = false;
208+
for (Map.Entry<String, ModelsMap> entry : allModels.entrySet()) {
209+
ModelsMap modelsMap = entry.getValue();
210+
for (ModelMap mo : modelsMap.getModels()) {
211+
CodegenModel cm = mo.getModel();
212+
213+
// Check if any field's type needs custom JSON and mark array fields appropriately
214+
for (CodegenProperty var : cm.vars) {
215+
String fieldType = var.complexType != null ? var.complexType : var.baseType;
216+
217+
// Handle arrays - check if the inner type has custom JSON
218+
if (var.isArray && var.items != null) {
219+
String innerType = var.items.complexType != null ? var.items.complexType : var.items.baseType;
220+
if (innerType != null && modelsWithCustomJson.contains(innerType)) {
221+
// Mark this array field as containing types with custom JSON
222+
var.vendorExtensions.put("x-is-array-with-custom-json", "true");
223+
var.vendorExtensions.put("x-array-inner-type", innerType);
224+
}
225+
fieldType = innerType;
226+
}
227+
228+
// Cascade custom JSON to parent model if not already marked
229+
if (fieldType != null && modelsWithCustomJson.contains(fieldType)) {
230+
if (!cm.vendorExtensions.containsKey("x-has-custom-json-names")) {
231+
cm.vendorExtensions.put("x-has-custom-json-names", true);
232+
modelsWithCustomJson.add(cm.classname);
233+
changed = true;
234+
}
235+
}
236+
}
237+
}
238+
}
239+
}
240+
241+
return allModels;
242+
}
243+
244+
/**
245+
* Strips surrounding quotes from integer enum values.
246+
* The base OpenAPI Generator stores all enum values as quoted strings (e.g., "0", "1", "2")
247+
* regardless of the enum's actual type. For Nim integer enums, we need the raw numbers
248+
* without quotes so they serialize correctly: %(0) instead of %("0")
249+
*/
250+
private void stripQuotesFromIntegerEnumValues(Map<String, Object> allowableValues) {
251+
if (allowableValues == null || !allowableValues.containsKey("enumVars")) {
252+
return;
253+
}
254+
255+
@SuppressWarnings("unchecked")
256+
List<Map<String, Object>> enumVars = (List<Map<String, Object>>) allowableValues.get("enumVars");
257+
for (Map<String, Object> enumVar : enumVars) {
258+
Object value = enumVar.get("value");
259+
if (value instanceof String) {
260+
String strValue = (String) value;
261+
// Remove surrounding quotes if present
262+
if (strValue.startsWith("\"") && strValue.endsWith("\"")) {
263+
enumVar.put("value", strValue.substring(1, strValue.length() - 1));
264+
}
265+
}
266+
}
267+
}
268+
175269
@Override
176270
public ModelsMap postProcessModels(ModelsMap objs) {
177271
objs = postProcessModelsEnum(objs);
178272

179-
// Mark top-level string enums for proper enum generation in template
180273
for (ModelMap mo : objs.getModels()) {
181274
CodegenModel cm = mo.getModel();
275+
182276
if (cm.isEnum && cm.allowableValues != null && cm.allowableValues.containsKey("enumVars")) {
183277
cm.vendorExtensions.put("x-is-top-level-enum", true);
278+
279+
// For integer enums, strip quotes from enum values
280+
if (cm.vendorExtensions.containsKey("x-is-integer-enum")) {
281+
stripQuotesFromIntegerEnumValues(cm.allowableValues);
282+
}
184283
}
185284

285+
// Check if any fields need custom JSON name mapping
286+
boolean hasCustomJsonNames = false;
287+
186288
// Fix dataType fields that contain underscored type names
187289
// This handles cases like Table[string, Record_string__foo__value]
290+
// Also wrap optional fields in Option[T]
188291
for (CodegenProperty var : cm.vars) {
189292
if (var.dataType != null && var.dataType.contains("Record_")) {
190293
var.dataType = fixRecordTypeReferences(var.dataType);
191294
}
192295
if (var.datatypeWithEnum != null && var.datatypeWithEnum.contains("Record_")) {
193296
var.datatypeWithEnum = fixRecordTypeReferences(var.datatypeWithEnum);
194297
}
298+
299+
// Check if the field name was changed from the original (baseName)
300+
// This happens for fields like "_id" which are renamed to "id"
301+
// But we need to exclude cases where the name is just escaped with backticks
302+
// (e.g., "from" becomes "`from`" because it's a reserved word)
303+
if (var.baseName != null && !var.baseName.equals(var.name)) {
304+
// Check if this is just a reserved word escaping (name is `baseName`)
305+
String escapedName = "`" + var.baseName + "`";
306+
if (!var.name.equals(escapedName)) {
307+
// This is a real rename, not just escaping
308+
var.vendorExtensions.put("x-json-name", var.baseName);
309+
hasCustomJsonNames = true;
310+
}
311+
}
312+
313+
// Wrap optional (non-required) or nullable fields in Option[T]
314+
// For non-enum fields only (enums are handled specially in the template)
315+
if ((!var.required || var.isNullable) && !var.isReadOnly && !var.isEnum) {
316+
String baseType = var.dataType;
317+
if (baseType != null && !baseType.startsWith("Option[")) {
318+
var.dataType = "Option[" + baseType + "]";
319+
if (var.datatypeWithEnum != null) {
320+
var.datatypeWithEnum = "Option[" + var.datatypeWithEnum + "]";
321+
}
322+
}
323+
}
324+
325+
// For enum fields, set x-is-optional if they are not required
326+
if (var.isEnum && (!var.required || var.isNullable)) {
327+
var.vendorExtensions.put("x-is-optional", true);
328+
}
329+
330+
// Always set x-is-optional based on the final dataType (for non-enum fields)
331+
// This ensures consistency between type declaration and JSON handling
332+
if (!var.isEnum && var.dataType != null && var.dataType.startsWith("Option[")) {
333+
var.vendorExtensions.put("x-is-optional", true);
334+
}
335+
}
336+
337+
// Mark the model as needing custom JSON deserialization if any fields have custom names
338+
if (hasCustomJsonNames) {
339+
cm.vendorExtensions.put("x-has-custom-json-names", true);
195340
}
196341
}
197342

@@ -213,7 +358,7 @@ private String fixRecordTypeReferences(String typeString) {
213358

214359
// Match Record_ followed by any characters until end or comma/bracket
215360
Pattern pattern = Pattern.compile("Record_[a-z_]+");
216-
java.util.regex.Matcher matcher = pattern.matcher(result);
361+
Matcher matcher = pattern.matcher(result);
217362

218363
StringBuffer sb = new StringBuffer();
219364
while (matcher.find()) {
@@ -311,7 +456,106 @@ private String normalizeSchemaName(String name) {
311456
public CodegenModel fromModel(String name, Schema schema) {
312457
// Normalize the schema name before any processing
313458
name = normalizeSchemaName(name);
314-
return super.fromModel(name, schema);
459+
CodegenModel mdl = super.fromModel(name, schema);
460+
461+
// Detect integer enums - check both the schema type and the dataType
462+
if (mdl.isEnum) {
463+
String schemaType = schema != null ? schema.getType() : null;
464+
if ("integer".equals(schemaType) || "int".equals(mdl.dataType) || "int64".equals(mdl.dataType)) {
465+
mdl.vendorExtensions.put("x-is-integer-enum", true);
466+
}
467+
}
468+
469+
// Handle oneOf/anyOf schemas to use Nim object variants
470+
if (mdl.getComposedSchemas() != null) {
471+
if (mdl.getComposedSchemas().getOneOf() != null && !mdl.getComposedSchemas().getOneOf().isEmpty()) {
472+
mdl.vendorExtensions.put("x-is-one-of", true);
473+
processComposedSchemaVariants(mdl, mdl.getComposedSchemas().getOneOf(), schema);
474+
} else if (mdl.getComposedSchemas().getAnyOf() != null && !mdl.getComposedSchemas().getAnyOf().isEmpty()) {
475+
mdl.vendorExtensions.put("x-is-any-of", true);
476+
processComposedSchemaVariants(mdl, mdl.getComposedSchemas().getAnyOf(), schema);
477+
}
478+
}
479+
480+
return mdl;
481+
}
482+
483+
/**
484+
* Process oneOf/anyOf schemas to generate proper variant names for Nim object variants.
485+
*/
486+
private void processComposedSchemaVariants(CodegenModel mdl, List<CodegenProperty> variants, Schema schema) {
487+
List<CodegenProperty> newVariants = new ArrayList<>();
488+
List<Schema> schemas = ModelUtils.getInterfaces(schema);
489+
490+
if (variants.size() != schemas.size()) {
491+
LOGGER.warn("Variant size does not match schema interfaces size for model " + mdl.name);
492+
return;
493+
}
494+
495+
for (int i = 0; i < variants.size(); i++) {
496+
CodegenProperty variant = variants.get(i);
497+
Schema variantSchema = schemas.get(i);
498+
499+
// Create a clone to avoid modifying the original
500+
CodegenProperty newVariant = variant.clone();
501+
502+
// Sanitize baseName to remove underscores and properly format for Nim
503+
if (newVariant.baseName != null) {
504+
// Remove trailing underscores and convert to proper format
505+
String sanitizedBase = newVariant.baseName.replaceAll("_+$", ""); // Remove trailing underscores
506+
if (sanitizedBase.length() > 0 && Character.isUpperCase(sanitizedBase.charAt(0))) {
507+
newVariant.baseName = toModelName(sanitizedBase);
508+
} else {
509+
newVariant.baseName = sanitizeNimIdentifier(sanitizedBase);
510+
}
511+
}
512+
513+
// Sanitize dataType to remove underscores and properly format for Nim
514+
// For model types (not primitives), use toModelName to get the proper type name
515+
if (newVariant.dataType != null) {
516+
// Check if this is a model type (starts with uppercase) vs primitive
517+
if (newVariant.dataType.length() > 0 && Character.isUpperCase(newVariant.dataType.charAt(0))) {
518+
// This is likely a model type, use toModelName to properly format it
519+
newVariant.dataType = toModelName(newVariant.dataType);
520+
} else {
521+
// Primitive type, just sanitize
522+
newVariant.dataType = sanitizeNimIdentifier(newVariant.dataType);
523+
}
524+
}
525+
if (newVariant.datatypeWithEnum != null) {
526+
if (newVariant.datatypeWithEnum.length() > 0 && Character.isUpperCase(newVariant.datatypeWithEnum.charAt(0))) {
527+
newVariant.datatypeWithEnum = toModelName(newVariant.datatypeWithEnum);
528+
} else {
529+
newVariant.datatypeWithEnum = sanitizeNimIdentifier(newVariant.datatypeWithEnum);
530+
}
531+
}
532+
533+
// Set variant name based on schema reference or type
534+
if (variantSchema.get$ref() != null && !variantSchema.get$ref().isEmpty()) {
535+
String refName = ModelUtils.getSimpleRef(variantSchema.get$ref());
536+
if (refName != null) {
537+
newVariant.setName(toModelName(refName));
538+
newVariant.setBaseName(refName);
539+
}
540+
} else if (variantSchema.getType() != null) {
541+
// For primitive types or inline schemas
542+
String typeName = variantSchema.getType();
543+
if (variantSchema.getTitle() != null && !variantSchema.getTitle().isEmpty()) {
544+
typeName = variantSchema.getTitle();
545+
}
546+
newVariant.setName(camelize(typeName));
547+
newVariant.setBaseName(typeName);
548+
}
549+
550+
newVariants.add(newVariant);
551+
}
552+
553+
// Replace the original variants with the processed ones
554+
if (mdl.getComposedSchemas().getOneOf() != null) {
555+
mdl.getComposedSchemas().setOneOf(newVariants);
556+
} else if (mdl.getComposedSchemas().getAnyOf() != null) {
557+
mdl.getComposedSchemas().setAnyOf(newVariants);
558+
}
315559
}
316560

317561
@Override

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

Lines changed: 6 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -18,10 +18,7 @@ const basepath = "{{{basePath}}}"
1818
template constructResult[T](response: Response): untyped =
1919
if response.code in {Http200, Http201, Http202, Http204, Http206}:
2020
try:
21-
when name(stripGenericParams(T.typedesc).typedesc) == name(Table):
22-
(some(json.to(parseJson(response.body), T.typedesc)), response)
23-
else:
24-
(some(marshal.to[T](response.body)), response)
21+
(some(to(parseJson(response.body), T)), response)
2522
except JsonParsingError:
2623
# The server returned a malformed response though the response code is 2XX
2724
# TODO: need better error handling
@@ -37,9 +34,11 @@ proc {{{operationId}}}*(httpClient: HttpClient{{#allParams}}, {{{paramName}}}: {
3734
httpClient.headers["Content-Type"] = "application/x-www-form-urlencoded"{{/isMultipart}}{{#isMultipart}}
3835
httpClient.headers["Content-Type"] = "multipart/form-data"{{/isMultipart}}{{/hasFormParams}}{{#hasHeaderParams}}{{#headerParams}}
3936
httpClient.headers["{{{baseName}}}"] = {{{paramName}}}{{#isArray}}.join(","){{/isArray}}{{/headerParams}}{{#description}} ## {{{.}}}{{/description}}{{/hasHeaderParams}}{{#hasQueryParams}}
40-
let url_encoded_query_params = encodeQuery([{{#queryParams}}
41-
("{{{baseName}}}", ${{{paramName}}}{{#isArray}}.join(","){{/isArray}}), # {{{description}}}{{/queryParams}}
42-
]){{/hasQueryParams}}{{#hasFormParams}}{{^isMultipart}}
37+
var query_params_list: seq[(string, string)] = @[]{{#queryParams}}{{#required}}
38+
query_params_list.add(("{{{baseName}}}", ${{{paramName}}}{{#isArray}}.join(","){{/isArray}})){{/required}}{{^required}}
39+
if {{#isArray}}{{{paramName}}}.len > 0{{/isArray}}{{^isArray}}${{{paramName}}} != ""{{/isArray}}:
40+
query_params_list.add(("{{{baseName}}}", ${{{paramName}}}{{#isArray}}.join(","){{/isArray}})){{/required}}{{/queryParams}}
41+
let url_encoded_query_params = encodeQuery(query_params_list){{/hasQueryParams}}{{#hasFormParams}}{{^isMultipart}}
4342
let form_data = encodeQuery([{{#formParams}}
4443
("{{{baseName}}}", ${{{paramName}}}{{#isArray}}.join(","){{/isArray}}), # {{{description}}}{{/formParams}}
4544
]){{/isMultipart}}{{#isMultipart}}

0 commit comments

Comments
 (0)