diff --git a/docs/generators/nim.md b/docs/generators/nim.md index d61882b2bc94..e7947d0874d1 100644 --- a/docs/generators/nim.md +++ b/docs/generators/nim.md @@ -226,8 +226,8 @@ These options may be applied as additional-properties (cli) or configOptions (pl |Polymorphism|✗|OAS2,OAS3 |Union|✗|OAS3 |allOf|✗|OAS2,OAS3 -|anyOf|✗|OAS3 -|oneOf|✗|OAS3 +|anyOf|✓|OAS3 +|oneOf|✓|OAS3 |not|✗|OAS3 ### Security Feature diff --git a/modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/NimClientCodegen.java b/modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/NimClientCodegen.java index 25501b96122f..8e13aea833e3 100644 --- a/modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/NimClientCodegen.java +++ b/modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/NimClientCodegen.java @@ -34,6 +34,8 @@ import java.io.File; import java.util.*; +import java.util.regex.Matcher; +import java.util.regex.Pattern; import static org.openapitools.codegen.utils.CamelizeOption.LOWERCASE_FIRST_LETTER; import static org.openapitools.codegen.utils.StringUtils.camelize; @@ -77,6 +79,10 @@ public NimClientCodegen() { .excludeSchemaSupportFeatures( SchemaSupportFeature.Polymorphism ) + .includeSchemaSupportFeatures( + SchemaSupportFeature.oneOf, + SchemaSupportFeature.anyOf + ) .excludeParameterFeatures( ParameterFeature.Cookie ) @@ -167,11 +173,203 @@ public NimClientCodegen() { typeMapping.put("DateTime", "string"); typeMapping.put("password", "string"); typeMapping.put("file", "string"); + typeMapping.put("object", "JsonNode"); + typeMapping.put("AnyType", "JsonNode"); + } + + + @Override + public Map postProcessAllModels(Map allModels) { + allModels = super.postProcessAllModels(allModels); + + // First pass: identify all models that have fields with custom JSON names + Set modelsWithCustomJson = new HashSet<>(); + + for (Map.Entry entry : allModels.entrySet()) { + ModelsMap modelsMap = entry.getValue(); + for (ModelMap mo : modelsMap.getModels()) { + CodegenModel cm = mo.getModel(); + + // Check if this model has fields with custom JSON names + for (CodegenProperty var : cm.vars) { + if (var.vendorExtensions.containsKey("x-json-name")) { + modelsWithCustomJson.add(cm.classname); + break; + } + } + } + } + + // Second pass: cascade custom JSON handling to parent models and mark array fields + // We need multiple passes to handle transitive dependencies + boolean changed = true; + while (changed) { + changed = false; + for (Map.Entry entry : allModels.entrySet()) { + ModelsMap modelsMap = entry.getValue(); + for (ModelMap mo : modelsMap.getModels()) { + CodegenModel cm = mo.getModel(); + + // Check if any field's type needs custom JSON and mark array fields appropriately + for (CodegenProperty var : cm.vars) { + String fieldType = var.complexType != null ? var.complexType : var.baseType; + + // Handle arrays - check if the inner type has custom JSON + if (var.isArray && var.items != null) { + String innerType = var.items.complexType != null ? var.items.complexType : var.items.baseType; + if (innerType != null && modelsWithCustomJson.contains(innerType)) { + // Mark this array field as containing types with custom JSON + var.vendorExtensions.put("x-is-array-with-custom-json", "true"); + var.vendorExtensions.put("x-array-inner-type", innerType); + } + fieldType = innerType; + } + + // Cascade custom JSON to parent model if not already marked + if (fieldType != null && modelsWithCustomJson.contains(fieldType)) { + if (!cm.vendorExtensions.containsKey("x-has-custom-json-names")) { + cm.vendorExtensions.put("x-has-custom-json-names", true); + modelsWithCustomJson.add(cm.classname); + changed = true; + } + } + } + } + } + } + + return allModels; + } + + /** + * Strips surrounding quotes from integer enum values. + * The base OpenAPI Generator stores all enum values as quoted strings (e.g., "0", "1", "2") + * regardless of the enum's actual type. For Nim integer enums, we need the raw numbers + * without quotes so they serialize correctly: %(0) instead of %("0") + */ + private void stripQuotesFromIntegerEnumValues(Map allowableValues) { + if (allowableValues == null || !allowableValues.containsKey("enumVars")) { + return; + } + + @SuppressWarnings("unchecked") + List> enumVars = (List>) allowableValues.get("enumVars"); + for (Map enumVar : enumVars) { + Object value = enumVar.get("value"); + if (value instanceof String) { + String strValue = (String) value; + // Remove surrounding quotes if present + if (strValue.startsWith("\"") && strValue.endsWith("\"")) { + enumVar.put("value", strValue.substring(1, strValue.length() - 1)); + } + } + } } @Override public ModelsMap postProcessModels(ModelsMap objs) { - return postProcessModelsEnum(objs); + objs = postProcessModelsEnum(objs); + + for (ModelMap mo : objs.getModels()) { + CodegenModel cm = mo.getModel(); + + if (cm.isEnum && cm.allowableValues != null && cm.allowableValues.containsKey("enumVars")) { + cm.vendorExtensions.put("x-is-top-level-enum", true); + + // For integer enums, strip quotes from enum values + if (cm.vendorExtensions.containsKey("x-is-integer-enum")) { + stripQuotesFromIntegerEnumValues(cm.allowableValues); + } + } + + // Check if any fields need custom JSON name mapping + boolean hasCustomJsonNames = false; + + // Fix dataType fields that contain underscored type names + // This handles cases like Table[string, Record_string__foo__value] + // Also wrap optional fields in Option[T] + for (CodegenProperty var : cm.vars) { + if (var.dataType != null && var.dataType.contains("Record_")) { + var.dataType = fixRecordTypeReferences(var.dataType); + } + if (var.datatypeWithEnum != null && var.datatypeWithEnum.contains("Record_")) { + var.datatypeWithEnum = fixRecordTypeReferences(var.datatypeWithEnum); + } + + // Check if the field name was changed from the original (baseName) + // This happens for fields like "_id" which are renamed to "id" + // But we need to exclude cases where the name is just escaped with backticks + // (e.g., "from" becomes "`from`" because it's a reserved word) + if (var.baseName != null && !var.baseName.equals(var.name)) { + // Check if this is just a reserved word escaping (name is `baseName`) + String escapedName = "`" + var.baseName + "`"; + if (!var.name.equals(escapedName)) { + // This is a real rename, not just escaping + var.vendorExtensions.put("x-json-name", var.baseName); + hasCustomJsonNames = true; + } + } + + // Wrap optional (non-required) or nullable fields in Option[T] + // For non-enum fields only (enums are handled specially in the template) + if ((!var.required || var.isNullable) && !var.isReadOnly && !var.isEnum) { + String baseType = var.dataType; + if (baseType != null && !baseType.startsWith("Option[")) { + var.dataType = "Option[" + baseType + "]"; + if (var.datatypeWithEnum != null) { + var.datatypeWithEnum = "Option[" + var.datatypeWithEnum + "]"; + } + } + } + + // For enum fields, set x-is-optional if they are not required + if (var.isEnum && (!var.required || var.isNullable)) { + var.vendorExtensions.put("x-is-optional", true); + } + + // Always set x-is-optional based on the final dataType (for non-enum fields) + // This ensures consistency between type declaration and JSON handling + if (!var.isEnum && var.dataType != null && var.dataType.startsWith("Option[")) { + var.vendorExtensions.put("x-is-optional", true); + } + } + + // Mark the model as needing custom JSON deserialization if any fields have custom names + if (hasCustomJsonNames) { + cm.vendorExtensions.put("x-has-custom-json-names", true); + } + } + + return objs; + } + + /** + * Fix underscored Record type references in dataType strings. + * Converts Record_string__foo___value to RecordStringFooValue. + */ + private String fixRecordTypeReferences(String typeString) { + if (typeString == null || !typeString.contains("Record_")) { + return typeString; + } + + // Pattern to match Record_string_... type names with underscores + // These are embedded in strings like: Table[string, Record_string__foo__value] + String result = typeString; + + // Match Record_ followed by any characters until end or comma/bracket + Pattern pattern = Pattern.compile("Record_[a-z_]+"); + Matcher matcher = pattern.matcher(result); + + StringBuffer sb = new StringBuffer(); + while (matcher.find()) { + String matched = matcher.group(); + // Camelize the matched Record type name + String camelized = camelize(matched); + matcher.appendReplacement(sb, camelized); + } + matcher.appendTail(sb); + + return sb.toString(); } @Override @@ -192,6 +390,8 @@ public void processOpts() { apiPackage = File.separator + packageName + File.separator + "apis"; modelPackage = File.separator + packageName + File.separator + "models"; supportingFiles.add(new SupportingFile("lib.mustache", "", packageName + ".nim")); + supportingFiles.add(new SupportingFile("model_any_type.mustache", packageName + File.separator + "models", "model_any_type.nim")); + supportingFiles.add(new SupportingFile("model_object.mustache", packageName + File.separator + "models", "model_object.nim")); } @Override @@ -215,11 +415,13 @@ public String escapeUnsafeCharacters(String input) { @Override public String toModelImport(String name) { + name = normalizeSchemaName(name); name = name.replaceAll("-", "_"); + if (importMapping.containsKey(name)) { - return "model_" + StringUtils.underscore(importMapping.get(name)); + return sanitizeNimIdentifier("model_" + StringUtils.underscore(importMapping.get(name))); } else { - return "model_" + StringUtils.underscore(name); + return sanitizeNimIdentifier("model_" + StringUtils.underscore(name)); } } @@ -227,22 +429,153 @@ public String toModelImport(String name) { public String toApiImport(String name) { name = name.replaceAll("-", "_"); if (importMapping.containsKey(name)) { - return "api_" + StringUtils.underscore(importMapping.get(name)); + return sanitizeNimIdentifier("api_" + StringUtils.underscore(importMapping.get(name))); } else { - return "api_" + StringUtils.underscore(name); + return sanitizeNimIdentifier("api_" + StringUtils.underscore(name)); } } + /** + * Normalize schema names to ensure consistency across filename, import, and type name generation. + * This is called early in the pipeline so downstream methods work with consistent names. + */ + private String normalizeSchemaName(String name) { + if (name == null) { + return null; + } + // Remove underscores around and before digits (HTTP status codes, version numbers, etc.) + // e.g., "GetComments_200_response" -> "GetComments200response" + // e.g., "Config_anyOf_1" -> "ConfiganyOf1" + // This ensures consistent handling whether the name comes with or without underscores + name = name.replaceAll("_(\\d+)_", "$1"); // Underscores on both sides + name = name.replaceAll("_(\\d+)$", "$1"); // Trailing underscore before digits + return name; + } + + @Override + public CodegenModel fromModel(String name, Schema schema) { + // Normalize the schema name before any processing + name = normalizeSchemaName(name); + CodegenModel mdl = super.fromModel(name, schema); + + // Detect integer enums - check both the schema type and the dataType + if (mdl.isEnum) { + String schemaType = schema != null ? schema.getType() : null; + if ("integer".equals(schemaType) || "int".equals(mdl.dataType) || "int64".equals(mdl.dataType)) { + mdl.vendorExtensions.put("x-is-integer-enum", true); + } + } + + // Handle oneOf/anyOf schemas to use Nim object variants + if (mdl.getComposedSchemas() != null) { + if (mdl.getComposedSchemas().getOneOf() != null && !mdl.getComposedSchemas().getOneOf().isEmpty()) { + mdl.vendorExtensions.put("x-is-one-of", true); + processComposedSchemaVariants(mdl, mdl.getComposedSchemas().getOneOf(), schema); + } else if (mdl.getComposedSchemas().getAnyOf() != null && !mdl.getComposedSchemas().getAnyOf().isEmpty()) { + mdl.vendorExtensions.put("x-is-any-of", true); + processComposedSchemaVariants(mdl, mdl.getComposedSchemas().getAnyOf(), schema); + } + } + + return mdl; + } + + /** + * Process oneOf/anyOf schemas to generate proper variant names for Nim object variants. + */ + private void processComposedSchemaVariants(CodegenModel mdl, List variants, Schema schema) { + List newVariants = new ArrayList<>(); + List schemas = ModelUtils.getInterfaces(schema); + + if (variants.size() != schemas.size()) { + LOGGER.warn("Variant size does not match schema interfaces size for model " + mdl.name); + return; + } + + for (int i = 0; i < variants.size(); i++) { + CodegenProperty variant = variants.get(i); + Schema variantSchema = schemas.get(i); + + // Create a clone to avoid modifying the original + CodegenProperty newVariant = variant.clone(); + + // Sanitize baseName to remove underscores and properly format for Nim + if (newVariant.baseName != null) { + // Remove trailing underscores and convert to proper format + String sanitizedBase = newVariant.baseName.replaceAll("_+$", ""); // Remove trailing underscores + if (sanitizedBase.length() > 0 && Character.isUpperCase(sanitizedBase.charAt(0))) { + newVariant.baseName = toModelName(sanitizedBase); + } else { + newVariant.baseName = sanitizeNimIdentifier(sanitizedBase); + } + } + + // Sanitize dataType to remove underscores and properly format for Nim + // For model types (not primitives), use toModelName to get the proper type name + if (newVariant.dataType != null) { + // Check if this is a model type (starts with uppercase) vs primitive + if (newVariant.dataType.length() > 0 && Character.isUpperCase(newVariant.dataType.charAt(0))) { + // This is likely a model type, use toModelName to properly format it + newVariant.dataType = toModelName(newVariant.dataType); + } else { + // Primitive type, just sanitize + newVariant.dataType = sanitizeNimIdentifier(newVariant.dataType); + } + } + if (newVariant.datatypeWithEnum != null) { + if (newVariant.datatypeWithEnum.length() > 0 && Character.isUpperCase(newVariant.datatypeWithEnum.charAt(0))) { + newVariant.datatypeWithEnum = toModelName(newVariant.datatypeWithEnum); + } else { + newVariant.datatypeWithEnum = sanitizeNimIdentifier(newVariant.datatypeWithEnum); + } + } + + // Set variant name based on schema reference or type + if (variantSchema.get$ref() != null && !variantSchema.get$ref().isEmpty()) { + String refName = ModelUtils.getSimpleRef(variantSchema.get$ref()); + if (refName != null) { + newVariant.setName(toModelName(refName)); + newVariant.setBaseName(refName); + } + } else if (variantSchema.getType() != null) { + // For primitive types or inline schemas + String typeName = variantSchema.getType(); + if (variantSchema.getTitle() != null && !variantSchema.getTitle().isEmpty()) { + typeName = variantSchema.getTitle(); + } + newVariant.setName(camelize(typeName)); + newVariant.setBaseName(typeName); + } + + newVariants.add(newVariant); + } + + // Replace the original variants with the processed ones + if (mdl.getComposedSchemas().getOneOf() != null) { + mdl.getComposedSchemas().setOneOf(newVariants); + } else if (mdl.getComposedSchemas().getAnyOf() != null) { + mdl.getComposedSchemas().setAnyOf(newVariants); + } + } + + @Override + public String toModelName(String name) { + // Name should be normalized by fromModel, but normalize again for safety + name = normalizeSchemaName(name); + return camelize(sanitizeName(name)); + } + @Override public String toModelFilename(String name) { + name = normalizeSchemaName(name); name = name.replaceAll("-", "_"); - return "model_" + StringUtils.underscore(name); + return sanitizeNimIdentifier("model_" + StringUtils.underscore(name)); } @Override public String toApiFilename(String name) { name = name.replaceAll("-", "_"); - return "api_" + StringUtils.underscore(name); + return sanitizeNimIdentifier("api_" + StringUtils.underscore(name)); } @Override @@ -262,6 +595,12 @@ public OperationsMap postProcessOperationsWithModels(OperationsMap objs, List operations = objectMap.getOperation(); for (CodegenOperation operation : operations) { operation.httpMethod = operation.httpMethod.toLowerCase(Locale.ROOT); + + // Set custom flag for DELETE operations with body to use different template logic + // Nim's httpClient.delete() doesn't support a body parameter + if ("delete".equals(operation.httpMethod) && operation.getHasBodyParam()) { + operation.vendorExtensions.put("x-nim-delete-with-body", true); + } } return objs; @@ -360,6 +699,24 @@ private boolean isValidIdentifier(String identifier) { return identifier.matches("^(?:[A-Z]|[a-z]|[\\x80-\\xff])(_?(?:[A-Z]|[a-z]|[\\x80-\\xff]|[0-9]))*$"); } + /** + * Sanitize a Nim identifier by removing trailing underscores and collapsing multiple underscores. + * Nim does not allow identifiers to end with underscores. + * + * @param name the identifier to sanitize + * @return the sanitized identifier + */ + private String sanitizeNimIdentifier(String name) { + if (name == null || name.isEmpty()) { + return name; + } + // Remove trailing underscores (Nim identifiers cannot end with underscore) + name = name.replaceAll("_+$", ""); + // Collapse multiple consecutive underscores to single underscore + name = name.replaceAll("_+", "_"); + return name; + } + @Override public String toEnumVarName(String name, String datatype) { name = name.replace(" ", "_"); diff --git a/modules/openapi-generator/src/main/resources/nim-client/api.mustache b/modules/openapi-generator/src/main/resources/nim-client/api.mustache index 77d403284dc8..4836dc962c47 100644 --- a/modules/openapi-generator/src/main/resources/nim-client/api.mustache +++ b/modules/openapi-generator/src/main/resources/nim-client/api.mustache @@ -18,10 +18,7 @@ const basepath = "{{{basePath}}}" template constructResult[T](response: Response): untyped = if response.code in {Http200, Http201, Http202, Http204, Http206}: try: - when name(stripGenericParams(T.typedesc).typedesc) == name(Table): - (some(json.to(parseJson(response.body), T.typedesc)), response) - else: - (some(marshal.to[T](response.body)), response) + (some(to(parseJson(response.body), T)), response) except JsonParsingError: # The server returned a malformed response though the response code is 2XX # TODO: need better error handling @@ -37,9 +34,11 @@ proc {{{operationId}}}*(httpClient: HttpClient{{#allParams}}, {{{paramName}}}: { httpClient.headers["Content-Type"] = "application/x-www-form-urlencoded"{{/isMultipart}}{{#isMultipart}} httpClient.headers["Content-Type"] = "multipart/form-data"{{/isMultipart}}{{/hasFormParams}}{{#hasHeaderParams}}{{#headerParams}} httpClient.headers["{{{baseName}}}"] = {{{paramName}}}{{#isArray}}.join(","){{/isArray}}{{/headerParams}}{{#description}} ## {{{.}}}{{/description}}{{/hasHeaderParams}}{{#hasQueryParams}} - let url_encoded_query_params = encodeQuery([{{#queryParams}} - ("{{{baseName}}}", ${{{paramName}}}{{#isArray}}.join(","){{/isArray}}), # {{{description}}}{{/queryParams}} - ]){{/hasQueryParams}}{{#hasFormParams}}{{^isMultipart}} + var query_params_list: seq[(string, string)] = @[]{{#queryParams}}{{#required}} + query_params_list.add(("{{{baseName}}}", ${{{paramName}}}{{#isArray}}.join(","){{/isArray}})){{/required}}{{^required}} + if {{#isArray}}{{{paramName}}}.len > 0{{/isArray}}{{^isArray}}${{{paramName}}} != ""{{/isArray}}: + query_params_list.add(("{{{baseName}}}", ${{{paramName}}}{{#isArray}}.join(","){{/isArray}})){{/required}}{{/queryParams}} + let url_encoded_query_params = encodeQuery(query_params_list){{/hasQueryParams}}{{#hasFormParams}}{{^isMultipart}} let form_data = encodeQuery([{{#formParams}} ("{{{baseName}}}", ${{{paramName}}}{{#isArray}}.join(","){{/isArray}}), # {{{description}}}{{/formParams}} ]){{/isMultipart}}{{#isMultipart}} @@ -47,9 +46,15 @@ proc {{{operationId}}}*(httpClient: HttpClient{{#allParams}}, {{{paramName}}}: { {{#formParams}} "{{{baseName}}}": ${{{paramName}}}{{#isArray}}.join(","){{/isArray}}, # {{{description}}} {{/formParams}} }){{/isMultipart}}{{/hasFormParams}}{{#returnType}} - +{{#vendorExtensions.x-nim-delete-with-body}} + let response = httpClient.request(basepath & {{^pathParams}}"{{{path}}}"{{/pathParams}}{{#hasPathParams}}fmt"{{{path}}}"{{/hasPathParams}}{{#hasQueryParams}} & "?" & url_encoded_query_params{{/hasQueryParams}}, httpMethod = HttpDelete{{#bodyParams}}, body = $(%{{{paramName}}}){{/bodyParams}}) +{{/vendorExtensions.x-nim-delete-with-body}}{{^vendorExtensions.x-nim-delete-with-body}} 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}}) - constructResult[{{{returnType}}}](response){{/returnType}}{{^returnType}} - 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}} +{{/vendorExtensions.x-nim-delete-with-body}} + constructResult[{{{returnType}}}](response){{/returnType}}{{^returnType}}{{#vendorExtensions.x-nim-delete-with-body}} + httpClient.request(basepath & {{^pathParams}}"{{{path}}}"{{/pathParams}}{{#hasPathParams}}fmt"{{{path}}}"{{/hasPathParams}}{{#hasQueryParams}} & "?" & url_encoded_query_params{{/hasQueryParams}}, httpMethod = HttpDelete{{#bodyParams}}, body = $(%{{{paramName}}}){{/bodyParams}}) +{{/vendorExtensions.x-nim-delete-with-body}}{{^vendorExtensions.x-nim-delete-with-body}} + 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}}) +{{/vendorExtensions.x-nim-delete-with-body}}{{/returnType}} {{/operation}}{{/operations}} \ No newline at end of file diff --git a/modules/openapi-generator/src/main/resources/nim-client/model.mustache b/modules/openapi-generator/src/main/resources/nim-client/model.mustache index 3fd6f13594dd..ead20075c5b2 100644 --- a/modules/openapi-generator/src/main/resources/nim-client/model.mustache +++ b/modules/openapi-generator/src/main/resources/nim-client/model.mustache @@ -1,23 +1,184 @@ {{>header}} import json import tables +import marshal +import options {{#imports}}import {{import}} -{{/imports}}{{#models}}{{#model}}{{#vars}}{{#isEnum}} +{{/imports}}{{#models}}{{#model}}{{#isEnum}} +type {{{classname}}}* {.pure.} = enum{{#allowableValues}}{{#enumVars}} + {{{name}}}{{/enumVars}}{{/allowableValues}} + +{{#vendorExtensions.x-is-integer-enum}} +func `%`*(v: {{{classname}}}): JsonNode = + result = case v:{{#allowableValues}}{{#enumVars}} + of {{{classname}}}.{{{name}}}: %({{{value}}}){{/enumVars}}{{/allowableValues}} + +{{/vendorExtensions.x-is-integer-enum}} +{{^vendorExtensions.x-is-integer-enum}} +func `%`*(v: {{{classname}}}): JsonNode = + result = case v:{{#allowableValues}}{{#enumVars}} + of {{{classname}}}.{{{name}}}: %{{{value}}}{{/enumVars}}{{/allowableValues}} + +{{/vendorExtensions.x-is-integer-enum}} +func `$`*(v: {{{classname}}}): string = + result = case v:{{#allowableValues}}{{#enumVars}} + of {{{classname}}}.{{{name}}}: $({{{value}}}){{/enumVars}}{{/allowableValues}} +{{#vendorExtensions.x-is-integer-enum}} +proc to*(node: JsonNode, T: typedesc[{{{classname}}}]): {{{classname}}} = + if node.kind != JInt: + raise newException(ValueError, "Expected integer for enum {{{classname}}}, got " & $node.kind) + let intVal = node.getInt() + case intVal:{{#allowableValues}}{{#enumVars}} + of {{{value}}}: + return {{{classname}}}.{{{name}}}{{/enumVars}}{{/allowableValues}} + else: + raise newException(ValueError, "Invalid enum value for {{{classname}}}: " & $intVal) +{{/vendorExtensions.x-is-integer-enum}}{{^vendorExtensions.x-is-integer-enum}} +proc to*(node: JsonNode, T: typedesc[{{{classname}}}]): {{{classname}}} = + if node.kind != JString: + raise newException(ValueError, "Expected string for enum {{{classname}}}, got " & $node.kind) + let strVal = node.getStr() + case strVal:{{#allowableValues}}{{#enumVars}} + of $({{{value}}}): + return {{{classname}}}.{{{name}}}{{/enumVars}}{{/allowableValues}} + else: + raise newException(ValueError, "Invalid enum value for {{{classname}}}: " & strVal) +{{/vendorExtensions.x-is-integer-enum}} +{{/isEnum}}{{^isEnum}}{{#vendorExtensions.x-is-one-of}} +# OneOf type +type {{{classname}}}Kind* {.pure.} = enum{{#composedSchemas.oneOf}} + {{{name}}}Variant{{/composedSchemas.oneOf}} + +type {{{classname}}}* = object + ## {{{description}}} + case kind*: {{{classname}}}Kind{{#composedSchemas.oneOf}} + of {{{classname}}}Kind.{{{name}}}Variant: + {{{baseName}}}Value*: {{{dataType}}}{{/composedSchemas.oneOf}} + +proc to*(node: JsonNode, T: typedesc[{{{classname}}}]): {{{classname}}} = + ## Custom deserializer for oneOf type - tries each variant{{#composedSchemas.oneOf}} + try: + return {{{classname}}}(kind: {{{classname}}}Kind.{{{name}}}Variant, {{{baseName}}}Value: to(node, {{{dataType}}})) + except Exception as e: + when defined(debug): + echo "Failed to deserialize as {{{dataType}}}: ", e.msg{{/composedSchemas.oneOf}} + raise newException(ValueError, "Unable to deserialize into any variant of {{{classname}}}. JSON: " & $node) +{{/vendorExtensions.x-is-one-of}}{{#vendorExtensions.x-is-any-of}} +# AnyOf type +type {{{classname}}}Kind* {.pure.} = enum{{#composedSchemas.anyOf}} + {{{name}}}Variant{{/composedSchemas.anyOf}} + +type {{{classname}}}* = object + ## {{{description}}} + case kind*: {{{classname}}}Kind{{#composedSchemas.anyOf}} + of {{{classname}}}Kind.{{{name}}}Variant: + {{{baseName}}}Value*: {{{dataType}}}{{/composedSchemas.anyOf}} + +proc to*(node: JsonNode, T: typedesc[{{{classname}}}]): {{{classname}}} = + ## Custom deserializer for anyOf type - tries each variant{{#composedSchemas.anyOf}} + try: + return {{{classname}}}(kind: {{{classname}}}Kind.{{{name}}}Variant, {{{baseName}}}Value: to(node, {{{dataType}}})) + except Exception as e: + when defined(debug): + echo "Failed to deserialize as {{{dataType}}}: ", e.msg{{/composedSchemas.anyOf}} + raise newException(ValueError, "Unable to deserialize into any variant of {{{classname}}}. JSON: " & $node) +{{/vendorExtensions.x-is-any-of}}{{^vendorExtensions.x-is-one-of}}{{^vendorExtensions.x-is-any-of}}{{#vars}}{{#isEnum}} type {{{enumName}}}* {.pure.} = enum{{#allowableValues}}{{#enumVars}} {{{name}}}{{/enumVars}}{{/allowableValues}} {{/isEnum}}{{/vars}} type {{{classname}}}* = object ## {{{description}}}{{#vars}} - {{{name}}}*: {{#isEnum}}{{{enumName}}}{{/isEnum}}{{^isEnum}}{{{dataType}}}{{/isEnum}}{{#description}} ## {{{.}}}{{/description}}{{/vars}} -{{#vars}}{{#isEnum}} + {{{name}}}*: {{#isEnum}}{{#vendorExtensions.x-is-optional}}Option[{{{enumName}}}]{{/vendorExtensions.x-is-optional}}{{^vendorExtensions.x-is-optional}}{{{enumName}}}{{/vendorExtensions.x-is-optional}}{{/isEnum}}{{^isEnum}}{{{dataType}}}{{/isEnum}}{{#description}} ## {{{.}}}{{/description}}{{/vars}} +{{#vars}}{{#isEnum}}{{#vendorExtensions.x-is-integer-enum}} func `%`*(v: {{{enumName}}}): JsonNode = - let str = case v:{{#allowableValues}}{{#enumVars}} - of {{{enumName}}}.{{{name}}}: {{{value}}}{{/enumVars}}{{/allowableValues}} - - JsonNode(kind: JString, str: str) - + result = case v:{{#allowableValues}}{{#enumVars}} + of {{{enumName}}}.{{{name}}}: %({{{value}}}){{/enumVars}}{{/allowableValues}} +{{/vendorExtensions.x-is-integer-enum}}{{^vendorExtensions.x-is-integer-enum}} +func `%`*(v: {{{enumName}}}): JsonNode = + result = case v:{{#allowableValues}}{{#enumVars}} + of {{{enumName}}}.{{{name}}}: %{{{value}}}{{/enumVars}}{{/allowableValues}} +{{/vendorExtensions.x-is-integer-enum}} func `$`*(v: {{{enumName}}}): string = result = case v:{{#allowableValues}}{{#enumVars}} - of {{{enumName}}}.{{{name}}}: {{{value}}}{{/enumVars}}{{/allowableValues}} -{{/isEnum}}{{/vars}}{{/model}}{{/models}} \ No newline at end of file + of {{{enumName}}}.{{{name}}}: $({{{value}}}){{/enumVars}}{{/allowableValues}} +{{#vendorExtensions.x-is-integer-enum}} +proc to*(node: JsonNode, T: typedesc[{{{enumName}}}]): {{{enumName}}} = + if node.kind != JInt: + raise newException(ValueError, "Expected integer for enum {{{enumName}}}, got " & $node.kind) + let intVal = node.getInt() + case intVal:{{#allowableValues}}{{#enumVars}} + of {{{value}}}: + return {{{enumName}}}.{{{name}}}{{/enumVars}}{{/allowableValues}} + else: + raise newException(ValueError, "Invalid enum value for {{{enumName}}}: " & $intVal) +{{/vendorExtensions.x-is-integer-enum}}{{^vendorExtensions.x-is-integer-enum}} +proc to*(node: JsonNode, T: typedesc[{{{enumName}}}]): {{{enumName}}} = + if node.kind != JString: + raise newException(ValueError, "Expected string for enum {{{enumName}}}, got " & $node.kind) + let strVal = node.getStr() + case strVal:{{#allowableValues}}{{#enumVars}} + of $({{{value}}}): + return {{{enumName}}}.{{{name}}}{{/enumVars}}{{/allowableValues}} + else: + raise newException(ValueError, "Invalid enum value for {{{enumName}}}: " & strVal) +{{/vendorExtensions.x-is-integer-enum}} +{{/isEnum}}{{/vars}}{{#vendorExtensions.x-has-custom-json-names}} + +# Custom JSON deserialization for {{{classname}}} with custom field names +proc to*(node: JsonNode, T: typedesc[{{{classname}}}]): {{{classname}}} = + result = {{{classname}}}() + if node.kind == JObject:{{#vars}}{{#vendorExtensions.x-json-name}}{{#vendorExtensions.x-is-optional}} + if node.hasKey("{{{vendorExtensions.x-json-name}}}") and node["{{{vendorExtensions.x-json-name}}}"].kind != JNull:{{#vendorExtensions.x-is-array-with-custom-json}} + # Optional array of types with custom JSON - manually iterate and deserialize + let arrayNode = node["{{{vendorExtensions.x-json-name}}}"] + if arrayNode.kind == JArray: + var arr: seq[{{{vendorExtensions.x-array-inner-type}}}] = @[] + for item in arrayNode.items: + arr.add(to(item, {{{vendorExtensions.x-array-inner-type}}})) + result.{{{name}}} = some(arr){{/vendorExtensions.x-is-array-with-custom-json}}{{^vendorExtensions.x-is-array-with-custom-json}}{{#vendorExtensions.x-is-external-enum}} + result.{{{name}}} = some({{{vendorExtensions.x-enum-module}}}.to(node["{{{vendorExtensions.x-json-name}}}"], {{{vendorExtensions.x-enum-type}}})){{/vendorExtensions.x-is-external-enum}}{{^vendorExtensions.x-is-external-enum}}{{#isEnum}} + result.{{{name}}} = some(to(node["{{{vendorExtensions.x-json-name}}}"], {{{enumName}}})){{/isEnum}}{{^isEnum}} + result.{{{name}}} = some(to(node["{{{vendorExtensions.x-json-name}}}"], typeof(result.{{{name}}}.get()))){{/isEnum}}{{/vendorExtensions.x-is-external-enum}}{{/vendorExtensions.x-is-array-with-custom-json}}{{/vendorExtensions.x-is-optional}}{{^vendorExtensions.x-is-optional}} + if node.hasKey("{{{vendorExtensions.x-json-name}}}"):{{#vendorExtensions.x-is-array-with-custom-json}} + # Array of types with custom JSON - manually iterate and deserialize + let arrayNode = node["{{{vendorExtensions.x-json-name}}}"] + if arrayNode.kind == JArray: + result.{{{name}}} = @[] + for item in arrayNode.items: + result.{{{name}}}.add(to(item, {{{vendorExtensions.x-array-inner-type}}})){{/vendorExtensions.x-is-array-with-custom-json}}{{^vendorExtensions.x-is-array-with-custom-json}}{{#vendorExtensions.x-is-external-enum}} + result.{{{name}}} = {{{vendorExtensions.x-enum-module}}}.to(node["{{{vendorExtensions.x-json-name}}}"], {{{vendorExtensions.x-enum-type}}}){{/vendorExtensions.x-is-external-enum}}{{^vendorExtensions.x-is-external-enum}}{{#isEnum}} + result.{{{name}}} = to(node["{{{vendorExtensions.x-json-name}}}"], {{{enumName}}}){{/isEnum}}{{^isEnum}} + result.{{{name}}} = to(node["{{{vendorExtensions.x-json-name}}}"], {{{dataType}}}){{/isEnum}}{{/vendorExtensions.x-is-external-enum}}{{/vendorExtensions.x-is-array-with-custom-json}}{{/vendorExtensions.x-is-optional}}{{/vendorExtensions.x-json-name}}{{^vendorExtensions.x-json-name}}{{#vendorExtensions.x-is-optional}} + if node.hasKey("{{{baseName}}}") and node["{{{baseName}}}"].kind != JNull:{{#vendorExtensions.x-is-array-with-custom-json}} + # Optional array of types with custom JSON - manually iterate and deserialize + let arrayNode = node["{{{baseName}}}"] + if arrayNode.kind == JArray: + var arr: seq[{{{vendorExtensions.x-array-inner-type}}}] = @[] + for item in arrayNode.items: + arr.add(to(item, {{{vendorExtensions.x-array-inner-type}}})) + result.{{{name}}} = some(arr){{/vendorExtensions.x-is-array-with-custom-json}}{{^vendorExtensions.x-is-array-with-custom-json}}{{#vendorExtensions.x-is-external-enum}} + result.{{{name}}} = some({{{vendorExtensions.x-enum-module}}}.to(node["{{{baseName}}}"], {{{vendorExtensions.x-enum-type}}})){{/vendorExtensions.x-is-external-enum}}{{^vendorExtensions.x-is-external-enum}}{{#isEnum}} + result.{{{name}}} = some(to(node["{{{baseName}}}"], {{{enumName}}})){{/isEnum}}{{^isEnum}} + result.{{{name}}} = some(to(node["{{{baseName}}}"], typeof(result.{{{name}}}.get()))){{/isEnum}}{{/vendorExtensions.x-is-external-enum}}{{/vendorExtensions.x-is-array-with-custom-json}}{{/vendorExtensions.x-is-optional}}{{^vendorExtensions.x-is-optional}} + if node.hasKey("{{{baseName}}}"):{{#vendorExtensions.x-is-array-with-custom-json}} + # Array of types with custom JSON - manually iterate and deserialize + let arrayNode = node["{{{baseName}}}"] + if arrayNode.kind == JArray: + result.{{{name}}} = @[] + for item in arrayNode.items: + result.{{{name}}}.add(to(item, {{{vendorExtensions.x-array-inner-type}}})){{/vendorExtensions.x-is-array-with-custom-json}}{{^vendorExtensions.x-is-array-with-custom-json}}{{#vendorExtensions.x-is-external-enum}} + result.{{{name}}} = {{{vendorExtensions.x-enum-module}}}.to(node["{{{baseName}}}"], {{{vendorExtensions.x-enum-type}}}){{/vendorExtensions.x-is-external-enum}}{{^vendorExtensions.x-is-external-enum}}{{#isEnum}} + result.{{{name}}} = to(node["{{{baseName}}}"], {{{enumName}}}){{/isEnum}}{{^isEnum}} + result.{{{name}}} = to(node["{{{baseName}}}"], {{{dataType}}}){{/isEnum}}{{/vendorExtensions.x-is-external-enum}}{{/vendorExtensions.x-is-array-with-custom-json}}{{/vendorExtensions.x-is-optional}}{{/vendorExtensions.x-json-name}}{{/vars}} + +# Custom JSON serialization for {{{classname}}} with custom field names +proc `%`*(obj: {{{classname}}}): JsonNode = + result = newJObject(){{#vars}}{{#vendorExtensions.x-json-name}}{{#vendorExtensions.x-is-optional}} + if obj.{{{name}}}.isSome(): + result["{{{vendorExtensions.x-json-name}}}"] = %obj.{{{name}}}.get(){{/vendorExtensions.x-is-optional}}{{^vendorExtensions.x-is-optional}} + result["{{{vendorExtensions.x-json-name}}}"] = %obj.{{{name}}}{{/vendorExtensions.x-is-optional}}{{/vendorExtensions.x-json-name}}{{^vendorExtensions.x-json-name}}{{#vendorExtensions.x-is-optional}} + if obj.{{{name}}}.isSome(): + result["{{{baseName}}}"] = %obj.{{{name}}}.get(){{/vendorExtensions.x-is-optional}}{{^vendorExtensions.x-is-optional}} + result["{{{baseName}}}"] = %obj.{{{name}}}{{/vendorExtensions.x-is-optional}}{{/vendorExtensions.x-json-name}}{{/vars}} +{{/vendorExtensions.x-has-custom-json-names}}{{/vendorExtensions.x-is-any-of}}{{/vendorExtensions.x-is-one-of}}{{/isEnum}}{{/model}}{{/models}} diff --git a/modules/openapi-generator/src/main/resources/nim-client/model_any_type.mustache b/modules/openapi-generator/src/main/resources/nim-client/model_any_type.mustache new file mode 100644 index 000000000000..f2a066d16fb2 --- /dev/null +++ b/modules/openapi-generator/src/main/resources/nim-client/model_any_type.mustache @@ -0,0 +1,6 @@ +{{>header}} +import json + +# AnyType represents any JSON value +# This is used for fields that can contain arbitrary JSON data +type AnyType* = JsonNode diff --git a/modules/openapi-generator/src/main/resources/nim-client/model_object.mustache b/modules/openapi-generator/src/main/resources/nim-client/model_object.mustache new file mode 100644 index 000000000000..690d9160277f --- /dev/null +++ b/modules/openapi-generator/src/main/resources/nim-client/model_object.mustache @@ -0,0 +1,7 @@ +{{>header}} +import json +import tables + +# Object represents an arbitrary JSON object +# Using JsonNode instead of the 'object' keyword to avoid Nim keyword conflicts +type Object* = JsonNode diff --git a/modules/openapi-generator/src/test/java/org/openapitools/codegen/nim/NimClientCodegenTest.java b/modules/openapi-generator/src/test/java/org/openapitools/codegen/nim/NimClientCodegenTest.java index b817ad80a8d7..bda478fbd325 100644 --- a/modules/openapi-generator/src/test/java/org/openapitools/codegen/nim/NimClientCodegenTest.java +++ b/modules/openapi-generator/src/test/java/org/openapitools/codegen/nim/NimClientCodegenTest.java @@ -1,6 +1,7 @@ package org.openapitools.codegen.nim; -import org.openapitools.codegen.CodegenConstants; +import io.swagger.v3.oas.models.media.ObjectSchema; +import org.openapitools.codegen.*; import org.openapitools.codegen.languages.NimClientCodegen; import org.testng.Assert; import org.testng.annotations.Test; @@ -35,4 +36,123 @@ public void testAdditionalPropertiesPutForConfigValues() throws Exception { Assert.assertEquals(codegen.additionalProperties().get(CodegenConstants.HIDE_GENERATION_TIMESTAMP), Boolean.FALSE); Assert.assertEquals(codegen.isHideGenerationTimestamp(), false); } + + @Test + public void testUnderscoresEdgeCases() throws Exception { + final NimClientCodegen codegen = new NimClientCodegen(); + + // Test model filename with trailing underscores + String result = codegen.toModelFilename("Record_string__before_string_or_null__after_string_or_null___value"); + Assert.assertFalse(result.endsWith("_"), "Model filename should not end with underscore: " + result); + + // Test model filename with multiple consecutive underscores + result = codegen.toModelFilename("Record_string_string_or_number__value"); + Assert.assertFalse(result.endsWith("_"), "Model filename should not end with underscore: " + result); + + // Verify no consecutive underscores remain (except the required prefix) + Assert.assertFalse(result.contains("__"), "Model filename should not contain consecutive underscores: " + result); + + // Test model import with trailing underscores + result = codegen.toModelImport("Record_string__before_string_or_null__after_string_or_null___value"); + Assert.assertFalse(result.endsWith("_"), "Model import should not end with underscore: " + result); + + // Test model import with multiple consecutive underscores + result = codegen.toModelImport("Record_string_string_or_number__value"); + Assert.assertFalse(result.endsWith("_"), "Model import should not end with underscore: " + result); + + // Test API filename with trailing underscores + result = codegen.toApiFilename("SomeApi_"); + Assert.assertFalse(result.endsWith("_"), "API filename should not end with underscore: " + result); + + // Test API import with trailing underscores + result = codegen.toApiImport("SomeApi_"); + Assert.assertFalse(result.endsWith("_"), "API import should not end with underscore: " + result); + } + + @Test + public void testSanitizationPreservesNormalNames() throws Exception { + final NimClientCodegen codegen = new NimClientCodegen(); + + // Verify that normal names without trailing underscores are not changed + String result = codegen.toModelFilename("UserData"); + Assert.assertTrue(result.startsWith("model_"), "Model filename should start with model_"); + Assert.assertFalse(result.endsWith("_"), "Model filename should not end with underscore"); + + result = codegen.toApiFilename("DefaultApi"); + Assert.assertTrue(result.startsWith("api_"), "API filename should start with api_"); + Assert.assertFalse(result.endsWith("_"), "API filename should not end with underscore"); + } + + @Test + public void testObjectTypeMapping() throws Exception { + final NimClientCodegen codegen = new NimClientCodegen(); + + // Test that object type is mapped to JsonNode to avoid Nim keyword conflict + ObjectSchema objectSchema = new ObjectSchema(); + String result = codegen.getTypeDeclaration(objectSchema); + + // object types without properties should map to JsonNode + Assert.assertEquals(result, "JsonNode", + "Free-form object type should map to JsonNode to avoid Nim 'object' keyword conflict"); + } + + @Test + public void testTypeNameConsistency() throws Exception { + final NimClientCodegen codegen = new NimClientCodegen(); + + // Test that response type names don't have underscores between number and text + String result = codegen.toModelName("GetComments_200_response"); + Assert.assertFalse(result.contains("_200_"), "Type name should not contain _200_: " + result); + Assert.assertTrue(result.contains("200"), "Type name should contain 200: " + result); + + // The filename should also be consistent + String filename = codegen.toModelFilename("GetComments_200_response"); + String importName = codegen.toModelImport("GetComments_200_response"); + + // Extract the type name from the filename and import + String filenameTypePart = filename.replace("model_", ""); + String importTypePart = importName.replace("model_", ""); + + Assert.assertEquals(filenameTypePart, importTypePart, + "Filename and import should reference the same type"); + } + + @Test + public void testImportConsistencyAfterProcessing() throws Exception { + final NimClientCodegen codegen = new NimClientCodegen(); + + // Simulate what happens during model processing: + // 1. Model is created with original schema name + String schemaName = "AddDomainConfig_200_response_anyOf"; + String typeName = codegen.toModelName(schemaName); // Camelizes to AddDomainConfig200ResponseAnyOf + String filename = codegen.toModelFilename(schemaName); // Creates model_add_domain_config_200_response_any_of + + // 2. When another model imports this type, it uses the camelized type name + String importFromTypeName = codegen.toModelImport(typeName); + + // The import should match the filename (or at least be loadable) + Assert.assertEquals(filename, importFromTypeName, + "Import generated from type name should match filename generated from schema name"); + } + + @Test + public void testNormalizeSchemaName() throws Exception { + final NimClientCodegen codegen = new NimClientCodegen(); + + // Test that schema names with _200_ are normalized + String result = codegen.toModelName("GetComments_200_response"); + Assert.assertEquals(result, "GetComments200response", + "Should normalize _200_ to 200: " + result); + + // Test that schema names with trailing _1 are normalized + result = codegen.toModelName("Config_anyOf_1"); + Assert.assertEquals(result, "ConfigAnyOf1", + "Should normalize _1 to 1: " + result); + + // Verify consistency between filename and import + String filename = codegen.toModelFilename("GetComments_200_response"); + String importPath = codegen.toModelImport("GetComments200response"); + Assert.assertEquals(filename, importPath, + "Filename and import should match after normalization"); + } } diff --git a/modules/openapi-generator/src/test/resources/3_0/nim/petstore.yaml b/modules/openapi-generator/src/test/resources/3_0/nim/petstore.yaml index a8f9809a1249..23c6f0fe4e8c 100644 --- a/modules/openapi-generator/src/test/resources/3_0/nim/petstore.yaml +++ b/modules/openapi-generator/src/test/resources/3_0/nim/petstore.yaml @@ -286,6 +286,162 @@ paths: description: file to upload type: string format: binary + '/pet/{petId}/favorite': + delete: + tags: + - pet + summary: Remove pet from favorites (tests DELETE with body) + description: 'Tests that DELETE operations with request bodies generate correct Nim code' + operationId: unfavoritePet + parameters: + - name: petId + in: path + description: ID of pet to unfavorite + required: true + schema: + type: integer + format: int64 + requestBody: + required: true + content: + application/json: + schema: + type: object + properties: + reason: + type: string + description: Reason for unfavoriting + responses: + '200': + description: successful operation + content: + application/json: + schema: + $ref: '#/components/schemas/GetPetStats_200_response' + security: + - petstore_auth: + - 'write:pets' + /pet/stats: + get: + tags: + - pet + summary: Get pet statistics (tests _200_ response normalization) + operationId: getPetStats + responses: + '200': + description: successful operation + content: + application/json: + schema: + $ref: '#/components/schemas/GetPetStats_200_response' + /comments: + get: + tags: + - pet + summary: Get pet reviews (tests _id field mapping and arrays) + operationId: getPetReviews + responses: + '200': + description: successful operation + content: + application/json: + schema: + $ref: '#/components/schemas/PetReviewsResponse' + post: + tags: + - pet + summary: Add a pet review (tests _id field mapping) + operationId: addPetReview + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/PetReview' + responses: + '200': + description: successful operation + content: + application/json: + schema: + $ref: '#/components/schemas/PetReview' + /comments/search: + get: + tags: + - pet + summary: Search pet reviews (tests anyOf with underscores) + operationId: searchPetReviews + responses: + '200': + description: successful operation + content: + application/json: + schema: + $ref: '#/components/schemas/GetPetReviews_200_response' + /notifications: + get: + tags: + - pet + summary: Get pet alerts (tests integer enum) + operationId: getPetAlerts + responses: + '200': + description: successful operation + content: + application/json: + schema: + type: array + items: + $ref: '#/components/schemas/PetAlert' + post: + tags: + - pet + summary: Create pet alert + operationId: createPetAlert + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/PetAlert' + responses: + '200': + description: successful operation + content: + application/json: + schema: + $ref: '#/components/schemas/PetAlert' + /audit: + get: + tags: + - pet + summary: Get pet audit logs (combined test) + operationId: getPetAuditLogs + responses: + '200': + description: successful operation + content: + application/json: + schema: + type: array + items: + $ref: '#/components/schemas/PetAuditLog' + /ignored: + post: + tags: + - pet + summary: Mark as ignored (tests inline enum) + operationId: markIgnored + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/IgnoredResponse' + responses: + '200': + description: successful operation + content: + application/json: + schema: + $ref: '#/components/schemas/IgnoredResponse' /store/inventory: get: tags: @@ -739,3 +895,128 @@ components: type: string message: type: string + # Test _200_ normalization in response schema names + GetPetStats_200_response: + type: object + properties: + totalPets: + type: integer + status: + type: string + # Test trailing underscores in schema names + PetMetadata_: + type: object + properties: + metadata: + type: string + # Test Record_string__ type patterns (complex generic types) + Record_string__before_string_or_null__after_string_or_null___value: + type: object + properties: + before: + type: string + after: + type: string + PetPositions: + type: object + properties: + positions: + type: object + additionalProperties: + $ref: '#/components/schemas/Record_string__before_string_or_null__after_string_or_null___value' + # Test numeric enums + PetPriority: + type: integer + enum: [0, 1, 2] + # Test anyOf with _1 suffix + PetConfig_anyOf_1: + type: object + properties: + version: + type: integer + + # Fields named _id get renamed to id in Nim + PetReview: + type: object + properties: + _id: + type: string + description: MongoDB style ID field that should be renamed to id in Nim + author: + type: string + content: + type: string + timestamp: + type: string + format: date-time + + # Array deserialization with custom JSON + PetReviewsResponse: + type: object + properties: + reviews: + type: array + items: + $ref: '#/components/schemas/PetReview' + totalCount: + type: integer + + # Integer enum deserialization + PetAlertLevel: + type: integer + description: Alert priority level with integer values + enum: [0, 1, 2, 10, 99] + + PetAlert: + type: object + properties: + id: + type: string + level: + $ref: '#/components/schemas/PetAlertLevel' + message: + type: string + + # Composed schema type naming (anyOf/oneOf with underscores) + GetPetReviews_Response_WithPresence: + type: object + properties: + review: + type: string + + GetPetReviews_200_response: + anyOf: + - $ref: '#/components/schemas/GetPetReviews_Response_WithPresence' + - $ref: '#/components/schemas/ApiResponse' + + # Inline enum handling + IgnoredResponse: + type: object + properties: + reason: + type: string + note: + type: string + enum: ["0", "1", "2", "3"] + description: Inline enum that should use enum type name not base type + + # Combined test: Model with _id field, arrays, and enums + PetAuditLog: + type: object + properties: + _id: + type: string + description: Audit log unique identifier + action: + type: string + enum: ["create", "update", "delete"] + priority: + $ref: '#/components/schemas/PetAlertLevel' + from: + type: string + enum: ["api", "web", "mobile"] + nullable: true + reviews: + type: array + items: + $ref: '#/components/schemas/PetReview' diff --git a/samples/client/petstore/nim/.openapi-generator/FILES b/samples/client/petstore/nim/.openapi-generator/FILES index ff650e2b75ac..f8e488383817 100644 --- a/samples/client/petstore/nim/.openapi-generator/FILES +++ b/samples/client/petstore/nim/.openapi-generator/FILES @@ -4,10 +4,27 @@ petstore.nim petstore/apis/api_pet.nim petstore/apis/api_store.nim petstore/apis/api_user.nim +petstore/models/model_any_type.nim petstore/models/model_api_response.nim petstore/models/model_category.nim +petstore/models/model_get_pet_reviews200response.nim +petstore/models/model_get_pet_reviews_response_with_presence.nim +petstore/models/model_get_pet_stats200response.nim +petstore/models/model_ignored_response.nim +petstore/models/model_object.nim petstore/models/model_order.nim petstore/models/model_pet.nim +petstore/models/model_pet_alert.nim +petstore/models/model_pet_alert_level.nim +petstore/models/model_pet_audit_log.nim +petstore/models/model_pet_config_any_of1.nim +petstore/models/model_pet_metadata.nim +petstore/models/model_pet_positions.nim +petstore/models/model_pet_priority.nim +petstore/models/model_pet_review.nim +petstore/models/model_pet_reviews_response.nim +petstore/models/model_record_string_before_string_or_null_after_string_or_null_value.nim petstore/models/model_tag.nim +petstore/models/model_unfavorite_pet_request.nim petstore/models/model_user.nim sample_client.nim diff --git a/samples/client/petstore/nim/README.md b/samples/client/petstore/nim/README.md index 6e76be972b32..36390ae66000 100644 --- a/samples/client/petstore/nim/README.md +++ b/samples/client/petstore/nim/README.md @@ -26,10 +26,19 @@ All URIs are relative to *http://petstore.swagger.io/v2* Module | Proc | HTTP request | Description ------------ | ------------- | ------------- | ------------- api_pet | addPet | **POST** /pet | Add a new pet to the store +api_pet | addPetReview | **POST** /comments | Add a pet review (tests _id field mapping) +api_pet | createPetAlert | **POST** /notifications | Create pet alert api_pet | deletePet | **DELETE** /pet/{petId} | Deletes a pet api_pet | findPetsByStatus | **GET** /pet/findByStatus | Finds Pets by status api_pet | findPetsByTags | **GET** /pet/findByTags | Finds Pets by tags +api_pet | getPetAlerts | **GET** /notifications | Get pet alerts (tests integer enum) +api_pet | getPetAuditLogs | **GET** /audit | Get pet audit logs (combined test) api_pet | getPetById | **GET** /pet/{petId} | Find pet by ID +api_pet | getPetReviews | **GET** /comments | Get pet reviews (tests _id field mapping and arrays) +api_pet | getPetStats | **GET** /pet/stats | Get pet statistics (tests _200_ response normalization) +api_pet | markIgnored | **POST** /ignored | Mark as ignored (tests inline enum) +api_pet | searchPetReviews | **GET** /comments/search | Search pet reviews (tests anyOf with underscores) +api_pet | unfavoritePet | **DELETE** /pet/{petId}/favorite | Remove pet from favorites (tests DELETE with body) api_pet | updatePet | **PUT** /pet | Update an existing pet api_pet | updatePetWithForm | **POST** /pet/{petId} | Updates a pet in the store with form data api_pet | uploadFile | **POST** /pet/{petId}/uploadImage | uploads an image diff --git a/samples/client/petstore/nim/petstore.nim b/samples/client/petstore/nim/petstore.nim index 21f854f172b6..6a0b825c31d8 100644 --- a/samples/client/petstore/nim/petstore.nim +++ b/samples/client/petstore/nim/petstore.nim @@ -10,16 +10,46 @@ # Models import petstore/models/model_api_response import petstore/models/model_category +import petstore/models/model_get_pet_reviews200response +import petstore/models/model_get_pet_reviews_response_with_presence +import petstore/models/model_get_pet_stats200response +import petstore/models/model_ignored_response import petstore/models/model_order import petstore/models/model_pet +import petstore/models/model_pet_alert +import petstore/models/model_pet_alert_level +import petstore/models/model_pet_audit_log +import petstore/models/model_pet_config_any_of1 +import petstore/models/model_pet_metadata +import petstore/models/model_pet_positions +import petstore/models/model_pet_priority +import petstore/models/model_pet_review +import petstore/models/model_pet_reviews_response +import petstore/models/model_record_string_before_string_or_null_after_string_or_null_value import petstore/models/model_tag +import petstore/models/model_unfavorite_pet_request import petstore/models/model_user export model_api_response export model_category +export model_get_pet_reviews200response +export model_get_pet_reviews_response_with_presence +export model_get_pet_stats200response +export model_ignored_response export model_order export model_pet +export model_pet_alert +export model_pet_alert_level +export model_pet_audit_log +export model_pet_config_any_of1 +export model_pet_metadata +export model_pet_positions +export model_pet_priority +export model_pet_review +export model_pet_reviews_response +export model_record_string_before_string_or_null_after_string_or_null_value export model_tag +export model_unfavorite_pet_request export model_user # APIs diff --git a/samples/client/petstore/nim/petstore.out b/samples/client/petstore/nim/petstore.out new file mode 100755 index 000000000000..dac73aad37ae Binary files /dev/null and b/samples/client/petstore/nim/petstore.out differ diff --git a/samples/client/petstore/nim/petstore/apis/api_pet.nim b/samples/client/petstore/nim/petstore/apis/api_pet.nim index 8d53b553a3d3..0544760014ac 100644 --- a/samples/client/petstore/nim/petstore/apis/api_pet.nim +++ b/samples/client/petstore/nim/petstore/apis/api_pet.nim @@ -19,17 +19,22 @@ import typetraits import uri import ../models/model_api_response +import ../models/model_get_pet_reviews200response +import ../models/model_get_pet_stats200response +import ../models/model_ignored_response import ../models/model_pet +import ../models/model_pet_alert +import ../models/model_pet_audit_log +import ../models/model_pet_review +import ../models/model_pet_reviews_response +import ../models/model_unfavorite_pet_request const basepath = "http://petstore.swagger.io/v2" template constructResult[T](response: Response): untyped = if response.code in {Http200, Http201, Http202, Http204, Http206}: try: - when name(stripGenericParams(T.typedesc).typedesc) == name(Table): - (some(json.to(parseJson(response.body), T.typedesc)), response) - else: - (some(marshal.to[T](response.body)), response) + (some(to(parseJson(response.body), T)), response) except JsonParsingError: # The server returned a malformed response though the response code is 2XX # TODO: need better error handling @@ -47,17 +52,34 @@ proc addPet*(httpClient: HttpClient, pet: Pet): (Option[Pet], Response) = constructResult[Pet](response) +proc addPetReview*(httpClient: HttpClient, petReview: PetReview): (Option[PetReview], Response) = + ## Add a pet review (tests _id field mapping) + httpClient.headers["Content-Type"] = "application/json" + + let response = httpClient.post(basepath & "/comments", $(%petReview)) + constructResult[PetReview](response) + + +proc createPetAlert*(httpClient: HttpClient, petAlert: PetAlert): (Option[PetAlert], Response) = + ## Create pet alert + httpClient.headers["Content-Type"] = "application/json" + + let response = httpClient.post(basepath & "/notifications", $(%petAlert)) + constructResult[PetAlert](response) + + proc deletePet*(httpClient: HttpClient, petId: int64, apiKey: string): Response = ## Deletes a pet httpClient.headers["api_key"] = apiKey httpClient.delete(basepath & fmt"/pet/{petId}") + proc findPetsByStatus*(httpClient: HttpClient, status: seq[Status]): (Option[seq[Pet]], Response) = ## Finds Pets by status - let url_encoded_query_params = encodeQuery([ - ("status", $status.join(",")), # Status values that need to be considered for filter - ]) + var query_params_list: seq[(string, string)] = @[] + query_params_list.add(("status", $status.join(","))) + let url_encoded_query_params = encodeQuery(query_params_list) let response = httpClient.get(basepath & "/pet/findByStatus" & "?" & url_encoded_query_params) constructResult[seq[Pet]](response) @@ -65,14 +87,28 @@ proc findPetsByStatus*(httpClient: HttpClient, status: seq[Status]): (Option[seq proc findPetsByTags*(httpClient: HttpClient, tags: seq[string]): (Option[seq[Pet]], Response) {.deprecated.} = ## Finds Pets by tags - let url_encoded_query_params = encodeQuery([ - ("tags", $tags.join(",")), # Tags to filter by - ]) + var query_params_list: seq[(string, string)] = @[] + query_params_list.add(("tags", $tags.join(","))) + let url_encoded_query_params = encodeQuery(query_params_list) let response = httpClient.get(basepath & "/pet/findByTags" & "?" & url_encoded_query_params) constructResult[seq[Pet]](response) +proc getPetAlerts*(httpClient: HttpClient): (Option[seq[PetAlert]], Response) = + ## Get pet alerts (tests integer enum) + + let response = httpClient.get(basepath & "/notifications") + constructResult[seq[PetAlert]](response) + + +proc getPetAuditLogs*(httpClient: HttpClient): (Option[seq[PetAuditLog]], Response) = + ## Get pet audit logs (combined test) + + let response = httpClient.get(basepath & "/audit") + constructResult[seq[PetAuditLog]](response) + + proc getPetById*(httpClient: HttpClient, petId: int64): (Option[Pet], Response) = ## Find pet by ID @@ -80,6 +116,42 @@ proc getPetById*(httpClient: HttpClient, petId: int64): (Option[Pet], Response) constructResult[Pet](response) +proc getPetReviews*(httpClient: HttpClient): (Option[PetReviewsResponse], Response) = + ## Get pet reviews (tests _id field mapping and arrays) + + let response = httpClient.get(basepath & "/comments") + constructResult[PetReviewsResponse](response) + + +proc getPetStats*(httpClient: HttpClient): (Option[GetPetStats_200_response], Response) = + ## Get pet statistics (tests _200_ response normalization) + + let response = httpClient.get(basepath & "/pet/stats") + constructResult[GetPetStats_200_response](response) + + +proc markIgnored*(httpClient: HttpClient, ignoredResponse: IgnoredResponse): (Option[IgnoredResponse], Response) = + ## Mark as ignored (tests inline enum) + httpClient.headers["Content-Type"] = "application/json" + + let response = httpClient.post(basepath & "/ignored", $(%ignoredResponse)) + constructResult[IgnoredResponse](response) + + +proc searchPetReviews*(httpClient: HttpClient): (Option[GetPetReviews_200_response], Response) = + ## Search pet reviews (tests anyOf with underscores) + + let response = httpClient.get(basepath & "/comments/search") + constructResult[GetPetReviews_200_response](response) + + +proc unfavoritePet*(httpClient: HttpClient, petId: int64, unfavoritePetRequest: UnfavoritePetRequest): (Option[GetPetStats_200_response], Response) = + ## Remove pet from favorites (tests DELETE with body) + httpClient.headers["Content-Type"] = "application/json" + let response = httpClient.request(basepath & fmt"/pet/{petId}/favorite", httpMethod = HttpDelete, body = $(%unfavoritePetRequest)) + constructResult[GetPetStats_200_response](response) + + proc updatePet*(httpClient: HttpClient, pet: Pet): (Option[Pet], Response) = ## Update an existing pet httpClient.headers["Content-Type"] = "application/json" @@ -98,6 +170,7 @@ proc updatePetWithForm*(httpClient: HttpClient, petId: int64, name: string, stat httpClient.post(basepath & fmt"/pet/{petId}", $form_data) + proc uploadFile*(httpClient: HttpClient, petId: int64, additionalMetadata: string, file: string): (Option[ApiResponse], Response) = ## uploads an image httpClient.headers["Content-Type"] = "multipart/form-data" diff --git a/samples/client/petstore/nim/petstore/apis/api_store.nim b/samples/client/petstore/nim/petstore/apis/api_store.nim index a2f018b7cf3d..3a2aaf4be2af 100644 --- a/samples/client/petstore/nim/petstore/apis/api_store.nim +++ b/samples/client/petstore/nim/petstore/apis/api_store.nim @@ -25,10 +25,7 @@ const basepath = "http://petstore.swagger.io/v2" template constructResult[T](response: Response): untyped = if response.code in {Http200, Http201, Http202, Http204, Http206}: try: - when name(stripGenericParams(T.typedesc).typedesc) == name(Table): - (some(json.to(parseJson(response.body), T.typedesc)), response) - else: - (some(marshal.to[T](response.body)), response) + (some(to(parseJson(response.body), T)), response) except JsonParsingError: # The server returned a malformed response though the response code is 2XX # TODO: need better error handling @@ -43,6 +40,7 @@ proc deleteOrder*(httpClient: HttpClient, orderId: string): Response = httpClient.delete(basepath & fmt"/store/order/{orderId}") + proc getInventory*(httpClient: HttpClient): (Option[Table[string, int]], Response) = ## Returns pet inventories by status diff --git a/samples/client/petstore/nim/petstore/apis/api_user.nim b/samples/client/petstore/nim/petstore/apis/api_user.nim index dbc56457d946..52c5c5aa81a3 100644 --- a/samples/client/petstore/nim/petstore/apis/api_user.nim +++ b/samples/client/petstore/nim/petstore/apis/api_user.nim @@ -25,10 +25,7 @@ const basepath = "http://petstore.swagger.io/v2" template constructResult[T](response: Response): untyped = if response.code in {Http200, Http201, Http202, Http204, Http206}: try: - when name(stripGenericParams(T.typedesc).typedesc) == name(Table): - (some(json.to(parseJson(response.body), T.typedesc)), response) - else: - (some(marshal.to[T](response.body)), response) + (some(to(parseJson(response.body), T)), response) except JsonParsingError: # The server returned a malformed response though the response code is 2XX # TODO: need better error handling @@ -44,23 +41,27 @@ proc createUser*(httpClient: HttpClient, user: User): Response = httpClient.post(basepath & "/user", $(%user)) + proc createUsersWithArrayInput*(httpClient: HttpClient, user: seq[User]): Response = ## Creates list of users with given input array httpClient.headers["Content-Type"] = "application/json" httpClient.post(basepath & "/user/createWithArray", $(%user)) + proc createUsersWithListInput*(httpClient: HttpClient, user: seq[User]): Response = ## Creates list of users with given input array httpClient.headers["Content-Type"] = "application/json" httpClient.post(basepath & "/user/createWithList", $(%user)) + proc deleteUser*(httpClient: HttpClient, username: string): Response = ## Delete user httpClient.delete(basepath & fmt"/user/{username}") + proc getUserByName*(httpClient: HttpClient, username: string): (Option[User], Response) = ## Get user by user name @@ -70,10 +71,10 @@ proc getUserByName*(httpClient: HttpClient, username: string): (Option[User], Re proc loginUser*(httpClient: HttpClient, username: string, password: string): (Option[string], Response) = ## Logs user into the system - let url_encoded_query_params = encodeQuery([ - ("username", $username), # The user name for login - ("password", $password), # The password for login in clear text - ]) + var query_params_list: seq[(string, string)] = @[] + query_params_list.add(("username", $username)) + query_params_list.add(("password", $password)) + let url_encoded_query_params = encodeQuery(query_params_list) let response = httpClient.get(basepath & "/user/login" & "?" & url_encoded_query_params) constructResult[string](response) @@ -84,8 +85,10 @@ proc logoutUser*(httpClient: HttpClient): Response = httpClient.get(basepath & "/user/logout") + proc updateUser*(httpClient: HttpClient, username: string, user: User): Response = ## Updated user httpClient.headers["Content-Type"] = "application/json" httpClient.put(basepath & fmt"/user/{username}", $(%user)) + diff --git a/samples/client/petstore/nim/petstore/models/model_any_type.nim b/samples/client/petstore/nim/petstore/models/model_any_type.nim new file mode 100644 index 000000000000..808715e6b6c5 --- /dev/null +++ b/samples/client/petstore/nim/petstore/models/model_any_type.nim @@ -0,0 +1,14 @@ +# +# OpenAPI Petstore +# +# This is a sample server Petstore server. For this sample, you can use the api key `special-key` to test the authorization filters. +# The version of the OpenAPI document: 1.0.0 +# +# Generated by: https://openapi-generator.tech +# + +import json + +# AnyType represents any JSON value +# This is used for fields that can contain arbitrary JSON data +type AnyType* = JsonNode diff --git a/samples/client/petstore/nim/petstore/models/model_api_response.nim b/samples/client/petstore/nim/petstore/models/model_api_response.nim index 78f22e081ca0..3342d3ffc486 100644 --- a/samples/client/petstore/nim/petstore/models/model_api_response.nim +++ b/samples/client/petstore/nim/petstore/models/model_api_response.nim @@ -9,10 +9,13 @@ import json import tables +import marshal +import options type ApiResponse* = object ## Describes the result of uploading an image resource - code*: int - `type`*: string - message*: string + code*: Option[int] + `type`*: Option[string] + message*: Option[string] + diff --git a/samples/client/petstore/nim/petstore/models/model_category.nim b/samples/client/petstore/nim/petstore/models/model_category.nim index a1331c1ef415..efd8f365c957 100644 --- a/samples/client/petstore/nim/petstore/models/model_category.nim +++ b/samples/client/petstore/nim/petstore/models/model_category.nim @@ -9,9 +9,12 @@ import json import tables +import marshal +import options type Category* = object ## A category for a pet - id*: int64 - name*: string + id*: Option[int64] + name*: Option[string] + diff --git a/samples/client/petstore/nim/petstore/models/model_get_pet_reviews200response.nim b/samples/client/petstore/nim/petstore/models/model_get_pet_reviews200response.nim new file mode 100644 index 000000000000..3dc550d70a8f --- /dev/null +++ b/samples/client/petstore/nim/petstore/models/model_get_pet_reviews200response.nim @@ -0,0 +1,44 @@ +# +# OpenAPI Petstore +# +# This is a sample server Petstore server. For this sample, you can use the api key `special-key` to test the authorization filters. +# The version of the OpenAPI document: 1.0.0 +# +# Generated by: https://openapi-generator.tech +# + +import json +import tables +import marshal +import options + +import model_api_response +import model_get_pet_reviews_response_with_presence + +# AnyOf type +type GetPetReviews200responseKind* {.pure.} = enum + GetPetReviewsResponseWithPresenceVariant + ApiResponseVariant + +type GetPetReviews200response* = object + ## + case kind*: GetPetReviews200responseKind + of GetPetReviews200responseKind.GetPetReviewsResponseWithPresenceVariant: + GetPetReviews_Response_WithPresenceValue*: GetPetReviewsResponseWithPresence + of GetPetReviews200responseKind.ApiResponseVariant: + ApiResponseValue*: ApiResponse + +proc to*(node: JsonNode, T: typedesc[GetPetReviews200response]): GetPetReviews200response = + ## Custom deserializer for anyOf type - tries each variant + try: + return GetPetReviews200response(kind: GetPetReviews200responseKind.GetPetReviewsResponseWithPresenceVariant, GetPetReviews_Response_WithPresenceValue: to(node, GetPetReviewsResponseWithPresence)) + except Exception as e: + when defined(debug): + echo "Failed to deserialize as GetPetReviewsResponseWithPresence: ", e.msg + try: + return GetPetReviews200response(kind: GetPetReviews200responseKind.ApiResponseVariant, ApiResponseValue: to(node, ApiResponse)) + except Exception as e: + when defined(debug): + echo "Failed to deserialize as ApiResponse: ", e.msg + raise newException(ValueError, "Unable to deserialize into any variant of GetPetReviews200response. JSON: " & $node) + diff --git a/samples/client/petstore/nim/petstore/models/model_get_pet_reviews_response_with_presence.nim b/samples/client/petstore/nim/petstore/models/model_get_pet_reviews_response_with_presence.nim new file mode 100644 index 000000000000..5b497629851e --- /dev/null +++ b/samples/client/petstore/nim/petstore/models/model_get_pet_reviews_response_with_presence.nim @@ -0,0 +1,19 @@ +# +# OpenAPI Petstore +# +# This is a sample server Petstore server. For this sample, you can use the api key `special-key` to test the authorization filters. +# The version of the OpenAPI document: 1.0.0 +# +# Generated by: https://openapi-generator.tech +# + +import json +import tables +import marshal +import options + + +type GetPetReviewsResponseWithPresence* = object + ## + review*: Option[string] + diff --git a/samples/client/petstore/nim/petstore/models/model_get_pet_stats200response.nim b/samples/client/petstore/nim/petstore/models/model_get_pet_stats200response.nim new file mode 100644 index 000000000000..59199ee6584b --- /dev/null +++ b/samples/client/petstore/nim/petstore/models/model_get_pet_stats200response.nim @@ -0,0 +1,20 @@ +# +# OpenAPI Petstore +# +# This is a sample server Petstore server. For this sample, you can use the api key `special-key` to test the authorization filters. +# The version of the OpenAPI document: 1.0.0 +# +# Generated by: https://openapi-generator.tech +# + +import json +import tables +import marshal +import options + + +type GetPetStats200response* = object + ## + totalPets*: Option[int] + status*: Option[string] + diff --git a/samples/client/petstore/nim/petstore/models/model_ignored_response.nim b/samples/client/petstore/nim/petstore/models/model_ignored_response.nim new file mode 100644 index 000000000000..e0d37705a7cf --- /dev/null +++ b/samples/client/petstore/nim/petstore/models/model_ignored_response.nim @@ -0,0 +1,55 @@ +# +# OpenAPI Petstore +# +# This is a sample server Petstore server. For this sample, you can use the api key `special-key` to test the authorization filters. +# The version of the OpenAPI document: 1.0.0 +# +# Generated by: https://openapi-generator.tech +# + +import json +import tables +import marshal +import options + + +type Note* {.pure.} = enum + `0` + `1` + `2` + `3` + +type IgnoredResponse* = object + ## + reason*: Option[string] + note*: Option[Note] ## Inline enum that should use enum type name not base type + +func `%`*(v: Note): JsonNode = + result = case v: + of Note.`0`: %"0" + of Note.`1`: %"1" + of Note.`2`: %"2" + of Note.`3`: %"3" +func `$`*(v: Note): string = + result = case v: + of Note.`0`: $("0") + of Note.`1`: $("1") + of Note.`2`: $("2") + of Note.`3`: $("3") + +proc to*(node: JsonNode, T: typedesc[Note]): Note = + if node.kind != JString: + raise newException(ValueError, "Expected string for enum Note, got " & $node.kind) + let strVal = node.getStr() + case strVal: + of $("0"): + return Note.`0` + of $("1"): + return Note.`1` + of $("2"): + return Note.`2` + of $("3"): + return Note.`3` + else: + raise newException(ValueError, "Invalid enum value for Note: " & strVal) + diff --git a/samples/client/petstore/nim/petstore/models/model_object.nim b/samples/client/petstore/nim/petstore/models/model_object.nim new file mode 100644 index 000000000000..6c86e864d60c --- /dev/null +++ b/samples/client/petstore/nim/petstore/models/model_object.nim @@ -0,0 +1,15 @@ +# +# OpenAPI Petstore +# +# This is a sample server Petstore server. For this sample, you can use the api key `special-key` to test the authorization filters. +# The version of the OpenAPI document: 1.0.0 +# +# Generated by: https://openapi-generator.tech +# + +import json +import tables + +# Object represents an arbitrary JSON object +# Using JsonNode instead of the 'object' keyword to avoid Nim keyword conflicts +type Object* = JsonNode diff --git a/samples/client/petstore/nim/petstore/models/model_order.nim b/samples/client/petstore/nim/petstore/models/model_order.nim index e2bb9e9cd7c7..279cdb825091 100644 --- a/samples/client/petstore/nim/petstore/models/model_order.nim +++ b/samples/client/petstore/nim/petstore/models/model_order.nim @@ -9,6 +9,8 @@ import json import tables +import marshal +import options type Status* {.pure.} = enum @@ -18,23 +20,35 @@ type Status* {.pure.} = enum type Order* = object ## An order for a pets from the pet store - id*: int64 - petId*: int64 - quantity*: int - shipDate*: string - status*: Status ## Order Status - complete*: bool + id*: Option[int64] + petId*: Option[int64] + quantity*: Option[int] + shipDate*: Option[string] + status*: Option[Status] ## Order Status + complete*: Option[bool] func `%`*(v: Status): JsonNode = - let str = case v: - of Status.Placed: "placed" - of Status.Approved: "approved" - of Status.Delivered: "delivered" - - JsonNode(kind: JString, str: str) - + result = case v: + of Status.Placed: %"placed" + of Status.Approved: %"approved" + of Status.Delivered: %"delivered" func `$`*(v: Status): string = result = case v: - of Status.Placed: "placed" - of Status.Approved: "approved" - of Status.Delivered: "delivered" + of Status.Placed: $("placed") + of Status.Approved: $("approved") + of Status.Delivered: $("delivered") + +proc to*(node: JsonNode, T: typedesc[Status]): Status = + if node.kind != JString: + raise newException(ValueError, "Expected string for enum Status, got " & $node.kind) + let strVal = node.getStr() + case strVal: + of $("placed"): + return Status.Placed + of $("approved"): + return Status.Approved + of $("delivered"): + return Status.Delivered + else: + raise newException(ValueError, "Invalid enum value for Status: " & strVal) + diff --git a/samples/client/petstore/nim/petstore/models/model_pet.nim b/samples/client/petstore/nim/petstore/models/model_pet.nim index c2431a743c66..5f2a47dcde98 100644 --- a/samples/client/petstore/nim/petstore/models/model_pet.nim +++ b/samples/client/petstore/nim/petstore/models/model_pet.nim @@ -9,6 +9,8 @@ import json import tables +import marshal +import options import model_category import model_tag @@ -20,23 +22,35 @@ type Status* {.pure.} = enum type Pet* = object ## A pet for sale in the pet store - id*: int64 - category*: Category + id*: Option[int64] + category*: Option[Category] name*: string photoUrls*: seq[string] - tags*: seq[Tag] - status*: Status ## pet status in the store + tags*: Option[seq[Tag]] + status*: Option[Status] ## pet status in the store func `%`*(v: Status): JsonNode = - let str = case v: - of Status.Available: "available" - of Status.Pending: "pending" - of Status.Sold: "sold" - - JsonNode(kind: JString, str: str) - + result = case v: + of Status.Available: %"available" + of Status.Pending: %"pending" + of Status.Sold: %"sold" func `$`*(v: Status): string = result = case v: - of Status.Available: "available" - of Status.Pending: "pending" - of Status.Sold: "sold" + of Status.Available: $("available") + of Status.Pending: $("pending") + of Status.Sold: $("sold") + +proc to*(node: JsonNode, T: typedesc[Status]): Status = + if node.kind != JString: + raise newException(ValueError, "Expected string for enum Status, got " & $node.kind) + let strVal = node.getStr() + case strVal: + of $("available"): + return Status.Available + of $("pending"): + return Status.Pending + of $("sold"): + return Status.Sold + else: + raise newException(ValueError, "Invalid enum value for Status: " & strVal) + diff --git a/samples/client/petstore/nim/petstore/models/model_pet_alert.nim b/samples/client/petstore/nim/petstore/models/model_pet_alert.nim new file mode 100644 index 000000000000..e63f46252cb9 --- /dev/null +++ b/samples/client/petstore/nim/petstore/models/model_pet_alert.nim @@ -0,0 +1,22 @@ +# +# OpenAPI Petstore +# +# This is a sample server Petstore server. For this sample, you can use the api key `special-key` to test the authorization filters. +# The version of the OpenAPI document: 1.0.0 +# +# Generated by: https://openapi-generator.tech +# + +import json +import tables +import marshal +import options + +import model_pet_alert_level + +type PetAlert* = object + ## + id*: Option[string] + level*: Option[PetAlertLevel] + message*: Option[string] + diff --git a/samples/client/petstore/nim/petstore/models/model_pet_alert_level.nim b/samples/client/petstore/nim/petstore/models/model_pet_alert_level.nim new file mode 100644 index 000000000000..2ad91a3a6b01 --- /dev/null +++ b/samples/client/petstore/nim/petstore/models/model_pet_alert_level.nim @@ -0,0 +1,55 @@ +# +# OpenAPI Petstore +# +# This is a sample server Petstore server. For this sample, you can use the api key `special-key` to test the authorization filters. +# The version of the OpenAPI document: 1.0.0 +# +# Generated by: https://openapi-generator.tech +# + +import json +import tables +import marshal +import options + + +type PetAlertLevel* {.pure.} = enum + `0` + `1` + `2` + `10` + `99` + +func `%`*(v: PetAlertLevel): JsonNode = + result = case v: + of PetAlertLevel.`0`: %(0) + of PetAlertLevel.`1`: %(1) + of PetAlertLevel.`2`: %(2) + of PetAlertLevel.`10`: %(10) + of PetAlertLevel.`99`: %(99) + +func `$`*(v: PetAlertLevel): string = + result = case v: + of PetAlertLevel.`0`: $(0) + of PetAlertLevel.`1`: $(1) + of PetAlertLevel.`2`: $(2) + of PetAlertLevel.`10`: $(10) + of PetAlertLevel.`99`: $(99) +proc to*(node: JsonNode, T: typedesc[PetAlertLevel]): PetAlertLevel = + if node.kind != JInt: + raise newException(ValueError, "Expected integer for enum PetAlertLevel, got " & $node.kind) + let intVal = node.getInt() + case intVal: + of 0: + return PetAlertLevel.`0` + of 1: + return PetAlertLevel.`1` + of 2: + return PetAlertLevel.`2` + of 10: + return PetAlertLevel.`10` + of 99: + return PetAlertLevel.`99` + else: + raise newException(ValueError, "Invalid enum value for PetAlertLevel: " & $intVal) + diff --git a/samples/client/petstore/nim/petstore/models/model_pet_audit_log.nim b/samples/client/petstore/nim/petstore/models/model_pet_audit_log.nim new file mode 100644 index 000000000000..b9443806dbcd --- /dev/null +++ b/samples/client/petstore/nim/petstore/models/model_pet_audit_log.nim @@ -0,0 +1,121 @@ +# +# OpenAPI Petstore +# +# This is a sample server Petstore server. For this sample, you can use the api key `special-key` to test the authorization filters. +# The version of the OpenAPI document: 1.0.0 +# +# Generated by: https://openapi-generator.tech +# + +import json +import tables +import marshal +import options + +import model_pet_alert_level +import model_pet_review + +type Action* {.pure.} = enum + Create + Update + Delete + +type `From`* {.pure.} = enum + Api + Web + Mobile + +type PetAuditLog* = object + ## + id*: Option[string] ## Audit log unique identifier + action*: Option[Action] + priority*: Option[PetAlertLevel] + `from`*: Option[`From`] + reviews*: Option[seq[PetReview]] + +func `%`*(v: Action): JsonNode = + result = case v: + of Action.Create: %"create" + of Action.Update: %"update" + of Action.Delete: %"delete" +func `$`*(v: Action): string = + result = case v: + of Action.Create: $("create") + of Action.Update: $("update") + of Action.Delete: $("delete") + +proc to*(node: JsonNode, T: typedesc[Action]): Action = + if node.kind != JString: + raise newException(ValueError, "Expected string for enum Action, got " & $node.kind) + let strVal = node.getStr() + case strVal: + of $("create"): + return Action.Create + of $("update"): + return Action.Update + of $("delete"): + return Action.Delete + else: + raise newException(ValueError, "Invalid enum value for Action: " & strVal) + +func `%`*(v: `From`): JsonNode = + result = case v: + of `From`.Api: %"api" + of `From`.Web: %"web" + of `From`.Mobile: %"mobile" +func `$`*(v: `From`): string = + result = case v: + of `From`.Api: $("api") + of `From`.Web: $("web") + of `From`.Mobile: $("mobile") + +proc to*(node: JsonNode, T: typedesc[`From`]): `From` = + if node.kind != JString: + raise newException(ValueError, "Expected string for enum `From`, got " & $node.kind) + let strVal = node.getStr() + case strVal: + of $("api"): + return `From`.Api + of $("web"): + return `From`.Web + of $("mobile"): + return `From`.Mobile + else: + raise newException(ValueError, "Invalid enum value for `From`: " & strVal) + + +# Custom JSON deserialization for PetAuditLog with custom field names +proc to*(node: JsonNode, T: typedesc[PetAuditLog]): PetAuditLog = + result = PetAuditLog() + if node.kind == JObject: + if node.hasKey("_id") and node["_id"].kind != JNull: + result.id = some(to(node["_id"], typeof(result.id.get()))) + if node.hasKey("action") and node["action"].kind != JNull: + result.action = some(to(node["action"], Action)) + if node.hasKey("priority") and node["priority"].kind != JNull: + result.priority = some(to(node["priority"], typeof(result.priority.get()))) + if node.hasKey("from") and node["from"].kind != JNull: + result.`from` = some(to(node["from"], `From`)) + if node.hasKey("reviews") and node["reviews"].kind != JNull: + # Optional array of types with custom JSON - manually iterate and deserialize + let arrayNode = node["reviews"] + if arrayNode.kind == JArray: + var arr: seq[PetReview] = @[] + for item in arrayNode.items: + arr.add(to(item, PetReview)) + result.reviews = some(arr) + +# Custom JSON serialization for PetAuditLog with custom field names +proc `%`*(obj: PetAuditLog): JsonNode = + result = newJObject() + if obj.id.isSome(): + result["_id"] = %obj.id.get() + if obj.action.isSome(): + result["action"] = %obj.action.get() + if obj.priority.isSome(): + result["priority"] = %obj.priority.get() + if obj.`from`.isSome(): + result["from"] = %obj.`from`.get() + if obj.reviews.isSome(): + result["reviews"] = %obj.reviews.get() + diff --git a/samples/client/petstore/nim/petstore/models/model_pet_config_any_of1.nim b/samples/client/petstore/nim/petstore/models/model_pet_config_any_of1.nim new file mode 100644 index 000000000000..2160657f55cb --- /dev/null +++ b/samples/client/petstore/nim/petstore/models/model_pet_config_any_of1.nim @@ -0,0 +1,19 @@ +# +# OpenAPI Petstore +# +# This is a sample server Petstore server. For this sample, you can use the api key `special-key` to test the authorization filters. +# The version of the OpenAPI document: 1.0.0 +# +# Generated by: https://openapi-generator.tech +# + +import json +import tables +import marshal +import options + + +type PetConfigAnyOf1* = object + ## + version*: Option[int] + diff --git a/samples/client/petstore/nim/petstore/models/model_pet_metadata.nim b/samples/client/petstore/nim/petstore/models/model_pet_metadata.nim new file mode 100644 index 000000000000..180cb903883b --- /dev/null +++ b/samples/client/petstore/nim/petstore/models/model_pet_metadata.nim @@ -0,0 +1,19 @@ +# +# OpenAPI Petstore +# +# This is a sample server Petstore server. For this sample, you can use the api key `special-key` to test the authorization filters. +# The version of the OpenAPI document: 1.0.0 +# +# Generated by: https://openapi-generator.tech +# + +import json +import tables +import marshal +import options + + +type PetMetadata* = object + ## + metadata*: Option[string] + diff --git a/samples/client/petstore/nim/petstore/models/model_pet_positions.nim b/samples/client/petstore/nim/petstore/models/model_pet_positions.nim new file mode 100644 index 000000000000..a232f1479348 --- /dev/null +++ b/samples/client/petstore/nim/petstore/models/model_pet_positions.nim @@ -0,0 +1,20 @@ +# +# OpenAPI Petstore +# +# This is a sample server Petstore server. For this sample, you can use the api key `special-key` to test the authorization filters. +# The version of the OpenAPI document: 1.0.0 +# +# Generated by: https://openapi-generator.tech +# + +import json +import tables +import marshal +import options + +import model_record_string_before_string_or_null_after_string_or_null_value + +type PetPositions* = object + ## + positions*: Option[Table[string, RecordStringBeforeStringOrNullAfterStringOrNullValue]] + diff --git a/samples/client/petstore/nim/petstore/models/model_pet_priority.nim b/samples/client/petstore/nim/petstore/models/model_pet_priority.nim new file mode 100644 index 000000000000..7ab3903dd76d --- /dev/null +++ b/samples/client/petstore/nim/petstore/models/model_pet_priority.nim @@ -0,0 +1,45 @@ +# +# OpenAPI Petstore +# +# This is a sample server Petstore server. For this sample, you can use the api key `special-key` to test the authorization filters. +# The version of the OpenAPI document: 1.0.0 +# +# Generated by: https://openapi-generator.tech +# + +import json +import tables +import marshal +import options + + +type PetPriority* {.pure.} = enum + `0` + `1` + `2` + +func `%`*(v: PetPriority): JsonNode = + result = case v: + of PetPriority.`0`: %(0) + of PetPriority.`1`: %(1) + of PetPriority.`2`: %(2) + +func `$`*(v: PetPriority): string = + result = case v: + of PetPriority.`0`: $(0) + of PetPriority.`1`: $(1) + of PetPriority.`2`: $(2) +proc to*(node: JsonNode, T: typedesc[PetPriority]): PetPriority = + if node.kind != JInt: + raise newException(ValueError, "Expected integer for enum PetPriority, got " & $node.kind) + let intVal = node.getInt() + case intVal: + of 0: + return PetPriority.`0` + of 1: + return PetPriority.`1` + of 2: + return PetPriority.`2` + else: + raise newException(ValueError, "Invalid enum value for PetPriority: " & $intVal) + diff --git a/samples/client/petstore/nim/petstore/models/model_pet_review.nim b/samples/client/petstore/nim/petstore/models/model_pet_review.nim new file mode 100644 index 000000000000..631cbcc3985c --- /dev/null +++ b/samples/client/petstore/nim/petstore/models/model_pet_review.nim @@ -0,0 +1,48 @@ +# +# OpenAPI Petstore +# +# This is a sample server Petstore server. For this sample, you can use the api key `special-key` to test the authorization filters. +# The version of the OpenAPI document: 1.0.0 +# +# Generated by: https://openapi-generator.tech +# + +import json +import tables +import marshal +import options + + +type PetReview* = object + ## + id*: Option[string] ## MongoDB style ID field that should be renamed to id in Nim + author*: Option[string] + content*: Option[string] + timestamp*: Option[string] + + +# Custom JSON deserialization for PetReview with custom field names +proc to*(node: JsonNode, T: typedesc[PetReview]): PetReview = + result = PetReview() + if node.kind == JObject: + if node.hasKey("_id") and node["_id"].kind != JNull: + result.id = some(to(node["_id"], typeof(result.id.get()))) + if node.hasKey("author") and node["author"].kind != JNull: + result.author = some(to(node["author"], typeof(result.author.get()))) + if node.hasKey("content") and node["content"].kind != JNull: + result.content = some(to(node["content"], typeof(result.content.get()))) + if node.hasKey("timestamp") and node["timestamp"].kind != JNull: + result.timestamp = some(to(node["timestamp"], typeof(result.timestamp.get()))) + +# Custom JSON serialization for PetReview with custom field names +proc `%`*(obj: PetReview): JsonNode = + result = newJObject() + if obj.id.isSome(): + result["_id"] = %obj.id.get() + if obj.author.isSome(): + result["author"] = %obj.author.get() + if obj.content.isSome(): + result["content"] = %obj.content.get() + if obj.timestamp.isSome(): + result["timestamp"] = %obj.timestamp.get() + diff --git a/samples/client/petstore/nim/petstore/models/model_pet_reviews_response.nim b/samples/client/petstore/nim/petstore/models/model_pet_reviews_response.nim new file mode 100644 index 000000000000..0fc682427719 --- /dev/null +++ b/samples/client/petstore/nim/petstore/models/model_pet_reviews_response.nim @@ -0,0 +1,45 @@ +# +# OpenAPI Petstore +# +# This is a sample server Petstore server. For this sample, you can use the api key `special-key` to test the authorization filters. +# The version of the OpenAPI document: 1.0.0 +# +# Generated by: https://openapi-generator.tech +# + +import json +import tables +import marshal +import options + +import model_pet_review + +type PetReviewsResponse* = object + ## + reviews*: Option[seq[PetReview]] + totalCount*: Option[int] + + +# Custom JSON deserialization for PetReviewsResponse with custom field names +proc to*(node: JsonNode, T: typedesc[PetReviewsResponse]): PetReviewsResponse = + result = PetReviewsResponse() + if node.kind == JObject: + if node.hasKey("reviews") and node["reviews"].kind != JNull: + # Optional array of types with custom JSON - manually iterate and deserialize + let arrayNode = node["reviews"] + if arrayNode.kind == JArray: + var arr: seq[PetReview] = @[] + for item in arrayNode.items: + arr.add(to(item, PetReview)) + result.reviews = some(arr) + if node.hasKey("totalCount") and node["totalCount"].kind != JNull: + result.totalCount = some(to(node["totalCount"], typeof(result.totalCount.get()))) + +# Custom JSON serialization for PetReviewsResponse with custom field names +proc `%`*(obj: PetReviewsResponse): JsonNode = + result = newJObject() + if obj.reviews.isSome(): + result["reviews"] = %obj.reviews.get() + if obj.totalCount.isSome(): + result["totalCount"] = %obj.totalCount.get() + diff --git a/samples/client/petstore/nim/petstore/models/model_record_string_before_string_or_null_after_string_or_null_value.nim b/samples/client/petstore/nim/petstore/models/model_record_string_before_string_or_null_after_string_or_null_value.nim new file mode 100644 index 000000000000..9c089c236b91 --- /dev/null +++ b/samples/client/petstore/nim/petstore/models/model_record_string_before_string_or_null_after_string_or_null_value.nim @@ -0,0 +1,20 @@ +# +# OpenAPI Petstore +# +# This is a sample server Petstore server. For this sample, you can use the api key `special-key` to test the authorization filters. +# The version of the OpenAPI document: 1.0.0 +# +# Generated by: https://openapi-generator.tech +# + +import json +import tables +import marshal +import options + + +type RecordStringBeforeStringOrNullAfterStringOrNullValue* = object + ## + before*: Option[string] + after*: Option[string] + diff --git a/samples/client/petstore/nim/petstore/models/model_tag.nim b/samples/client/petstore/nim/petstore/models/model_tag.nim index 28f518d6b6c0..b5af0450ad45 100644 --- a/samples/client/petstore/nim/petstore/models/model_tag.nim +++ b/samples/client/petstore/nim/petstore/models/model_tag.nim @@ -9,9 +9,12 @@ import json import tables +import marshal +import options type Tag* = object ## A tag for a pet - id*: int64 - name*: string + id*: Option[int64] + name*: Option[string] + diff --git a/samples/client/petstore/nim/petstore/models/model_unfavorite_pet_request.nim b/samples/client/petstore/nim/petstore/models/model_unfavorite_pet_request.nim new file mode 100644 index 000000000000..122cafe366b7 --- /dev/null +++ b/samples/client/petstore/nim/petstore/models/model_unfavorite_pet_request.nim @@ -0,0 +1,19 @@ +# +# OpenAPI Petstore +# +# This is a sample server Petstore server. For this sample, you can use the api key `special-key` to test the authorization filters. +# The version of the OpenAPI document: 1.0.0 +# +# Generated by: https://openapi-generator.tech +# + +import json +import tables +import marshal +import options + + +type UnfavoritePetRequest* = object + ## + reason*: Option[string] ## Reason for unfavoriting + diff --git a/samples/client/petstore/nim/petstore/models/model_user.nim b/samples/client/petstore/nim/petstore/models/model_user.nim index e487793080e5..c2c0ed251a6f 100644 --- a/samples/client/petstore/nim/petstore/models/model_user.nim +++ b/samples/client/petstore/nim/petstore/models/model_user.nim @@ -9,15 +9,18 @@ import json import tables +import marshal +import options type User* = object ## A User who is purchasing from the pet store - id*: int64 - username*: string - firstName*: string - lastName*: string - email*: string - password*: string - phone*: string - userStatus*: int ## User Status + id*: Option[int64] + username*: Option[string] + firstName*: Option[string] + lastName*: Option[string] + email*: Option[string] + password*: Option[string] + phone*: Option[string] + userStatus*: Option[int] ## User Status +