Skip to content

Commit ad612fa

Browse files
committed
Only include required parameters or non-empty optional parameters in URLs, support oneOf/anyOf via nim object variants, optional field support
1 parent f8c3172 commit ad612fa

3 files changed

Lines changed: 135 additions & 10 deletions

File tree

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

Lines changed: 87 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,10 @@ public NimClientCodegen() {
7878
.excludeSchemaSupportFeatures(
7979
SchemaSupportFeature.Polymorphism
8080
)
81+
.includeSchemaSupportFeatures(
82+
SchemaSupportFeature.oneOf,
83+
SchemaSupportFeature.anyOf
84+
)
8185
.excludeParameterFeatures(
8286
ParameterFeature.Cookie
8387
)
@@ -172,6 +176,7 @@ public NimClientCodegen() {
172176
typeMapping.put("AnyType", "JsonNode");
173177
}
174178

179+
175180
@Override
176181
public ModelsMap postProcessModels(ModelsMap objs) {
177182
objs = postProcessModelsEnum(objs);
@@ -185,13 +190,26 @@ public ModelsMap postProcessModels(ModelsMap objs) {
185190

186191
// Fix dataType fields that contain underscored type names
187192
// This handles cases like Table[string, Record_string__foo__value]
193+
// Also wrap optional fields in Option[T]
188194
for (CodegenProperty var : cm.vars) {
189195
if (var.dataType != null && var.dataType.contains("Record_")) {
190196
var.dataType = fixRecordTypeReferences(var.dataType);
191197
}
192198
if (var.datatypeWithEnum != null && var.datatypeWithEnum.contains("Record_")) {
193199
var.datatypeWithEnum = fixRecordTypeReferences(var.datatypeWithEnum);
194200
}
201+
202+
// 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;
205+
if (baseType != null && !baseType.startsWith("Option[")) {
206+
var.dataType = "Option[" + baseType + "]";
207+
if (var.datatypeWithEnum != null) {
208+
var.datatypeWithEnum = "Option[" + var.datatypeWithEnum + "]";
209+
}
210+
var.vendorExtensions.put("x-is-optional", true);
211+
}
212+
}
195213
}
196214
}
197215

@@ -311,7 +329,75 @@ private String normalizeSchemaName(String name) {
311329
public CodegenModel fromModel(String name, Schema schema) {
312330
// Normalize the schema name before any processing
313331
name = normalizeSchemaName(name);
314-
return super.fromModel(name, schema);
332+
CodegenModel mdl = super.fromModel(name, schema);
333+
334+
// Handle oneOf/anyOf schemas to use Nim object variants
335+
if (mdl.getComposedSchemas() != null) {
336+
if (mdl.getComposedSchemas().getOneOf() != null && !mdl.getComposedSchemas().getOneOf().isEmpty()) {
337+
mdl.vendorExtensions.put("x-is-one-of", true);
338+
processComposedSchemaVariants(mdl, mdl.getComposedSchemas().getOneOf(), schema);
339+
} else if (mdl.getComposedSchemas().getAnyOf() != null && !mdl.getComposedSchemas().getAnyOf().isEmpty()) {
340+
mdl.vendorExtensions.put("x-is-any-of", true);
341+
processComposedSchemaVariants(mdl, mdl.getComposedSchemas().getAnyOf(), schema);
342+
}
343+
}
344+
345+
return mdl;
346+
}
347+
348+
/**
349+
* Process oneOf/anyOf schemas to generate proper variant names for Nim object variants.
350+
*/
351+
private void processComposedSchemaVariants(CodegenModel mdl, List<CodegenProperty> variants, Schema schema) {
352+
List<CodegenProperty> newVariants = new ArrayList<>();
353+
List<Schema> schemas = ModelUtils.getInterfaces(schema);
354+
355+
if (variants.size() != schemas.size()) {
356+
LOGGER.warn("Variant size does not match schema interfaces size for model " + mdl.name);
357+
return;
358+
}
359+
360+
for (int i = 0; i < variants.size(); i++) {
361+
CodegenProperty variant = variants.get(i);
362+
Schema variantSchema = schemas.get(i);
363+
364+
// Create a clone to avoid modifying the original
365+
CodegenProperty newVariant = variant.clone();
366+
367+
// Sanitize dataType to remove trailing underscores (Nim doesn't allow them)
368+
if (newVariant.dataType != null) {
369+
newVariant.dataType = sanitizeNimIdentifier(newVariant.dataType);
370+
}
371+
if (newVariant.datatypeWithEnum != null) {
372+
newVariant.datatypeWithEnum = sanitizeNimIdentifier(newVariant.datatypeWithEnum);
373+
}
374+
375+
// Set variant name based on schema reference or type
376+
if (variantSchema.get$ref() != null && !variantSchema.get$ref().isEmpty()) {
377+
String refName = ModelUtils.getSimpleRef(variantSchema.get$ref());
378+
if (refName != null) {
379+
newVariant.setName(toModelName(refName));
380+
newVariant.setBaseName(refName);
381+
}
382+
} else if (variantSchema.getType() != null) {
383+
// For primitive types or inline schemas
384+
String typeName = variantSchema.getType();
385+
if (variantSchema.getTitle() != null && !variantSchema.getTitle().isEmpty()) {
386+
typeName = variantSchema.getTitle();
387+
}
388+
newVariant.setName(camelize(typeName));
389+
newVariant.setBaseName(typeName);
390+
}
391+
392+
newVariants.add(newVariant);
393+
}
394+
395+
// Replace the original variants with the processed ones
396+
if (mdl.getComposedSchemas().getOneOf() != null) {
397+
mdl.getComposedSchemas().setOneOf(newVariants);
398+
} else if (mdl.getComposedSchemas().getAnyOf() != null) {
399+
mdl.getComposedSchemas().setAnyOf(newVariants);
400+
}
315401
}
316402

317403
@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}}

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

