Skip to content

Commit 16051dd

Browse files
committed
Improved handling of enums
1 parent f97c3bb commit 16051dd

19 files changed

Lines changed: 1026 additions & 25 deletions

File tree

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

Lines changed: 166 additions & 8 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;
@@ -177,17 +178,113 @@ public NimClientCodegen() {
177178
}
178179

179180

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+
180269
@Override
181270
public ModelsMap postProcessModels(ModelsMap objs) {
182271
objs = postProcessModelsEnum(objs);
183272

184-
// Mark top-level string enums for proper enum generation in template
185273
for (ModelMap mo : objs.getModels()) {
186274
CodegenModel cm = mo.getModel();
275+
187276
if (cm.isEnum && cm.allowableValues != null && cm.allowableValues.containsKey("enumVars")) {
188277
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+
}
189283
}
190284

285+
// Check if any fields need custom JSON name mapping
286+
boolean hasCustomJsonNames = false;
287+
191288
// Fix dataType fields that contain underscored type names
192289
// This handles cases like Table[string, Record_string__foo__value]
193290
// Also wrap optional fields in Option[T]
@@ -199,17 +296,47 @@ public ModelsMap postProcessModels(ModelsMap objs) {
199296
var.datatypeWithEnum = fixRecordTypeReferences(var.datatypeWithEnum);
200297
}
201298

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+
202313
// Wrap optional (non-required) or nullable fields in Option[T]
203-
if ((!var.required || var.isNullable) && !var.isReadOnly) {
204-
String baseType = var.datatypeWithEnum != null ? var.datatypeWithEnum : var.dataType;
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;
205317
if (baseType != null && !baseType.startsWith("Option[")) {
206318
var.dataType = "Option[" + baseType + "]";
207319
if (var.datatypeWithEnum != null) {
208320
var.datatypeWithEnum = "Option[" + var.datatypeWithEnum + "]";
209321
}
210-
var.vendorExtensions.put("x-is-optional", true);
211322
}
212323
}
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);
213340
}
214341
}
215342

@@ -231,7 +358,7 @@ private String fixRecordTypeReferences(String typeString) {
231358

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

236363
StringBuffer sb = new StringBuffer();
237364
while (matcher.find()) {
@@ -331,6 +458,14 @@ public CodegenModel fromModel(String name, Schema schema) {
331458
name = normalizeSchemaName(name);
332459
CodegenModel mdl = super.fromModel(name, schema);
333460

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+
334469
// Handle oneOf/anyOf schemas to use Nim object variants
335470
if (mdl.getComposedSchemas() != null) {
336471
if (mdl.getComposedSchemas().getOneOf() != null && !mdl.getComposedSchemas().getOneOf().isEmpty()) {
@@ -364,12 +499,35 @@ private void processComposedSchemaVariants(CodegenModel mdl, List<CodegenPropert
364499
// Create a clone to avoid modifying the original
365500
CodegenProperty newVariant = variant.clone();
366501

367-
// Sanitize dataType to remove trailing underscores (Nim doesn't allow them)
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
368515
if (newVariant.dataType != null) {
369-
newVariant.dataType = sanitizeNimIdentifier(newVariant.dataType);
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+
}
370524
}
371525
if (newVariant.datatypeWithEnum != null) {
372-
newVariant.datatypeWithEnum = sanitizeNimIdentifier(newVariant.datatypeWithEnum);
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+
}
373531
}
374532

375533
// Set variant name based on schema reference or type

0 commit comments

Comments
 (0)