3434
3535import java .io .File ;
3636import java .util .*;
37+ import java .util .regex .Matcher ;
3738import java .util .regex .Pattern ;
3839
3940import 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
0 commit comments