Lines changed: 42 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
{{>header}}
22
import json
33
import tables
4+
import marshal
5+
import options
46

57
{{#imports}}import {{import}}
68
{{/imports}}{{#models}}{{#model}}{{#isEnum}}
@@ -14,7 +16,45 @@ func `%`*(v: {{{classname}}}): JsonNode =
1416
func `$`*(v: {{{classname}}}): string =
1517
result = case v:{{#allowableValues}}{{#enumVars}}
1618
of {{{classname}}}.{{{name}}}: $({{{value}}}){{/enumVars}}{{/allowableValues}}
17-
{{/isEnum}}{{^isEnum}}{{#vars}}{{#isEnum}}
19+
{{/isEnum}}{{^isEnum}}{{#vendorExtensions.x-is-one-of}}
20+
# OneOf type
21+
type {{{classname}}}Kind* {.pure.} = enum{{#composedSchemas.oneOf}}
22+
{{{name}}}Variant{{/composedSchemas.oneOf}}
23+
24+
type {{{classname}}}* = object
25+
## {{{description}}}
26+
case kind*: {{{classname}}}Kind{{#composedSchemas.oneOf}}
27+
of {{{classname}}}Kind.{{{name}}}Variant:
28+
{{{baseName}}}Value*: {{{dataType}}}{{/composedSchemas.oneOf}}
29+
30+
proc to*(node: JsonNode, T: typedesc[{{{classname}}}]): {{{classname}}} =
31+
## Custom deserializer for oneOf type - tries each variant{{#composedSchemas.oneOf}}
32+
try:
33+
return {{{classname}}}(kind: {{{classname}}}Kind.{{{name}}}Variant, {{{baseName}}}Value: json.to(node, {{{dataType}}}))
34+
except Exception as e:
35+
when defined(debug):
36+
echo "Failed to deserialize as {{{dataType}}}: ", e.msg{{/composedSchemas.oneOf}}
37+
raise newException(ValueError, "Unable to deserialize into any variant of {{{classname}}}. JSON: " & $node)
38+
{{/vendorExtensions.x-is-one-of}}{{#vendorExtensions.x-is-any-of}}
39+
# AnyOf type
40+
type {{{classname}}}Kind* {.pure.} = enum{{#composedSchemas.anyOf}}
41+
{{{name}}}Variant{{/composedSchemas.anyOf}}
42+
43+
type {{{classname}}}* = object
44+
## {{{description}}}
45+
case kind*: {{{classname}}}Kind{{#composedSchemas.anyOf}}
46+
of {{{classname}}}Kind.{{{name}}}Variant:
47+
{{{baseName}}}Value*: {{{dataType}}}{{/composedSchemas.anyOf}}
48+
49+
proc to*(node: JsonNode, T: typedesc[{{{classname}}}]): {{{classname}}} =
50+
## Custom deserializer for anyOf type - tries each variant{{#composedSchemas.anyOf}}
51+
try:
52+
return {{{classname}}}(kind: {{{classname}}}Kind.{{{name}}}Variant, {{{baseName}}}Value: json.to(node, {{{dataType}}}))
53+
except Exception as e:
54+
when defined(debug):
55+
echo "Failed to deserialize as {{{dataType}}}: ", e.msg{{/composedSchemas.anyOf}}
56+
raise newException(ValueError, "Unable to deserialize into any variant of {{{classname}}}. JSON: " & $node)
57+
{{/vendorExtensions.x-is-any-of}}{{^vendorExtensions.x-is-one-of}}{{^vendorExtensions.x-is-any-of}}{{#vars}}{{#isEnum}}
1858
type {{{enumName}}}* {.pure.} = enum{{#allowableValues}}{{#enumVars}}
1959
{{{name}}}{{/enumVars}}{{/allowableValues}}
2060
{{/isEnum}}{{/vars}}
@@ -29,4 +69,4 @@ func `%`*(v: {{{enumName}}}): JsonNode =
2969
func `$`*(v: {{{enumName}}}): string =
3070
result = case v:{{#allowableValues}}{{#enumVars}}
3171
of {{{enumName}}}.{{{name}}}: $({{{value}}}){{/enumVars}}{{/allowableValues}}
32-
{{/isEnum}}{{/vars}}{{/isEnum}}{{/model}}{{/models}}
72+
{{/isEnum}}{{/vars}}{{/vendorExtensions.x-is-any-of}}{{/vendorExtensions.x-is-one-of}}{{/isEnum}}{{/model}}{{/models}}

0 commit comments

Comments
 (0)