Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
generatorName: rust
outputDir: samples/client/others/rust/reqwest-deep-object-free-form-query-param
library: reqwest
inputSpec: modules/openapi-generator/src/test/resources/3_0/rust/deep-object-free-form-query-param.yaml
templateDir: modules/openapi-generator/src/main/resources/rust
additionalProperties:
supportAsync: false
packageName: deep-object-free-form-query-param
3 changes: 2 additions & 1 deletion docs/generators/kotlin-spring.md
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,7 @@ These options may be applied as additional-properties (cli) or configOptions (pl
|useFlowForArrayReturnType|Whether to use Flow for array/collection return types when reactive is enabled. If false, will use List instead.| |true|
|useJackson3|Use Jackson 3 dependencies (tools.jackson package). Only available with `useSpringBoot4`. Defaults to true when `useSpringBoot4` is enabled.| |false|
|useResponseEntity|Whether (when false) to return actual type (e.g. List<Fruit>) and handle non-happy path responses via exceptions flow or (when true) return entire ResponseEntity (e.g. ResponseEntity<List<Fruit>>). If disabled, method are annotated using a @ResponseStatus annotation, which has the status of the first response declared in the Api definition| |true|
|useSealedDiscriminatorInterfaces|Generate sealed interfaces instead of plain interfaces for allOf discriminator parent models. When true (default), discriminator parents rendered as `sealed interface`, enabling exhaustive `when` matching and preventing external implementors (which cannot know all subtypes). Set to false to restore the legacy plain `interface` behavior, e.g. when you implement the generated interface from a module outside the generated package.| |true|
|useSealedResponseInterfaces|Generate sealed interfaces for endpoint responses that all possible response types implement. Allows controllers to return any valid response type in a type-safe manner (e.g., sealed interface CreateUserResponse implemented by User, ConflictResponse, ErrorResponse)| |false|
|useSpringBoot3|Generate code and provide dependencies for use with Spring Boot ≥ 3 (use jakarta instead of javax in imports). Enabling this option will also enable `useJakartaEe`.| |false|
|useSpringBoot4|Generate code and provide dependencies for use with Spring Boot 4.x. Enabling this option will also enable `useJakartaEe`.| |false|
Expand Down Expand Up @@ -325,7 +326,7 @@ These options may be applied as additional-properties (cli) or configOptions (pl
|Composite|✓|OAS2,OAS3
|Polymorphism|✓|OAS2,OAS3
|Union|✗|OAS3
|allOf|✗|OAS2,OAS3
|allOf|✓|OAS2,OAS3
|anyOf|✗|OAS3
|oneOf|✓|OAS3
|not|✗|OAS3
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -201,24 +201,19 @@ public String sanitizeIdentifier(String name, CasingType casingType, String esca
// Sanitize any other special characters that weren't replaced
name = sanitizeName(name);

// Keep track of modifications prior to casing
boolean nameWasModified = !originalName.equals(name);

// Convert casing
name = casingFunction.apply(name);

// If word starts with number add a prefix
// Note: this must be done after casing since CamelCase will strip leading underscores
if (name.matches("^\\d.*")) {
nameWasModified = true;
name = casingFunction.apply(escapePrefix + '_' + name);
}

// Escape reserved words - this is case-sensitive so must be done after casing
// There is currently a bug in Rust where this doesn't work for a few reserved words :(
// https://internals.rust-lang.org/t/raw-identifiers-dont-work-for-all-identifiers/9094
if (isReservedWord(name)) {
nameWasModified = true;
if (this.keywordsThatDoNotSupportRawIdentifiers.contains(name) || !allowRawIdentifiers) {
name = casingFunction.apply(escapePrefix + '_' + name);
} else {
Expand All @@ -227,8 +222,9 @@ public String sanitizeIdentifier(String name, CasingType casingType, String esca
}

// If the name had to be modified (not just because of casing), log the change
if (nameWasModified) {
LOGGER.warn("{} cannot be used as a {} name. Renamed to {}", casingFunction.apply(originalName), type, name);
String casedOriginalName = casingFunction.apply(originalName);
if (!casedOriginalName.equals(name)) {
LOGGER.warn("{} cannot be used as a {} name. Renamed to {}", casedOriginalName, type, name);
}

return name;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1157,6 +1157,14 @@ public OperationsMap postProcessOperationsWithModels(OperationsMap objs, List<Mo
.filter(isSerializable)
.collect(Collectors.toList());
operation.hasProduces = operation.produces != null && !operation.produces.isEmpty();

// form style with explode puts a map-typed query parameter on the wire as one parameter
// per entry; api.mustache adds those after the declared query parameters
for (CodegenParameter param : operation.queryParams) {
if (param.isMap && param.isExplode && !param.isDeepObject) {
param.vendorExtensions.put("x-kotlin-explode-form-object", true);
}
}
}

// set multipart against all relevant operations
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -115,6 +115,7 @@ public class KotlinSpringServerCodegen extends AbstractKotlinCodegen
public static final String GENERATE_PAGEABLE_CONSTRAINT_VALIDATION = "generatePageableConstraintValidation";
public static final String SUBSTITUTE_GENERIC_PAGED_MODEL = "substituteGenericPagedModel";
public static final String USE_SEALED_RESPONSE_INTERFACES = "useSealedResponseInterfaces";
public static final String USE_SEALED_DISCRIMINATOR_INTERFACES = "useSealedDiscriminatorInterfaces";
public static final String COMPANION_OBJECT = "companionObject";
public static final String SUSPEND_FUNCTIONS = "suspendFunctions";

Expand Down Expand Up @@ -201,6 +202,7 @@ public void setAutoXSpringPaginated(boolean autoXSpringPaginated) {
@Setter private boolean generatePageableConstraintValidation = false;
@Setter private boolean substituteGenericPagedModel = false;
@Setter private boolean useSealedResponseInterfaces = false;
@Setter private boolean useSealedDiscriminatorInterfaces = true;
@Setter private boolean companionObject = false;
@Setter private boolean useEnumValueInterface = false;
private String valuedEnumClassName = "ValuedEnum";
Expand Down Expand Up @@ -262,7 +264,8 @@ public KotlinSpringServerCodegen() {
)
.includeSchemaSupportFeatures(
SchemaSupportFeature.Polymorphism,
SchemaSupportFeature.oneOf
SchemaSupportFeature.oneOf,
SchemaSupportFeature.allOf
)
.includeParameterFeatures(
ParameterFeature.Cookie
Expand Down Expand Up @@ -329,6 +332,13 @@ public KotlinSpringServerCodegen() {
addSwitch(USE_SEALED_RESPONSE_INTERFACES,
"Generate sealed interfaces for endpoint responses that all possible response types implement. Allows controllers to return any valid response type in a type-safe manner (e.g., sealed interface CreateUserResponse implemented by User, ConflictResponse, ErrorResponse)",
useSealedResponseInterfaces);
addSwitch(USE_SEALED_DISCRIMINATOR_INTERFACES,
"Generate sealed interfaces instead of plain interfaces for allOf discriminator parent models. " +
"When true (default), discriminator parents rendered as `sealed interface`, enabling exhaustive " +
"`when` matching and preventing external implementors (which cannot know all subtypes). " +
"Set to false to restore the legacy plain `interface` behavior, e.g. when you implement " +
"the generated interface from a module outside the generated package.",
useSealedDiscriminatorInterfaces);
addOption(X_KOTLIN_IMPLEMENTS_SKIP, "A list of fully qualified interfaces that should NOT be implemented despite their presence in vendor extension `x-kotlin-implements`. Example: yaml `xKotlinImplementsSkip: [com.some.pack.WithPhotoUrls]` skips implementing the interface in any schema", "empty list");
addOption(X_KOTLIN_IMPLEMENTS_FIELDS_SKIP, "A list of fields per schema name that should NOT be created with `override` keyword despite their presence in vendor extension `x-kotlin-implements-fields` for the schema. Example: yaml `xKotlinImplementsFieldsSkip: Pet: [photoUrls]` skips `override` for `photoUrls` in schema `Pet`", "empty map");
addOption(SCHEMA_IMPLEMENTS, "A map of single interface or a list of interfaces per schema name that should be implemented (serves similar purpose as `x-kotlin-implements`, but is fully decoupled from the api spec). Example: yaml `schemaImplements: {Pet: com.some.pack.WithId, Category: [com.some.pack.CategoryInterface], Dog: [com.some.pack.Canine, com.some.pack.OtherInterface]}` implements interfaces in schemas `Pet` (interface `com.some.pack.WithId`), `Category` (interface `com.some.pack.CategoryInterface`), `Dog`(interfaces `com.some.pack.Canine`, `com.some.pack.OtherInterface`)", "empty map");
Expand Down Expand Up @@ -652,6 +662,12 @@ public void processOpts() {
}
writePropertyBack(USE_SEALED_RESPONSE_INTERFACES, useSealedResponseInterfaces);

if (additionalProperties.containsKey(USE_SEALED_DISCRIMINATOR_INTERFACES)) {
this.setUseSealedDiscriminatorInterfaces(
Boolean.parseBoolean(additionalProperties.get(USE_SEALED_DISCRIMINATOR_INTERFACES).toString()));
}
writePropertyBack(USE_SEALED_DISCRIMINATOR_INTERFACES, useSealedDiscriminatorInterfaces);

if (additionalProperties.containsKey(COMPANION_OBJECT)) {
this.setCompanionObject(convertPropertyToBooleanAndWriteBack(COMPANION_OBJECT));
} else {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -140,7 +140,7 @@
{{#hasOnlyReadOnly}}
[JsonConstructorAttribute]
{{/hasOnlyReadOnly}}
public {{classname}}({{#readWriteVars}}{{{datatypeWithEnum}}}{{#isEnum}}{{^isContainer}}{{^required}}?{{/required}}{{/isContainer}}{{/isEnum}} {{#lambda.camelcase_sanitize_param}}{{name}}{{/lambda.camelcase_sanitize_param}} = {{#defaultValue}}{{^isDateTime}}{{#isString}}{{^isEnum}}@{{/isEnum}}{{/isString}}{{{defaultValue}}}{{/isDateTime}}{{#isDateTime}}default{{/isDateTime}}{{/defaultValue}}{{^defaultValue}}default{{/defaultValue}}{{^-last}}, {{/-last}}{{/readWriteVars}}){{#parent}} : base({{#parentVars}}{{#lambda.camelcase_sanitize_param}}{{name}}{{/lambda.camelcase_sanitize_param}}{{^-last}}, {{/-last}}{{/parentVars}}){{/parent}}
public {{classname}}({{#readWriteVars}}{{{datatypeWithEnum}}}{{#isEnum}}{{^isContainer}}{{^required}}?{{/required}}{{/isContainer}}{{/isEnum}}{{^isEnum}}{{#conditionalSerialization}}{{#vendorExtensions.x-csharp-value-type}}{{^required}}{{^defaultValue}}?{{/defaultValue}}{{/required}}{{/vendorExtensions.x-csharp-value-type}}{{/conditionalSerialization}}{{/isEnum}} {{#lambda.camelcase_sanitize_param}}{{name}}{{/lambda.camelcase_sanitize_param}} = {{#defaultValue}}{{^isDateTime}}{{#isString}}{{^isEnum}}@{{/isEnum}}{{/isString}}{{{defaultValue}}}{{/isDateTime}}{{#isDateTime}}default{{/isDateTime}}{{/defaultValue}}{{^defaultValue}}default{{/defaultValue}}{{^-last}}, {{/-last}}{{/readWriteVars}}){{#parent}} : base({{#parentVars}}{{#lambda.camelcase_sanitize_param}}{{name}}{{/lambda.camelcase_sanitize_param}}{{^-last}}, {{/-last}}{{/parentVars}}){{/parent}}
{
{{#vars}}
{{^isInherited}}
Expand Down Expand Up @@ -196,11 +196,20 @@
this.{{name}} = {{#lambda.camelcase_sanitize_param}}{{name}}{{/lambda.camelcase_sanitize_param}};
{{/conditionalSerialization}}
{{#conditionalSerialization}}
{{#vendorExtensions.x-csharp-value-type}}
if ({{#lambda.camelcase_sanitize_param}}{{name}}{{/lambda.camelcase_sanitize_param}} != null)
{
this._{{name}} = {{#lambda.camelcase_sanitize_param}}{{name}}{{/lambda.camelcase_sanitize_param}}.Value;
this._flag{{name}} = true;
}
{{/vendorExtensions.x-csharp-value-type}}
{{^vendorExtensions.x-csharp-value-type}}
this._{{name}} = {{#lambda.camelcase_sanitize_param}}{{name}}{{/lambda.camelcase_sanitize_param}};
if (this.{{name}} != null)
{
this._flag{{name}} = true;
}
{{/vendorExtensions.x-csharp-value-type}}
{{/conditionalSerialization}}
{{/defaultValue}}
{{/required}}
Expand Down
43 changes: 41 additions & 2 deletions modules/openapi-generator/src/main/resources/go/client.mustache
Original file line number Diff line number Diff line change
Expand Up @@ -191,10 +191,14 @@ func parameterAddToHeaderOrQuery(headerOrQueryParams interface{}, keyPrefix stri
for i:=0;i<lenIndValue;i++ {
var arrayValue = indValue.Index(i)
var keyPrefixForCollectionType = keyPrefix
var styleForElement = style
if style == "deepObject" {
keyPrefixForCollectionType = keyPrefix + "[" + strconv.Itoa(i) + "]"
} else if style == "form" {
// only the parameter's own map is flattened; a map inside an array keeps its bracketed path
styleForElement = ""
}
parameterAddToHeaderOrQuery(headerOrQueryParams, keyPrefixForCollectionType, arrayValue.Interface(), style, collectionType)
parameterAddToHeaderOrQuery(headerOrQueryParams, keyPrefixForCollectionType, arrayValue.Interface(), styleForElement, collectionType)
}
return

Expand All @@ -206,13 +210,36 @@ func parameterAddToHeaderOrQuery(headerOrQueryParams interface{}, keyPrefix stri
iter := indValue.MapRange()
for iter.Next() {
k,v := iter.Key(), iter.Value()
parameterAddToHeaderOrQuery(headerOrQueryParams, fmt.Sprintf("%s[%s]", keyPrefix, k.String()), v.Interface(), style, collectionType)
var keyPrefixForMapEntry = fmt.Sprintf("%s[%s]", keyPrefix, k.String())
var styleForMapEntry = style
if style == "form" {
// form style: one query parameter per entry, keyed by the property name; anything nested keeps its bracketed path
keyPrefixForMapEntry = k.String()
styleForMapEntry = ""
// a nil entry, or a nil item of a list entry, is left out rather than sent as "null"
entry, ok := parameterValueIndirect(v)
if !ok {
continue
}
if entry.Kind() == reflect.Slice {
for i := 0; i < entry.Len(); i++ {
if element, ok := parameterValueIndirect(entry.Index(i)); ok {
parameterAddToHeaderOrQuery(headerOrQueryParams, keyPrefixForMapEntry, element.Interface(), styleForMapEntry, collectionType)
}
}
continue
}
}
parameterAddToHeaderOrQuery(headerOrQueryParams, keyPrefixForMapEntry, v.Interface(), styleForMapEntry, collectionType)
}
return

case reflect.Interface:
fallthrough
case reflect.Ptr:
if v.IsNil() {
return
}
parameterAddToHeaderOrQuery(headerOrQueryParams, keyPrefix, v.Elem().Interface(), style, collectionType)
return

Expand Down Expand Up @@ -247,6 +274,18 @@ func parameterAddToHeaderOrQuery(headerOrQueryParams interface{}, keyPrefix stri
}
}

// parameterValueIndirect unwraps interfaces and pointers down to the value they hold,
// reporting false when that value is nil
func parameterValueIndirect(v reflect.Value) (reflect.Value, bool) {
for v.Kind() == reflect.Interface || v.Kind() == reflect.Ptr {
if v.IsNil() {
return v, false
}
v = v.Elem()
}
return v, v.IsValid()
}

// helper for converting interface{} parameters to json strings
func parameterToJson(obj interface{}) (string, error) {
jsonBuf, err := json.Marshal(obj)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ import com.squareup.moshi.adapters.EnumJsonAdapter
{{#enumVars}}
{{#-last}}
.add({{modelPackage}}.{{classname}}::class.java, EnumJsonAdapter.create({{modelPackage}}.{{classname}}::class.java)
.withUnknownFallback({{modelPackage}}.{{classname}}.{{&name}}))
.withUnknownFallback({{modelPackage}}.{{classname}}.{{&name}}).nullSafe())
{{/-last}}
{{/enumVars}}
{{/allowableValues}}
Expand All @@ -33,7 +33,7 @@ import com.squareup.moshi.adapters.EnumJsonAdapter
{{#enumVars}}
{{#-last}}
.add({{modelPackage}}.{{classname}}.{{{nameInPascalCase}}}::class.java, EnumJsonAdapter.create({{modelPackage}}.{{classname}}.{{{nameInPascalCase}}}::class.java)
.withUnknownFallback({{modelPackage}}.{{classname}}.{{{nameInPascalCase}}}.{{&name}}))
.withUnknownFallback({{modelPackage}}.{{classname}}.{{{nameInPascalCase}}}.{{&name}}).nullSafe())
{{/-last}}
{{/enumVars}}
{{/allowableValues}}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -209,6 +209,7 @@ import {{packageName}}.infrastructure.Serializer
{{/hasQueryParams}}{{#hasQueryParams}}mutableMapOf<kotlin.String, kotlin.collections.List<kotlin.String>>()
.apply {
{{#queryParams}}
{{^vendorExtensions.x-kotlin-explode-form-object}}
{{^required}}
if ({{{paramName}}} != null) {
{{#isModel}}
Expand Down Expand Up @@ -251,6 +252,23 @@ import {{packageName}}.infrastructure.Serializer
{{/isModel}}
{{/isNullable}}
{{/required}}
{{/vendorExtensions.x-kotlin-explode-form-object}}
{{/queryParams}}
{{#queryParams}}
{{#vendorExtensions.x-kotlin-explode-form-object}}
// form style, explode: one query parameter per entry, keyed by the property name, appended after the declared ones; null is left out, a collection repeats the key
({{{paramName}}} as? kotlin.collections.Map<*, *>)?.forEach { (key, value) ->
val values = when (value) {
is kotlin.collections.Iterable<*> -> value.toList()
is kotlin.Array<*> -> value.toList()
else -> listOf(value)
}.filterNotNull().map { parameterToString(it) }
if (key != null && values.isNotEmpty()) {
val name = key.toString()
put(name, getOrElse(name) { emptyList() } + values)
}
}
{{/vendorExtensions.x-kotlin-explode-form-object}}
{{/queryParams}}
}
{{/hasQueryParams}}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
/**
* {{{description}}}
{{#requiredVars}}
* @param {{name}} {{{description}}}
* @param {{{name}}} {{{description}}}
{{/requiredVars}}
{{#optionalVars}}
* @param {{name}} {{{description}}}
* @param {{{name}}} {{{description}}}
{{/optionalVars}}
*/{{#discriminator}}
{{>typeInfoAnnotation}}{{/discriminator}}
Expand All @@ -14,7 +14,7 @@
{{#vendorExtensions.x-class-extra-annotation}}
{{{.}}}
{{/vendorExtensions.x-class-extra-annotation}}
{{#discriminator}}interface {{classname}}{{/discriminator}}{{^discriminator}}{{#vendorExtensions.x-is-open-class}}open {{/vendorExtensions.x-is-open-class}}{{^vendorExtensions.x-is-open-class}}{{#hasVars}}data {{/hasVars}}{{/vendorExtensions.x-is-open-class}}class {{classname}}(
{{#discriminator}}{{#useSealedDiscriminatorInterfaces}}sealed {{/useSealedDiscriminatorInterfaces}}interface {{classname}}{{/discriminator}}{{^discriminator}}{{#vendorExtensions.x-is-open-class}}open {{/vendorExtensions.x-is-open-class}}{{^vendorExtensions.x-is-open-class}}{{#hasVars}}data {{/hasVars}}{{/vendorExtensions.x-is-open-class}}class {{classname}}(
{{#requiredVars}}
{{>dataClassReqVar}}{{^-last}},
{{/-last}}{{/requiredVars}}{{#hasRequired}}{{#hasOptional}},
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import com.fasterxml.jackson.annotation.JsonInclude
/**
* {{{description}}}
{{#vars}}
* @param {{name}} {{{description}}}
* @param {{{name}}} {{{description}}}
{{/vars}}
*/
{{#parcelizeModels}}
Expand Down
Loading
Loading