) webhooks) {
+ groups.add(webhook.getWebhooks());
+ }
+ }
+ for (OperationMap group : groups) {
+ if (group == null || group.getOperation() == null) {
+ continue;
+ }
+ for (CodegenOperation op : group.getOperation()) {
+ hasVerbatimMethod |= op.vendorExtensions.containsKey("x-kotlin-custom-method");
+ hasQueryStringParam |= op.vendorExtensions.containsKey("x-kotlin-querystring-params");
+ }
+ }
+ if (hasVerbatimMethod) {
+ objs.put("x-kotlin-verbatim-methods", true);
+ }
+ if (hasQueryStringParam) {
+ objs.put("x-kotlin-querystring", true);
+ }
+ return objs;
+ }
}
diff --git a/modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/PhpClientCodegen.java b/modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/PhpClientCodegen.java
index cc666a5723b0..fd01c026428a 100644
--- a/modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/PhpClientCodegen.java
+++ b/modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/PhpClientCodegen.java
@@ -20,17 +20,25 @@
import org.apache.commons.lang3.StringUtils;
import org.openapitools.codegen.CliOption;
import org.openapitools.codegen.CodegenConstants;
+import org.openapitools.codegen.CodegenOperation;
import org.openapitools.codegen.CodegenType;
import org.openapitools.codegen.SupportingFile;
import org.openapitools.codegen.meta.features.*;
+import org.openapitools.codegen.model.ModelMap;
+import org.openapitools.codegen.model.OperationsMap;
+import org.openapitools.codegen.model.WebhooksMap;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.File;
import java.util.ArrayList;
+import java.util.Arrays;
import java.util.Collections;
import java.util.EnumSet;
+import java.util.HashSet;
+import java.util.Iterator;
import java.util.List;
+import java.util.Set;
/**
* Mustache templates are located in
@@ -143,4 +151,108 @@ public void processOpts() {
}
}
+
+ @Override
+ public boolean supportsAdditionalOperations() {
+ // only the guzzle template can emit arbitrary methods verbatim: psr-18
+ // delegates method handling to the injected PSR-17/PSR-18 implementation
+ // (Guzzle factory up-cases, Symfony rejects non-uppercase tokens)
+ return GUZZLE.equals(getLibrary());
+ }
+
+ @Override
+ public boolean supportsQueryStringParameters() {
+ return GUZZLE.equals(getLibrary());
+ }
+
+ private static final Set STANDARD_HTTP_METHODS = new HashSet<>(Arrays.asList(
+ "GET", "PUT", "POST", "DELETE", "OPTIONS", "HEAD", "PATCH", "TRACE", "CONNECT"));
+
+ /**
+ * Local variable names declared inside the generated api functions
+ * (php/api.mustache). A spec parameter whose final param name matches any of
+ * these collides with the internal variable in the same function scope - e.g.
+ * a parameter named {@code query} is overwritten by
+ * {@code $query = ObjectSerializer::buildQuery($queryParams)} before the
+ * {@code in: querystring} append would read it, silently dropping the caller's
+ * value. Such parameters are renamed by {@link #toParamName(String)}.
+ */
+ private static final Set INTERNAL_VARIABLE_NAMES = new HashSet<>(Arrays.asList(
+ // signature internals appended after the spec params
+ "hostIndex", "variables", "contentType", "associative_array",
+ // xxxRequest() locals
+ "resourcePath", "formParams", "queryParams", "headerParams", "httpBody",
+ "multipart", "formDataProcessor", "formData", "multipartContents",
+ "formParamName", "formParamValue", "formParamValueItems", "formParamValueItem",
+ "headers", "apiKey", "defaultHeaders", "hostSettings", "operationHost", "query",
+ "returnType",
+ // verbatim-method request accumulator
+ "__requestUri",
+ // $this is not a legal parameter name in PHP
+ "this",
+ // sync/async wrapper locals
+ "request", "options", "response", "statusCode", "data", "content", "e", "exception"));
+
+ @Override
+ public String toParamName(String name) {
+ if (parameterNameMapping.containsKey(name)) {
+ return parameterNameMapping.get(name);
+ }
+ String paramName = super.toParamName(name);
+ if (INTERNAL_VARIABLE_NAMES.contains(paramName)) {
+ paramName = toVarName("param_" + name);
+ }
+ return paramName;
+ }
+
+ // RFC 9110 tchar: method tokens the generated client can send verbatim
+ private static final java.util.regex.Pattern HTTP_METHOD_TOKEN_PATTERN =
+ java.util.regex.Pattern.compile("[!#$%&'*+\\-.^_`|~0-9A-Za-z]+");
+
+ /**
+ * Marks OpenAPI 3.2 (query/additionalOperations) HTTP methods for verbatim emission and
+ * operations carrying {@code in: querystring} parameters. Operation names that are not
+ * valid RFC 9110 tokens are warned about and skipped.
+ *
+ * Extensions set: {@code x-php-verbatim-method} (non-standard method present),
+ * {@code x-php-http-method-literal} (token escaped for a PHP single-quoted string -
+ * {@code '} is valid tchar but would terminate the literal), and
+ * {@code x-php-http-method-doc} ({@code |} escaped for markdown tables).
+ */
+ private void flagVerbatimHttpMethods(List operationList) {
+ Iterator it = operationList.iterator();
+ while (it.hasNext()) {
+ CodegenOperation op = it.next();
+ if (op.allParams != null && op.allParams.stream().anyMatch(p -> p.isQueryStringParam)) {
+ op.vendorExtensions.put("x-php-has-querystring-param", true);
+ }
+ if (op.httpMethod == null || STANDARD_HTTP_METHODS.contains(op.httpMethod)) {
+ continue;
+ }
+ if (!HTTP_METHOD_TOKEN_PATTERN.matcher(op.httpMethod).matches()) {
+ LOGGER.warn("Skipping operation {}: HTTP method name '{}' is not a valid "
+ + "RFC 9110 token and cannot be emitted as a PHP string literal.",
+ op.operationId, op.httpMethod);
+ it.remove();
+ continue;
+ }
+ op.vendorExtensions.put("x-php-verbatim-method", true);
+ op.vendorExtensions.put("x-php-http-method-literal", op.httpMethod.replace("'", "\\'"));
+ op.vendorExtensions.put("x-php-http-method-doc", op.httpMethod.replace("|", "\\|"));
+ }
+ }
+
+ @Override
+ public OperationsMap postProcessOperationsWithModels(OperationsMap objs, List allModels) {
+ OperationsMap map = super.postProcessOperationsWithModels(objs, allModels);
+ flagVerbatimHttpMethods(map.getOperations().getOperation());
+ return map;
+ }
+
+ @Override
+ public WebhooksMap postProcessWebhooksWithModels(WebhooksMap objs, List allModels) {
+ WebhooksMap map = super.postProcessWebhooksWithModels(objs, allModels);
+ flagVerbatimHttpMethods(map.getWebhooks().getOperation());
+ return map;
+ }
}
diff --git a/modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/PythonClientCodegen.java b/modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/PythonClientCodegen.java
index 61936225db59..035bf30d9ac6 100644
--- a/modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/PythonClientCodegen.java
+++ b/modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/PythonClientCodegen.java
@@ -28,6 +28,7 @@
import org.openapitools.codegen.model.ModelMap;
import org.openapitools.codegen.model.ModelsMap;
import org.openapitools.codegen.model.OperationsMap;
+import org.openapitools.codegen.model.WebhooksMap;
import org.openapitools.codegen.utils.ModelUtils;
import org.openapitools.codegen.utils.ProcessUtils;
import org.slf4j.Logger;
@@ -35,6 +36,7 @@
import java.io.File;
import java.util.ArrayList;
+import java.util.Arrays;
import java.util.EnumSet;
import java.util.HashMap;
import java.util.HashSet;
@@ -42,6 +44,7 @@
import java.util.Locale;
import java.util.Map;
import java.util.Set;
+import java.util.regex.Pattern;
import static org.openapitools.codegen.utils.ModelUtils.hasAnyOf;
import static org.openapitools.codegen.utils.ModelUtils.hasOneOf;
@@ -616,6 +619,30 @@ protected void addAdditionPropertiesToCodeGenModel(CodegenModel codegenModel, Sc
}
}
+ // HTTP methods emitted in the conventional 'GET' single-quoted style;
+ // OpenAPI 3.2 methods outside this set are emitted as unescaped
+ // double-quoted literals via x-python-http-method-literal so valid token
+ // punctuation (e.g. CHECK&FETCH) is not HTML-escaped by the template engine
+ private static final Set STANDARD_HTTP_METHODS = new HashSet<>(Arrays.asList(
+ "GET", "PUT", "POST", "DELETE", "HEAD", "OPTIONS", "TRACE", "PATCH", "CONNECT"));
+
+ @Override
+ public boolean supportsAdditionalOperations() {
+ // the urllib3 rest client dispatches non-standard methods through
+ // urlopen(), preserving verbatim casing. aiohttp (asyncio library)
+ // and httpx unconditionally call method.upper() in their Request
+ // objects, which would corrupt e.g. 'customMethod' into
+ // 'CUSTOMMETHOD', so 3.2 operations are only enabled for urllib3
+ return "urllib3".equals(getLibrary());
+ }
+
+ @Override
+ public boolean supportsQueryStringParameters() {
+ // the raw, already-encoded query string is appended to the request
+ // path verbatim by the api template
+ return true;
+ }
+
@Override
public OperationsMap postProcessOperationsWithModels(
OperationsMap objs, List allModels) {
@@ -623,7 +650,43 @@ public OperationsMap postProcessOperationsWithModels(
renameIndependentClientOperationMembers(
objs.getOperations().getOperation());
}
- return super.postProcessOperationsWithModels(objs, allModels);
+ OperationsMap result = super.postProcessOperationsWithModels(objs, allModels);
+ flagOpenAPI32Operations(result.getOperations().getOperation());
+ return result;
+ }
+
+ @Override
+ public WebhooksMap postProcessWebhooksWithModels(WebhooksMap objs, List allModels) {
+ // webhooks render through api.mustache as well, so their non-standard
+ // methods and querystring parameters need the same flags
+ objs = super.postProcessWebhooksWithModels(objs, allModels);
+ flagOpenAPI32Operations(objs.getWebhooks().getOperation());
+ return objs;
+ }
+
+ // RFC 9110 tchar — additionalOperations keys must match this to be
+ // emitted as a Python string literal
+ private static final Pattern HTTP_TOKEN = Pattern.compile("[!#$%&'*+\\-.^_`|~0-9A-Za-z]+");
+
+ private void flagOpenAPI32Operations(List operations) {
+ for (java.util.Iterator it = operations.iterator(); it.hasNext(); ) {
+ CodegenOperation op = it.next();
+ if (op.httpMethod != null && !STANDARD_HTTP_METHODS.contains(op.httpMethod)) {
+ if (!HTTP_TOKEN.matcher(op.httpMethod).matches()) {
+ LOGGER.warn("HTTP method '{}' is not a valid RFC 9110 token; skipping operation {}",
+ op.httpMethod, op.operationId);
+ it.remove();
+ continue;
+ }
+ op.vendorExtensions.put("x-python-http-method-literal", true);
+ }
+ for (CodegenParameter cp : op.allParams) {
+ if (cp.isQueryStringParam) {
+ op.vendorExtensions.put("x-python-has-querystring-param", true);
+ break;
+ }
+ }
+ }
}
private void renameIndependentClientOperationMembers(
diff --git a/modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/RubyClientCodegen.java b/modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/RubyClientCodegen.java
index d4b5e3a8b8a3..b3e2f123cc82 100644
--- a/modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/RubyClientCodegen.java
+++ b/modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/RubyClientCodegen.java
@@ -28,6 +28,7 @@
import org.openapitools.codegen.model.ModelsMap;
import org.openapitools.codegen.model.OperationMap;
import org.openapitools.codegen.model.OperationsMap;
+import org.openapitools.codegen.model.WebhooksMap;
import org.openapitools.codegen.model.EnumVarMap;
import org.openapitools.codegen.utils.ModelUtils;
import org.slf4j.Logger;
@@ -602,6 +603,63 @@ protected void addAdditionPropertiesToCodeGenModel(CodegenModel codegenModel, Sc
}
}
+ @Override
+ public boolean supportsAdditionalOperations() {
+ // only httpx can emit arbitrary methods verbatim: typhoeus crashes on
+ // non-alphanumeric tokens and up-cases the rest, faraday rejects them outright
+ return HTTPX.equals(getLibrary());
+ }
+
+ @Override
+ public boolean supportsQueryStringParameters() {
+ return HTTPX.equals(getLibrary());
+ }
+
+ private static final Set STANDARD_HTTP_METHODS = new HashSet<>(Arrays.asList(
+ "GET", "PUT", "POST", "DELETE", "OPTIONS", "HEAD", "PATCH", "TRACE", "CONNECT"));
+
+ // RFC 9110 tchar: method tokens the generated client can send verbatim
+ private static final java.util.regex.Pattern HTTP_METHOD_TOKEN_PATTERN =
+ java.util.regex.Pattern.compile("[!#$%&'*+\\-.^_`|~0-9A-Za-z]+");
+
+ /**
+ * Flags non-standard (OpenAPI 3.2 query/additionalOperations) HTTP methods, stores a
+ * doc-safe variant ({@code x-ruby-http-method-doc}) because {@code |} is a valid RFC 9110
+ * tchar but breaks markdown tables, and a symbol-safe variant
+ * ({@code x-ruby-http-method-symbol}) escaping {@code #} so {@code :"...#$x"} does not
+ * interpolate Ruby variables. Operation names that are not valid RFC 9110 tokens are
+ * warned about and skipped.
+ */
+ private void flagVerbatimHttpMethods(List operationList) {
+ Iterator it = operationList.iterator();
+ while (it.hasNext()) {
+ CodegenOperation op = it.next();
+ if (op.httpMethod == null) {
+ continue;
+ }
+ if (STANDARD_HTTP_METHODS.contains(op.httpMethod)) {
+ op.vendorExtensions.put("x-ruby-http-method-symbol", op.httpMethod);
+ continue;
+ }
+ if (!HTTP_METHOD_TOKEN_PATTERN.matcher(op.httpMethod).matches()) {
+ LOGGER.warn("Skipping operation {}: HTTP method name '{}' is not a valid "
+ + "RFC 9110 token and cannot be emitted as a Ruby method symbol.",
+ op.operationId, op.httpMethod);
+ it.remove();
+ continue;
+ }
+ op.vendorExtensions.put("x-ruby-http-method-doc", op.httpMethod.replace("|", "\\|"));
+ op.vendorExtensions.put("x-ruby-http-method-symbol", op.httpMethod.replace("#", "\\#"));
+ }
+ }
+
+ @Override
+ public WebhooksMap postProcessWebhooksWithModels(WebhooksMap objs, List allModels) {
+ WebhooksMap map = super.postProcessWebhooksWithModels(objs, allModels);
+ flagVerbatimHttpMethods(map.getWebhooks().getOperation());
+ return map;
+ }
+
@Override
public OperationsMap postProcessOperationsWithModels(OperationsMap objs, List allModels) {
objs = super.postProcessOperationsWithModels(objs, allModels);
@@ -610,6 +668,7 @@ public OperationsMap postProcessOperationsWithModels(OperationsMap objs, List processedModelMaps = new HashMap<>();
List operationList = operations.getOperation();
+ flagVerbatimHttpMethods(operationList);
for (CodegenOperation op : operationList) {
for (CodegenParameter p : op.allParams) {
p.vendorExtensions.put("x-ruby-example", constructExampleCode(p, modelMaps, processedModelMaps));
diff --git a/modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/RustClientCodegen.java b/modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/RustClientCodegen.java
index d2d4dcc51fd9..943385cd4eec 100644
--- a/modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/RustClientCodegen.java
+++ b/modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/RustClientCodegen.java
@@ -33,6 +33,7 @@
import org.openapitools.codegen.model.ModelsMap;
import org.openapitools.codegen.model.OperationMap;
import org.openapitools.codegen.model.OperationsMap;
+import org.openapitools.codegen.model.WebhooksMap;
import org.openapitools.codegen.templating.mustache.ReplaceAllLambda;
import org.openapitools.codegen.utils.ModelUtils;
import org.openapitools.codegen.utils.StringUtils;
@@ -782,11 +783,100 @@ public void postProcessParameter(CodegenParameter parameter) {
}
}
+ @Override
+ public boolean supportsAdditionalOperations() {
+ // reqwest's Method::from_bytes keeps arbitrary method names verbatim; the
+ // hyper templates need Rust identifiers, so hyper/reqwest-trait stay unsupported
+ return REQWEST_LIBRARY.equals(getLibrary());
+ }
+
+ @Override
+ public boolean supportsQueryStringParameters() {
+ return REQWEST_LIBRARY.equals(getLibrary());
+ }
+
+ private static final Set STANDARD_HTTP_METHODS = new HashSet<>(Arrays.asList(
+ "GET", "PUT", "POST", "DELETE", "OPTIONS", "HEAD", "PATCH", "TRACE", "CONNECT"));
+
+ // RFC 9110 tchar: the method token reqwest::Method::from_bytes accepts verbatim
+ private static final java.util.regex.Pattern HTTP_METHOD_TOKEN_PATTERN =
+ java.util.regex.Pattern.compile("[!#$%&'*+\\-.^_`|~0-9A-Za-z]+");
+
+ /**
+ * Snapshot non-standard (OpenAPI 3.2 additionalOperations) HTTP method names before the
+ * per-library case conversion below so they can be restored verbatim for
+ * {@code reqwest::Method::from_bytes}.
+ */
+ private Map snapshotVerbatimHttpMethods(List operations) {
+ Map verbatim = new IdentityHashMap<>();
+ for (CodegenOperation operation : operations) {
+ // exact match only: a lower/mixed-case key like `get` in additionalOperations is a
+ // distinct (non-standard) method on the wire and must be emitted verbatim, while
+ // non-ASCII names must reach the RFC 9110 token check rather than be upper-cased
+ if (operation.httpMethod != null
+ && !STANDARD_HTTP_METHODS.contains(operation.httpMethod)) {
+ verbatim.put(operation, operation.httpMethod);
+ }
+ }
+ return verbatim;
+ }
+
+ /**
+ * Restore verbatim HTTP methods captured by {@link #snapshotVerbatimHttpMethods} and mark
+ * them via {@code x-rust-http-method-literal} so templates emit them as method literals.
+ * Operation names that are not valid RFC 9110 tokens are warned about and skipped.
+ */
+ private void restoreVerbatimHttpMethods(List operations,
+ Map verbatim) {
+ Iterator it = operations.iterator();
+ while (it.hasNext()) {
+ CodegenOperation operation = it.next();
+ String method = verbatim.get(operation);
+ if (method == null) {
+ continue;
+ }
+ if (!HTTP_METHOD_TOKEN_PATTERN.matcher(method).matches()) {
+ LOGGER.warn("Skipping operation {}: HTTP method name '{}' is not a valid "
+ + "RFC 9110 token and cannot be emitted as a Rust method literal.",
+ operation.operationId, method);
+ it.remove();
+ continue;
+ }
+ operation.httpMethod = method;
+ operation.vendorExtensions.put("x-rust-http-method-literal", true);
+ // `|`, `*`, `_` and a backtick are valid RFC 9110 tchars but have special
+ // meaning in the markdown tables/code spans used by the doc templates
+ operation.vendorExtensions.put("x-rust-http-method-doc",
+ method.replaceAll("([*|_`])", "\\\\$1"));
+ }
+ }
+
+ @Override
+ public WebhooksMap postProcessWebhooksWithModels(WebhooksMap objs, List allModels) {
+ WebhooksMap map = super.postProcessWebhooksWithModels(objs, allModels);
+ if (REQWEST_LIBRARY.equals(getLibrary())) {
+ List operations = map.getWebhooks().getOperation();
+ restoreVerbatimHttpMethods(operations, snapshotVerbatimHttpMethods(operations));
+ for (CodegenOperation operation : operations) {
+ for (CodegenParameter p : operation.allParams) {
+ if (p.isQueryStringParam) {
+ operation.vendorExtensions.put("x-rust-has-querystring-param", true);
+ break;
+ }
+ }
+ }
+ }
+ return map;
+ }
+
@Override
public OperationsMap postProcessOperationsWithModels(OperationsMap objs, List allModels) {
OperationMap objectMap = objs.getOperations();
boolean useAsyncFileStream = false;
List operations = objectMap.getOperation();
+ Map verbatimHttpMethods = REQWEST_LIBRARY.equals(getLibrary())
+ ? snapshotVerbatimHttpMethods(operations)
+ : Collections.emptyMap();
for (CodegenOperation operation : operations) {
// For types with `isAnyType` we assume it's a `serde_json::Value` type.
// However for path, query, and headers it's unlikely to be JSON so we default to `String`.
@@ -928,6 +1018,21 @@ public OperationsMap postProcessOperationsWithModels(OperationsMap objs, List STANDARD_HTTP_METHODS = new HashSet<>(Arrays.asList(
+ "GET", "PUT", "POST", "DELETE", "HEAD", "OPTIONS", "TRACE", "PATCH", "CONNECT"));
+
+ // RFC 9110 tchar — additionalOperations keys must match this to be
+ // emitted as a TypeScript string literal
+ private static final Pattern HTTP_TOKEN = Pattern.compile("[!#$%&'*+\\-.^_`|~0-9A-Za-z]+");
+
+ // methods the WHATWG fetch spec forbids — RequestInit throws TypeError
+ private static final Set FETCH_FORBIDDEN_METHODS = new HashSet<>(Arrays.asList(
+ "CONNECT", "TRACE", "TRACK"));
+
+ @Override
+ public boolean supportsAdditionalOperations() {
+ // fetch() passes RequestInit.method through verbatim, preserving
+ // arbitrary OpenAPI 3.2 method names
+ return true;
+ }
+
+ @Override
+ public boolean supportsQueryStringParameters() {
+ // the raw, already-encoded query string is appended to the request
+ // path verbatim by the api template
+ return true;
+ }
+
+ @Override
+ public WebhooksMap postProcessWebhooksWithModels(WebhooksMap objs, List allModels) {
+ // webhooks render through apis.mustache as well, so their
+ // non-standard methods and querystring parameters need the same flags
+ objs = super.postProcessWebhooksWithModels(objs, allModels);
+ flagOpenAPI32Operations(objs.getWebhooks().getOperation());
+ return objs;
+ }
+
+ private void flagOpenAPI32Operations(List operations) {
+ for (Iterator it = operations.iterator(); it.hasNext(); ) {
+ CodegenOperation op = it.next();
+ if (op.httpMethod == null) {
+ continue;
+ }
+ // markdown docs render the method inside table cells where a '|'
+ // (a valid tchar) would split the cell
+ op.vendorExtensions.put("x-ts-http-method-doc", op.httpMethod.replace("|", "\\|"));
+ // CONNECT and TRACE are standard methods yet forbidden by the fetch
+ // spec, and CONNECT only ever arrives via additionalOperations
+ // (PathItem has no native field for it) - evaluate this independently
+ // of the standard-method branch below or it would never fire
+ if (FETCH_FORBIDDEN_METHODS.contains(op.httpMethod.toUpperCase(Locale.ROOT))) {
+ LOGGER.warn("HTTP method '{}' is forbidden by the fetch specification; "
+ + "the generated client will throw TypeError when calling operation {}",
+ op.httpMethod, op.operationId);
+ }
+ if (!STANDARD_HTTP_METHODS.contains(op.httpMethod)) {
+ if (!HTTP_TOKEN.matcher(op.httpMethod).matches()) {
+ LOGGER.warn("HTTP method '{}' is not a valid RFC 9110 token; skipping operation {}",
+ op.httpMethod, op.operationId);
+ it.remove();
+ continue;
+ }
+ // fetch() byte-uppercases only DELETE/GET/HEAD/OPTIONS/POST/PUT;
+ // every other method reaches the wire exactly as written
+ op.vendorExtensions.put("x-ts-http-method-literal", true);
+ }
+ for (CodegenParameter cp : op.allParams) {
+ if (cp.isQueryStringParam) {
+ op.vendorExtensions.put("x-ts-has-querystring-param", true);
+ break;
+ }
+ }
+
+ }
+ }
+
@Override
public Map postProcessSupportingFileData(Map objs) {
Map parentObjs = super.postProcessSupportingFileData(objs);
@@ -1565,6 +1646,7 @@ public ExtendedCodegenParameter(CodegenParameter cp) {
this.isDeprecated = cp.isDeprecated;
this.isFormParam = cp.isFormParam;
this.isQueryParam = cp.isQueryParam;
+ this.isQueryStringParam = cp.isQueryStringParam;
this.isPathParam = cp.isPathParam;
this.isHeaderParam = cp.isHeaderParam;
this.isCookieParam = cp.isCookieParam;
@@ -1643,6 +1725,39 @@ public ExtendedCodegenParameter(CodegenParameter cp) {
this.minItems = cp.minItems;
this.uniqueItems = cp.uniqueItems;
this.multipleOf = cp.multipleOf;
+ // fields added to CodegenParameter after this constructor was
+ // written; without them the extended copy silently drops them
+ // (isQueryStringParam above was one such casualty)
+ this.isDeepObject = cp.isDeepObject;
+ this.isMatrix = cp.isMatrix;
+ this.isAllowEmptyValue = cp.isAllowEmptyValue;
+ this.isFormStyle = cp.isFormStyle;
+ this.isSpaceDelimited = cp.isSpaceDelimited;
+ this.isPipeDelimited = cp.isPipeDelimited;
+ this.enumDefaultValue = cp.enumDefaultValue;
+ this.nameInCamelCase = cp.nameInCamelCase;
+ this.nameInPascalCase = cp.nameInPascalCase;
+ this.nameInSnakeCase = cp.nameInSnakeCase;
+ this.examples = cp.examples;
+ this.isPassword = cp.isPassword;
+ this.isShort = cp.isShort;
+ this.isUnboundedInteger = cp.isUnboundedInteger;
+ this.queryIsJsonMimeType = cp.queryIsJsonMimeType;
+ this.isOptional = cp.isOptional;
+ this.containerType = cp.containerType;
+ this.containerTypeMapped = cp.containerTypeMapped;
+ this.isNull = cp.isNull;
+ this.isVoid = cp.isVoid;
+ this.setSchema(cp.getSchema());
+ this.setContent(cp.getContent());
+ this.setRef(cp.getRef());
+ this.setComposedSchemas(cp.getComposedSchemas());
+ this.setRequiredVarsMap(cp.getRequiredVarsMap());
+ this.setHasMultipleTypes(cp.getHasMultipleTypes());
+ this.setUniqueItemsBoolean(cp.getUniqueItemsBoolean());
+ this.setSchemaIsFromAdditionalProperties(cp.getSchemaIsFromAdditionalProperties());
+ this.setHasDiscriminatorWithNonEmptyMapping(cp.getHasDiscriminatorWithNonEmptyMapping());
+ this.setAdditionalPropertiesIsAnyType(cp.getAdditionalPropertiesIsAnyType());
this.setHasVars(cp.getHasVars());
this.setHasRequired(cp.getHasRequired());
this.setMaxProperties(cp.getMaxProperties());
diff --git a/modules/openapi-generator/src/main/java/org/openapitools/codegen/serializer/PathItemSerializer.java b/modules/openapi-generator/src/main/java/org/openapitools/codegen/serializer/PathItemSerializer.java
index 9adea7f45543..a2c96b937d41 100644
--- a/modules/openapi-generator/src/main/java/org/openapitools/codegen/serializer/PathItemSerializer.java
+++ b/modules/openapi-generator/src/main/java/org/openapitools/codegen/serializer/PathItemSerializer.java
@@ -49,6 +49,12 @@ public void serialize(PathItem value, JsonGenerator gen, SerializerProvider seri
if (value.getTrace() != null) {
gen.writeObjectField("trace", value.getTrace());
}
+ if (value.getQuery() != null) {
+ gen.writeObjectField("query", value.getQuery());
+ }
+ if (value.getAdditionalOperations() != null) {
+ gen.writeObjectField("additionalOperations", value.getAdditionalOperations());
+ }
if (value.getServers() != null) {
gen.writeObjectField("servers", value.getServers());
}
diff --git a/modules/openapi-generator/src/main/resources/Java/libraries/okhttp-gson/ApiClient.mustache b/modules/openapi-generator/src/main/resources/Java/libraries/okhttp-gson/ApiClient.mustache
index f52c6e17238a..c4d73ec6ea39 100644
--- a/modules/openapi-generator/src/main/resources/Java/libraries/okhttp-gson/ApiClient.mustache
+++ b/modules/openapi-generator/src/main/resources/Java/libraries/okhttp-gson/ApiClient.mustache
@@ -1913,6 +1913,15 @@ public class ApiClient {
addOperationLookupEntry(path, "HEAD", pathItem.getHead());
addOperationLookupEntry(path, "PATCH", pathItem.getPatch());
addOperationLookupEntry(path, "TRACE", pathItem.getTrace());
+ {{#hasQueryOrAdditionalOperations}}
+ {{! requires a swagger-parser version with OpenAPI 3.2 model support }}
+ addOperationLookupEntry(path, "QUERY", pathItem.getQuery());
+ if (pathItem.getAdditionalOperations() != null) {
+ for (Map.Entry additionalOperation : pathItem.getAdditionalOperations().entrySet()) {
+ addOperationLookupEntry(path, additionalOperation.getKey(), additionalOperation.getValue());
+ }
+ }
+ {{/hasQueryOrAdditionalOperations}}
}
return this;
}
@@ -1959,6 +1968,12 @@ public class ApiClient {
case "cookie":
cookieParams.put(param.getName(), parameterToString(value));
break;
+ case "querystring":
+ // OpenAPI 3.2: the value is the whole (already encoded) query string
+ if (value != null) {
+ path = path + (path.contains("?") ? "&" : "?") + value.toString();
+ }
+ break;
default:
throw new IllegalStateException("Unexpected param in: " + param.getIn());
}
diff --git a/modules/openapi-generator/src/main/resources/Java/libraries/okhttp-gson/api.mustache b/modules/openapi-generator/src/main/resources/Java/libraries/okhttp-gson/api.mustache
index 18b18f01477a..b31904fb1527 100644
--- a/modules/openapi-generator/src/main/resources/Java/libraries/okhttp-gson/api.mustache
+++ b/modules/openapi-generator/src/main/resources/Java/libraries/okhttp-gson/api.mustache
@@ -170,7 +170,7 @@ public class {{classname}} {
{{^dynamicOperations}}
{{#queryParams}}
if ({{paramName}} != null) {
- {{#isFreeFormObject}}localVarQueryParams.addAll(localVarApiClient.freeFormParameterToPairs({{paramName}}));{{/isFreeFormObject}}{{^isFreeFormObject}}{{#collectionFormat}}localVarCollectionQueryParams.addAll(localVarApiClient.parameterToPairs("{{{.}}}", {{/collectionFormat}}{{^collectionFormat}}localVarQueryParams.addAll(localVarApiClient.parameterToPair({{/collectionFormat}}"{{baseName}}", {{paramName}}));{{/isFreeFormObject}}
+ {{#isQueryStringParam}}{{! OpenAPI 3.2 in: querystring - the value is the whole (already encoded) query string }}localVarPath = localVarPath + (localVarPath.contains("?") ? "&" : "?") + {{paramName}};{{/isQueryStringParam}}{{^isQueryStringParam}}{{#isFreeFormObject}}localVarQueryParams.addAll(localVarApiClient.freeFormParameterToPairs({{paramName}}));{{/isFreeFormObject}}{{^isFreeFormObject}}{{#collectionFormat}}localVarCollectionQueryParams.addAll(localVarApiClient.parameterToPairs("{{{.}}}", {{/collectionFormat}}{{^collectionFormat}}localVarQueryParams.addAll(localVarApiClient.parameterToPair({{/collectionFormat}}"{{baseName}}", {{paramName}}));{{/isFreeFormObject}}{{/isQueryStringParam}}
}
{{/queryParams}}
diff --git a/modules/openapi-generator/src/main/resources/csharp/README.mustache b/modules/openapi-generator/src/main/resources/csharp/README.mustache
index 0ef94566a1db..79f3cfd0b558 100644
--- a/modules/openapi-generator/src/main/resources/csharp/README.mustache
+++ b/modules/openapi-generator/src/main/resources/csharp/README.mustache
@@ -223,7 +223,7 @@ All URIs are relative to *{{{basePath}}}*
Class | Method | HTTP request | Description
------------ | ------------- | ------------- | -------------
-{{#apiInfo}}{{#apis}}{{#operations}}{{#operation}}*{{classname}}* | [**{{operationId}}**]({{apiDocPath}}{{classname}}.md#{{operationIdLowerCase}}) | **{{httpMethod}}** {{path}} | {{{summary}}}
+{{#apiInfo}}{{#apis}}{{#operations}}{{#operation}}*{{classname}}* | [**{{operationId}}**]({{apiDocPath}}{{classname}}.md#{{operationIdLowerCase}}) | **{{#vendorExtensions.x-csharp-http-method-doc}}{{{vendorExtensions.x-csharp-http-method-doc}}}{{/vendorExtensions.x-csharp-http-method-doc}}{{^vendorExtensions.x-csharp-http-method-doc}}{{httpMethod}}{{/vendorExtensions.x-csharp-http-method-doc}}** {{path}} | {{{summary}}}
{{/operation}}{{/operations}}{{/apis}}{{/apiInfo}}
diff --git a/modules/openapi-generator/src/main/resources/csharp/api_doc.mustache b/modules/openapi-generator/src/main/resources/csharp/api_doc.mustache
index ca85bfa7ffaa..00afb9e4e174 100644
--- a/modules/openapi-generator/src/main/resources/csharp/api_doc.mustache
+++ b/modules/openapi-generator/src/main/resources/csharp/api_doc.mustache
@@ -7,7 +7,7 @@ All URIs are relative to *{{{basePath}}}*
|--------|--------------|-------------|
{{#operations}}
{{#operation}}
-| [**{{operationId}}**]({{classname}}.md#{{operationIdLowerCase}}) | **{{httpMethod}}** {{path}} | {{summary}} |
+| [**{{operationId}}**]({{classname}}.md#{{operationIdLowerCase}}) | **{{#vendorExtensions.x-csharp-http-method-doc}}{{{vendorExtensions.x-csharp-http-method-doc}}}{{/vendorExtensions.x-csharp-http-method-doc}}{{^vendorExtensions.x-csharp-http-method-doc}}{{httpMethod}}{{/vendorExtensions.x-csharp-http-method-doc}}** {{path}} | {{summary}} |
{{/operation}}
{{/operations}}
diff --git a/modules/openapi-generator/src/main/resources/csharp/libraries/generichost/api.mustache b/modules/openapi-generator/src/main/resources/csharp/libraries/generichost/api.mustache
index 9a12abf424bb..0224efa02b41 100644
--- a/modules/openapi-generator/src/main/resources/csharp/libraries/generichost/api.mustache
+++ b/modules/openapi-generator/src/main/resources/csharp/libraries/generichost/api.mustache
@@ -431,7 +431,9 @@ namespace {{packageName}}.{{apiPackage}}
{{#-first}}
{{/-first}}
+ {{^isQueryStringParam}}
parseQueryStringLocalVar["{{baseName}}"] = ClientUtils.ParameterToString({{paramName}});
+ {{/isQueryStringParam}}
{{/required}}
{{/queryParams}}
@@ -443,9 +445,11 @@ namespace {{packageName}}.{{apiPackage}}
{{/constantParams}}
{{#queryParams}}
{{^required}}
+ {{^isQueryStringParam}}
if ({{paramName}}.IsSet)
parseQueryStringLocalVar["{{baseName}}"] = ClientUtils.ParameterToString({{paramName}}.Value);
+ {{/isQueryStringParam}}
{{/required}}
{{#-last}}
uriBuilderLocalVar.Query = parseQueryStringLocalVar.ToString();
@@ -624,6 +628,20 @@ namespace {{packageName}}.{{apiPackage}}
{{/isKeyInQuery}}
{{/isApiKey}}
{{/authMethods}}
+ {{#allParams}}
+ {{#isQueryStringParam}}
+ // OpenAPI 3.2 `in: querystring`: the value IS the query component,
+ // appended verbatim - UriBuilder keeps pre-encoded escapes as-is
+ {{#required}}
+ uriBuilderLocalVar.Query = string.IsNullOrEmpty(uriBuilderLocalVar.Query) ? {{paramName}} : uriBuilderLocalVar.Query + "&" + {{paramName}};
+ {{/required}}
+ {{^required}}
+ if ({{paramName}}.IsSet)
+ uriBuilderLocalVar.Query = string.IsNullOrEmpty(uriBuilderLocalVar.Query) ? {{paramName}}.Value : uriBuilderLocalVar.Query + "&" + {{paramName}}.Value;
+
+ {{/required}}
+ {{/isQueryStringParam}}
+ {{/allParams}}
httpRequestMessageLocalVar.RequestUri = uriBuilderLocalVar.Uri;
{{#authMethods}}
{{#isBasicBasic}}
@@ -704,6 +722,14 @@ namespace {{packageName}}.{{apiPackage}}
httpRequestMessageLocalVar.Headers.Accept.Add(acceptLocalVar);
{{/-first}}
{{/produces}}
+ {{#vendorExtensions.x-csharp-http-method-literal}}
+
+ // OpenAPI 3.2: emit the RFC 9110 token verbatim. HttpMethod.Query
+ // only exists on net10+ and HttpClient normalizes case-variants,
+ // so a literal keeps identical wire behavior across TFMs
+ httpRequestMessageLocalVar.Method = new HttpMethod("{{{vendorExtensions.x-csharp-http-method-literal}}}");
+ {{/vendorExtensions.x-csharp-http-method-literal}}
+ {{^vendorExtensions.x-csharp-http-method-literal}}
{{#net60OrLater}}
httpRequestMessageLocalVar.Method = HttpMethod.{{#lambda.titlecase}}{{#lambda.lowercase}}{{httpMethod}}{{/lambda.lowercase}}{{/lambda.titlecase}};
@@ -711,6 +737,7 @@ namespace {{packageName}}.{{apiPackage}}
{{^net60OrLater}}
httpRequestMessageLocalVar.Method = new HttpMethod("{{#lambda.uppercase}}{{httpMethod}}{{/lambda.uppercase}}");
{{/net60OrLater}}
+ {{/vendorExtensions.x-csharp-http-method-literal}}
DateTime requestedAtLocalVar = DateTime.UtcNow;
diff --git a/modules/openapi-generator/src/main/resources/go/api.mustache b/modules/openapi-generator/src/main/resources/go/api.mustache
index 2be5011b6546..915ed6a34175 100644
--- a/modules/openapi-generator/src/main/resources/go/api.mustache
+++ b/modules/openapi-generator/src/main/resources/go/api.mustache
@@ -110,7 +110,12 @@ func (a *{{{classname}}}Service) {{{nickname}}}(ctx context.Context{{#pathParams
{{/isDeprecated}}
func (a *{{{classname}}}Service) {{nickname}}Execute(r {{#structPrefix}}{{&classname}}{{/structPrefix}}{{^structPrefix}}Api{{/structPrefix}}{{operationId}}Request) ({{#returnType}}{{^isArray}}{{^returnTypeIsPrimitive}}{{^isResponseFile}}*{{/isResponseFile}}{{/returnTypeIsPrimitive}}{{/isArray}}{{{.}}}, {{/returnType}}*http.Response, error) {
var (
+ {{#vendorExtensions.x-go-http-method-literal}}
+ localVarHTTPMethod = "{{{httpMethod}}}"{{! OpenAPI 3.2: non-standard method sent verbatim; unescaped so valid token punctuation (& ' ` etc.) is preserved }}
+ {{/vendorExtensions.x-go-http-method-literal}}
+ {{^vendorExtensions.x-go-http-method-literal}}
localVarHTTPMethod = http.Method{{httpMethod}}
+ {{/vendorExtensions.x-go-http-method-literal}}
localVarPostBody interface{}
formFiles []formFile
{{#returnType}}
@@ -182,6 +187,18 @@ func (a *{{{classname}}}Service) {{nickname}}Execute(r {{#structPrefix}}{{&class
{{/allParams}}
{{#queryParams}}
+ {{#isQueryStringParam}}
+ // OpenAPI 3.2 in: querystring - the value is the whole, already-encoded
+ // query string; append it verbatim instead of a name=value pair
+ if r.{{paramName}} != nil {
+ if strings.Contains(localVarPath, "?") {
+ localVarPath = localVarPath + "&" + *r.{{paramName}}
+ } else {
+ localVarPath = localVarPath + "?" + *r.{{paramName}}
+ }
+ }
+ {{/isQueryStringParam}}
+ {{^isQueryStringParam}}
{{#required}}
{{#isCollectionFormatMulti}}
{
@@ -229,6 +246,7 @@ func (a *{{{classname}}}Service) {{nickname}}Execute(r {{#structPrefix}}{{&class
{{/isArray}}
{{/defaultValue}}}
{{/required}}
+ {{/isQueryStringParam}}
{{/queryParams}}
// to determine the Content-Type header
{{=<% %>=}}
diff --git a/modules/openapi-generator/src/main/resources/go/client.mustache b/modules/openapi-generator/src/main/resources/go/client.mustache
index 6accea635955..7c7d9c228365 100644
--- a/modules/openapi-generator/src/main/resources/go/client.mustache
+++ b/modules/openapi-generator/src/main/resources/go/client.mustache
@@ -427,6 +427,12 @@ func (c *APIClient) prepareRequest(
}
// Adding Query Param
+ // OpenAPI 3.2: an "in: querystring" parameter embeds the whole, already
+ // encoded query string into the path. Capture it before url.Query() would
+ // merge and re-encode it, and keep it verbatim ahead of the encoded
+ // parameters (path-declared query data first, as before).
+ rawQueryString := url.RawQuery
+ url.RawQuery = ""
query := url.Query()
for k, v := range queryParams {
for _, iv := range v {
@@ -435,11 +441,19 @@ func (c *APIClient) prepareRequest(
}
// Encode the parameters.
- url.RawQuery = queryParamSplit.ReplaceAllStringFunc(query.Encode(), func(s string) string {
+ encodedQuery := queryParamSplit.ReplaceAllStringFunc(query.Encode(), func(s string) string {
pieces := strings.Split(s, "=")
pieces[0] = queryDescape.Replace(pieces[0])
return strings.Join(pieces, "=")
})
+ if rawQueryString != "" {
+ if encodedQuery != "" {
+ rawQueryString += "&"
+ }
+ url.RawQuery = rawQueryString + encodedQuery
+ } else {
+ url.RawQuery = encodedQuery
+ }
// Generate a new request
if body != nil {
diff --git a/modules/openapi-generator/src/main/resources/kotlin-client/README.mustache b/modules/openapi-generator/src/main/resources/kotlin-client/README.mustache
index 2f6ea7932919..6a2d1f88cf0e 100644
--- a/modules/openapi-generator/src/main/resources/kotlin-client/README.mustache
+++ b/modules/openapi-generator/src/main/resources/kotlin-client/README.mustache
@@ -61,7 +61,7 @@ All URIs are relative to *{{{basePath}}}*
| Class | Method | HTTP request | Description |
| ------------ | ------------- | ------------- | ------------- |
-{{#apiInfo}}{{#apis}}{{#operations}}{{#operation}}| *{{classname}}* | [**{{operationId}}**]({{apiDocPath}}{{classname}}.md#{{operationIdLowerCase}}) | **{{httpMethod}}** {{path}} | {{{summary}}} |
+{{#apiInfo}}{{#apis}}{{#operations}}{{#operation}}| *{{classname}}* | [**{{operationId}}**]({{apiDocPath}}{{classname}}.md#{{operationIdLowerCase}}) | **{{#vendorExtensions.x-kotlin-http-method-doc}}{{{vendorExtensions.x-kotlin-http-method-doc}}}{{/vendorExtensions.x-kotlin-http-method-doc}}{{^vendorExtensions.x-kotlin-http-method-doc}}{{httpMethod}}{{/vendorExtensions.x-kotlin-http-method-doc}}** {{path}} | {{{summary}}} |
{{/operation}}{{/operations}}{{/apis}}{{/apiInfo}}
{{/generateApiDocs}}
diff --git a/modules/openapi-generator/src/main/resources/kotlin-client/api_doc.mustache b/modules/openapi-generator/src/main/resources/kotlin-client/api_doc.mustache
index facfb481b9ec..67b628f74f74 100644
--- a/modules/openapi-generator/src/main/resources/kotlin-client/api_doc.mustache
+++ b/modules/openapi-generator/src/main/resources/kotlin-client/api_doc.mustache
@@ -5,7 +5,7 @@ All URIs are relative to *{{basePath}}*
| Method | HTTP request | Description |
| ------------- | ------------- | ------------- |
-{{#operations}}{{#operation}}| [**{{operationId}}**]({{classname}}.md#{{operationId}}) | **{{httpMethod}}** {{path}} | {{summary}} |
+{{#operations}}{{#operation}}| [**{{operationId}}**]({{classname}}.md#{{operationId}}) | **{{#vendorExtensions.x-kotlin-http-method-doc}}{{{vendorExtensions.x-kotlin-http-method-doc}}}{{/vendorExtensions.x-kotlin-http-method-doc}}{{^vendorExtensions.x-kotlin-http-method-doc}}{{httpMethod}}{{/vendorExtensions.x-kotlin-http-method-doc}}** {{path}} | {{summary}} |
{{/operation}}{{/operations}}
{{#operations}}
diff --git a/modules/openapi-generator/src/main/resources/kotlin-client/libraries/jvm-ktor/api.mustache b/modules/openapi-generator/src/main/resources/kotlin-client/libraries/jvm-ktor/api.mustache
index 3a830ca88390..2ae0436f35c5 100644
--- a/modules/openapi-generator/src/main/resources/kotlin-client/libraries/jvm-ktor/api.mustache
+++ b/modules/openapi-generator/src/main/resources/kotlin-client/libraries/jvm-ktor/api.mustache
@@ -76,19 +76,19 @@ import com.fasterxml.jackson.databind.ObjectMapper
{{^isArray}}
{{^isString}}
{{^isNumber}}
- {{{paramName}}}?.apply { append("{{{baseName}}}", {{{paramName}}}.toString()) }
+ {{{paramName}}}?.apply { append("{{#lambda.escapeDollar}}{{{baseName}}}{{/lambda.escapeDollar}}", {{{paramName}}}.toString()) }
{{/isNumber}}
{{#isNumber}}
- {{{paramName}}}?.apply { append("{{{baseName}}}", {{{paramName}}}.toString()) }
+ {{{paramName}}}?.apply { append("{{#lambda.escapeDollar}}{{{baseName}}}{{/lambda.escapeDollar}}", {{{paramName}}}.toString()) }
{{/isNumber}}
{{/isString}}
{{#isString}}
- {{{paramName}}}?.apply { append("{{{baseName}}}", {{{paramName}}}) }
+ {{{paramName}}}?.apply { append("{{#lambda.escapeDollar}}{{{baseName}}}{{/lambda.escapeDollar}}", {{{paramName}}}) }
{{/isString}}
{{/isArray}}
{{#isArray}}
for (x in {{paramName}} ?: listOf()) {
- {{#isFile}}append(it){{/isFile}}{{^isFile}}append("{{{baseName}}}", x.toString()){{/isFile}}
+ {{#isFile}}append(x){{/isFile}}{{^isFile}}append("{{#lambda.escapeDollar}}{{{baseName}}}{{/lambda.escapeDollar}}", x.toString()){{/isFile}}
}
{{/isArray}}
{{/isFile}}
@@ -96,28 +96,28 @@ import com.fasterxml.jackson.databind.ObjectMapper
}
{{/isMultipart}}
{{^isMultipart}}
- ParametersBuilder().also {
+ ParametersBuilder().also { localVariableBuilder ->
{{#formParams}}
{{#isFile}}
- {{{paramName}}}?.apply { it.append("{{{baseName}}}", {{{paramName}}}) }
+ {{{paramName}}}?.apply { localVariableBuilder.append("{{#lambda.escapeDollar}}{{{baseName}}}{{/lambda.escapeDollar}}", {{{paramName}}}) }
{{/isFile}}
{{^isFile}}
{{^isArray}}
{{^isString}}
{{^isNumber}}
- {{{paramName}}}?.apply { it.append("{{{baseName}}}", {{{paramName}}}.toString()) }
+ {{{paramName}}}?.apply { localVariableBuilder.append("{{#lambda.escapeDollar}}{{{baseName}}}{{/lambda.escapeDollar}}", {{{paramName}}}.toString()) }
{{/isNumber}}
{{#isNumber}}
- {{{paramName}}}?.apply { it.append("{{{baseName}}}", {{{paramName}}}.toString()) }
+ {{{paramName}}}?.apply { localVariableBuilder.append("{{#lambda.escapeDollar}}{{{baseName}}}{{/lambda.escapeDollar}}", {{{paramName}}}.toString()) }
{{/isNumber}}
{{/isString}}
{{#isString}}
- {{{paramName}}}?.apply { it.append("{{{baseName}}}", {{{paramName}}}) }
+ {{{paramName}}}?.apply { localVariableBuilder.append("{{#lambda.escapeDollar}}{{{baseName}}}{{/lambda.escapeDollar}}", {{{paramName}}}) }
{{/isString}}
{{/isArray}}
{{#isArray}}
for (x in {{paramName}} ?: listOf()) {
- append("{{{baseName}}}", x.toString())
+ localVariableBuilder.append("{{#lambda.escapeDollar}}{{{baseName}}}{{/lambda.escapeDollar}}", x.toString())
}
{{/isArray}}
{{/isFile}}
@@ -135,54 +135,54 @@ import com.fasterxml.jackson.databind.ObjectMapper
{{#isModel}}
{{#isDeepObject}}
{{#vars}}
- {{{paramName}}}?.{{{name}}}?.let { localVariableQuery["{{vendorExtensions.x-kotlin-param-base-name}}[{{baseName}}]"] = listOf("$it") }
+ {{{paramName}}}?.{{{name}}}?.let { localVariableQuery["{{#lambda.escapeDollar}}{{vendorExtensions.x-kotlin-param-base-name}}{{/lambda.escapeDollar}}[{{#lambda.escapeDollar}}{{baseName}}{{/lambda.escapeDollar}}]"] = listOf("$it") }
{{/vars}}
{{/isDeepObject}}
{{^isDeepObject}}
{{#isExplode}}
{{#vars}}
- {{{paramName}}}?.{{{name}}}?.let { localVariableQuery["{{baseName}}"] = listOf("$it") }
+ {{{paramName}}}?.{{{name}}}?.let { localVariableQuery["{{#lambda.escapeDollar}}{{baseName}}{{/lambda.escapeDollar}}"] = listOf("$it") }
{{/vars}}
{{/isExplode}}
{{^isExplode}}
- {{{paramName}}}?.let { _model -> listOfNotNull({{#vars}}_model.{{{name}}}?.let { "{{baseName}},$it" }{{^-last}}, {{/-last}}{{/vars}}).takeIf { it.isNotEmpty() }?.let { localVariableQuery["{{baseName}}"] = listOf(it.joinToString(",")) } }
+ {{{paramName}}}?.let { _model -> listOfNotNull({{#vars}}_model.{{{name}}}?.let { "{{#lambda.escapeDollar}}{{baseName}}{{/lambda.escapeDollar}},$it" }{{^-last}}, {{/-last}}{{/vars}}).takeIf { it.isNotEmpty() }?.let { localVariableQuery["{{#lambda.escapeDollar}}{{baseName}}{{/lambda.escapeDollar}}"] = listOf(it.joinToString(",")) } }
{{/isExplode}}
{{/isDeepObject}}
{{/isModel}}
{{^isModel}}
{{#isMap}}
{{#isDeepObject}}
- {{{paramName}}}?.forEach { (key, value) -> localVariableQuery["{{baseName}}[$key]"] = listOf("$value") }
+ {{{paramName}}}?.forEach { (key, value) -> localVariableQuery["{{#lambda.escapeDollar}}{{baseName}}{{/lambda.escapeDollar}}[$key]"] = listOf("$value") }
{{/isDeepObject}}
{{^isDeepObject}}
{{#isExplode}}
{{{paramName}}}?.forEach { (key, value) -> localVariableQuery[key] = listOf("$value") }
{{/isExplode}}
{{^isExplode}}
- {{{paramName}}}?.takeIf { it.isNotEmpty() }?.let { localVariableQuery["{{baseName}}"] = listOf(it.entries.joinToString(",") { (k, v) -> "$k,$v" }) }
+ {{{paramName}}}?.takeIf { it.isNotEmpty() }?.let { localVariableQuery["{{#lambda.escapeDollar}}{{baseName}}{{/lambda.escapeDollar}}"] = listOf(it.entries.joinToString(",") { (k, v) -> "$k,$v" }) }
{{/isExplode}}
{{/isDeepObject}}
{{/isMap}}
{{^isMap}}
- {{{paramName}}}?.apply { localVariableQuery["{{baseName}}"] = {{#isContainer}}toMultiValue(this, "{{collectionFormat}}"){{/isContainer}}{{^isContainer}}listOf("${{{paramName}}}"){{/isContainer}} }
+ {{{paramName}}}?.apply { localVariableQuery["{{#lambda.escapeDollar}}{{baseName}}{{/lambda.escapeDollar}}"] = {{#isContainer}}toMultiValue(this, "{{collectionFormat}}"){{/isContainer}}{{^isContainer}}listOf("${{{paramName}}}"){{/isContainer}} }
{{/isMap}}
{{/isModel}}
{{/queryParams}}
val localVariableHeaders = mutableMapOf()
{{#headerParams}}
- {{{paramName}}}?.apply { localVariableHeaders["{{baseName}}"] = {{#isContainer}}this.joinToString(separator = collectionDelimiter("{{collectionFormat}}")){{/isContainer}}{{^isContainer}}this.toString(){{/isContainer}} }
+ {{{paramName}}}?.apply { localVariableHeaders["{{#lambda.escapeDollar}}{{baseName}}{{/lambda.escapeDollar}}"] = {{#isContainer}}this.joinToString(separator = collectionDelimiter("{{collectionFormat}}")){{/isContainer}}{{^isContainer}}this.toString(){{/isContainer}} }
{{/headerParams}}
val localVariableConfig = RequestConfig(
RequestMethod.{{httpMethod}},
- "{{{path}}}"{{#pathParams}}.replace("{" + "{{baseName}}" + "}", "${{{paramName}}}"){{/pathParams}},
+ "{{#lambda.escapeDollar}}{{{path}}}{{/lambda.escapeDollar}}"{{#pathParams}}.replace("{" + "{{#lambda.escapeDollar}}{{baseName}}{{/lambda.escapeDollar}}" + "}", "${{{paramName}}}"){{/pathParams}},
query = localVariableQuery,
headers = localVariableHeaders,
requiresAuthentication = {{#hasAuthMethods}}true{{/hasAuthMethods}}{{^hasAuthMethods}}false{{/hasAuthMethods}},
)
- return {{#hasBodyParam}}jsonRequest{{/hasBodyParam}}{{^hasBodyParam}}{{#hasFormParams}}{{#isMultipart}}multipartFormRequest{{/isMultipart}}{{^isMultipart}}urlEncodedFormRequest{{/isMultipart}}{{/hasFormParams}}{{^hasFormParams}}request{{/hasFormParams}}{{/hasBodyParam}}(
+ return {{#hasBodyParam}}this.jsonRequest{{/hasBodyParam}}{{^hasBodyParam}}{{#hasFormParams}}{{#isMultipart}}this.multipartFormRequest{{/isMultipart}}{{^isMultipart}}this.urlEncodedFormRequest{{/isMultipart}}{{/hasFormParams}}{{^hasFormParams}}this.request{{/hasFormParams}}{{/hasBodyParam}}(
localVariableConfig,
localVariableBody,
localVariableAuthNames
diff --git a/modules/openapi-generator/src/main/resources/kotlin-client/libraries/jvm-ktor/infrastructure/ApiClient.kt.mustache b/modules/openapi-generator/src/main/resources/kotlin-client/libraries/jvm-ktor/infrastructure/ApiClient.kt.mustache
index 43542d634bcc..764022212481 100644
--- a/modules/openapi-generator/src/main/resources/kotlin-client/libraries/jvm-ktor/infrastructure/ApiClient.kt.mustache
+++ b/modules/openapi-generator/src/main/resources/kotlin-client/libraries/jvm-ktor/infrastructure/ApiClient.kt.mustache
@@ -73,7 +73,7 @@ import {{packageName}}.auth.*
mapOf({{#authMethods}}{{#isBasic}}{{#isBasicBasic}}
"{{name}}" to HttpBasicAuth(){{/isBasicBasic}}{{#isBasicBearer}}
"{{name}}" to HttpBearerAuth("{{scheme}}"){{/isBasicBearer}}{{/isBasic}}{{#isApiKey}}
- "{{name}}" to ApiKeyAuth({{#isKeyInHeader}}"header"{{/isKeyInHeader}}{{^isKeyInHeader}}"query"{{/isKeyInHeader}}, "{{keyParamName}}"){{/isApiKey}}{{#isOAuth}}
+ "{{name}}" to ApiKeyAuth({{#isKeyInHeader}}"header"{{/isKeyInHeader}}{{^isKeyInHeader}}"query"{{/isKeyInHeader}}, "{{#lambda.escapeDollar}}{{keyParamName}}{{/lambda.escapeDollar}}"){{/isApiKey}}{{#isOAuth}}
"{{name}}" to OAuth(){{/isOAuth}}{{^-last}}, {{/-last}}{{/authMethods}})
}
{{/hasAuthMethods}}
diff --git a/modules/openapi-generator/src/main/resources/kotlin-client/libraries/jvm-okhttp/api.mustache b/modules/openapi-generator/src/main/resources/kotlin-client/libraries/jvm-okhttp/api.mustache
index 8f412c86ee54..557982065762 100644
--- a/modules/openapi-generator/src/main/resources/kotlin-client/libraries/jvm-okhttp/api.mustache
+++ b/modules/openapi-generator/src/main/resources/kotlin-client/libraries/jvm-okhttp/api.mustache
@@ -209,18 +209,19 @@ import {{packageName}}.infrastructure.Serializer
{{/hasQueryParams}}{{#hasQueryParams}}mutableMapOf>()
.apply {
{{#queryParams}}
+ {{^isQueryStringParam}}
{{^vendorExtensions.x-kotlin-explode-form-object}}
{{^required}}
if ({{{paramName}}} != null) {
{{#isModel}}
{{#vars}}
if ({{{paramName}}}.{{name}} != null) {
- put("{{#isDeepObject}}{{{paramName}}}[{{/isDeepObject}}{{#lambda.escapeDollar}}{{baseName}}{{/lambda.escapeDollar}}{{#isDeepObject}}]{{/isDeepObject}}", {{#isContainer}}toMultiValue({{{paramName}}}.{{name}}.toList(), "{{collectionFormat}}"){{/isContainer}}{{^isContainer}}listOf({{#isDateTime}}parseDateToQueryString<{{{dataType}}}>({{{paramName}}}.{{name}}){{/isDateTime}}{{#isDate}}parseDateToQueryString<{{{dataType}}}>({{{paramName}}}.{{name}}){{/isDate}}{{#isEnum}}{{#isString}}{{{paramName}}}.{{name}}.value{{/isString}}{{^isString}}{{{paramName}}}.{{name}}.toString(){{/isString}}{{/isEnum}}{{^isEnum}}{{^isDateTime}}{{^isDate}}{{{paramName}}}.{{name}}.toString(){{/isDate}}{{/isDateTime}}{{/isEnum}}){{/isContainer}})
+ put("{{#isDeepObject}}{{#lambda.escapeDollar}}{{vendorExtensions.x-kotlin-param-base-name}}{{/lambda.escapeDollar}}[{{/isDeepObject}}{{#lambda.escapeDollar}}{{baseName}}{{/lambda.escapeDollar}}{{#isDeepObject}}]{{/isDeepObject}}", {{#isContainer}}toMultiValue({{{paramName}}}.{{name}}.toList(), "{{collectionFormat}}"){{/isContainer}}{{^isContainer}}listOf({{#isDateTime}}this@{{classname}}.parseDateToQueryString<{{{dataType}}}>({{{paramName}}}.{{name}}){{/isDateTime}}{{#isDate}}this@{{classname}}.parseDateToQueryString<{{{dataType}}}>({{{paramName}}}.{{name}}){{/isDate}}{{#isEnum}}{{#isString}}{{{paramName}}}.{{name}}.value{{/isString}}{{^isString}}{{{paramName}}}.{{name}}.toString(){{/isString}}{{/isEnum}}{{^isEnum}}{{^isDateTime}}{{^isDate}}{{{paramName}}}.{{name}}.toString(){{/isDate}}{{/isDateTime}}{{/isEnum}}){{/isContainer}})
}
{{/vars}}
{{/isModel}}
{{^isModel}}
- put("{{#lambda.escapeDollar}}{{baseName}}{{/lambda.escapeDollar}}", {{#isContainer}}toMultiValue({{{paramName}}}.toList(), "{{collectionFormat}}"){{/isContainer}}{{^isContainer}}listOf({{#isDateTime}}parseDateToQueryString<{{{dataType}}}>({{{paramName}}}){{/isDateTime}}{{#isDate}}parseDateToQueryString<{{{dataType}}}>({{{paramName}}}){{/isDate}}{{#isEnum}}{{#isString}}{{{paramName}}}.value{{/isString}}{{^isString}}{{{paramName}}}.toString(){{/isString}}{{/isEnum}}{{^isEnum}}{{^isDateTime}}{{^isDate}}{{{paramName}}}.toString(){{/isDate}}{{/isDateTime}}{{/isEnum}}){{/isContainer}})
+ put("{{#lambda.escapeDollar}}{{baseName}}{{/lambda.escapeDollar}}", {{#isContainer}}toMultiValue({{{paramName}}}.toList(), "{{collectionFormat}}"){{/isContainer}}{{^isContainer}}listOf({{#isDateTime}}this@{{classname}}.parseDateToQueryString<{{{dataType}}}>({{{paramName}}}){{/isDateTime}}{{#isDate}}this@{{classname}}.parseDateToQueryString<{{{dataType}}}>({{{paramName}}}){{/isDate}}{{#isEnum}}{{#isString}}{{{paramName}}}.value{{/isString}}{{^isString}}{{{paramName}}}.toString(){{/isString}}{{/isEnum}}{{^isEnum}}{{^isDateTime}}{{^isDate}}{{{paramName}}}.toString(){{/isDate}}{{/isDateTime}}{{/isEnum}}){{/isContainer}})
{{/isModel}}
}
{{/required}}
@@ -230,12 +231,12 @@ import {{packageName}}.infrastructure.Serializer
{{#isModel}}
{{#vars}}
if ({{{paramName}}}.{{name}} != null) {
- put("{{#isDeepObject}}{{{paramName}}}[{{/isDeepObject}}{{#lambda.escapeDollar}}{{baseName}}{{/lambda.escapeDollar}}{{#isDeepObject}}]{{/isDeepObject}}", {{#isContainer}}toMultiValue({{{paramName}}}.{{name}}.toList(), "{{collectionFormat}}"){{/isContainer}}{{^isContainer}}listOf({{#isDateTime}}parseDateToQueryString<{{{dataType}}}>({{{paramName}}}.{{name}}){{/isDateTime}}{{#isDate}}parseDateToQueryString<{{{dataType}}}>({{{paramName}}}.{{name}}){{/isDate}}{{#isEnum}}{{#isString}}{{{paramName}}}.{{name}}.value{{/isString}}{{^isString}}{{{paramName}}}.{{name}}.toString(){{/isString}}{{/isEnum}}{{^isEnum}}{{^isDateTime}}{{^isDate}}{{{paramName}}}.{{name}}.toString(){{/isDate}}{{/isDateTime}}{{/isEnum}}){{/isContainer}})
+ put("{{#isDeepObject}}{{#lambda.escapeDollar}}{{vendorExtensions.x-kotlin-param-base-name}}{{/lambda.escapeDollar}}[{{/isDeepObject}}{{#lambda.escapeDollar}}{{baseName}}{{/lambda.escapeDollar}}{{#isDeepObject}}]{{/isDeepObject}}", {{#isContainer}}toMultiValue({{{paramName}}}.{{name}}.toList(), "{{collectionFormat}}"){{/isContainer}}{{^isContainer}}listOf({{#isDateTime}}this@{{classname}}.parseDateToQueryString<{{{dataType}}}>({{{paramName}}}.{{name}}){{/isDateTime}}{{#isDate}}this@{{classname}}.parseDateToQueryString<{{{dataType}}}>({{{paramName}}}.{{name}}){{/isDate}}{{#isEnum}}{{#isString}}{{{paramName}}}.{{name}}.value{{/isString}}{{^isString}}{{{paramName}}}.{{name}}.toString(){{/isString}}{{/isEnum}}{{^isEnum}}{{^isDateTime}}{{^isDate}}{{{paramName}}}.{{name}}.toString(){{/isDate}}{{/isDateTime}}{{/isEnum}}){{/isContainer}})
}
{{/vars}}
{{/isModel}}
{{^isModel}}
- put("{{#lambda.escapeDollar}}{{baseName}}{{/lambda.escapeDollar}}", {{#isContainer}}toMultiValue({{{paramName}}}.toList(), "{{collectionFormat}}"){{/isContainer}}{{^isContainer}}listOf({{#isDateTime}}parseDateToQueryString<{{{dataType}}}>({{{paramName}}}){{/isDateTime}}{{#isDate}}parseDateToQueryString<{{{dataType}}}>({{{paramName}}}){{/isDate}}{{#isEnum}}{{#isString}}{{{paramName}}}.value{{/isString}}{{^isString}}{{{paramName}}}.toString(){{/isString}}{{/isEnum}}{{^isEnum}}{{^isDateTime}}{{^isDate}}{{{paramName}}}.toString(){{/isDate}}{{/isDateTime}}{{/isEnum}}){{/isContainer}})
+ put("{{#lambda.escapeDollar}}{{baseName}}{{/lambda.escapeDollar}}", {{#isContainer}}toMultiValue({{{paramName}}}.toList(), "{{collectionFormat}}"){{/isContainer}}{{^isContainer}}listOf({{#isDateTime}}this@{{classname}}.parseDateToQueryString<{{{dataType}}}>({{{paramName}}}){{/isDateTime}}{{#isDate}}this@{{classname}}.parseDateToQueryString<{{{dataType}}}>({{{paramName}}}){{/isDate}}{{#isEnum}}{{#isString}}{{{paramName}}}.value{{/isString}}{{^isString}}{{{paramName}}}.toString(){{/isString}}{{/isEnum}}{{^isEnum}}{{^isDateTime}}{{^isDate}}{{{paramName}}}.toString(){{/isDate}}{{/isDateTime}}{{/isEnum}}){{/isContainer}})
{{/isModel}}
}
{{/isNullable}}
@@ -243,29 +244,30 @@ import {{packageName}}.infrastructure.Serializer
{{#isModel}}
{{#vars}}
if ({{{paramName}}}.{{name}} != null) {
- put("{{#isDeepObject}}{{{paramName}}}[{{/isDeepObject}}{{#lambda.escapeDollar}}{{baseName}}{{/lambda.escapeDollar}}{{#isDeepObject}}]{{/isDeepObject}}", {{#isContainer}}toMultiValue({{{paramName}}}.{{name}}.toList(), "{{collectionFormat}}"){{/isContainer}}{{^isContainer}}listOf({{#isDateTime}}parseDateToQueryString<{{{dataType}}}>({{{paramName}}}.{{name}}){{/isDateTime}}{{#isDate}}parseDateToQueryString<{{{dataType}}}>({{{paramName}}}.{{name}}){{/isDate}}{{#isEnum}}{{#isString}}{{{paramName}}}.{{name}}.value{{/isString}}{{^isString}}{{{paramName}}}.{{name}}.toString(){{/isString}}{{/isEnum}}{{^isEnum}}{{^isDateTime}}{{^isDate}}{{{paramName}}}.{{name}}.toString(){{/isDate}}{{/isDateTime}}{{/isEnum}}){{/isContainer}})
+ put("{{#isDeepObject}}{{#lambda.escapeDollar}}{{vendorExtensions.x-kotlin-param-base-name}}{{/lambda.escapeDollar}}[{{/isDeepObject}}{{#lambda.escapeDollar}}{{baseName}}{{/lambda.escapeDollar}}{{#isDeepObject}}]{{/isDeepObject}}", {{#isContainer}}toMultiValue({{{paramName}}}.{{name}}.toList(), "{{collectionFormat}}"){{/isContainer}}{{^isContainer}}listOf({{#isDateTime}}this@{{classname}}.parseDateToQueryString<{{{dataType}}}>({{{paramName}}}.{{name}}){{/isDateTime}}{{#isDate}}this@{{classname}}.parseDateToQueryString<{{{dataType}}}>({{{paramName}}}.{{name}}){{/isDate}}{{#isEnum}}{{#isString}}{{{paramName}}}.{{name}}.value{{/isString}}{{^isString}}{{{paramName}}}.{{name}}.toString(){{/isString}}{{/isEnum}}{{^isEnum}}{{^isDateTime}}{{^isDate}}{{{paramName}}}.{{name}}.toString(){{/isDate}}{{/isDateTime}}{{/isEnum}}){{/isContainer}})
}
{{/vars}}
{{/isModel}}
{{^isModel}}
- put("{{#lambda.escapeDollar}}{{baseName}}{{/lambda.escapeDollar}}", {{#isContainer}}toMultiValue({{{paramName}}}.toList(), "{{collectionFormat}}"){{/isContainer}}{{^isContainer}}listOf({{#isDateTime}}parseDateToQueryString<{{{dataType}}}>({{{paramName}}}){{/isDateTime}}{{#isDate}}parseDateToQueryString<{{{dataType}}}>({{{paramName}}}){{/isDate}}{{#isEnum}}{{#isString}}{{{paramName}}}.value{{/isString}}{{^isString}}{{{paramName}}}.toString(){{/isString}}{{/isEnum}}{{^isEnum}}{{^isDateTime}}{{^isDate}}{{{paramName}}}.toString(){{/isDate}}{{/isDateTime}}{{/isEnum}}){{/isContainer}})
+ put("{{#lambda.escapeDollar}}{{baseName}}{{/lambda.escapeDollar}}", {{#isContainer}}toMultiValue({{{paramName}}}.toList(), "{{collectionFormat}}"){{/isContainer}}{{^isContainer}}listOf({{#isDateTime}}this@{{classname}}.parseDateToQueryString<{{{dataType}}}>({{{paramName}}}){{/isDateTime}}{{#isDate}}this@{{classname}}.parseDateToQueryString<{{{dataType}}}>({{{paramName}}}){{/isDate}}{{#isEnum}}{{#isString}}{{{paramName}}}.value{{/isString}}{{^isString}}{{{paramName}}}.toString(){{/isString}}{{/isEnum}}{{^isEnum}}{{^isDateTime}}{{^isDate}}{{{paramName}}}.toString(){{/isDate}}{{/isDateTime}}{{/isEnum}}){{/isContainer}})
{{/isModel}}
{{/isNullable}}
{{/required}}
{{/vendorExtensions.x-kotlin-explode-form-object}}
+ {{/isQueryStringParam}}
{{/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) {
+ val localVariableValues = 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)
+ }.filterNotNull().map { this@{{classname}}.parameterToString(it) }
+ if (key != null && localVariableValues.isNotEmpty()) {
+ val localVariableName = key.toString()
+ put(localVariableName, getOrElse(localVariableName) { emptyList() } + localVariableValues)
}
}
{{/vendorExtensions.x-kotlin-explode-form-object}}
@@ -280,9 +282,18 @@ import {{packageName}}.infrastructure.Serializer
{{/headerParams}}
return RequestConfig(
+{{#vendorExtensions.x-kotlin-custom-method}}
+ method = RequestMethod.GET,
+ customMethod = "{{{vendorExtensions.x-kotlin-custom-method}}}",
+{{/vendorExtensions.x-kotlin-custom-method}}
+{{^vendorExtensions.x-kotlin-custom-method}}
method = RequestMethod.{{httpMethod}},
- path = "{{{path}}}"{{#pathParams}}.replace("{"+"{{#lambda.escapeDollar}}{{baseName}}{{/lambda.escapeDollar}}"+"}", encodeURIComponent({{#isContainer}}{{paramName}}.joinToString(","){{/isContainer}}{{^isContainer}}{{{paramName}}}{{#isEnum}}{{^required}}?{{/required}}.value{{/isEnum}}.toString(){{/isContainer}})){{/pathParams}},
+{{/vendorExtensions.x-kotlin-custom-method}}
+ path = "{{#lambda.escapeDollar}}{{{path}}}{{/lambda.escapeDollar}}"{{#pathParams}}.replace("{"+"{{#lambda.escapeDollar}}{{baseName}}{{/lambda.escapeDollar}}"+"}", this@{{classname}}.encodeURIComponent({{#isContainer}}{{paramName}}.joinToString(","){{/isContainer}}{{^isContainer}}{{{paramName}}}{{#isEnum}}{{^required}}?{{/required}}.value{{/isEnum}}.toString(){{/isContainer}})){{/pathParams}},
query = localVariableQuery,
+{{#vendorExtensions.x-kotlin-querystring-params}}
+ encodedQueryString = listOfNotNull({{{vendorExtensions.x-kotlin-querystring-params}}}).joinToString("&"),
+{{/vendorExtensions.x-kotlin-querystring-params}}
headers = localVariableHeaders,
requiresAuthentication = {{#hasAuthMethods}}true{{/hasAuthMethods}}{{^hasAuthMethods}}false{{/hasAuthMethods}},
body = localVariableBody
diff --git a/modules/openapi-generator/src/main/resources/kotlin-client/libraries/jvm-okhttp/infrastructure/ApiClient.kt.mustache b/modules/openapi-generator/src/main/resources/kotlin-client/libraries/jvm-okhttp/infrastructure/ApiClient.kt.mustache
index 3b9223fd5380..91fc2e5a0cfe 100644
--- a/modules/openapi-generator/src/main/resources/kotlin-client/libraries/jvm-okhttp/infrastructure/ApiClient.kt.mustache
+++ b/modules/openapi-generator/src/main/resources/kotlin-client/libraries/jvm-okhttp/infrastructure/ApiClient.kt.mustache
@@ -84,6 +84,13 @@ import com.squareup.moshi.adapter
)
protected const val Authorization: String = AUTHORIZATION
+ {{#x-kotlin-verbatim-methods}}
+ // OpenAPI 3.2: OkHttp 5 rejects a null body for these methods
+ // (its requiresRequestBody set), so they always carry a possibly-empty body
+ @PublishedApi
+ internal val REQUIRES_REQUEST_BODY_METHODS = setOf("QUERY", "REPORT", "PROPPATCH")
+
+ {{/x-kotlin-verbatim-methods}}
protected const val JSON_MEDIA_TYPE: String = "application/json"
@Deprecated(
message = "Please use the capitalized constant `JSON_MEDIA_TYPE` instead.",
@@ -431,25 +438,25 @@ import com.squareup.moshi.adapter
{{#authMethods}}
{{#isApiKey}}
{{#isKeyInHeader}}
- if (requestConfig.headers["{{keyParamName}}"].isNullOrEmpty()) {
+ if (requestConfig.headers["{{#lambda.escapeDollar}}{{keyParamName}}{{/lambda.escapeDollar}}"].isNullOrEmpty()) {
{{/isKeyInHeader}}
{{#isKeyInQuery}}
- if (requestConfig.query["{{keyParamName}}"].isNullOrEmpty()) {
+ if (requestConfig.query["{{#lambda.escapeDollar}}{{keyParamName}}{{/lambda.escapeDollar}}"].isNullOrEmpty()) {
{{/isKeyInQuery}}
- if (apiKey["{{keyParamName}}"] != null) {
- if (apiKeyPrefix["{{keyParamName}}"] != null) {
+ if (apiKey["{{#lambda.escapeDollar}}{{keyParamName}}{{/lambda.escapeDollar}}"] != null) {
+ if (apiKeyPrefix["{{#lambda.escapeDollar}}{{keyParamName}}{{/lambda.escapeDollar}}"] != null) {
{{#isKeyInHeader}}
- requestConfig.headers["{{keyParamName}}"] = apiKeyPrefix["{{keyParamName}}"]!! + " " + apiKey["{{keyParamName}}"]!!
+ requestConfig.headers["{{#lambda.escapeDollar}}{{keyParamName}}{{/lambda.escapeDollar}}"] = apiKeyPrefix["{{#lambda.escapeDollar}}{{keyParamName}}{{/lambda.escapeDollar}}"]!! + " " + apiKey["{{#lambda.escapeDollar}}{{keyParamName}}{{/lambda.escapeDollar}}"]!!
{{/isKeyInHeader}}
{{#isKeyInQuery}}
- requestConfig.query["{{keyParamName}}"] = listOf(apiKeyPrefix["{{keyParamName}}"]!! + " " + apiKey["{{keyParamName}}"]!!)
+ requestConfig.query["{{#lambda.escapeDollar}}{{keyParamName}}{{/lambda.escapeDollar}}"] = listOf(apiKeyPrefix["{{#lambda.escapeDollar}}{{keyParamName}}{{/lambda.escapeDollar}}"]!! + " " + apiKey["{{#lambda.escapeDollar}}{{keyParamName}}{{/lambda.escapeDollar}}"]!!)
{{/isKeyInQuery}}
} else {
{{#isKeyInHeader}}
- requestConfig.headers["{{keyParamName}}"] = apiKey["{{keyParamName}}"]!!
+ requestConfig.headers["{{#lambda.escapeDollar}}{{keyParamName}}{{/lambda.escapeDollar}}"] = apiKey["{{#lambda.escapeDollar}}{{keyParamName}}{{/lambda.escapeDollar}}"]!!
{{/isKeyInHeader}}
{{#isKeyInQuery}}
- requestConfig.query["{{keyParamName}}"] = listOf(apiKey["{{keyParamName}}"]!!)
+ requestConfig.query["{{#lambda.escapeDollar}}{{keyParamName}}{{/lambda.escapeDollar}}"] = listOf(apiKey["{{#lambda.escapeDollar}}{{keyParamName}}{{/lambda.escapeDollar}}"]!!)
{{/isKeyInQuery}}
}
}
@@ -506,6 +513,14 @@ import com.squareup.moshi.adapter
addQueryParameter(query.key, queryValue)
}
}
+ {{#x-kotlin-querystring}}
+ // OpenAPI 3.2 `in: querystring`: the value is already encoded, so it is
+ // appended verbatim via encodedQuery (query() would percent-encode it again)
+ requestConfig.encodedQueryString?.takeIf { it.isNotEmpty() }?.let { raw ->
+ val existing = build().encodedQuery
+ encodedQuery(if (existing.isNullOrEmpty()) raw else "$existing&$raw")
+ }
+ {{/x-kotlin-querystring}}
}.build()
// take content-type/accept from spec or set to default (application/json) if not defined
@@ -536,7 +551,21 @@ import com.squareup.moshi.adapter
RequestMethod.PUT -> Request.Builder().url(url).put(requestBody(requestConfig.body, contentType))
RequestMethod.POST -> Request.Builder().url(url).post(requestBody(requestConfig.body, contentType))
RequestMethod.OPTIONS -> Request.Builder().url(url).method("OPTIONS", null)
- }.apply {
+ }.{{#x-kotlin-verbatim-methods}}let { builder ->
+ if (requestConfig.customMethod == null) {
+ builder
+ } else {
+ // OpenAPI 3.2 query/additionalOperations: Request.Builder.method() sends the
+ // token verbatim. OkHttp >= 5 rejects a body-less QUERY/REPORT/PROPPATCH,
+ // so methods in its requiresRequestBody set always carry a (possibly empty) body.
+ val customBody = if (requestConfig.body != null || requestConfig.customMethod in REQUIRES_REQUEST_BODY_METHODS) {
+ requestBody(requestConfig.body, contentType)
+ } else {
+ null
+ }
+ builder.method(requestConfig.customMethod, customBody)
+ }
+ }.apply {{/x-kotlin-verbatim-methods}}{{^x-kotlin-verbatim-methods}}apply {{/x-kotlin-verbatim-methods}}{
val headersBuilder = Headers.Builder()
headers.forEach { header ->
{{#useNonAsciiHeaders}}
diff --git a/modules/openapi-generator/src/main/resources/kotlin-client/libraries/jvm-okhttp/infrastructure/RequestConfig.kt.mustache b/modules/openapi-generator/src/main/resources/kotlin-client/libraries/jvm-okhttp/infrastructure/RequestConfig.kt.mustache
new file mode 100644
index 000000000000..0639f616c8fd
--- /dev/null
+++ b/modules/openapi-generator/src/main/resources/kotlin-client/libraries/jvm-okhttp/infrastructure/RequestConfig.kt.mustache
@@ -0,0 +1,28 @@
+package {{packageName}}.infrastructure
+
+/**
+ * Defines a config object for a given request.
+ * NOTE: This object doesn't include 'body' because it
+ * allows for caching of the constructed object
+ * for many request definitions.
+ * NOTE: Headers is a Map because rfc2616 defines
+ * multi-valued headers as csv-only.
+ */
+{{#nonPublicApi}}internal {{/nonPublicApi}}{{^nonPublicApi}}{{#explicitApi}}public {{/explicitApi}}{{/nonPublicApi}}data class RequestConfig(
+ val method: RequestMethod,
+ val path: String,
+ val headers: MutableMap = mutableMapOf(),
+ val params: MutableMap = mutableMapOf(),
+ val query: MutableMap> = mutableMapOf(),
+ val requiresAuthentication: Boolean,
+ val body: T? = null{{#x-kotlin-verbatim-methods}},
+ /**
+ * OpenAPI 3.2 `query`/`additionalOperations`: non-standard HTTP method token,
+ * sent verbatim. Takes precedence over [method] when set.
+ */
+ val customMethod: String? = null{{/x-kotlin-verbatim-methods}}{{#x-kotlin-querystring}},
+ /**
+ * OpenAPI 3.2 `in: querystring`: already-encoded query string, appended verbatim.
+ */
+ val encodedQueryString: String? = null{{/x-kotlin-querystring}}
+)
diff --git a/modules/openapi-generator/src/main/resources/kotlin-client/libraries/jvm-retrofit2/api.mustache b/modules/openapi-generator/src/main/resources/kotlin-client/libraries/jvm-retrofit2/api.mustache
index 9cd7ef3fea02..5f433eebec9a 100644
--- a/modules/openapi-generator/src/main/resources/kotlin-client/libraries/jvm-retrofit2/api.mustache
+++ b/modules/openapi-generator/src/main/resources/kotlin-client/libraries/jvm-retrofit2/api.mustache
@@ -155,7 +155,7 @@ import okhttp3.MultipartBody
{{/-first}}
{{/prioritizedContentTypes}}
{{/formParams}}
- @{{httpMethod}}("{{{path}}}")
+ @{{httpMethod}}("{{#lambda.escapeDollar}}{{{path}}}{{/lambda.escapeDollar}}")
{{^nonPublicApi}}{{#explicitApi}}public {{/explicitApi}}{{/nonPublicApi}}{{^doNotUseRxAndCoroutines}}{{#useCoroutines}}suspend {{/useCoroutines}}{{/doNotUseRxAndCoroutines}}fun {{operationId}}({{^allParams}}){{/allParams}}{{#allParams}}{{>queryParams}}{{>pathParams}}{{>headerParams}}{{>bodyParams}}{{>formParams}}{{^-last}}, {{/-last}}{{#-last}}){{/-last}}{{/allParams}}{{#returnType}}: {{/returnType}}{{^returnType}}{{#useResponseAsReturnType}}: {{/useResponseAsReturnType}}{{/returnType}}{{^doNotUseRxAndCoroutines}}{{#useRxJava}}Observable<{{#isResponseFile}}ResponseBody{{/isResponseFile}}{{^isResponseFile}}{{{returnType}}}{{^returnType}}Unit{{/returnType}}{{/isResponseFile}}>{{/useRxJava}}{{#useRxJava2}}{{#returnType}}Single<{{#isResponseFile}}ResponseBody{{/isResponseFile}}{{^isResponseFile}}{{{returnType}}}{{/isResponseFile}}>{{/returnType}}{{^returnType}}Completable{{/returnType}}{{/useRxJava2}}{{#useRxJava3}}{{#returnType}}Single<{{#isResponseFile}}ResponseBody{{/isResponseFile}}{{^isResponseFile}}{{{returnType}}}{{/isResponseFile}}>{{/returnType}}{{^returnType}}Completable{{/returnType}}{{/useRxJava3}}{{#useCoroutines}}{{#useResponseAsReturnType}}Response<{{/useResponseAsReturnType}}{{#isResponseFile}}ResponseBody{{/isResponseFile}}{{^isResponseFile}}{{{returnType}}}{{^returnType}}{{#useResponseAsReturnType}}Unit{{/useResponseAsReturnType}}{{/returnType}}{{/isResponseFile}}{{#useResponseAsReturnType}}>{{/useResponseAsReturnType}}{{/useCoroutines}}{{/doNotUseRxAndCoroutines}}{{#doNotUseRxAndCoroutines}}Call<{{#isResponseFile}}ResponseBody{{/isResponseFile}}{{^isResponseFile}}{{{returnType}}}{{^returnType}}Unit{{/returnType}}{{/isResponseFile}}>{{/doNotUseRxAndCoroutines}}
{{/operation}}
diff --git a/modules/openapi-generator/src/main/resources/kotlin-client/libraries/jvm-retrofit2/explodedQueryParam.mustache b/modules/openapi-generator/src/main/resources/kotlin-client/libraries/jvm-retrofit2/explodedQueryParam.mustache
index 3aaa4e67fbf4..162d18e2b3c6 100644
--- a/modules/openapi-generator/src/main/resources/kotlin-client/libraries/jvm-retrofit2/explodedQueryParam.mustache
+++ b/modules/openapi-generator/src/main/resources/kotlin-client/libraries/jvm-retrofit2/explodedQueryParam.mustache
@@ -1 +1 @@
-@Query("{{baseName}}") {{{baseName}}}: {{#collectionFormat}}{{#isCollectionFormatMulti}}{{{dataType}}}{{/isCollectionFormatMulti}}{{^isCollectionFormatMulti}}{{{collectionFormat.toUpperCase}}}Params{{/isCollectionFormatMulti}}{{/collectionFormat}}{{^collectionFormat}}{{{dataType}}}{{/collectionFormat}}{{#required}}{{#defaultValue}} = {{^isNumber}}{{{defaultValue}}}{{/isNumber}}{{#isNumber}}{{{dataType}}}("{{{defaultValue}}}"){{/isNumber}}{{/defaultValue}}{{/required}}{{^required}}?{{#defaultValue}} = {{^isNumber}}{{{defaultValue}}}{{/isNumber}}{{#isNumber}}{{{dataType}}}("{{{defaultValue}}}"){{/isNumber}}{{/defaultValue}}{{^defaultValue}} = null{{/defaultValue}}{{/required}}
\ No newline at end of file
+@Query("{{#lambda.escapeDollar}}{{baseName}}{{/lambda.escapeDollar}}") {{{name}}}: {{#collectionFormat}}{{#isCollectionFormatMulti}}{{{dataType}}}{{/isCollectionFormatMulti}}{{^isCollectionFormatMulti}}{{{collectionFormat.toUpperCase}}}Params{{/isCollectionFormatMulti}}{{/collectionFormat}}{{^collectionFormat}}{{{dataType}}}{{/collectionFormat}}{{#required}}{{#defaultValue}} = {{^isNumber}}{{{defaultValue}}}{{/isNumber}}{{#isNumber}}{{{dataType}}}("{{{defaultValue}}}"){{/isNumber}}{{/defaultValue}}{{/required}}{{^required}}?{{#defaultValue}} = {{^isNumber}}{{{defaultValue}}}{{/isNumber}}{{#isNumber}}{{{dataType}}}("{{{defaultValue}}}"){{/isNumber}}{{/defaultValue}}{{^defaultValue}} = null{{/defaultValue}}{{/required}}
\ No newline at end of file
diff --git a/modules/openapi-generator/src/main/resources/kotlin-client/libraries/jvm-retrofit2/formParams.mustache b/modules/openapi-generator/src/main/resources/kotlin-client/libraries/jvm-retrofit2/formParams.mustache
index fe5c9db69053..bf92485b7aea 100644
--- a/modules/openapi-generator/src/main/resources/kotlin-client/libraries/jvm-retrofit2/formParams.mustache
+++ b/modules/openapi-generator/src/main/resources/kotlin-client/libraries/jvm-retrofit2/formParams.mustache
@@ -1 +1 @@
-{{#isFormParam}}{{^isFile}}{{#isMultipart}}@Part{{/isMultipart}}{{^isMultipart}}@Field{{/isMultipart}}("{{baseName}}") {{{paramName}}}: {{{dataType}}}{{#required}}{{#defaultValue}} = {{^isNumber}}{{{defaultValue}}}{{/isNumber}}{{#isNumber}}{{{dataType}}}("{{{defaultValue}}}"){{/isNumber}}{{/defaultValue}}{{/required}}{{^required}}?{{#defaultValue}} = {{^isNumber}}{{{defaultValue}}}{{/isNumber}}{{#isNumber}}{{{dataType}}}("{{{defaultValue}}}"){{/isNumber}}{{/defaultValue}}{{^defaultValue}} = null{{/defaultValue}}{{/required}}{{/isFile}}{{#isFile}}{{#isMultipart}}@Part{{/isMultipart}}{{^isMultipart}}@Field("{{baseName}}"){{/isMultipart}} {{{paramName}}}: {{#isCollectionFormatMulti}}List<{{/isCollectionFormatMulti}}MultipartBody.Part{{#isCollectionFormatMulti}}>{{/isCollectionFormatMulti}}{{^required}}? = null{{/required}}{{/isFile}}{{/isFormParam}}
\ No newline at end of file
+{{#isFormParam}}{{^isFile}}{{#isMultipart}}@Part{{/isMultipart}}{{^isMultipart}}@Field{{/isMultipart}}("{{#lambda.escapeDollar}}{{baseName}}{{/lambda.escapeDollar}}") {{{paramName}}}: {{{dataType}}}{{#required}}{{#defaultValue}} = {{^isNumber}}{{{defaultValue}}}{{/isNumber}}{{#isNumber}}{{{dataType}}}("{{{defaultValue}}}"){{/isNumber}}{{/defaultValue}}{{/required}}{{^required}}?{{#defaultValue}} = {{^isNumber}}{{{defaultValue}}}{{/isNumber}}{{#isNumber}}{{{dataType}}}("{{{defaultValue}}}"){{/isNumber}}{{/defaultValue}}{{^defaultValue}} = null{{/defaultValue}}{{/required}}{{/isFile}}{{#isFile}}{{#isMultipart}}@Part{{/isMultipart}}{{^isMultipart}}@Field("{{#lambda.escapeDollar}}{{baseName}}{{/lambda.escapeDollar}}"){{/isMultipart}} {{{paramName}}}: {{#isCollectionFormatMulti}}List<{{/isCollectionFormatMulti}}MultipartBody.Part{{#isCollectionFormatMulti}}>{{/isCollectionFormatMulti}}{{^required}}? = null{{/required}}{{/isFile}}{{/isFormParam}}
\ No newline at end of file
diff --git a/modules/openapi-generator/src/main/resources/kotlin-client/libraries/jvm-retrofit2/headerParams.mustache b/modules/openapi-generator/src/main/resources/kotlin-client/libraries/jvm-retrofit2/headerParams.mustache
index eaf1e7c3bf60..417bef8a2d79 100644
--- a/modules/openapi-generator/src/main/resources/kotlin-client/libraries/jvm-retrofit2/headerParams.mustache
+++ b/modules/openapi-generator/src/main/resources/kotlin-client/libraries/jvm-retrofit2/headerParams.mustache
@@ -1 +1 @@
-{{#isHeaderParam}}@Header("{{baseName}}") {{{paramName}}}: {{#isEnum}}{{enumName}}{{operationIdCamelCase}}{{/isEnum}}{{^isEnum}}{{{dataType}}}{{/isEnum}}{{#required}}{{#defaultValue}} = {{^isNumber}}{{#isEnum}}{{enumName}}{{operationIdCamelCase}}.{{&enumDefaultValue}}{{/isEnum}}{{^isEnum}}{{{defaultValue}}}{{/isEnum}}{{/isNumber}}{{#isNumber}}{{{dataType}}}("{{{defaultValue}}}"){{/isNumber}}{{/defaultValue}}{{/required}}{{^required}}?{{#defaultValue}} = {{^isNumber}}{{#isEnum}}{{enumName}}{{operationIdCamelCase}}.{{&enumDefaultValue}}{{/isEnum}}{{^isEnum}}{{{defaultValue}}}{{/isEnum}}{{/isNumber}}{{#isNumber}}{{{dataType}}}("{{{defaultValue}}}"){{/isNumber}}{{/defaultValue}}{{^defaultValue}} = null{{/defaultValue}}{{/required}}{{/isHeaderParam}}
\ No newline at end of file
+{{#isHeaderParam}}@Header("{{#lambda.escapeDollar}}{{baseName}}{{/lambda.escapeDollar}}") {{{paramName}}}: {{#isEnum}}{{enumName}}{{operationIdCamelCase}}{{/isEnum}}{{^isEnum}}{{{dataType}}}{{/isEnum}}{{#required}}{{#defaultValue}} = {{^isNumber}}{{#isEnum}}{{enumName}}{{operationIdCamelCase}}.{{&enumDefaultValue}}{{/isEnum}}{{^isEnum}}{{{defaultValue}}}{{/isEnum}}{{/isNumber}}{{#isNumber}}{{{dataType}}}("{{{defaultValue}}}"){{/isNumber}}{{/defaultValue}}{{/required}}{{^required}}?{{#defaultValue}} = {{^isNumber}}{{#isEnum}}{{enumName}}{{operationIdCamelCase}}.{{&enumDefaultValue}}{{/isEnum}}{{^isEnum}}{{{defaultValue}}}{{/isEnum}}{{/isNumber}}{{#isNumber}}{{{dataType}}}("{{{defaultValue}}}"){{/isNumber}}{{/defaultValue}}{{^defaultValue}} = null{{/defaultValue}}{{/required}}{{/isHeaderParam}}
\ No newline at end of file
diff --git a/modules/openapi-generator/src/main/resources/kotlin-client/libraries/jvm-retrofit2/infrastructure/ApiClient.kt.mustache b/modules/openapi-generator/src/main/resources/kotlin-client/libraries/jvm-retrofit2/infrastructure/ApiClient.kt.mustache
index c48089c023d9..bd712ff3dd09 100644
--- a/modules/openapi-generator/src/main/resources/kotlin-client/libraries/jvm-retrofit2/infrastructure/ApiClient.kt.mustache
+++ b/modules/openapi-generator/src/main/resources/kotlin-client/libraries/jvm-retrofit2/infrastructure/ApiClient.kt.mustache
@@ -139,7 +139,7 @@ import okhttp3.MediaType.Companion.toMediaType
val auth: Interceptor? = when (authName) { {{#authMethods}}
{{#isBasicBasic}}"{{name}}" -> HttpBasicAuth()
{{/isBasicBasic}}{{#isBasicBearer}}"{{name}}" -> HttpBearerAuth("{{scheme}}")
- {{/isBasicBearer}}{{#isApiKey}}"{{name}}" -> ApiKeyAuth({{#isKeyInHeader}}"header"{{/isKeyInHeader}}{{#isKeyInQuery}}"query"{{/isKeyInQuery}}{{#isKeyInCookie}}"cookie"{{/isKeyInCookie}}, "{{keyParamName}}")
+ {{/isBasicBearer}}{{#isApiKey}}"{{name}}" -> ApiKeyAuth({{#isKeyInHeader}}"header"{{/isKeyInHeader}}{{#isKeyInQuery}}"query"{{/isKeyInQuery}}{{#isKeyInCookie}}"cookie"{{/isKeyInCookie}}, "{{#lambda.escapeDollar}}{{keyParamName}}{{/lambda.escapeDollar}}")
{{/isApiKey}}{{#isOAuth}}"{{name}}" -> OAuth(OAuthFlow.{{flow}}, "{{authorizationUrl}}", "{{tokenUrl}}", "{{#scopes}}{{scope}}{{^-last}}, {{/-last}}{{/scopes}}")
{{/isOAuth}}{{^isBasicBasic}}{{^isBasicBearer}}{{^isApiKey}}{{^isOAuth}}"{{name}}" -> null{{/isOAuth}}{{/isApiKey}}{{/isBasicBearer}}{{/isBasicBasic}}{{/authMethods}}
else -> throw RuntimeException("auth name $authName not found in available auth names")
diff --git a/modules/openapi-generator/src/main/resources/kotlin-client/libraries/jvm-retrofit2/pathParams.mustache b/modules/openapi-generator/src/main/resources/kotlin-client/libraries/jvm-retrofit2/pathParams.mustache
index 685c514e6a90..cc1ade84683c 100644
--- a/modules/openapi-generator/src/main/resources/kotlin-client/libraries/jvm-retrofit2/pathParams.mustache
+++ b/modules/openapi-generator/src/main/resources/kotlin-client/libraries/jvm-retrofit2/pathParams.mustache
@@ -1 +1 @@
-{{#isPathParam}}@Path("{{baseName}}") {{{paramName}}}: {{{dataType}}}{{#required}}{{#defaultValue}} = {{^isNumber}}{{{defaultValue}}}{{/isNumber}}{{#isNumber}}{{{dataType}}}("{{{defaultValue}}}"){{/isNumber}}{{/defaultValue}}{{/required}}{{^required}}?{{#defaultValue}} = {{^isNumber}}{{{defaultValue}}}{{/isNumber}}{{#isNumber}}{{{dataType}}}("{{{defaultValue}}}"){{/isNumber}}{{/defaultValue}}{{^defaultValue}} = null{{/defaultValue}}{{/required}}{{/isPathParam}}
\ No newline at end of file
+{{#isPathParam}}@Path("{{#lambda.escapeDollar}}{{baseName}}{{/lambda.escapeDollar}}") {{{paramName}}}: {{{dataType}}}{{#required}}{{#defaultValue}} = {{^isNumber}}{{{defaultValue}}}{{/isNumber}}{{#isNumber}}{{{dataType}}}("{{{defaultValue}}}"){{/isNumber}}{{/defaultValue}}{{/required}}{{^required}}?{{#defaultValue}} = {{^isNumber}}{{{defaultValue}}}{{/isNumber}}{{#isNumber}}{{{dataType}}}("{{{defaultValue}}}"){{/isNumber}}{{/defaultValue}}{{^defaultValue}} = null{{/defaultValue}}{{/required}}{{/isPathParam}}
\ No newline at end of file
diff --git a/modules/openapi-generator/src/main/resources/kotlin-client/libraries/jvm-retrofit2/queryParam.mustache b/modules/openapi-generator/src/main/resources/kotlin-client/libraries/jvm-retrofit2/queryParam.mustache
index e5c9833fb8d5..a0213c39d72d 100644
--- a/modules/openapi-generator/src/main/resources/kotlin-client/libraries/jvm-retrofit2/queryParam.mustache
+++ b/modules/openapi-generator/src/main/resources/kotlin-client/libraries/jvm-retrofit2/queryParam.mustache
@@ -1 +1 @@
-@Query("{{baseName}}") {{{paramName}}}: {{#collectionFormat}}{{#isCollectionFormatMulti}}@JvmSuppressWildcards {{{dataType}}}{{/isCollectionFormatMulti}}{{^isCollectionFormatMulti}}{{{collectionFormat.toUpperCase}}}Params{{/isCollectionFormatMulti}}{{/collectionFormat}}{{^collectionFormat}}{{#isEnum}}{{enumName}}{{operationIdCamelCase}}{{/isEnum}}{{^isEnum}}{{{dataType}}}{{/isEnum}}{{/collectionFormat}}{{#required}}{{#defaultValue}} = {{^isNumber}}{{#isEnum}}{{enumName}}{{operationIdCamelCase}}.{{&enumDefaultValue}}{{/isEnum}}{{^isEnum}}{{{defaultValue}}}{{/isEnum}}{{/isNumber}}{{#isNumber}}{{{dataType}}}("{{{defaultValue}}}"){{/isNumber}}{{/defaultValue}}{{/required}}{{^required}}?{{#defaultValue}} = {{^isNumber}}{{#isEnum}}{{enumName}}{{operationIdCamelCase}}.{{&enumDefaultValue}}{{/isEnum}}{{^isEnum}}{{{defaultValue}}}{{/isEnum}}{{/isNumber}}{{#isNumber}}{{{dataType}}}("{{{defaultValue}}}"){{/isNumber}}{{/defaultValue}}{{^defaultValue}} = null{{/defaultValue}}{{/required}}
\ No newline at end of file
+@Query("{{#lambda.escapeDollar}}{{baseName}}{{/lambda.escapeDollar}}") {{{paramName}}}: {{#collectionFormat}}{{#isCollectionFormatMulti}}@JvmSuppressWildcards {{{dataType}}}{{/isCollectionFormatMulti}}{{^isCollectionFormatMulti}}{{{collectionFormat.toUpperCase}}}Params{{/isCollectionFormatMulti}}{{/collectionFormat}}{{^collectionFormat}}{{#isEnum}}{{enumName}}{{operationIdCamelCase}}{{/isEnum}}{{^isEnum}}{{{dataType}}}{{/isEnum}}{{/collectionFormat}}{{#required}}{{#defaultValue}} = {{^isNumber}}{{#isEnum}}{{enumName}}{{operationIdCamelCase}}.{{&enumDefaultValue}}{{/isEnum}}{{^isEnum}}{{{defaultValue}}}{{/isEnum}}{{/isNumber}}{{#isNumber}}{{{dataType}}}("{{{defaultValue}}}"){{/isNumber}}{{/defaultValue}}{{/required}}{{^required}}?{{#defaultValue}} = {{^isNumber}}{{#isEnum}}{{enumName}}{{operationIdCamelCase}}.{{&enumDefaultValue}}{{/isEnum}}{{^isEnum}}{{{defaultValue}}}{{/isEnum}}{{/isNumber}}{{#isNumber}}{{{dataType}}}("{{{defaultValue}}}"){{/isNumber}}{{/defaultValue}}{{^defaultValue}} = null{{/defaultValue}}{{/required}}
\ No newline at end of file
diff --git a/modules/openapi-generator/src/main/resources/kotlin-client/libraries/jvm-spring-restclient/api.mustache b/modules/openapi-generator/src/main/resources/kotlin-client/libraries/jvm-spring-restclient/api.mustache
index ece096ef5788..480ebd97e1aa 100644
--- a/modules/openapi-generator/src/main/resources/kotlin-client/libraries/jvm-spring-restclient/api.mustache
+++ b/modules/openapi-generator/src/main/resources/kotlin-client/libraries/jvm-spring-restclient/api.mustache
@@ -82,9 +82,9 @@ import {{packageName}}.infrastructure.*
@Deprecated(message = "This operation is deprecated.")
{{/isDeprecated}}
{{^nonPublicApi}}{{#explicitApi}}public {{/explicitApi}}{{/nonPublicApi}}fun {{operationId}}({{#allParams}}{{{paramName}}}: {{#isEnum}}{{#isContainer}}kotlin.collections.List<{{enumName}}{{operationIdCamelCase}}>{{/isContainer}}{{^isContainer}}{{enumName}}{{operationIdCamelCase}}{{/isContainer}}{{/isEnum}}{{^isEnum}}{{{dataType}}}{{/isEnum}}{{^required}}?{{#defaultValue}} = {{>param_default_value}}{{/defaultValue}}{{^defaultValue}} = null{{/defaultValue}}{{/required}}{{^-last}}, {{/-last}}{{/allParams}}): {{#returnType}}{{{returnType}}}{{#nullableReturnType}}?{{/nullableReturnType}}{{/returnType}}{{^returnType}}Unit{{/returnType}} {
- {{#returnType}}val result = {{/returnType}}{{operationId}}WithHttpInfo({{#allParams}}{{{paramName}}} = {{{paramName}}}{{^-last}}, {{/-last}}{{/allParams}})
+ {{#returnType}}val localVariableResult = {{/returnType}}{{operationId}}WithHttpInfo({{#allParams}}{{{paramName}}} = {{{paramName}}}{{^-last}}, {{/-last}}{{/allParams}})
{{#returnType}}
- return result.body{{^nullableReturnType}}!!{{/nullableReturnType}}
+ return localVariableResult.body{{^nullableReturnType}}!!{{/nullableReturnType}}
{{/returnType}}
}
@@ -94,7 +94,7 @@ import {{packageName}}.infrastructure.*
{{/isDeprecated}}
{{^nonPublicApi}}{{#explicitApi}}public {{/explicitApi}}{{/nonPublicApi}}fun {{operationId}}WithHttpInfo({{#allParams}}{{{paramName}}}: {{#isEnum}}{{#isContainer}}kotlin.collections.List<{{enumName}}{{operationIdCamelCase}}>{{/isContainer}}{{^isContainer}}{{enumName}}{{operationIdCamelCase}}{{/isContainer}}{{/isEnum}}{{^isEnum}}{{{dataType}}}{{/isEnum}}{{^required}}?{{#defaultValue}} = {{>param_default_value}}{{/defaultValue}}{{^defaultValue}} = null{{/defaultValue}}{{/required}}{{^-last}}, {{/-last}}{{/allParams}}): ResponseEntity<{{#returnType}}{{{returnType}}}{{#nullableReturnType}}?{{/nullableReturnType}}{{/returnType}}{{^returnType}}Unit{{/returnType}}> {
val localVariableConfig = {{operationId}}RequestConfig({{#allParams}}{{{paramName}}} = {{{paramName}}}{{^-last}}, {{/-last}}{{/allParams}})
- return request<{{#hasBodyParam}}{{#bodyParams}}{{{dataType}}}{{/bodyParams}}{{/hasBodyParam}}{{^hasBodyParam}}{{^hasFormParams}}Unit{{/hasFormParams}}{{#hasFormParams}}Map>{{/hasFormParams}}{{/hasBodyParam}}, {{#returnType}}{{{returnType}}}{{#nullableReturnType}}?{{/nullableReturnType}}{{/returnType}}{{^returnType}}Unit{{/returnType}}>(
+ return this.request<{{#hasBodyParam}}{{#bodyParams}}{{{dataType}}}{{/bodyParams}}{{/hasBodyParam}}{{^hasBodyParam}}{{^hasFormParams}}Unit{{/hasFormParams}}{{#hasFormParams}}Map>{{/hasFormParams}}{{/hasBodyParam}}, {{#returnType}}{{{returnType}}}{{#nullableReturnType}}?{{/nullableReturnType}}{{/returnType}}{{^returnType}}Unit{{/returnType}}>(
localVariableConfig
)
}
@@ -107,7 +107,7 @@ import {{packageName}}.infrastructure.*
}}{{#bodyParams}}{{{paramName}}}{{/bodyParams}}{{/hasBodyParam}}{{^hasBodyParam}}{{!
}}{{^hasFormParams}}null{{/hasFormParams}}{{!
}}{{#hasFormParams}}mapOf({{#formParams}}
- "{{{baseName}}}" to PartConfig(body = {{{paramName}}}{{#isEnum}}{{^required}}?{{/required}}.value{{/isEnum}}, headers = mutableMapOf({{#contentType}}"Content-Type" to "{{contentType}}"{{/contentType}})),{{!
+ "{{#lambda.escapeDollar}}{{{baseName}}}{{/lambda.escapeDollar}}" to PartConfig(body = {{{paramName}}}{{#isEnum}}{{^required}}?{{/required}}.value{{/isEnum}}, headers = mutableMapOf({{#contentType}}"Content-Type" to "{{contentType}}"{{/contentType}})),{{!
}}{{/formParams}}){{/hasFormParams}}{{!
}}{{/hasBodyParam}}
val localVariableQuery = {{^hasQueryParams}}mutableMapOf>()
@@ -116,17 +116,17 @@ import {{packageName}}.infrastructure.*
{{#queryParams}}
{{^required}}
if ({{{paramName}}} != null) {
- put("{{baseName}}", {{#isContainer}}toMultiValue({{{paramName}}}.toList(), "{{collectionFormat}}"){{/isContainer}}{{^isContainer}}listOf({{#isDateTime}}parseDateToQueryString<{{{dataType}}}>({{{paramName}}}){{/isDateTime}}{{#isDate}}parseDateToQueryString<{{{dataType}}}>({{{paramName}}}){{/isDate}}{{^isDateTime}}{{^isDate}}{{{paramName}}}.toString(){{/isDate}}{{/isDateTime}}){{/isContainer}})
+ put("{{#lambda.escapeDollar}}{{baseName}}{{/lambda.escapeDollar}}", {{#isContainer}}toMultiValue({{{paramName}}}.toList(), "{{collectionFormat}}"){{/isContainer}}{{^isContainer}}listOf({{#isDateTime}}parseDateToQueryString<{{{dataType}}}>({{{paramName}}}){{/isDateTime}}{{#isDate}}parseDateToQueryString<{{{dataType}}}>({{{paramName}}}){{/isDate}}{{^isDateTime}}{{^isDate}}{{{paramName}}}.toString(){{/isDate}}{{/isDateTime}}){{/isContainer}})
}
{{/required}}
{{#required}}
{{#isNullable}}
if ({{{paramName}}} != null) {
- put("{{baseName}}", {{#isContainer}}toMultiValue({{{paramName}}}.toList(), "{{collectionFormat}}"){{/isContainer}}{{^isContainer}}listOf({{#isDateTime}}parseDateToQueryString<{{{dataType}}}>({{{paramName}}}){{/isDateTime}}{{#isDate}}parseDateToQueryString<{{{dataType}}}>({{{paramName}}}){{/isDate}}{{^isDateTime}}{{^isDate}}{{{paramName}}}.toString(){{/isDate}}{{/isDateTime}}){{/isContainer}})
+ put("{{#lambda.escapeDollar}}{{baseName}}{{/lambda.escapeDollar}}", {{#isContainer}}toMultiValue({{{paramName}}}.toList(), "{{collectionFormat}}"){{/isContainer}}{{^isContainer}}listOf({{#isDateTime}}parseDateToQueryString<{{{dataType}}}>({{{paramName}}}){{/isDateTime}}{{#isDate}}parseDateToQueryString<{{{dataType}}}>({{{paramName}}}){{/isDate}}{{^isDateTime}}{{^isDate}}{{{paramName}}}.toString(){{/isDate}}{{/isDateTime}}){{/isContainer}})
}
{{/isNullable}}
{{^isNullable}}
- put("{{baseName}}", {{#isContainer}}toMultiValue({{{paramName}}}.toList(), "{{collectionFormat}}"){{/isContainer}}{{^isContainer}}listOf({{#isDateTime}}parseDateToQueryString<{{{dataType}}}>({{{paramName}}}){{/isDateTime}}{{#isDate}}parseDateToQueryString<{{{dataType}}}>({{{paramName}}}){{/isDate}}{{^isDateTime}}{{^isDate}}{{{paramName}}}.toString(){{/isDate}}{{/isDateTime}}){{/isContainer}})
+ put("{{#lambda.escapeDollar}}{{baseName}}{{/lambda.escapeDollar}}", {{#isContainer}}toMultiValue({{{paramName}}}.toList(), "{{collectionFormat}}"){{/isContainer}}{{^isContainer}}listOf({{#isDateTime}}parseDateToQueryString<{{{dataType}}}>({{{paramName}}}){{/isDateTime}}{{#isDate}}parseDateToQueryString<{{{dataType}}}>({{{paramName}}}){{/isDate}}{{^isDateTime}}{{^isDate}}{{{paramName}}}.toString(){{/isDate}}{{/isDateTime}}){{/isContainer}})
{{/isNullable}}
{{/required}}
{{/queryParams}}
@@ -136,19 +136,19 @@ import {{packageName}}.infrastructure.*
{{^hasFormParams}}{{#hasConsumes}}{{#consumes}}localVariableHeaders["Content-Type"] = "{{{mediaType}}}"
{{/consumes}}{{/hasConsumes}}{{/hasFormParams}}{{#hasProduces}}localVariableHeaders["Accept"] = "{{#produces}}{{{mediaType}}}{{^-last}}, {{/-last}}{{/produces}}"{{/hasProduces}}
{{#headerParams}}
- {{{paramName}}}{{^required}}?{{/required}}.apply { localVariableHeaders["{{baseName}}"] = {{#isContainer}}this.joinToString(separator = collectionDelimiter("{{collectionFormat}}")){{/isContainer}}{{^isContainer}}this.toString(){{/isContainer}} }
+ {{{paramName}}}{{^required}}?{{/required}}.apply { localVariableHeaders["{{#lambda.escapeDollar}}{{baseName}}{{/lambda.escapeDollar}}"] = {{#isContainer}}this.joinToString(separator = collectionDelimiter("{{collectionFormat}}")){{/isContainer}}{{^isContainer}}this.toString(){{/isContainer}} }
{{/headerParams}}
- val params = mutableMapOf(
+ val localVariableParams = mutableMapOf(
{{#pathParams}}
- "{{baseName}}" to {{#isContainer}}{{paramName}}.joinToString(","){{/isContainer}}{{^isContainer}}{{{paramName}}}{{#isEnum}}{{^required}}?{{/required}}.value{{/isEnum}}{{/isContainer}},
+ "{{#lambda.escapeDollar}}{{baseName}}{{/lambda.escapeDollar}}" to {{#isContainer}}{{paramName}}.joinToString(","){{/isContainer}}{{^isContainer}}{{{paramName}}}{{#isEnum}}{{^required}}?{{/required}}.value{{/isEnum}}{{/isContainer}},
{{/pathParams}}
)
return RequestConfig(
method = RequestMethod.{{httpMethod}},
- path = "{{{path}}}",
- params = params,
+ path = "{{#lambda.escapeDollar}}{{{path}}}{{/lambda.escapeDollar}}",
+ params = localVariableParams,
query = localVariableQuery,
headers = localVariableHeaders,
requiresAuthentication = {{#hasAuthMethods}}true{{/hasAuthMethods}}{{^hasAuthMethods}}false{{/hasAuthMethods}},
diff --git a/modules/openapi-generator/src/main/resources/kotlin-client/libraries/jvm-spring-webclient/api.mustache b/modules/openapi-generator/src/main/resources/kotlin-client/libraries/jvm-spring-webclient/api.mustache
index dfe2b36cd6b9..8bb236ce6fd3 100644
--- a/modules/openapi-generator/src/main/resources/kotlin-client/libraries/jvm-spring-webclient/api.mustache
+++ b/modules/openapi-generator/src/main/resources/kotlin-client/libraries/jvm-spring-webclient/api.mustache
@@ -81,7 +81,7 @@ import {{packageName}}.infrastructure.*
{{/isDeprecated}}
{{^nonPublicApi}}{{#explicitApi}}public {{/explicitApi}}{{/nonPublicApi}}fun {{operationId}}WithHttpInfo({{#allParams}}{{{paramName}}}: {{#isEnum}}{{#isContainer}}kotlin.collections.List<{{enumName}}{{operationIdCamelCase}}>{{/isContainer}}{{^isContainer}}{{enumName}}{{operationIdCamelCase}}{{/isContainer}}{{/isEnum}}{{^isEnum}}{{{dataType}}}{{/isEnum}}{{^required}}?{{#defaultValue}} = {{>param_default_value}}{{/defaultValue}}{{^defaultValue}} = null{{/defaultValue}}{{/required}}{{^-last}}, {{/-last}}{{/allParams}}): Mono> {
val localVariableConfig = {{operationId}}RequestConfig({{#allParams}}{{{paramName}}} = {{{paramName}}}{{^-last}}, {{/-last}}{{/allParams}})
- return request<{{#hasBodyParam}}{{#bodyParams}}{{{dataType}}}{{/bodyParams}}{{/hasBodyParam}}{{^hasBodyParam}}{{^hasFormParams}}Unit{{/hasFormParams}}{{#hasFormParams}}Map>{{/hasFormParams}}{{/hasBodyParam}}, {{{returnType}}}{{^returnType}}Unit{{/returnType}}>(
+ return this.request<{{#hasBodyParam}}{{#bodyParams}}{{{dataType}}}{{/bodyParams}}{{/hasBodyParam}}{{^hasBodyParam}}{{^hasFormParams}}Unit{{/hasFormParams}}{{#hasFormParams}}Map>{{/hasFormParams}}{{/hasBodyParam}}, {{{returnType}}}{{^returnType}}Unit{{/returnType}}>(
localVariableConfig
)
}
@@ -94,7 +94,7 @@ import {{packageName}}.infrastructure.*
}}{{#bodyParams}}{{{paramName}}}{{/bodyParams}}{{/hasBodyParam}}{{^hasBodyParam}}{{!
}}{{^hasFormParams}}null{{/hasFormParams}}{{!
}}{{#hasFormParams}}mapOf({{#formParams}}
- "{{{baseName}}}" to PartConfig(body = {{{paramName}}}{{#isEnum}}{{^required}}?{{/required}}.value{{/isEnum}}, headers = mutableMapOf({{#contentType}}"Content-Type" to "{{contentType}}"{{/contentType}})),{{!
+ "{{#lambda.escapeDollar}}{{{baseName}}}{{/lambda.escapeDollar}}" to PartConfig(body = {{{paramName}}}{{#isEnum}}{{^required}}?{{/required}}.value{{/isEnum}}, headers = mutableMapOf({{#contentType}}"Content-Type" to "{{contentType}}"{{/contentType}})),{{!
}}{{/formParams}}){{/hasFormParams}}{{!
}}{{/hasBodyParam}}
val localVariableQuery = {{^hasQueryParams}}mutableMapOf>()
@@ -103,17 +103,17 @@ import {{packageName}}.infrastructure.*
{{#queryParams}}
{{^required}}
if ({{{paramName}}} != null) {
- put("{{baseName}}", {{#isContainer}}toMultiValue({{{paramName}}}.toList(), "{{collectionFormat}}"){{/isContainer}}{{^isContainer}}listOf({{#isDateTime}}parseDateToQueryString<{{{dataType}}}>({{{paramName}}}){{/isDateTime}}{{#isDate}}parseDateToQueryString<{{{dataType}}}>({{{paramName}}}){{/isDate}}{{^isDateTime}}{{^isDate}}{{{paramName}}}.toString(){{/isDate}}{{/isDateTime}}){{/isContainer}})
+ put("{{#lambda.escapeDollar}}{{baseName}}{{/lambda.escapeDollar}}", {{#isContainer}}toMultiValue({{{paramName}}}.toList(), "{{collectionFormat}}"){{/isContainer}}{{^isContainer}}listOf({{#isDateTime}}parseDateToQueryString<{{{dataType}}}>({{{paramName}}}){{/isDateTime}}{{#isDate}}parseDateToQueryString<{{{dataType}}}>({{{paramName}}}){{/isDate}}{{^isDateTime}}{{^isDate}}{{{paramName}}}.toString(){{/isDate}}{{/isDateTime}}){{/isContainer}})
}
{{/required}}
{{#required}}
{{#isNullable}}
if ({{{paramName}}} != null) {
- put("{{baseName}}", {{#isContainer}}toMultiValue({{{paramName}}}.toList(), "{{collectionFormat}}"){{/isContainer}}{{^isContainer}}listOf({{#isDateTime}}parseDateToQueryString<{{{dataType}}}>({{{paramName}}}){{/isDateTime}}{{#isDate}}parseDateToQueryString<{{{dataType}}}>({{{paramName}}}){{/isDate}}{{^isDateTime}}{{^isDate}}{{{paramName}}}.toString(){{/isDate}}{{/isDateTime}}){{/isContainer}})
+ put("{{#lambda.escapeDollar}}{{baseName}}{{/lambda.escapeDollar}}", {{#isContainer}}toMultiValue({{{paramName}}}.toList(), "{{collectionFormat}}"){{/isContainer}}{{^isContainer}}listOf({{#isDateTime}}parseDateToQueryString<{{{dataType}}}>({{{paramName}}}){{/isDateTime}}{{#isDate}}parseDateToQueryString<{{{dataType}}}>({{{paramName}}}){{/isDate}}{{^isDateTime}}{{^isDate}}{{{paramName}}}.toString(){{/isDate}}{{/isDateTime}}){{/isContainer}})
}
{{/isNullable}}
{{^isNullable}}
- put("{{baseName}}", {{#isContainer}}toMultiValue({{{paramName}}}.toList(), "{{collectionFormat}}"){{/isContainer}}{{^isContainer}}listOf({{#isDateTime}}parseDateToQueryString<{{{dataType}}}>({{{paramName}}}){{/isDateTime}}{{#isDate}}parseDateToQueryString<{{{dataType}}}>({{{paramName}}}){{/isDate}}{{^isDateTime}}{{^isDate}}{{{paramName}}}.toString(){{/isDate}}{{/isDateTime}}){{/isContainer}})
+ put("{{#lambda.escapeDollar}}{{baseName}}{{/lambda.escapeDollar}}", {{#isContainer}}toMultiValue({{{paramName}}}.toList(), "{{collectionFormat}}"){{/isContainer}}{{^isContainer}}listOf({{#isDateTime}}parseDateToQueryString<{{{dataType}}}>({{{paramName}}}){{/isDateTime}}{{#isDate}}parseDateToQueryString<{{{dataType}}}>({{{paramName}}}){{/isDate}}{{^isDateTime}}{{^isDate}}{{{paramName}}}.toString(){{/isDate}}{{/isDateTime}}){{/isContainer}})
{{/isNullable}}
{{/required}}
{{/queryParams}}
@@ -123,19 +123,19 @@ import {{packageName}}.infrastructure.*
{{^hasFormParams}}{{#hasConsumes}}{{#consumes}}localVariableHeaders["Content-Type"] = "{{{mediaType}}}"
{{/consumes}}{{/hasConsumes}}{{/hasFormParams}}{{#hasProduces}}localVariableHeaders["Accept"] = "{{#produces}}{{{mediaType}}}{{^-last}}, {{/-last}}{{/produces}}"{{/hasProduces}}
{{#headerParams}}
- {{{paramName}}}{{^required}}?{{/required}}.apply { localVariableHeaders["{{baseName}}"] = {{#isContainer}}this.joinToString(separator = collectionDelimiter("{{collectionFormat}}")){{/isContainer}}{{^isContainer}}this.toString(){{/isContainer}} }
+ {{{paramName}}}{{^required}}?{{/required}}.apply { localVariableHeaders["{{#lambda.escapeDollar}}{{baseName}}{{/lambda.escapeDollar}}"] = {{#isContainer}}this.joinToString(separator = collectionDelimiter("{{collectionFormat}}")){{/isContainer}}{{^isContainer}}this.toString(){{/isContainer}} }
{{/headerParams}}
- val params = mutableMapOf(
+ val localVariableParams = mutableMapOf(
{{#pathParams}}
- "{{baseName}}" to {{#isContainer}}{{paramName}}.joinToString(","){{/isContainer}}{{^isContainer}}{{{paramName}}}{{#isEnum}}{{^required}}?{{/required}}.value{{/isEnum}}{{/isContainer}},
+ "{{#lambda.escapeDollar}}{{baseName}}{{/lambda.escapeDollar}}" to {{#isContainer}}{{paramName}}.joinToString(","){{/isContainer}}{{^isContainer}}{{{paramName}}}{{#isEnum}}{{^required}}?{{/required}}.value{{/isEnum}}{{/isContainer}},
{{/pathParams}}
)
return RequestConfig(
method = RequestMethod.{{httpMethod}},
- path = "{{{path}}}",
- params = params,
+ path = "{{#lambda.escapeDollar}}{{{path}}}{{/lambda.escapeDollar}}",
+ params = localVariableParams,
query = localVariableQuery,
headers = localVariableHeaders,
requiresAuthentication = {{#hasAuthMethods}}true{{/hasAuthMethods}}{{^hasAuthMethods}}false{{/hasAuthMethods}},
diff --git a/modules/openapi-generator/src/main/resources/kotlin-client/libraries/jvm-vertx/api.mustache b/modules/openapi-generator/src/main/resources/kotlin-client/libraries/jvm-vertx/api.mustache
index a82b32cbfb7a..297ab517d7db 100644
--- a/modules/openapi-generator/src/main/resources/kotlin-client/libraries/jvm-vertx/api.mustache
+++ b/modules/openapi-generator/src/main/resources/kotlin-client/libraries/jvm-vertx/api.mustache
@@ -105,18 +105,18 @@ import {{packageName}}.infrastructure.*
@Deprecated(message = "This operation is deprecated.")
{{/isDeprecated}}
{{^nonPublicApi}}{{#explicitApi}}public {{/explicitApi}}{{/nonPublicApi}}{{#useCoroutines}}suspend {{/useCoroutines}}fun {{operationId}}({{#allParams}}{{{paramName}}}: {{#isEnum}}{{#isContainer}}kotlin.collections.List<{{enumName}}{{operationIdCamelCase}}>{{/isContainer}}{{^isContainer}}{{enumName}}{{operationIdCamelCase}}{{/isContainer}}{{/isEnum}}{{^isEnum}}{{{dataType}}}{{/isEnum}}{{#required}}{{#defaultValue}} = {{^isNumber}}{{#isEnum}}{{enumName}}{{operationIdCamelCase}}.{{&enumDefaultValue}}{{/isEnum}}{{^isEnum}}{{{defaultValue}}}{{/isEnum}}{{/isNumber}}{{#isNumber}}{{{dataType}}}("{{{defaultValue}}}"){{/isNumber}}{{/defaultValue}}{{/required}}{{^required}}?{{#defaultValue}} = {{^isNumber}}{{#isEnum}}{{enumName}}{{operationIdCamelCase}}.{{&enumDefaultValue}}{{/isEnum}}{{^isEnum}}{{{defaultValue}}}{{/isEnum}}{{/isNumber}}{{#isNumber}}{{{dataType}}}("{{{defaultValue}}}"){{/isNumber}}{{/defaultValue}}{{^defaultValue}} = null{{/defaultValue}}{{/required}}{{^-last}}, {{/-last}}{{/allParams}}) : {{^useCoroutines}}Future<{{/useCoroutines}}{{#returnType}}{{{returnType}}}{{#nullableReturnType}}{{^isResponseOptional}}?{{/isResponseOptional}}{{/nullableReturnType}}{{#isResponseOptional}}?{{/isResponseOptional}}{{/returnType}}{{^returnType}}Unit{{/returnType}}{{^useCoroutines}}>{{/useCoroutines}} {
- return {{operationId}}WithHttpInfo({{#allParams}}{{{paramName}}} = {{{paramName}}}{{^-last}}, {{/-last}}{{/allParams}}).map { localVarResponse ->
- when (localVarResponse.responseType) {
- ResponseType.Success -> {{#returnType}}(localVarResponse as Success<*>).data as {{{returnType}}}{{#nullableReturnType}}{{^isResponseOptional}}?{{/isResponseOptional}}{{/nullableReturnType}}{{#isResponseOptional}}?{{/isResponseOptional}}{{/returnType}}{{^returnType}}Unit{{/returnType}}
+ return {{operationId}}WithHttpInfo({{#allParams}}{{{paramName}}} = {{{paramName}}}{{^-last}}, {{/-last}}{{/allParams}}).map { localVariableResponse ->
+ when (localVariableResponse.responseType) {
+ ResponseType.Success -> {{#returnType}}(localVariableResponse as Success<*>).data as {{{returnType}}}{{#nullableReturnType}}{{^isResponseOptional}}?{{/isResponseOptional}}{{/nullableReturnType}}{{#isResponseOptional}}?{{/isResponseOptional}}{{/returnType}}{{^returnType}}Unit{{/returnType}}
ResponseType.Informational -> throw UnsupportedOperationException("Client does not support Informational responses.")
ResponseType.Redirection -> throw UnsupportedOperationException("Client does not support Redirection responses.")
ResponseType.ClientError -> {
- val localVarError = localVarResponse as ClientError<*>
- throw ClientException("Client error : ${localVarError.statusCode} ${localVarError.message.orEmpty()}", localVarError.statusCode, localVarResponse)
+ val localVariableError = localVariableResponse as ClientError<*>
+ throw ClientException("Client error : ${localVariableError.statusCode} ${localVariableError.message.orEmpty()}", localVariableError.statusCode, localVariableResponse)
}
ResponseType.ServerError -> {
- val localVarError = localVarResponse as ServerError<*>
- throw ServerException("Server error : ${localVarError.statusCode} ${localVarError.message.orEmpty()}", localVarError.statusCode, localVarResponse)
+ val localVariableError = localVariableResponse as ServerError<*>
+ throw ServerException("Server error : ${localVariableError.statusCode} ${localVariableError.message.orEmpty()}", localVariableError.statusCode, localVariableResponse)
}
}
}{{#useCoroutines}}.coAwait(){{/useCoroutines}}
@@ -137,82 +137,107 @@ import {{packageName}}.infrastructure.*
@Deprecated(message = "This operation is deprecated.")
{{/isDeprecated}}
{{^nonPublicApi}}{{#explicitApi}}public {{/explicitApi}}{{/nonPublicApi}}fun {{operationId}}WithHttpInfo({{#allParams}}{{{paramName}}}: {{#isEnum}}{{#isContainer}}kotlin.collections.List<{{enumName}}{{operationIdCamelCase}}>{{/isContainer}}{{^isContainer}}{{enumName}}{{operationIdCamelCase}}{{/isContainer}}{{/isEnum}}{{^isEnum}}{{{dataType}}}{{/isEnum}}{{^required}}?{{/required}}{{^-last}}, {{/-last}}{{/allParams}}) : Future> {
- val vertxClient = WebClient.create(vertx)
- val request = vertxClient.requestAbs(HttpMethod.{{httpMethod}}, UriTemplate.of("$basePath{{{path}}}"{{#pathParams}}.replace("{"+"{{baseName}}"+"}", encodeURIComponent({{#isContainer}}{{paramName}}.joinToString(","){{/isContainer}}{{^isContainer}}{{{paramName}}}{{#isEnum}}{{^required}}?{{/required}}.value{{/isEnum}}.toString(){{/isContainer}})){{/pathParams}}))
+ val localVariableClient = WebClient.create(this.vertx)
+ val localVariableRequest = localVariableClient.requestAbs(HttpMethod.{{httpMethod}}, UriTemplate.of("${this.basePath}{{#lambda.escapeDollar}}{{{path}}}{{/lambda.escapeDollar}}"{{#pathParams}}.replace("{"+"{{#lambda.escapeDollar}}{{baseName}}{{/lambda.escapeDollar}}"+"}", this.encodeURIComponent({{#isContainer}}{{paramName}}.joinToString(","){{/isContainer}}{{^isContainer}}{{{paramName}}}{{#isEnum}}{{^required}}?{{/required}}.value{{/isEnum}}.toString(){{/isContainer}})){{/pathParams}}))
- {{#hasFormParams}}request.putHeader("Content-Type", {{^consumes}}"multipart/form-data"{{/consumes}}{{#consumes.0}}"{{{mediaType}}}"{{/consumes.0}}){{/hasFormParams}}
- {{#headerParams}}{{{paramName}}}{{^required}}?{{/required}}.apply { request.putHeader("{{baseName}}", {{#isContainer}}this.joinToString(separator = collectionDelimiter("{{collectionFormat}}")){{/isContainer}}{{^isContainer}}this.toString(){{/isContainer}})}{{/headerParams}}
+ {{#hasFormParams}}localVariableRequest.putHeader("Content-Type", {{^consumes}}"multipart/form-data"{{/consumes}}{{#consumes.0}}"{{{mediaType}}}"{{/consumes.0}}){{/hasFormParams}}
+ {{#headerParams}}{{{paramName}}}{{^required}}?{{/required}}.apply { localVariableRequest.putHeader("{{#lambda.escapeDollar}}{{baseName}}{{/lambda.escapeDollar}}", {{#isContainer}}this.joinToString(separator = collectionDelimiter("{{collectionFormat}}")){{/isContainer}}{{^isContainer}}this.toString(){{/isContainer}})}{{/headerParams}}
{{^hasFormParams}}{{#hasConsumes}}
{{#consumes}}
- request.putHeader("Content-Type", "{{{mediaType}}}")
+ localVariableRequest.putHeader("Content-Type", "{{{mediaType}}}")
{{/consumes}}
{{/hasConsumes}}{{/hasFormParams}}
- {{#hasProduces}}request.putHeader("Accept", "{{#produces}}{{{mediaType}}}{{^-last}}, {{/-last}}{{/produces}}"){{/hasProduces}}
+ {{#hasProduces}}localVariableRequest.putHeader("Accept", "{{#produces}}{{{mediaType}}}{{^-last}}, {{/-last}}{{/produces}}"){{/hasProduces}}
{{#hasFormParams}}
- val form = io.vertx.core.MultiMap.caseInsensitiveMultiMap();
+ {{#isMultipart}}val localVariableForm = io.vertx.ext.web.multipart.MultipartForm.create(){{/isMultipart}}{{^isMultipart}}val localVariableForm = io.vertx.core.MultiMap.caseInsensitiveMultiMap(){{/isMultipart}}
{{#formParams}}
- {{{paramName}}}{{^required}}?{{/required}}.let { form.add("{{{baseName}}}", {{{paramName}}}{{#isEnum}}{{^required}}?{{/required}}.value{{/isEnum}}{{^isString}}.toString(){{/isString}}) }
+ {{#isMultipart}}
+ {{#isFile}}
+ {{{paramName}}}{{^required}}?{{/required}}.{{#isArray}}forEach{{/isArray}}{{^isArray}}let{{/isArray}} { localVariableForm.binaryFileUpload("{{#lambda.escapeDollar}}{{{baseName}}}{{/lambda.escapeDollar}}", it.name, it.absolutePath, {{#contentType}}"{{{contentType}}}"{{/contentType}}{{^contentType}}"application/octet-stream"{{/contentType}}) }
+ {{/isFile}}
+ {{^isFile}}
+ {{#isArray}}
+ {{{paramName}}}{{^required}}?{{/required}}.let { toMultiValue(it.toList(), "{{collectionFormat}}").forEach { localVariableForm.attribute("{{#lambda.escapeDollar}}{{{baseName}}}{{/lambda.escapeDollar}}", it) } }
+ {{/isArray}}
+ {{^isArray}}
+ {{{paramName}}}{{^required}}?{{/required}}.let { localVariableForm.attribute("{{#lambda.escapeDollar}}{{{baseName}}}{{/lambda.escapeDollar}}", {{{paramName}}}{{#isEnum}}{{^required}}?{{/required}}.value{{/isEnum}}{{^isString}}.toString(){{/isString}}) }
+ {{/isArray}}
+ {{/isFile}}
+ {{/isMultipart}}
+ {{^isMultipart}}
+ {{#isArray}}
+ {{{paramName}}}{{^required}}?{{/required}}.let { localVariableForm.add("{{#lambda.escapeDollar}}{{{baseName}}}{{/lambda.escapeDollar}}", toMultiValue(it.toList(), "{{collectionFormat}}")) }
+ {{/isArray}}
+ {{^isArray}}
+ {{{paramName}}}{{^required}}?{{/required}}.let { localVariableForm.add("{{#lambda.escapeDollar}}{{{baseName}}}{{/lambda.escapeDollar}}", {{{paramName}}}{{#isEnum}}{{^required}}?{{/required}}.value{{/isEnum}}{{^isString}}.toString(){{/isString}}) }
+ {{/isArray}}
+ {{/isMultipart}}
{{/formParams}}
{{/hasFormParams}}
{{#hasQueryParams}}
{{#queryParams}}
- {{{paramName}}}{{^required}}?{{/required}}.let { request.queryParams().add("{{baseName}}", {{#isContainer}}toMultiValue(it.toList(), "{{collectionFormat}}"){{/isContainer}}{{^isContainer}}listOf({{#isDateTime}}parseDateToQueryString<{{{dataType}}}>(it){{/isDateTime}}{{#isDate}}parseDateToQueryString<{{{dataType}}}>(it){{/isDate}}{{^isDateTime}}{{^isDate}}it.toString(){{/isDate}}{{/isDateTime}}){{/isContainer}}) }
+ {{{paramName}}}{{^required}}?{{/required}}.let { localVariableRequest.queryParams().add("{{#lambda.escapeDollar}}{{baseName}}{{/lambda.escapeDollar}}", {{#isContainer}}toMultiValue(it.toList(), "{{collectionFormat}}"){{/isContainer}}{{^isContainer}}listOf({{#isDateTime}}this.parseDateToQueryString<{{{dataType}}}>(it){{/isDateTime}}{{#isDate}}this.parseDateToQueryString<{{{dataType}}}>(it){{/isDate}}{{^isDateTime}}{{^isDate}}it.toString(){{/isDate}}{{/isDateTime}}){{/isContainer}}) }
{{/queryParams}}
{{/hasQueryParams}}
{{#authMethods}}
{{#isApiKey}}
- if (apiKey["{{keyParamName}}"] != null) {
- if (apiKeyPrefix["{{keyParamName}}"] != null) {
+ if (this.apiKey["{{#lambda.escapeDollar}}{{keyParamName}}{{/lambda.escapeDollar}}"] != null) {
+ if (this.apiKeyPrefix["{{#lambda.escapeDollar}}{{keyParamName}}{{/lambda.escapeDollar}}"] != null) {
{{#isKeyInHeader}}
- request.putHeader("{{keyParamName}}", apiKeyPrefix["{{keyParamName}}"]!! + " " + apiKey["{{keyParamName}}"]!!)
+ localVariableRequest.putHeader("{{#lambda.escapeDollar}}{{keyParamName}}{{/lambda.escapeDollar}}", this.apiKeyPrefix["{{#lambda.escapeDollar}}{{keyParamName}}{{/lambda.escapeDollar}}"]!! + " " + this.apiKey["{{#lambda.escapeDollar}}{{keyParamName}}{{/lambda.escapeDollar}}"]!!)
{{/isKeyInHeader}}
{{#isKeyInQuery}}
- request.queryParams().add("{{keyParamName}}", apiKeyPrefix["{{keyParamName}}"]!! + " " + apiKey["{{keyParamName}}"]!!)
+ localVariableRequest.queryParams().add("{{#lambda.escapeDollar}}{{keyParamName}}{{/lambda.escapeDollar}}", this.apiKeyPrefix["{{#lambda.escapeDollar}}{{keyParamName}}{{/lambda.escapeDollar}}"]!! + " " + this.apiKey["{{#lambda.escapeDollar}}{{keyParamName}}{{/lambda.escapeDollar}}"]!!)
{{/isKeyInQuery}}
} else {
{{#isKeyInHeader}}
- request.putHeader("{{keyParamName}}", apiKey["{{keyParamName}}"]!!)
+ localVariableRequest.putHeader("{{#lambda.escapeDollar}}{{keyParamName}}{{/lambda.escapeDollar}}", this.apiKey["{{#lambda.escapeDollar}}{{keyParamName}}{{/lambda.escapeDollar}}"]!!)
{{/isKeyInHeader}}
{{#isKeyInQuery}}
- request.queryParams().add("{{keyParamName}}", apiKey["{{keyParamName}}"]!!)
+ localVariableRequest.queryParams().add("{{#lambda.escapeDollar}}{{keyParamName}}{{/lambda.escapeDollar}}", this.apiKey["{{#lambda.escapeDollar}}{{keyParamName}}{{/lambda.escapeDollar}}"]!!)
{{/isKeyInQuery}}
}
}
{{/isApiKey}}
{{#isBasic}}
{{#isBasicBasic}}
- username?.let { username ->
- password?.let { password ->
- request.basicAuthentication(username, password)
+ this.username?.let { localVariableUsername ->
+ this.password?.let { localVariablePassword ->
+ localVariableRequest.basicAuthentication(localVariableUsername, localVariablePassword)
}
}
{{/isBasicBasic}}
{{#isBasicBearer}}
- accessToken?.let { accessToken ->
- request.bearerTokenAuthentication(accessToken)
+ this.accessToken?.let { localVariableAccessToken ->
+ localVariableRequest.bearerTokenAuthentication(localVariableAccessToken)
}
{{/isBasicBearer}}
{{/isBasic}}
{{#isOAuth}}
- accessToken?.let { accessToken ->
- request.bearerTokenAuthentication(accessToken)
+ this.accessToken?.let { localVariableAccessToken ->
+ localVariableRequest.bearerTokenAuthentication(localVariableAccessToken)
}
{{/isOAuth}}
{{/authMethods}}
- return request
+ return localVariableRequest
{{#hasBodyParam}}
- .sendBuffer(responseBody({{#bodyParams}}{{{paramName}}}{{/bodyParams}}))
+ .sendBuffer(this.responseBody({{#bodyParams}}{{{paramName}}}{{/bodyParams}}))
{{/hasBodyParam}}
{{^hasBodyParam}}
+ {{#hasFormParams}}
+ {{#isMultipart}}.sendMultipartForm(localVariableForm){{/isMultipart}}{{^isMultipart}}.sendForm(localVariableForm){{/isMultipart}}
+ {{/hasFormParams}}
+ {{^hasFormParams}}
.send()
+ {{/hasFormParams}}
{{/hasBodyParam}}
.map {
- val apiResponse: ApiResponse<{{#returnType}}{{{returnType}}}?{{/returnType}}{{^returnType}}Unit?{{/returnType}}> = handleResponse(it)
- apiResponse
+ val localVariableApiResponse: ApiResponse<{{#returnType}}{{{returnType}}}?{{/returnType}}{{^returnType}}Unit?{{/returnType}}> = this.handleResponse(it)
+ localVariableApiResponse
}
}
diff --git a/modules/openapi-generator/src/main/resources/kotlin-client/libraries/jvm-volley/api.mustache b/modules/openapi-generator/src/main/resources/kotlin-client/libraries/jvm-volley/api.mustache
index 246b5e73d169..7a8e830b58e2 100644
--- a/modules/openapi-generator/src/main/resources/kotlin-client/libraries/jvm-volley/api.mustache
+++ b/modules/openapi-generator/src/main/resources/kotlin-client/libraries/jvm-volley/api.mustache
@@ -48,14 +48,14 @@ import {{packageName}}.infrastructure.CollectionFormats.*
{{/isDeprecated}}
{{^nonPublicApi}}{{#explicitApi}}public {{/explicitApi}}{{/nonPublicApi}}suspend fun {{operationId}}({{^allParams}}){{/allParams}}{{#allParams}}{{>queryParams}}{{>pathParams}}{{>headerParams}}{{>bodyParams}}{{>formParams}}{{^-last}}, {{/-last}}{{#-last}}{{#operationAuthMethod}}, opAuthHeaderFactory = () -> map{{/operationAuthMethod}}){{/-last}}{{/allParams}}: {{#returnType}}{{{returnType}}}?{{/returnType}}{{^returnType}}Unit{{/returnType}} {
{{#bodyParam}}
- val body: Any? = {{paramName}}
+ val localVariableBody: Any? = {{paramName}}
{{/bodyParam}}
{{^bodyParam}}
- val body: Any? = null
+ val localVariableBody: Any? = null
{{/bodyParam}}
- val contentTypes : Array = arrayOf({{#consumes}}"{{{mediaType}}}"{{^-last}},{{/-last}}{{/consumes}})
- val contentType: String = if (contentTypes.isNotEmpty()) { contentTypes.first() } else { "application/json" }
+ val localVariableContentTypes : Array = arrayOf({{#consumes}}"{{{mediaType}}}"{{^-last}},{{/-last}}{{/consumes}})
+ val localVariableContentType: String = if (localVariableContentTypes.isNotEmpty()) { localVariableContentTypes.first() } else { "application/json" }
// Do some work or avoid some work based on what we know about the model,
// before we delegate to a pluggable request factory template
@@ -63,53 +63,53 @@ import {{packageName}}.infrastructure.CollectionFormats.*
// to make it easy to override with your own.
// create path and map variables
- val path = "{{{path}}}"{{#pathParams}}.replace("{" + "{{baseName}}" + "}", {{#isContainer}}{{paramName}}.joinToString(","){{/isContainer}}{{^isContainer}}IRequestFactory.escapeString({{{paramName}}}.toString()){{/isContainer}}){{/pathParams}}
+ val localVariablePath = "{{#lambda.escapeDollar}}{{{path}}}{{/lambda.escapeDollar}}"{{#pathParams}}.replace("{" + "{{#lambda.escapeDollar}}{{baseName}}{{/lambda.escapeDollar}}" + "}", {{#isContainer}}{{paramName}}.joinToString(","){{/isContainer}}{{^isContainer}}IRequestFactory.escapeString({{{paramName}}}.toString()){{/isContainer}}){{/pathParams}}
- val formParams = mapOf({{^formParams}}){{/formParams}}{{#formParams}}
- "{{baseName}}" to IRequestFactory.parameterToString({{paramName}}){{^-last}},{{/-last}}{{#-last}}
+ val localVariableFormParams = mapOf({{^formParams}}){{/formParams}}{{#formParams}}
+ "{{#lambda.escapeDollar}}{{baseName}}{{/lambda.escapeDollar}}" to IRequestFactory.parameterToString({{paramName}}){{^-last}},{{/-last}}{{#-last}}
){{/-last}}{{/formParams}}
// TODO: Cater for allowing empty values
// TODO, if its apikey auth, then add the header names here and the hardcoded auth key
// Only support hard coded apikey in query param auth for when we do this first path
- val queryParams = mapOf({{^queryParams}}){{/queryParams}}{{#queryParams}}
- "{{baseName}}" to IRequestFactory.parameterToString({{paramName}}){{^-last}},{{/-last}}{{#-last}}
+ val localVariableQueryParams = mapOf({{^queryParams}}){{/queryParams}}{{#queryParams}}
+ "{{#lambda.escapeDollar}}{{baseName}}{{/lambda.escapeDollar}}" to IRequestFactory.parameterToString({{paramName}}){{^-last}},{{/-last}}{{#-last}}
){{/-last}}{{/queryParams}}
.filter { it.value.isNotEmpty() }
- val headerParams: Map = mapOf({{^headerParams}}){{/headerParams}}{{#headerParams}}
- "{{baseName}}" to IRequestFactory.parameterToString({{paramName}}){{^-last}},{{/-last}}{{#-last}}
+ val localVariableHeaderParams: Map = mapOf({{^headerParams}}){{/headerParams}}{{#headerParams}}
+ "{{#lambda.escapeDollar}}{{baseName}}{{/lambda.escapeDollar}}" to IRequestFactory.parameterToString({{paramName}}){{^-last}},{{/-last}}{{#-last}}
){{/-last}}{{/headerParams}}
return suspendCoroutine { continuation ->
- val responseListener = Response.Listener<{{#returnType}}{{{returnType}}}{{/returnType}}{{^returnType}}Unit{{/returnType}}> { response ->
+ val localVariableResponseListener = Response.Listener<{{#returnType}}{{{returnType}}}{{/returnType}}{{^returnType}}Unit{{/returnType}}> { response ->
continuation.resume(response)
}
- val errorListener = Response.ErrorListener { error ->
+ val localVariableErrorListener = Response.ErrorListener { error ->
continuation.resumeWithException(error)
}
- val responseType = object : TypeToken<{{#returnType}}{{{returnType}}}{{/returnType}}{{^returnType}}Unit{{/returnType}}>() {}.type
+ val localVariableResponseType = object : TypeToken<{{#returnType}}{{{returnType}}}{{/returnType}}{{^returnType}}Unit{{/returnType}}>() {}.type
// Call the correct request builder based on whether we have a return type or a body.
// All other switching on types must be done in code inside the builder
- val request: Request<{{#returnType}}{{{returnType}}}{{/returnType}}{{^returnType}}Unit{{/returnType}}> = requestFactory.build(
+ val localVariableRequest: Request<{{#returnType}}{{{returnType}}}{{/returnType}}{{^returnType}}Unit{{/returnType}}> = this.requestFactory.build(
Request.Method.{{httpMethod}},
- "$basePath$path",
- body,
- headerParams,
- queryParams,
- formParams,
- contentType,
- responseType,
- responseListener,
- errorListener)
-
- postProcessors.forEach { it.invoke(request) }
-
- requestQueue.value.add(request)
+ "${this.basePath}$localVariablePath",
+ localVariableBody,
+ localVariableHeaderParams,
+ localVariableQueryParams,
+ localVariableFormParams,
+ localVariableContentType,
+ localVariableResponseType,
+ localVariableResponseListener,
+ localVariableErrorListener)
+
+ this.postProcessors.forEach { it.invoke(localVariableRequest) }
+
+ this.requestQueue.value.add(localVariableRequest)
}
}
{{/operation}}
diff --git a/modules/openapi-generator/src/main/resources/kotlin-client/libraries/multiplatform/api.mustache b/modules/openapi-generator/src/main/resources/kotlin-client/libraries/multiplatform/api.mustache
index 82f266017caf..3bc479b86f4f 100644
--- a/modules/openapi-generator/src/main/resources/kotlin-client/libraries/multiplatform/api.mustache
+++ b/modules/openapi-generator/src/main/resources/kotlin-client/libraries/multiplatform/api.mustache
@@ -73,28 +73,28 @@ import kotlinx.serialization.encoding.*
{{#formParams}}
{{#isArray}}
{{{paramName}}}?.onEach {
- {{#isFile}}append(it){{/isFile}}{{^isFile}}append("{{{baseName}}}", it.toString()){{/isFile}}
+ {{#isFile}}append(it){{/isFile}}{{^isFile}}append("{{#lambda.escapeDollar}}{{{baseName}}}{{/lambda.escapeDollar}}", it.toString()){{/isFile}}
}
{{/isArray}}
{{^isArray}}
{{#isFile}}
- {{{paramName}}}?.apply { append({{{baseName}}}) }
+ {{{paramName}}}?.apply { append({{{paramName}}}) }
{{/isFile}}
{{^isFile}}
{{#isPrimitiveType}}
{{#isString}}
- {{{paramName}}}?.apply { append("{{{baseName}}}", {{{paramName}}}) }
+ {{{paramName}}}?.apply { append("{{#lambda.escapeDollar}}{{{baseName}}}{{/lambda.escapeDollar}}", {{{paramName}}}) }
{{/isString}}
{{^isString}}
- {{{paramName}}}?.apply { append("{{{baseName}}}", {{{paramName}}}.toString()) }
+ {{{paramName}}}?.apply { append("{{#lambda.escapeDollar}}{{{baseName}}}{{/lambda.escapeDollar}}", {{{paramName}}}.toString()) }
{{/isString}}
{{/isPrimitiveType}}
{{^isPrimitiveType}}
{{#isEnumOrRef}}
- {{{paramName}}}?.apply { append("{{{baseName}}}", {{{paramName}}}.value.toString()) }
+ {{{paramName}}}?.apply { append("{{#lambda.escapeDollar}}{{{baseName}}}{{/lambda.escapeDollar}}", {{{paramName}}}.value.toString()) }
{{/isEnumOrRef}}
{{^isEnumOrRef}}
- {{{paramName}}}?.apply { append("{{{baseName}}}", ApiClient.JSON_DEFAULT.encodeToString({{{dataType}}}.serializer(), {{{paramName}}})) }
+ {{{paramName}}}?.apply { append("{{#lambda.escapeDollar}}{{{baseName}}}{{/lambda.escapeDollar}}", ApiClient.JSON_DEFAULT.encodeToString({{{dataType}}}.serializer(), {{{paramName}}})) }
{{/isEnumOrRef}}
{{/isPrimitiveType}}
{{/isFile}}
@@ -103,9 +103,9 @@ import kotlinx.serialization.encoding.*
}
{{/isMultipart}}
{{^isMultipart}}
- ParametersBuilder().also {
+ ParametersBuilder().also { localVariableBuilder ->
{{#formParams}}
- {{{paramName}}}?.apply { it.append("{{{baseName}}}", {{{paramName}}}.toString()) }
+ {{{paramName}}}?.apply { localVariableBuilder.append("{{#lambda.escapeDollar}}{{{baseName}}}{{/lambda.escapeDollar}}", {{{paramName}}}.toString()) }
{{/formParams}}
}.build()
{{/isMultipart}}
@@ -116,23 +116,23 @@ import kotlinx.serialization.encoding.*
{{/hasBodyParam}}
val localVariableQuery = mutableMapOf>(){{#queryParams}}
- {{{paramName}}}?.apply { localVariableQuery["{{baseName}}"] = {{#isContainer}}toMultiValue(this, "{{collectionFormat}}"){{/isContainer}}{{^isContainer}}listOf({{^isEnumOrRef}}"${{{paramName}}}"{{/isEnumOrRef}}{{#isEnumOrRef}}"${ {{paramName}}.value }"{{/isEnumOrRef}}){{/isContainer}} }{{/queryParams}}
+ {{{paramName}}}?.apply { localVariableQuery["{{#lambda.escapeDollar}}{{baseName}}{{/lambda.escapeDollar}}"] = {{#isContainer}}toMultiValue(this, "{{collectionFormat}}"){{/isContainer}}{{^isContainer}}listOf({{^isEnumOrRef}}"${{{paramName}}}"{{/isEnumOrRef}}{{#isEnumOrRef}}"${ {{paramName}}.value }"{{/isEnumOrRef}}){{/isContainer}} }{{/queryParams}}
val localVariableHeaders = mutableMapOf(){{#headerParams}}
- {{{paramName}}}?.apply { localVariableHeaders["{{baseName}}"] = {{#isContainer}}this.joinToString(separator = collectionDelimiter("{{collectionFormat}}")){{/isContainer}}{{^isContainer}}this.toString(){{/isContainer}} }{{/headerParams}}
+ {{{paramName}}}?.apply { localVariableHeaders["{{#lambda.escapeDollar}}{{baseName}}{{/lambda.escapeDollar}}"] = {{#isContainer}}this.joinToString(separator = collectionDelimiter("{{collectionFormat}}")){{/isContainer}}{{^isContainer}}this.toString(){{/isContainer}} }{{/headerParams}}
val localVariableConfig = RequestConfig(
RequestMethod.{{httpMethod}},
- "{{{path}}}"{{#pathParams}}.replace("{" + "{{baseName}}" + "}", {{#isContainer}}{{paramName}}.joinToString(","){{/isContainer}}{{^isContainer}}{{^isEnumOrRef}}"${{{paramName}}}"{{/isEnumOrRef}}{{#isEnumOrRef}}"${ {{paramName}}.value }"{{/isEnumOrRef}}{{/isContainer}}){{/pathParams}},
+ "{{#lambda.escapeDollar}}{{{path}}}{{/lambda.escapeDollar}}"{{#pathParams}}.replace("{" + "{{#lambda.escapeDollar}}{{baseName}}{{/lambda.escapeDollar}}" + "}", {{#isContainer}}{{paramName}}.joinToString(","){{/isContainer}}{{^isContainer}}{{^isEnumOrRef}}"${{{paramName}}}"{{/isEnumOrRef}}{{#isEnumOrRef}}"${ {{paramName}}.value }"{{/isEnumOrRef}}{{/isContainer}}){{/pathParams}},
query = localVariableQuery,
headers = localVariableHeaders,
requiresAuthentication = {{#hasAuthMethods}}true{{/hasAuthMethods}}{{^hasAuthMethods}}false{{/hasAuthMethods}},
)
- return {{#hasBodyParam}}jsonRequest{{/hasBodyParam}}{{^hasBodyParam}}{{#hasFormParams}}{{#isMultipart}}multipartFormRequest{{/isMultipart}}{{^isMultipart}}urlEncodedFormRequest{{/isMultipart}}{{/hasFormParams}}{{^hasFormParams}}request{{/hasFormParams}}{{/hasBodyParam}}(
+ return {{#hasBodyParam}}this.jsonRequest{{/hasBodyParam}}{{^hasBodyParam}}{{#hasFormParams}}{{#isMultipart}}this.multipartFormRequest{{/isMultipart}}{{^isMultipart}}this.urlEncodedFormRequest{{/isMultipart}}{{/hasFormParams}}{{^hasFormParams}}this.request{{/hasFormParams}}{{/hasBodyParam}}(
localVariableConfig,
localVariableBody,
localVariableAuthNames
- ).{{#isArray}}wrap<{{operationIdCamelCase}}Response>().map { value{{^isList}}.toTypedArray(){{/isList}} }{{/isArray}}{{^isArray}}{{#isMap}}wrap<{{operationIdCamelCase}}Response>().map { value }{{/isMap}}{{^isMap}}wrap(){{/isMap}}{{/isArray}}
+ ).{{#isArray}}wrap<{{operationIdCamelCase}}Response>().map { this.value{{^isList}}.toTypedArray(){{/isList}} }{{/isArray}}{{^isArray}}{{#isMap}}wrap<{{operationIdCamelCase}}Response>().map { this.value }{{/isMap}}{{^isMap}}wrap(){{/isMap}}{{/isArray}}
}
{{#hasBodyParam}}
diff --git a/modules/openapi-generator/src/main/resources/kotlin-client/libraries/multiplatform/infrastructure/ApiClient.kt.mustache b/modules/openapi-generator/src/main/resources/kotlin-client/libraries/multiplatform/infrastructure/ApiClient.kt.mustache
index d2a1fe0ebf2e..3eb45309f9de 100644
--- a/modules/openapi-generator/src/main/resources/kotlin-client/libraries/multiplatform/infrastructure/ApiClient.kt.mustache
+++ b/modules/openapi-generator/src/main/resources/kotlin-client/libraries/multiplatform/infrastructure/ApiClient.kt.mustache
@@ -54,7 +54,7 @@ import {{packageName}}.auth.*
mapOf({{#authMethods}}{{#isBasic}}{{#isBasicBasic}}
"{{name}}" to HttpBasicAuth(){{/isBasicBasic}}{{#isBasicBearer}}
"{{name}}" to HttpBearerAuth("{{scheme}}"){{/isBasicBearer}}{{/isBasic}}{{#isApiKey}}
- "{{name}}" to ApiKeyAuth({{#isKeyInHeader}}"header"{{/isKeyInHeader}}{{^isKeyInHeader}}"query"{{/isKeyInHeader}}, "{{keyParamName}}"){{/isApiKey}}{{#isOAuth}}
+ "{{name}}" to ApiKeyAuth({{#isKeyInHeader}}"header"{{/isKeyInHeader}}{{^isKeyInHeader}}"query"{{/isKeyInHeader}}, "{{#lambda.escapeDollar}}{{keyParamName}}{{/lambda.escapeDollar}}"){{/isApiKey}}{{#isOAuth}}
"{{name}}" to OAuth(){{/isOAuth}}{{^-last}}, {{/-last}}{{/authMethods}})
}
{{/hasAuthMethods}}
diff --git a/modules/openapi-generator/src/main/resources/php/README.mustache b/modules/openapi-generator/src/main/resources/php/README.mustache
index e4a17f15d17b..b5dd0ad9fb38 100644
--- a/modules/openapi-generator/src/main/resources/php/README.mustache
+++ b/modules/openapi-generator/src/main/resources/php/README.mustache
@@ -78,7 +78,7 @@ All URIs are relative to *{{basePath}}*
Class | Method | HTTP request | Description
------------ | ------------- | ------------- | -------------
-{{#apiInfo}}{{#apis}}{{#operations}}{{#operation}}*{{classname}}* | [**{{operationId}}**]({{apiDocPath}}/{{classname}}.md#{{operationIdLowerCase}}) | **{{httpMethod}}** {{path}} | {{summary}}
+{{#apiInfo}}{{#apis}}{{#operations}}{{#operation}}*{{classname}}* | [**{{operationId}}**]({{apiDocPath}}/{{classname}}.md#{{operationIdLowerCase}}) | **{{#vendorExtensions.x-php-http-method-doc}}{{{vendorExtensions.x-php-http-method-doc}}}{{/vendorExtensions.x-php-http-method-doc}}{{^vendorExtensions.x-php-http-method-doc}}{{httpMethod}}{{/vendorExtensions.x-php-http-method-doc}}** {{path}} | {{summary}}
{{/operation}}{{/operations}}{{/apis}}{{/apiInfo}}
## Models
diff --git a/modules/openapi-generator/src/main/resources/php/api.mustache b/modules/openapi-generator/src/main/resources/php/api.mustache
index eb8540f3b618..ac11c607ebee 100644
--- a/modules/openapi-generator/src/main/resources/php/api.mustache
+++ b/modules/openapi-generator/src/main/resources/php/api.mustache
@@ -608,6 +608,7 @@ use {{invokerPackage}}\ObjectSerializer;
$multipart = false;
{{#queryParams}}
+ {{^isQueryStringParam}}
// query params
$queryParams = array_merge($queryParams, ObjectSerializer::toQueryValue(
${{paramName}},
@@ -617,6 +618,7 @@ use {{invokerPackage}}\ObjectSerializer;
{{#isExplode}}true{{/isExplode}}{{^isExplode}}false{{/isExplode}}, // explode
{{required}} // required
) ?? []);
+ {{/isQueryStringParam}}
{{/queryParams}}
{{#headerParams}}
@@ -777,12 +779,62 @@ use {{invokerPackage}}\ObjectSerializer;
$operationHost = $this->config->getHost();
{{/servers.0}}
$query = ObjectSerializer::buildQuery($queryParams);
+ {{#vendorExtensions.x-php-has-querystring-param}}
+ // $__-prefixed so a spec parameter named e.g. `uri` cannot shadow it
+ $__requestUri = $operationHost . $resourcePath . ($query ? "?{$query}" : '');
+ {{#allParams}}
+ {{#isQueryStringParam}}
+ // OpenAPI 3.2 `in: querystring`: the value IS the query component, appended verbatim
+ {{#required}}
+ $__requestUri .= (str_contains($__requestUri, '?') ? '&' : '?') . ${{paramName}};
+ {{/required}}
+ {{^required}}
+ if (${{paramName}} !== null) {
+ $__requestUri .= (str_contains($__requestUri, '?') ? '&' : '?') . ${{paramName}};
+ }
+ {{/required}}
+ {{/isQueryStringParam}}
+ {{/allParams}}
+ {{/vendorExtensions.x-php-has-querystring-param}}
+ {{#vendorExtensions.x-php-verbatim-method}}
+ // OpenAPI 3.2 custom verb: guzzlehttp/psr7 upper-cases the method in
+ // Request::__construct/withMethod, but Guzzle's handlers only ever read
+ // RequestInterface::getMethod(), so keep the verbatim token in a subclass.
+ return new class(
+ '{{{vendorExtensions.x-php-http-method-literal}}}',
+ {{#vendorExtensions.x-php-has-querystring-param}}$__requestUri{{/vendorExtensions.x-php-has-querystring-param}}{{^vendorExtensions.x-php-has-querystring-param}}$operationHost . $resourcePath . ($query ? "?{$query}" : ''){{/vendorExtensions.x-php-has-querystring-param}},
+ $headers,
+ $httpBody
+ ) extends Request {
+ private string $verbatimMethod;
+
+ public function __construct(string $method, $uri, array $headers = [], $body = null, string $version = '1.1')
+ {
+ parent::__construct($method, $uri, $headers, $body, $version);
+ $this->verbatimMethod = $method;
+ }
+
+ public function getMethod(): string
+ {
+ return $this->verbatimMethod;
+ }
+
+ public function withMethod($method): RequestInterface
+ {
+ $new = parent::withMethod($method);
+ $new->verbatimMethod = $method;
+ return $new;
+ }
+ };
+ {{/vendorExtensions.x-php-verbatim-method}}
+ {{^vendorExtensions.x-php-verbatim-method}}
return new Request(
'{{httpMethod}}',
- $operationHost . $resourcePath . ($query ? "?{$query}" : ''),
+ {{#vendorExtensions.x-php-has-querystring-param}}$__requestUri{{/vendorExtensions.x-php-has-querystring-param}}{{^vendorExtensions.x-php-has-querystring-param}}$operationHost . $resourcePath . ($query ? "?{$query}" : ''){{/vendorExtensions.x-php-has-querystring-param}},
$headers,
$httpBody
);
+ {{/vendorExtensions.x-php-verbatim-method}}
}
{{#servers.0}}
diff --git a/modules/openapi-generator/src/main/resources/php/api_doc.mustache b/modules/openapi-generator/src/main/resources/php/api_doc.mustache
index ea5dad63de83..b72b486f3b5c 100644
--- a/modules/openapi-generator/src/main/resources/php/api_doc.mustache
+++ b/modules/openapi-generator/src/main/resources/php/api_doc.mustache
@@ -6,7 +6,7 @@ All URIs are relative to {{basePath}}, except if the operation defines another b
| Method | HTTP request | Description |
| ------------- | ------------- | ------------- |
-{{#operations}}{{#operation}}| [**{{operationId}}()**]({{classname}}.md#{{operationId}}) | **{{httpMethod}}** {{path}} | {{summary}} |
+{{#operations}}{{#operation}}| [**{{operationId}}()**]({{classname}}.md#{{operationId}}) | **{{#vendorExtensions.x-php-http-method-doc}}{{{vendorExtensions.x-php-http-method-doc}}}{{/vendorExtensions.x-php-http-method-doc}}{{^vendorExtensions.x-php-http-method-doc}}{{httpMethod}}{{/vendorExtensions.x-php-http-method-doc}}** {{path}} | {{summary}} |
{{/operation}}{{/operations}}{{#operations}}{{#operation}}
## `{{{operationId}}}()`
diff --git a/modules/openapi-generator/src/main/resources/php/composer.mustache b/modules/openapi-generator/src/main/resources/php/composer.mustache
index 3137dc473dd2..e2e2f811c11c 100644
--- a/modules/openapi-generator/src/main/resources/php/composer.mustache
+++ b/modules/openapi-generator/src/main/resources/php/composer.mustache
@@ -29,7 +29,7 @@
"ext-json": "*",
"ext-mbstring": "*",
"guzzlehttp/guzzle": "^7.3",
- "guzzlehttp/psr7": "^1.7 || ^2.0"
+ "guzzlehttp/psr7": "^2.10"
},
"require-dev": {
"phpunit/phpunit": "^8.0 || ^9.0",
diff --git a/modules/openapi-generator/src/main/resources/python/api.mustache b/modules/openapi-generator/src/main/resources/python/api.mustache
index 3cee5c88c1b8..1392b43a1845 100644
--- a/modules/openapi-generator/src/main/resources/python/api.mustache
+++ b/modules/openapi-generator/src/main/resources/python/api.mustache
@@ -319,6 +319,7 @@ https://github.com/OpenAPITools/openapi-generator/blob/c84b949df1a9ec04ba75989cb
{{/pathParams}}
# process the query parameters
{{#queryParams}}
+{{^isQueryStringParam}}
if {{paramName}} is not None:
{{#isDateTime}}
if isinstance({{paramName}}, datetime):
@@ -379,6 +380,7 @@ https://github.com/OpenAPITools/openapi-generator/blob/c84b949df1a9ec04ba75989cb
{{/isModel}}
{{/isMap}}
{{/isDate}}{{/isDateTime}}
+{{/isQueryStringParam}}
{{/queryParams}}
# process the header parameters
{{#headerParams}}
@@ -456,9 +458,31 @@ https://github.com/OpenAPITools/openapi-generator/blob/c84b949df1a9ec04ba75989cb
'{{name}}'{{^-last}}, {{/-last}}{{/authMethods}}
]
+{{#vendorExtensions.x-python-has-querystring-param}}
+ _resource_path = '{{{path}}}'
+{{#queryParams}}
+{{#isQueryStringParam}}
+ # OpenAPI 3.2 in: querystring - the value is the whole, already-encoded
+ # query string; append it verbatim instead of a name=value pair.
+ # Note: urllib3 may normalize percent-escape hex casing on the wire.
+ if {{paramName}} is not None:
+ _resource_path += ('&' if '?' in _resource_path else '?') + {{paramName}}
+{{/isQueryStringParam}}
+{{/queryParams}}
+{{/vendorExtensions.x-python-has-querystring-param}}
return self.api_client.param_serialize(
+ {{#vendorExtensions.x-python-http-method-literal}}
+ method="{{{httpMethod}}}",{{! OpenAPI 3.2: non-standard method sent verbatim, unescaped }}
+ {{/vendorExtensions.x-python-http-method-literal}}
+ {{^vendorExtensions.x-python-http-method-literal}}
method='{{httpMethod}}',
+ {{/vendorExtensions.x-python-http-method-literal}}
+ {{#vendorExtensions.x-python-has-querystring-param}}
+ resource_path=_resource_path,
+ {{/vendorExtensions.x-python-has-querystring-param}}
+ {{^vendorExtensions.x-python-has-querystring-param}}
resource_path='{{{path}}}',
+ {{/vendorExtensions.x-python-has-querystring-param}}
path_params=_path_params,
query_params=_query_params,
header_params=_header_params,
diff --git a/modules/openapi-generator/src/main/resources/python/api_client.mustache b/modules/openapi-generator/src/main/resources/python/api_client.mustache
index 973a8de03b2b..6d6843fa977e 100644
--- a/modules/openapi-generator/src/main/resources/python/api_client.mustache
+++ b/modules/openapi-generator/src/main/resources/python/api_client.mustache
@@ -385,7 +385,9 @@ https://github.com/OpenAPITools/openapi-generator/blob/c84b949df1a9ec04ba75989cb
query_params,
collection_formats
)
- url += "?" + url_query
+ # OpenAPI 3.2 in: querystring may already have put a '?' in the
+ # path (e.g. combined with apiKey-in-query auth params)
+ url += ("&" if "?" in url else "?") + url_query
return method, url, header_params, body, post_params
diff --git a/modules/openapi-generator/src/main/resources/python/rest.mustache b/modules/openapi-generator/src/main/resources/python/rest.mustache
index 5f56a2d60b32..9bc1791c0164 100644
--- a/modules/openapi-generator/src/main/resources/python/rest.mustache
+++ b/modules/openapi-generator/src/main/resources/python/rest.mustache
@@ -172,6 +172,23 @@ class RESTClientObject:
self.pool_manager.clear()
{{/useIndependentImplicitClients}}
+ # urllib3's RequestMethods.request() unconditionally calls method.upper(),
+ # which would corrupt OpenAPI 3.2 method names (e.g. 'customMethod').
+ # Dispatch non-standard methods through the encode helpers directly,
+ # exactly as request() does after its upper() call. Relies on urllib3
+ # 2.x internals (_encode_url_methods); the generated client pins
+ # urllib3 >= 2.7.0, < 3.0.0.
+ _STANDARD_METHODS = frozenset([
+ 'GET', 'HEAD', 'DELETE', 'POST', 'PUT', 'PATCH', 'OPTIONS'
+ ])
+
+ def _pool_request(self, method, url, **kwargs):
+ if method in self._STANDARD_METHODS:
+ return self.pool_manager.request(method, url, **kwargs)
+ if method.upper() in self.pool_manager._encode_url_methods:
+ return self.pool_manager.request_encode_url(method, url, **kwargs)
+ return self.pool_manager.request_encode_body(method, url, **kwargs)
+
def request(
self,
method,
@@ -195,16 +212,18 @@ class RESTClientObject:
timeout. It can also be a pair (tuple) of
(connection, read) timeouts.
"""
- method = method.upper()
- assert method in [
- 'GET',
- 'HEAD',
- 'DELETE',
- 'POST',
- 'PUT',
- 'PATCH',
- 'OPTIONS'
- ]
+ # OpenAPI 3.2 allows arbitrary HTTP method names (query operations,
+ # additionalOperations keys). The generator emits standard methods
+ # already uppercase; anything else is validated as an HTTP token
+ # (RFC 9110 tchar) and sent verbatim so casing like 'customMethod'
+ # or 'get' (as a distinct additionalOperations key) survives. Do not
+ # use `assert` here: it is stripped under `python -O`.
+ if method not in self._STANDARD_METHODS:
+ if re.fullmatch(r"[!#$%&'*+\-.^_`|~0-9A-Za-z]+", method) is None:
+ raise ApiValueError(
+ "Unrecognized HTTP method name %r: not a valid "
+ "RFC 9110 token" % method
+ )
if post_params and body:
raise ApiValueError(
@@ -228,8 +247,13 @@ class RESTClientObject:
)
try:
- # For `POST`, `PUT`, `PATCH`, `OPTIONS`, `DELETE`
- if method in ['POST', 'PUT', 'PATCH', 'OPTIONS', 'DELETE']:
+ # For `POST`, `PUT`, `PATCH`, `OPTIONS`, `DELETE` - or a
+ # non-standard OpenAPI 3.2 method that actually carries a
+ # body/form data. GET/HEAD keep the historic no-body path even
+ # when a spec defines one.
+ if (method in ['POST', 'PUT', 'PATCH', 'OPTIONS', 'DELETE']
+ or (method not in self._STANDARD_METHODS
+ and (body is not None or post_params))):
content_type = headers.get('Content-Type')
is_json = (
@@ -252,7 +276,7 @@ class RESTClientObject:
request_body = None
if body is not None:
request_body = json.dumps(body{{#setEnsureAsciiToFalse}}, ensure_ascii=False{{/setEnsureAsciiToFalse}})
- r = self.pool_manager.request(
+ r = self._pool_request(
method,
url,
body=request_body,
@@ -261,7 +285,7 @@ class RESTClientObject:
preload_content=False
)
elif contenttype_matches(content_type, 'application', 'x-www-form-urlencoded'):
- r = self.pool_manager.request(
+ r = self._pool_request(
method,
url,
fields=post_params,
@@ -277,7 +301,7 @@ class RESTClientObject:
del headers['Content-Type']
# Ensures that dict objects are serialized
post_params = [(a, json.dumps(b)) if isinstance(b, dict) else (a,b) for a, b in post_params]
- r = self.pool_manager.request(
+ r = self._pool_request(
method,
url,
fields=post_params,
@@ -290,7 +314,7 @@ class RESTClientObject:
# other content types than JSON when `body` argument is
# provided in serialized form.
elif isinstance(body, str) or isinstance(body, bytes):
- r = self.pool_manager.request(
+ r = self._pool_request(
method,
url,
body=body,
@@ -300,7 +324,7 @@ class RESTClientObject:
)
elif content_type.startswith('text/') and isinstance(body, bool):
request_body = "true" if body else "false"
- r = self.pool_manager.request(
+ r = self._pool_request(
method,
url,
body=request_body,
@@ -315,7 +339,7 @@ class RESTClientObject:
raise ApiException(status=0, reason=msg)
# For `GET`, `HEAD`
else:
- r = self.pool_manager.request(
+ r = self._pool_request(
method,
url,
fields={},
diff --git a/modules/openapi-generator/src/main/resources/ruby-client/README.mustache b/modules/openapi-generator/src/main/resources/ruby-client/README.mustache
index 5f5893469607..cf35a4289844 100644
--- a/modules/openapi-generator/src/main/resources/ruby-client/README.mustache
+++ b/modules/openapi-generator/src/main/resources/ruby-client/README.mustache
@@ -123,7 +123,7 @@ All URIs are relative to *{{basePath}}*
Class | Method | HTTP request | Description
------------ | ------------- | ------------- | -------------
-{{#apiInfo}}{{#apis}}{{#operations}}{{#operation}}*{{moduleName}}::{{classname}}* | [**{{operationId}}**]({{apiDocPath}}{{classname}}.md#{{operationId}}) | **{{httpMethod}}** {{path}} | {{{summary}}}
+{{#apiInfo}}{{#apis}}{{#operations}}{{#operation}}*{{moduleName}}::{{classname}}* | [**{{operationId}}**]({{apiDocPath}}{{classname}}.md#{{operationId}}) | **{{#vendorExtensions.x-ruby-http-method-doc}}{{{vendorExtensions.x-ruby-http-method-doc}}}{{/vendorExtensions.x-ruby-http-method-doc}}{{^vendorExtensions.x-ruby-http-method-doc}}{{httpMethod}}{{/vendorExtensions.x-ruby-http-method-doc}}** {{path}} | {{{summary}}}
{{/operation}}{{/operations}}{{/apis}}{{/apiInfo}}
## Documentation for Models
diff --git a/modules/openapi-generator/src/main/resources/ruby-client/api.mustache b/modules/openapi-generator/src/main/resources/ruby-client/api.mustache
index 60bc7672aaac..793da6e67c33 100644
--- a/modules/openapi-generator/src/main/resources/ruby-client/api.mustache
+++ b/modules/openapi-generator/src/main/resources/ruby-client/api.mustache
@@ -171,10 +171,22 @@ module {{moduleName}}
{{/allParams}}
# resource path
local_var_path = '{{{path}}}'{{#pathParams}}.sub({{=<< >>=}}'{<>}'<<={{ }}=>>, CGI.escape({{paramName}}.to_s){{^strictSpecBehavior}}.gsub('%2F', '/'){{/strictSpecBehavior}}){{/pathParams}}
+ {{#allParams}}
+ {{#isQueryStringParam}}
+ {{#required}}
+ # OpenAPI 3.2 `in: querystring`: the value IS the query component, appended verbatim
+ local_var_path = local_var_path + (local_var_path.include?('?') ? '&' : '?') + {{{paramName}}}.to_s
+ {{/required}}
+ {{^required}}
+ local_var_path = local_var_path + (local_var_path.include?('?') ? '&' : '?') + opts[:'{{{paramName}}}'].to_s if !opts[:'{{{paramName}}}'].nil?
+ {{/required}}
+ {{/isQueryStringParam}}
+ {{/allParams}}
# query parameters
query_params = opts[:query_params] || {}
{{#queryParams}}
+ {{^isQueryStringParam}}
{{#required}}
{{#queryIsJsonMimeType}}
query_params[:'{{{baseName}}}'] = {{{paramName}}}.to_json
@@ -199,8 +211,10 @@ module {{moduleName}}
{{/isMap}}
{{/queryIsJsonMimeType}}
{{/required}}
+ {{/isQueryStringParam}}
{{/queryParams}}
{{#queryParams}}
+ {{^isQueryStringParam}}
{{^required}}
{{#queryIsJsonMimeType}}
query_params[:'{{{baseName}}}'] = opts[:'{{{paramName}}}'].to_json if !opts[:'{{{paramName}}}'].nil?
@@ -225,6 +239,7 @@ module {{moduleName}}
{{/isMap}}
{{/queryIsJsonMimeType}}
{{/required}}
+ {{/isQueryStringParam}}
{{/queryParams}}
# header parameters
@@ -283,7 +298,7 @@ module {{moduleName}}
:return_type => return_type
)
- data, status_code, headers = @api_client.call_api(:{{httpMethod}}, local_var_path, new_options)
+ data, status_code, headers = @api_client.call_api(:"{{{vendorExtensions.x-ruby-http-method-symbol}}}", local_var_path, new_options)
if @api_client.config.debugging
@api_client.config.logger.debug "API called: {{classname}}#{{operationId}}\nData: #{data.inspect}\nStatus code: #{status_code}\nHeaders: #{headers}"
end
diff --git a/modules/openapi-generator/src/main/resources/ruby-client/api_client.mustache b/modules/openapi-generator/src/main/resources/ruby-client/api_client.mustache
index a4609a59fc27..7513a21e187c 100644
--- a/modules/openapi-generator/src/main/resources/ruby-client/api_client.mustache
+++ b/modules/openapi-generator/src/main/resources/ruby-client/api_client.mustache
@@ -155,9 +155,12 @@ module {{moduleName}}
end
def build_request_url(path, opts = {})
- # Add leading and trailing slashes to path
- path = "/#{path}".gsub(/\/+/, '/')
- @config.base_url(opts[:operation]) + path
+ # Add leading and trailing slashes to path. An OpenAPI 3.2
+ # `in: querystring` value is appended verbatim to `path`, so only the
+ # part before '?' may have its slashes collapsed.
+ path_only, sep, query = path.partition('?')
+ path_only = "/#{path_only}".gsub(/\/+/, '/')
+ @config.base_url(opts[:operation]) + path_only + sep + query
end
# Update header and query params based on authentication settings.
diff --git a/modules/openapi-generator/src/main/resources/ruby-client/api_client_httpx_partial.mustache b/modules/openapi-generator/src/main/resources/ruby-client/api_client_httpx_partial.mustache
index 7dd5221daccb..ad3c1bf0e214 100644
--- a/modules/openapi-generator/src/main/resources/ruby-client/api_client_httpx_partial.mustache
+++ b/modules/openapi-generator/src/main/resources/ruby-client/api_client_httpx_partial.mustache
@@ -1,3 +1,7 @@
+ # HTTP verbs that HTTPX emits through its normal (upper-cased) dispatch path.
+ # OpenAPI 3.2 query/additionalOperations verbs are handled verbatim below.
+ STANDARD_HTTP_METHODS = %w[GET POST PUT PATCH DELETE OPTIONS HEAD TRACE CONNECT].freeze
+
# Call an API with given options.
#
# @return [Array<(Object, Integer, Hash)>] an array of 3 elements:
@@ -51,10 +55,12 @@
update_params_for_auth! header_params, query_params, opts[:auth_names]
- if %w[POST PATCH PUT DELETE].include?(http_method)
+ # OpenAPI 3.2 query/additionalOperations verbs may carry a body too; for
+ # non-standard methods we always attempt to build one (nil when absent).
+ if %w[POST PATCH PUT DELETE].include?(http_method) || !STANDARD_HTTP_METHODS.include?(http_method.to_s)
body_params = build_request_body(header_params, form_params, opts[:body])
if config.debugging
- config.logger.debug "HTTP request body param ~BEGIN~\n#{req_body}\n~END~\n"
+ config.logger.debug "HTTP request body param ~BEGIN~\n#{body_params}\n~END~\n"
end
end
req_opts = {
@@ -62,7 +68,18 @@
}
req_opts.merge!(body_params) if body_params
req_opts[:params] = query_params if query_params && !query_params.empty?
- session.request(http_method, url, **req_opts)
+ if STANDARD_HTTP_METHODS.include?(http_method.to_s)
+ session.request(http_method, url, **req_opts)
+ else
+ # OpenAPI 3.2 (query/additionalOperations): emit the verb verbatim.
+ # NOTE: HTTPX::Request internally stores @verb = verb.to_s.upcase and @verb is
+ # only used when emitting the request line, so we restore it after building.
+ # This depends on HTTPX internals - if a future httpx release changes how the
+ # verb is stored/emitted, this needs revisiting.
+ request = session.build_request(http_method, url, req_opts)
+ request.instance_variable_set(:@verb, http_method.to_s)
+ session.request(request)
+ end
end
# Builds the HTTP request body
diff --git a/modules/openapi-generator/src/main/resources/ruby-client/api_doc.mustache b/modules/openapi-generator/src/main/resources/ruby-client/api_doc.mustache
index ab2e666e1eb9..ba466f8a4ae5 100644
--- a/modules/openapi-generator/src/main/resources/ruby-client/api_doc.mustache
+++ b/modules/openapi-generator/src/main/resources/ruby-client/api_doc.mustache
@@ -8,7 +8,7 @@ All URIs are relative to *{{basePath}}*
| ------ | ------------ | ----------- |
{{#operations}}
{{#operation}}
-| [**{{operationId}}**]({{classname}}.md#{{operationId}}) | **{{httpMethod}}** {{path}} | {{summary}} |
+| [**{{operationId}}**]({{classname}}.md#{{operationId}}) | **{{#vendorExtensions.x-ruby-http-method-doc}}{{{vendorExtensions.x-ruby-http-method-doc}}}{{/vendorExtensions.x-ruby-http-method-doc}}{{^vendorExtensions.x-ruby-http-method-doc}}{{httpMethod}}{{/vendorExtensions.x-ruby-http-method-doc}}** {{path}} | {{summary}} |
{{/operation}}
{{/operations}}
diff --git a/modules/openapi-generator/src/main/resources/rust/README.mustache b/modules/openapi-generator/src/main/resources/rust/README.mustache
index 0e8bfb373f99..79fda06b3efd 100644
--- a/modules/openapi-generator/src/main/resources/rust/README.mustache
+++ b/modules/openapi-generator/src/main/resources/rust/README.mustache
@@ -34,7 +34,7 @@ All URIs are relative to *{{{basePath}}}*
Class | Method | HTTP request | Description
------------ | ------------- | ------------- | -------------
-{{#apiInfo}}{{#apis}}{{#operations}}{{#operation}}*{{{classname}}}* | [**{{{operationId}}}**]({{{apiDocPath}}}{{classname}}.md#{{{operationIdLowerCase}}}) | **{{{httpMethod}}}** {{{path}}} | {{{summary}}}
+{{#apiInfo}}{{#apis}}{{#operations}}{{#operation}}*{{{classname}}}* | [**{{{operationId}}}**]({{{apiDocPath}}}{{classname}}.md#{{{operationIdLowerCase}}}) | **{{#vendorExtensions.x-rust-http-method-doc}}{{{vendorExtensions.x-rust-http-method-doc}}}{{/vendorExtensions.x-rust-http-method-doc}}{{^vendorExtensions.x-rust-http-method-doc}}{{{httpMethod}}}{{/vendorExtensions.x-rust-http-method-doc}}** {{{path}}} | {{{summary}}}
{{/operation}}{{/operations}}{{/apis}}{{/apiInfo}}
## Documentation For Models
diff --git a/modules/openapi-generator/src/main/resources/rust/api_doc.mustache b/modules/openapi-generator/src/main/resources/rust/api_doc.mustache
index c36081755400..d9aa615f267b 100644
--- a/modules/openapi-generator/src/main/resources/rust/api_doc.mustache
+++ b/modules/openapi-generator/src/main/resources/rust/api_doc.mustache
@@ -6,7 +6,7 @@ All URIs are relative to *{{{basePath}}}*
Method | HTTP request | Description
------------- | ------------- | -------------
-{{#operations}}{{#operation}}[**{{{operationId}}}**]({{{classname}}}.md#{{{operationId}}}) | **{{{httpMethod}}}** {{{path}}} | {{{summary}}}
+{{#operations}}{{#operation}}[**{{{operationId}}}**]({{{classname}}}.md#{{{operationId}}}) | **{{#vendorExtensions.x-rust-http-method-doc}}{{{vendorExtensions.x-rust-http-method-doc}}}{{/vendorExtensions.x-rust-http-method-doc}}{{^vendorExtensions.x-rust-http-method-doc}}{{{httpMethod}}}{{/vendorExtensions.x-rust-http-method-doc}}** {{{path}}} | {{{summary}}}
{{/operation}}{{/operations}}
{{#operations}}
diff --git a/modules/openapi-generator/src/main/resources/rust/reqwest/api.mustache b/modules/openapi-generator/src/main/resources/rust/reqwest/api.mustache
index b052f452744a..8ea8216450a9 100644
--- a/modules/openapi-generator/src/main/resources/rust/reqwest/api.mustache
+++ b/modules/openapi-generator/src/main/resources/rust/reqwest/api.mustache
@@ -153,10 +153,27 @@ pub {{#supportAsync}}async {{/supportAsync}}fn {{{operationId}}}(configuration:
{{/allParams}}
{{/vendorExtensions.x-group-parameters}}
- let uri_str = format!("{}{{{path}}}", configuration.base_path{{#pathParams}}, {{{baseName}}}={{#isString}}crate::apis::urlencode({{/isString}}{{{vendorExtensions.x-rust-param-identifier}}}{{^required}}.unwrap(){{/required}}{{#required}}{{#isNullable}}.unwrap(){{/isNullable}}{{/required}}{{#isArray}}.join(",").as_ref(){{/isArray}}{{^isString}}{{^isUuid}}{{^isPrimitiveType}}{{^isContainer}}.to_string(){{/isContainer}}{{/isPrimitiveType}}{{/isUuid}}{{/isString}}{{#isString}}){{/isString}}{{/pathParams}});
- let mut req_builder = configuration.client.request(reqwest::Method::{{{httpMethod}}}, &uri_str);
+ let {{#vendorExtensions.x-rust-has-querystring-param}}mut {{/vendorExtensions.x-rust-has-querystring-param}}uri_str = format!("{}{{{path}}}", configuration.base_path{{#pathParams}}, {{{baseName}}}={{#isString}}crate::apis::urlencode({{/isString}}{{{vendorExtensions.x-rust-param-identifier}}}{{^required}}.unwrap(){{/required}}{{#required}}{{#isNullable}}.unwrap(){{/isNullable}}{{/required}}{{#isArray}}.join(",").as_ref(){{/isArray}}{{^isString}}{{^isUuid}}{{^isPrimitiveType}}{{^isContainer}}.to_string(){{/isContainer}}{{/isPrimitiveType}}{{/isUuid}}{{/isString}}{{#isString}}){{/isString}}{{/pathParams}});
+ {{#allParams}}
+ {{#isQueryStringParam}}
+ {{#required}}
+ // OpenAPI 3.2 `in: querystring`: the parameter value IS the query component
+ // (without the leading `?`), appended verbatim without re-encoding
+ uri_str.push_str(if uri_str.contains('?') { "&" } else { "?" });
+ uri_str.push_str(&{{{vendorExtensions.x-rust-param-identifier}}});
+ {{/required}}
+ {{^required}}
+ if let Some(ref param_value) = {{{vendorExtensions.x-rust-param-identifier}}} {
+ uri_str.push_str(if uri_str.contains('?') { "&" } else { "?" });
+ uri_str.push_str(param_value);
+ }
+ {{/required}}
+ {{/isQueryStringParam}}
+ {{/allParams}}
+ let mut req_builder = configuration.client.request({{#vendorExtensions.x-rust-http-method-literal}}reqwest::Method::from_bytes(b"{{{httpMethod}}}").expect("valid HTTP method"){{/vendorExtensions.x-rust-http-method-literal}}{{^vendorExtensions.x-rust-http-method-literal}}reqwest::Method::{{{httpMethod}}}{{/vendorExtensions.x-rust-http-method-literal}}, &uri_str);
{{#queryParams}}
+ {{^isQueryStringParam}}
{{#required}}
{{#isArray}}
req_builder = match "{{collectionFormat}}" {
@@ -289,6 +306,7 @@ pub {{#supportAsync}}async {{/supportAsync}}fn {{{operationId}}}(configuration:
{{/isArray}}
}
{{/required}}
+ {{/isQueryStringParam}}
{{/queryParams}}
{{#hasAuthMethods}}
{{#authMethods}}
diff --git a/modules/openapi-generator/src/main/resources/typescript-fetch/README.mustache b/modules/openapi-generator/src/main/resources/typescript-fetch/README.mustache
index b73c64591fc0..0d9d1bf2e892 100644
--- a/modules/openapi-generator/src/main/resources/typescript-fetch/README.mustache
+++ b/modules/openapi-generator/src/main/resources/typescript-fetch/README.mustache
@@ -26,7 +26,7 @@ All URIs are relative to *{{basePath}}*
| Class | Method | HTTP request | Description
| ----- | ------ | ------------ | -------------
-{{#apiInfo}}{{#apis}}{{#operations}}{{#operation}}*{{classname}}* | [**{{operationId}}**]({{apiDocPath}}/{{classname}}.md#{{operationIdLowerCase}}) | **{{httpMethod}}** {{path}} | {{summary}}
+{{#apiInfo}}{{#apis}}{{#operations}}{{#operation}}*{{classname}}* | [**{{operationId}}**]({{apiDocPath}}/{{classname}}.md#{{operationIdLowerCase}}) | **{{vendorExtensions.x-ts-http-method-doc}}** {{path}} | {{summary}}
{{/operation}}{{/operations}}{{/apis}}{{/apiInfo}}
### Models
diff --git a/modules/openapi-generator/src/main/resources/typescript-fetch/api_doc.mustache b/modules/openapi-generator/src/main/resources/typescript-fetch/api_doc.mustache
index f5c49271a9fc..6be95ae3425a 100644
--- a/modules/openapi-generator/src/main/resources/typescript-fetch/api_doc.mustache
+++ b/modules/openapi-generator/src/main/resources/typescript-fetch/api_doc.mustache
@@ -6,7 +6,7 @@ All URIs are relative to *{{basePath}}*
| Method | HTTP request | Description |
|------------- | ------------- | -------------|
-{{#operations}}{{#operation}}| [**{{operationId}}**]({{classname}}.md#{{operationIdLowerCase}}) | **{{httpMethod}}** {{commonPath}}{{path}} | {{summary}} |
+{{#operations}}{{#operation}}| [**{{operationId}}**]({{classname}}.md#{{operationIdLowerCase}}) | **{{vendorExtensions.x-ts-http-method-doc}}** {{commonPath}}{{path}} | {{summary}} |
{{/operation}}{{/operations}}
{{#operations}}
diff --git a/modules/openapi-generator/src/main/resources/typescript-fetch/apis.mustache b/modules/openapi-generator/src/main/resources/typescript-fetch/apis.mustache
index 8d939f5e29db..842d559154f3 100644
--- a/modules/openapi-generator/src/main/resources/typescript-fetch/apis.mustache
+++ b/modules/openapi-generator/src/main/resources/typescript-fetch/apis.mustache
@@ -188,6 +188,7 @@ export class {{classname}} extends runtime.BaseAPI {
const queryParameters: any = {};
{{#queryParams}}
+ {{^isQueryStringParam}}
{{#isArray}}
if (requestParameters['{{paramName}}'] != null) {
{{#isCollectionFormatMulti}}
@@ -226,6 +227,7 @@ export class {{classname}} extends runtime.BaseAPI {
}
{{/isArray}}
+ {{/isQueryStringParam}}
{{/queryParams}}
const headerParameters: runtime.HTTPHeaders = {};
@@ -347,6 +349,19 @@ export class {{classname}} extends runtime.BaseAPI {
{{/isDateType}}
{{/isDateTimeType}}
{{/pathParams}}
+ {{#vendorExtensions.x-ts-has-querystring-param}}
+ {{#queryParams}}
+ {{#isQueryStringParam}}
+ // OpenAPI 3.2 in: querystring - the value is the whole, already-encoded
+ // query string; append it verbatim instead of a name=value pair
+ // ('?' is only searched before any '#': a '?' inside the fragment is
+ // fragment content, not an existing query string)
+ if (requestParameters['{{paramName}}'] != null && requestParameters['{{paramName}}'] !== '') {
+ urlPath += (urlPath.split('#')[0].includes('?') ? '&' : '?') + requestParameters['{{paramName}}'];
+ }
+ {{/isQueryStringParam}}
+ {{/queryParams}}
+ {{/vendorExtensions.x-ts-has-querystring-param}}
{{#hasContentTypeRequestVariants}}
{{! `contentType` selects the body's type, so narrow on it before serialising }}
@@ -371,7 +386,12 @@ export class {{classname}} extends runtime.BaseAPI {
return {
path: urlPath,
+ {{#vendorExtensions.x-ts-http-method-literal}}
+ method: "{{{httpMethod}}}",{{! OpenAPI 3.2: non-standard method sent verbatim, unescaped }}
+ {{/vendorExtensions.x-ts-http-method-literal}}
+ {{^vendorExtensions.x-ts-http-method-literal}}
method: '{{httpMethod}}',
+ {{/vendorExtensions.x-ts-http-method-literal}}
headers: headerParameters,
query: queryParameters,
body: body,
@@ -381,7 +401,12 @@ export class {{classname}} extends runtime.BaseAPI {
{{^hasContentTypeRequestVariants}}
return {
path: urlPath,
+ {{#vendorExtensions.x-ts-http-method-literal}}
+ method: "{{{httpMethod}}}",
+ {{/vendorExtensions.x-ts-http-method-literal}}
+ {{^vendorExtensions.x-ts-http-method-literal}}
method: '{{httpMethod}}',
+ {{/vendorExtensions.x-ts-http-method-literal}}
headers: headerParameters,
query: queryParameters,
{{#hasBodyParam}}
diff --git a/modules/openapi-generator/src/main/resources/typescript-fetch/runtime.mustache b/modules/openapi-generator/src/main/resources/typescript-fetch/runtime.mustache
index 58ca2968a407..e62da8bcabbe 100644
--- a/modules/openapi-generator/src/main/resources/typescript-fetch/runtime.mustache
+++ b/modules/openapi-generator/src/main/resources/typescript-fetch/runtime.mustache
@@ -135,7 +135,9 @@ export class BaseAPI {
// only add the querystring to the URL if there are query parameters.
// this is done to avoid urls ending with a "?" character which buggy webservers
// do not handle correctly sometimes.
- url += '?' + this.configuration.queryParamsStringify(context.query);
+ // the '?' check must only consider the part before the fragment: a '?' inside
+ // '#...' is fragment content, not an existing query string.
+ url += (url.split('#')[0].includes('?') ? '&' : '?') + this.configuration.queryParamsStringify(context.query);
}
const headers = Object.assign({}, this.configuration.headers, context.headers);
@@ -310,7 +312,9 @@ export type ExclusiveUnion = U extends unknown
{{/splitOperationsByContentType}}
export type Json = any;
-export type HTTPMethod = 'GET' | 'POST' | 'PUT' | 'PATCH' | 'DELETE' | 'OPTIONS' | 'HEAD';
+// (string & {}) keeps editor autocomplete for the standard methods while
+// admitting arbitrary OpenAPI 3.2 method names (query, additionalOperations)
+export type HTTPMethod = 'GET' | 'POST' | 'PUT' | 'PATCH' | 'DELETE' | 'OPTIONS' | 'HEAD' | (string & {});
export type HTTPHeaders = { [key: string]: string };
export type HTTPQuery = { [key: string]: string | number | null | boolean | Array | Set | HTTPQuery };
export type HTTPBody = Json | FormData | URLSearchParams;
diff --git a/modules/openapi-generator/src/test/java/org/openapitools/codegen/DefaultCodegenTest.java b/modules/openapi-generator/src/test/java/org/openapitools/codegen/DefaultCodegenTest.java
index 7ce9bfcc1b7d..7784d7d08b39 100644
--- a/modules/openapi-generator/src/test/java/org/openapitools/codegen/DefaultCodegenTest.java
+++ b/modules/openapi-generator/src/test/java/org/openapitools/codegen/DefaultCodegenTest.java
@@ -29,6 +29,7 @@
import io.swagger.v3.oas.models.OpenAPI;
import io.swagger.v3.oas.models.Operation;
import io.swagger.v3.oas.models.PathItem;
+import io.swagger.v3.oas.models.callbacks.Callback;
import io.swagger.v3.oas.models.headers.Header;
import io.swagger.v3.oas.models.media.*;
import io.swagger.v3.oas.models.parameters.QueryParameter;
@@ -388,6 +389,232 @@ public void testOAS31ContentMediaTypeBinaryFormParameter() {
assertFalse(image.isFile);
}
+ @Test
+ public void testAllOfFormRequiredMatchesByBaseName() {
+ // required matching must use the schema property name (baseName), not the
+ // normalized paramName: `user_id` -> `userId` used to silently drop the flag
+ final OpenAPI openAPI = TestUtils.parseSpec("src/test/resources/3_0/form-allof-required.yaml");
+
+ final DefaultCodegen codegen = new DefaultCodegen();
+ codegen.setOpenAPI(openAPI);
+
+ RequestBody requestBody = openAPI.getPaths().get("/register").getPost().getRequestBody();
+ List formParams = codegen.fromRequestBodyToFormParameters(requestBody, new HashSet<>());
+ Map paramsByBaseName = formParams.stream()
+ .collect(Collectors.toMap(param -> param.baseName, param -> param));
+
+ assertTrue(paramsByBaseName.get("query").required,
+ "allOf member required must apply");
+ assertTrue(paramsByBaseName.get("user_id").required,
+ "required must match the schema name even when paramName is normalized to userId");
+ assertFalse(paramsByBaseName.get("nickname").required,
+ "non-required allOf member stays optional");
+ }
+
+ @Test
+ public void testTopLevelRequiredDoesNotMaskAllOfMemberRequired() {
+ // sibling bug: a top-level `required` used to bypass the allOf-member
+ // required lists entirely
+ final OpenAPI openAPI = TestUtils.parseSpec("src/test/resources/3_0/form-allof-required.yaml");
+
+ final DefaultCodegen codegen = new DefaultCodegen();
+ codegen.setOpenAPI(openAPI);
+
+ RequestBody requestBody = openAPI.getPaths().get("/register-top").getPost().getRequestBody();
+ List formParams = codegen.fromRequestBodyToFormParameters(requestBody, new HashSet<>());
+ Map paramsByBaseName = formParams.stream()
+ .collect(Collectors.toMap(param -> param.baseName, param -> param));
+
+ assertTrue(paramsByBaseName.get("top").required,
+ "top-level required must apply");
+ assertTrue(paramsByBaseName.get("member_req").required,
+ "allOf member required must apply alongside top-level required");
+ assertFalse(paramsByBaseName.get("member_opt").required,
+ "non-required member stays optional");
+ }
+
+ @Test
+ public void testOneOfBranchesDoNotForceFormRequired() {
+ // a schema carrying both `properties`/`required` and oneOf branches is not
+ // caught by ModelUtils.isOneOf (which requires empty properties); without
+ // care the allOf-style required union would also force branch-only fields
+ final OpenAPI openAPI = TestUtils.parseSpec("src/test/resources/3_0/form-allof-required.yaml");
+
+ final DefaultCodegen codegen = new DefaultCodegen();
+ codegen.setOpenAPI(openAPI);
+
+ RequestBody requestBody = openAPI.getPaths().get("/register-mixed").getPost().getRequestBody();
+ List formParams = codegen.fromRequestBodyToFormParameters(requestBody, new HashSet<>());
+ Map paramsByBaseName = formParams.stream()
+ .collect(Collectors.toMap(param -> param.baseName, param -> param));
+
+ assertTrue(paramsByBaseName.get("common").required,
+ "top-level required must apply");
+ assertFalse(paramsByBaseName.get("a").required,
+ "oneOf branch required must not force the form parameter");
+ assertFalse(paramsByBaseName.get("b").required,
+ "oneOf branch required must not force the form parameter");
+ }
+
+ @Test
+ public void testSingleAllOfWrapperOwnRequired() {
+ // the single-allOf unwrapping keeps the wrapper schema in `original`; its
+ // own `required` list must still apply to member properties
+ final OpenAPI openAPI = TestUtils.parseSpec("src/test/resources/3_0/form-allof-required.yaml");
+
+ final DefaultCodegen codegen = new DefaultCodegen();
+ codegen.setOpenAPI(openAPI);
+
+ RequestBody requestBody = openAPI.getPaths().get("/register-wrapper").getPost().getRequestBody();
+ List formParams = codegen.fromRequestBodyToFormParameters(requestBody, new HashSet<>());
+ Map paramsByBaseName = formParams.stream()
+ .collect(Collectors.toMap(param -> param.baseName, param -> param));
+
+ assertTrue(paramsByBaseName.get("wrapper_req").required,
+ "allOf wrapper's own required must apply");
+ assertFalse(paramsByBaseName.get("wrapper_opt").required,
+ "non-required member stays optional");
+ }
+
+ @Test
+ public void testAllOfRefChainCollectsRequiredFromEveryLevel() {
+ // a 3-level allOf chain through $ref (ChainA -> ChainB -> ChainC): the
+ // required traversal must follow every hop, not just the first
+ final OpenAPI openAPI = TestUtils.parseSpec("src/test/resources/3_0/form-allof-required.yaml");
+
+ final DefaultCodegen codegen = new DefaultCodegen();
+ codegen.setOpenAPI(openAPI);
+
+ RequestBody requestBody = openAPI.getPaths().get("/register-chain").getPost().getRequestBody();
+ List formParams = codegen.fromRequestBodyToFormParameters(requestBody, new HashSet<>());
+ Map paramsByBaseName = formParams.stream()
+ .collect(Collectors.toMap(param -> param.baseName, param -> param));
+
+ assertTrue(paramsByBaseName.get("chain_a").required,
+ "required from the first $ref target must apply");
+ assertTrue(paramsByBaseName.get("chain_b").required,
+ "required from the second $ref hop must apply");
+ assertTrue(paramsByBaseName.get("chain_c").required,
+ "required from the third $ref hop must apply");
+ assertFalse(paramsByBaseName.get("chain_a_opt").required,
+ "non-required property stays optional");
+ assertFalse(paramsByBaseName.get("chain_c_opt").required,
+ "non-required property stays optional");
+ }
+
+ @Test
+ public void testOneOfInsideAllOfMemberDoesNotForceRequired() {
+ // an allOf member that itself mixes `properties`/`required` with oneOf
+ // branches: the member's own required applies, but required entries
+ // inside the alternative branches must not force the form parameters
+ final OpenAPI openAPI = TestUtils.parseSpec("src/test/resources/3_0/form-allof-required.yaml");
+
+ final DefaultCodegen codegen = new DefaultCodegen();
+ codegen.setOpenAPI(openAPI);
+
+ RequestBody requestBody = openAPI.getPaths().get("/register-member-oneof").getPost().getRequestBody();
+ List formParams = codegen.fromRequestBodyToFormParameters(requestBody, new HashSet<>());
+ Map paramsByBaseName = formParams.stream()
+ .collect(Collectors.toMap(param -> param.baseName, param -> param));
+
+ assertTrue(paramsByBaseName.get("c").required,
+ "the allOf member's own required must apply");
+ assertFalse(paramsByBaseName.get("x").required,
+ "oneOf branch required must not force the form parameter");
+ assertFalse(paramsByBaseName.get("y").required,
+ "oneOf branch required must not force the form parameter");
+ }
+
+ @Test
+ public void testCircularAllOfRefsDoNotStackOverflow() {
+ // CycA allOf-> CycB and CycB allOf-> CycA form a $ref cycle; both the
+ // property collection and the required traversal must terminate via
+ // the visited-schema guard instead of recursing forever
+ final OpenAPI openAPI = TestUtils.parseSpec("src/test/resources/3_0/form-allof-required.yaml");
+
+ final DefaultCodegen codegen = new DefaultCodegen();
+ codegen.setOpenAPI(openAPI);
+
+ RequestBody requestBody = openAPI.getPaths().get("/register-cyclic").getPost().getRequestBody();
+ List formParams = codegen.fromRequestBodyToFormParameters(requestBody, new HashSet<>());
+ Map paramsByBaseName = formParams.stream()
+ .collect(Collectors.toMap(param -> param.baseName, param -> param));
+
+ assertTrue(paramsByBaseName.get("ca").required,
+ "required collected before the cycle closes must apply");
+ assertTrue(paramsByBaseName.get("cb").required,
+ "required collected before the cycle closes must apply");
+ }
+
+ @Test
+ public void testAllOfRefChainRequiredMatchesSnakeCaseBaseName() {
+ // snake_case names are normalized to camelCase paramNames (user_id ->
+ // userId); required matching must stay on the baseName even when the
+ // allOf member is reached through a $ref
+ final OpenAPI openAPI = TestUtils.parseSpec("src/test/resources/3_0/form-allof-required.yaml");
+
+ final DefaultCodegen codegen = new DefaultCodegen();
+ codegen.setOpenAPI(openAPI);
+
+ RequestBody requestBody = openAPI.getPaths().get("/register-ref-snake").getPost().getRequestBody();
+ List formParams = codegen.fromRequestBodyToFormParameters(requestBody, new HashSet<>());
+ Map paramsByBaseName = formParams.stream()
+ .collect(Collectors.toMap(param -> param.baseName, param -> param));
+
+ assertTrue(paramsByBaseName.get("user_id").required,
+ "required must match the schema name even when paramName is normalized to userId");
+ assertTrue(paramsByBaseName.get("plain_req").required,
+ "required must match the schema name even when paramName is normalized to plainReq");
+ assertFalse(paramsByBaseName.get("snake_opt").required,
+ "non-required property stays optional");
+ }
+
+ @Test
+ public void testComposedPropertyRequiredDoesNotLeak() {
+ // a property that is itself a composed schema with an inner `required`
+ // must not leak that entry into the enclosing form's required set
+ final OpenAPI openAPI = TestUtils.parseSpec("src/test/resources/3_0/form-allof-required.yaml");
+
+ final DefaultCodegen codegen = new DefaultCodegen();
+ codegen.setOpenAPI(openAPI);
+
+ RequestBody requestBody = openAPI.getPaths().get("/register-nested-composed").getPost().getRequestBody();
+ List formParams = codegen.fromRequestBodyToFormParameters(requestBody, new HashSet<>());
+ Map paramsByBaseName = formParams.stream()
+ .collect(Collectors.toMap(param -> param.baseName, param -> param));
+
+ assertTrue(paramsByBaseName.get("outer_req").required,
+ "top-level required must apply");
+ assertFalse(paramsByBaseName.get("nested").required,
+ "the composed property itself is not in the required list");
+ // tripwire: the optional top-level `z` would wrongly turn required if
+ // the inner `required: [z]` leaked into the enclosing required set
+ assertFalse(paramsByBaseName.get("z").required,
+ "required inside a property's own composed schema must not leak outward");
+ }
+
+ @Test
+ public void testAnyOfBranchesDoNotForceFormRequired() {
+ // same mixed shape as testOneOfBranchesDoNotForceFormRequired but with
+ // anyOf: required entries inside the alternatives must stay optional
+ final OpenAPI openAPI = TestUtils.parseSpec("src/test/resources/3_0/form-allof-required.yaml");
+
+ final DefaultCodegen codegen = new DefaultCodegen();
+ codegen.setOpenAPI(openAPI);
+
+ RequestBody requestBody = openAPI.getPaths().get("/register-anyof-mixed").getPost().getRequestBody();
+ List formParams = codegen.fromRequestBodyToFormParameters(requestBody, new HashSet<>());
+ Map paramsByBaseName = formParams.stream()
+ .collect(Collectors.toMap(param -> param.baseName, param -> param));
+
+ assertTrue(paramsByBaseName.get("common").required,
+ "top-level required must apply");
+ assertFalse(paramsByBaseName.get("p").required,
+ "anyOf branch required must not force the form parameter");
+ assertFalse(paramsByBaseName.get("q").required,
+ "anyOf branch required must not force the form parameter");
+ }
+
@Test
public void testOriginalOpenApiDocumentVersion() {
// Test with OAS 2.0 document.
@@ -2140,6 +2367,32 @@ public void testCallbacks() {
});
}
+ @Test
+ public void testCallbackAdditionalOperationMethodCasingPreserved() {
+ // OpenAPI 3.2: additionalOperations keys are HTTP method names that must be
+ // sent verbatim - "customMethod" must not be normalized to CUSTOMMETHOD
+ DefaultCodegen codegen = new DefaultCodegen() {
+ @Override
+ public boolean supportsAdditionalOperations() {
+ return true;
+ }
+ };
+ codegen.setOpenAPI(TestUtils.createOpenAPI());
+
+ PathItem callbackPath = new PathItem();
+ callbackPath.addAdditionalOperation("customMethod",
+ new Operation().operationId("customCallback")
+ .responses(new ApiResponses()
+ .addApiResponse("200", new ApiResponse().description("ok"))));
+ Callback callback = new Callback();
+ callback.addPathItem("{$request.body#/callbackUrl}", callbackPath);
+
+ CodegenCallback cb = codegen.fromCallback("onEvent", callback, null);
+ Assert.assertEquals(cb.urls.size(), 1);
+ Assert.assertEquals(cb.urls.get(0).requests.size(), 1);
+ Assert.assertEquals(cb.urls.get(0).requests.get(0).httpMethod, "customMethod");
+ }
+
@Test
public void testLeadingSlashIsAddedIfMissing() {
OpenAPI openAPI = TestUtils.createOpenAPI();
@@ -2556,6 +2809,44 @@ private ModelsMap codegenModelWithXEnumVarNameAsMap() {
return TestUtils.createCodegenModelWrapper(cm);
}
+ @Test
+ public void queryStringParameterSetsFlagAndLandsInQueryParams() {
+ final OpenAPI openAPI = TestUtils.parseFlattenSpec("src/test/resources/3_2/query-operation.yaml");
+ final DefaultCodegen codegen = new DefaultCodegen();
+ codegen.setOpenAPI(openAPI);
+
+ Operation queryOp = openAPI.getPaths().get("/pets").getQuery();
+ assertNotNull(queryOp, "3.2 query operation should be bound by the parser");
+ CodegenOperation co = codegen.fromOperation("/pets", "query", queryOp, null);
+
+ assertEquals(co.queryParams.size(), 1);
+ CodegenParameter p = co.queryParams.get(0);
+ assertTrue(p.isQueryStringParam, "in: querystring parameter must set isQueryStringParam");
+ assertFalse(p.isQueryParam);
+ assertEquals(co.allParams.size(), 1);
+ }
+
+ @Test
+ public void queryStringParameterWithObjectContentBecomesString() {
+ // `in: querystring` describes the whole query string via `content`.
+ // Even when the content schema is an object/model, the codegen parameter
+ // must be a plain String (caller supplies the encoded query string), so
+ // no typed model is pulled into the operation signature.
+ final OpenAPI openAPI = TestUtils.parseFlattenSpec("src/test/resources/3_2/querystring-object.yaml");
+ final DefaultCodegen codegen = new DefaultCodegen();
+ codegen.setOpenAPI(openAPI);
+
+ Operation getOp = openAPI.getPaths().get("/pets").getGet();
+ CodegenOperation co = codegen.fromOperation("/pets", "get", getOp, null);
+
+ assertEquals(co.queryParams.size(), 1);
+ CodegenParameter p = co.queryParams.get(0);
+ assertTrue(p.isQueryStringParam);
+ assertEquals(p.dataType, "String");
+ assertFalse(p.isModel);
+ assertFalse(p.isMap);
+ }
+
@Test
public void objectQueryParamIdentifyAsObject() {
final OpenAPI openAPI = TestUtils.parseFlattenSpec("src/test/resources/3_0/objectQueryParam.yaml");
diff --git a/modules/openapi-generator/src/test/java/org/openapitools/codegen/DefaultGeneratorTest.java b/modules/openapi-generator/src/test/java/org/openapitools/codegen/DefaultGeneratorTest.java
index 5da01b91254f..9f38eb4b9e80 100644
--- a/modules/openapi-generator/src/test/java/org/openapitools/codegen/DefaultGeneratorTest.java
+++ b/modules/openapi-generator/src/test/java/org/openapitools/codegen/DefaultGeneratorTest.java
@@ -448,6 +448,102 @@ public void testProcessPaths() throws Exception {
Assert.assertEquals(defaultList.get(3).allParams.size(), 1);
}
+ @Test
+ public void testProcessPathsQueryAndAdditionalOperations() throws Exception {
+ OpenAPI openAPI = TestUtils.createOpenAPI();
+ openAPI.setPaths(new Paths());
+ Operation queryOp = new Operation().operationId("queryPets")
+ .responses(new ApiResponses().addApiResponse("200", new ApiResponse().description("OK")));
+ Operation purgeOp = new Operation().operationId("purgePets")
+ .responses(new ApiResponses().addApiResponse("204", new ApiResponse().description("done")));
+ Operation customOp = new Operation().operationId("customPets")
+ .responses(new ApiResponses().addApiResponse("204", new ApiResponse().description("done")));
+ openAPI.getPaths().addPathItem("/pets",
+ new PathItem().query(queryOp)
+ .addAdditionalOperation("PURGE", purgeOp)
+ .addAdditionalOperation("customMethod", customOp));
+
+ ClientOptInput opts = new ClientOptInput();
+ opts.openAPI(openAPI);
+ opts.config(new DefaultCodegen() {
+ @Override
+ public boolean supportsAdditionalOperations() {
+ return true;
+ }
+ });
+
+ DefaultGenerator generator = new DefaultGenerator();
+ generator.opts(opts);
+ Map> result = generator.processPaths(openAPI.getPaths());
+ Assert.assertEquals(result.size(), 1);
+ List defaultList = result.get("Default");
+ Assert.assertEquals(defaultList.size(), 3);
+ Assert.assertEquals(defaultList.get(0).operationId, "queryPets");
+ Assert.assertEquals(defaultList.get(0).httpMethod, "QUERY");
+ Assert.assertEquals(defaultList.get(1).operationId, "purgePets");
+ Assert.assertEquals(defaultList.get(1).httpMethod, "PURGE");
+ // additionalOperations keys are HTTP method names and must be sent verbatim
+ Assert.assertEquals(defaultList.get(2).operationId, "customPets");
+ Assert.assertEquals(defaultList.get(2).httpMethod, "customMethod");
+ }
+
+ @Test
+ public void testProcessPathsSkips32OperationsWhenUnsupported() throws Exception {
+ // generators that cannot emit arbitrary HTTP methods must skip
+ // query/additionalOperations instead of producing uncompilable code
+ OpenAPI openAPI = TestUtils.createOpenAPI();
+ openAPI.setPaths(new Paths());
+ Operation queryOp = new Operation().operationId("queryPets")
+ .responses(new ApiResponses().addApiResponse("200", new ApiResponse().description("OK")));
+ Operation purgeOp = new Operation().operationId("purgePets")
+ .responses(new ApiResponses().addApiResponse("204", new ApiResponse().description("done")));
+ Operation getOp = new Operation().operationId("getPets")
+ .responses(new ApiResponses().addApiResponse("200", new ApiResponse().description("OK")));
+ openAPI.getPaths().addPathItem("/pets",
+ new PathItem().get(getOp).query(queryOp).addAdditionalOperation("PURGE", purgeOp));
+
+ ClientOptInput opts = new ClientOptInput();
+ opts.openAPI(openAPI);
+ opts.config(new DefaultCodegen()); // default: supportsAdditionalOperations() == false
+
+ DefaultGenerator generator = new DefaultGenerator();
+ generator.opts(opts);
+ Map> result = generator.processPaths(openAPI.getPaths());
+ List defaultList = result.get("Default");
+ Assert.assertEquals(defaultList.size(), 1);
+ Assert.assertEquals(defaultList.get(0).operationId, "getPets");
+ }
+
+ @Test
+ public void testProcessWebhooksQueryAndAdditionalOperations() throws Exception {
+ OpenAPI openAPI = TestUtils.createOpenAPI();
+ Operation queryOp = new Operation().operationId("queryHook")
+ .responses(new ApiResponses().addApiResponse("200", new ApiResponse().description("OK")));
+ Operation customOp = new Operation().operationId("customHook")
+ .responses(new ApiResponses().addApiResponse("204", new ApiResponse().description("done")));
+ PathItem webhook = new PathItem().query(queryOp).addAdditionalOperation("RETRY", customOp);
+
+ ClientOptInput opts = new ClientOptInput();
+ opts.openAPI(openAPI);
+ opts.config(new DefaultCodegen() {
+ @Override
+ public boolean supportsAdditionalOperations() {
+ return true;
+ }
+ });
+
+ DefaultGenerator generator = new DefaultGenerator();
+ generator.opts(opts);
+ Map> result =
+ generator.processWebhooks(Collections.singletonMap("hook", webhook));
+ List ops = result.get("Default");
+ Assert.assertEquals(ops.size(), 2);
+ Assert.assertEquals(ops.get(0).operationId, "queryHook");
+ Assert.assertEquals(ops.get(0).httpMethod, "QUERY");
+ Assert.assertEquals(ops.get(1).operationId, "customHook");
+ Assert.assertEquals(ops.get(1).httpMethod, "RETRY");
+ }
+
@Test
public void testRefModelValidationProperties() {
OpenAPI openAPI = TestUtils.parseFlattenSpec("src/test/resources/2_0/refAliasedPrimitiveWithValidation.yml");
diff --git a/modules/openapi-generator/src/test/java/org/openapitools/codegen/InlineModelResolverTest.java b/modules/openapi-generator/src/test/java/org/openapitools/codegen/InlineModelResolverTest.java
index b6b81d765a03..35dc94abc2ed 100644
--- a/modules/openapi-generator/src/test/java/org/openapitools/codegen/InlineModelResolverTest.java
+++ b/modules/openapi-generator/src/test/java/org/openapitools/codegen/InlineModelResolverTest.java
@@ -23,6 +23,7 @@
import io.swagger.v3.oas.models.Operation;
import io.swagger.v3.oas.models.PathItem;
import io.swagger.v3.oas.models.Paths;
+import io.swagger.v3.oas.models.callbacks.Callback;
import io.swagger.v3.oas.models.headers.Header;
import io.swagger.v3.oas.models.media.*;
import io.swagger.v3.oas.models.parameters.Parameter;
@@ -1240,6 +1241,122 @@ public void callbacks() {
assertTrue(properties.get("data") instanceof StringSchema);
}
+ @Test
+ public void callbacksInsideAdditionalOperations() {
+ // OpenAPI 3.2: callbacks attached to an additionalOperations entry must be
+ // discovered for inline model flattening just like fixed-method operations
+ Operation callbackPost = new Operation()
+ .operationId("hookReceive")
+ .requestBody(new RequestBody().content(new Content()
+ .addMediaType("application/json", new MediaType()
+ .schema(new ObjectSchema()
+ .addProperties("id", new StringSchema())))))
+ .responses(new ApiResponses().addApiResponse("200",
+ new ApiResponse().description("ok")));
+ // an additionalOperations entry *inside* a callback PathItem must be flattened too
+ Operation callbackRetry = new Operation()
+ .operationId("hookRetry")
+ .requestBody(new RequestBody().content(new Content()
+ .addMediaType("application/json", new MediaType()
+ .schema(new ObjectSchema()
+ .addProperties("retryId", new StringSchema())))))
+ .responses(new ApiResponses().addApiResponse("200",
+ new ApiResponse().description("ok")));
+ Callback hook = new Callback().addPathItem("{$request.body#/url}",
+ new PathItem().post(callbackPost)
+ .addAdditionalOperation("RETRY", callbackRetry));
+ Operation notifyOp = new Operation()
+ .operationId("notifyOp")
+ .responses(new ApiResponses().addApiResponse("200",
+ new ApiResponse().description("ok")))
+ .addCallback("hook", hook);
+ OpenAPI openAPI = new OpenAPI()
+ .paths(new Paths().addPathItem("/pets", new PathItem()
+ .addAdditionalOperation("NOTIFY", notifyOp)));
+
+ new InlineModelResolver().flatten(openAPI);
+
+ RequestBody callbackRequestBody = openAPI.getPaths().get("/pets")
+ .getAdditionalOperations().get("NOTIFY")
+ .getCallbacks().get("hook")
+ .get("{$request.body#/url}")
+ .getPost().getRequestBody();
+ Schema> schema = callbackRequestBody.getContent().get("application/json").getSchema();
+ assertNotNull(schema.get$ref());
+ assertTrue("inline callback schema in an additionalOperations entry must be extracted",
+ schema.get$ref().startsWith("#/components/schemas/"));
+ Schema> resolved = openAPI.getComponents().getSchemas()
+ .get(ModelUtils.getSimpleRef(schema.get$ref()));
+ assertNotNull(resolved);
+ assertTrue(resolved.getProperties().get("id") instanceof StringSchema);
+
+ Schema> retrySchema = openAPI.getPaths().get("/pets")
+ .getAdditionalOperations().get("NOTIFY")
+ .getCallbacks().get("hook")
+ .get("{$request.body#/url}")
+ .getAdditionalOperations().get("RETRY")
+ .getRequestBody().getContent().get("application/json").getSchema();
+ assertNotNull(retrySchema.get$ref());
+ assertTrue("inline schema of a callback's additionalOperations entry must be extracted",
+ retrySchema.get$ref().startsWith("#/components/schemas/"));
+ Schema> resolvedRetry = openAPI.getComponents().getSchemas()
+ .get(ModelUtils.getSimpleRef(retrySchema.get$ref()));
+ assertNotNull(resolvedRetry);
+ assertTrue(resolvedRetry.getProperties().get("retryId") instanceof StringSchema);
+ }
+
+ @Test
+ public void callbacksOn32OperationsSkippedWhenUnsupported() {
+ // OpenAPI 3.2: when the target generator does not support
+ // query/additionalOperations those operations are dropped, so callbacks
+ // attached to them must not have their inline schemas flattened either
+ Operation queryOp = new Operation()
+ .operationId("queryPets")
+ .responses(new ApiResponses().addApiResponse("200",
+ new ApiResponse().description("ok")))
+ .addCallback("hook", callbackWithInlineBody("queryHookReceive"));
+ Operation notifyOp = new Operation()
+ .operationId("notifyPets")
+ .responses(new ApiResponses().addApiResponse("200",
+ new ApiResponse().description("ok")))
+ .addCallback("hook", callbackWithInlineBody("notifyHookReceive"));
+ // a fixed-method operation's callback must still be discovered
+ Operation getOp = new Operation()
+ .operationId("getPets")
+ .responses(new ApiResponses().addApiResponse("200",
+ new ApiResponse().description("ok")))
+ .addCallback("hook", callbackWithInlineBody("getHookReceive"));
+ OpenAPI openAPI = new OpenAPI()
+ .paths(new Paths().addPathItem("/pets", new PathItem()
+ .get(getOp)
+ .query(queryOp)
+ .addAdditionalOperation("NOTIFY", notifyOp)));
+
+ InlineModelResolver resolver = new InlineModelResolver();
+ resolver.setCodegen(new DefaultCodegen()); // default: supportsAdditionalOperations() == false
+ resolver.flatten(openAPI);
+
+ // the QUERY/additionalOperations callbacks keep their inline schema
+ assertNull(openAPI.getPaths().get("/pets").getQuery()
+ .getCallbacks().get("hook").get("{$request.body#/url}")
+ .getPost().getRequestBody().getContent().get("application/json")
+ .getSchema().get$ref());
+ assertNull(openAPI.getPaths().get("/pets").getAdditionalOperations().get("NOTIFY")
+ .getCallbacks().get("hook").get("{$request.body#/url}")
+ .getPost().getRequestBody().getContent().get("application/json")
+ .getSchema().get$ref());
+ // the GET callback's inline schema is still extracted to components
+ Schema> getCallbackSchema = openAPI.getPaths().get("/pets").getGet()
+ .getCallbacks().get("hook").get("{$request.body#/url}")
+ .getPost().getRequestBody().getContent().get("application/json").getSchema();
+ assertNotNull(getCallbackSchema.get$ref());
+ assertTrue(getCallbackSchema.get$ref().startsWith("#/components/schemas/"));
+ Schema> resolved = openAPI.getComponents().getSchemas()
+ .get(ModelUtils.getSimpleRef(getCallbackSchema.get$ref()));
+ assertNotNull(resolved);
+ assertTrue(resolved.getProperties().get("id") instanceof StringSchema);
+ }
+
@Test
public void testInlineSchemaNameMapping() {
OpenAPI openAPI = TestUtils.parseSpec("src/test/resources/3_0/inline_model_resolver.yaml");
@@ -1636,6 +1753,19 @@ private static Schema refToDuplicate() {
return new Schema<>().$ref("#/components/schemas/Duplicate");
}
+ /** A callback whose single POST operation has an inline request-body schema. */
+ private static Callback callbackWithInlineBody(String operationId) {
+ return new Callback().addPathItem("{$request.body#/url}",
+ new PathItem().post(new Operation()
+ .operationId(operationId)
+ .requestBody(new RequestBody().content(new Content()
+ .addMediaType("application/json", new MediaType()
+ .schema(new ObjectSchema()
+ .addProperties("id", new StringSchema())))))
+ .responses(new ApiResponses().addApiResponse("200",
+ new ApiResponse().description("ok")))));
+ }
+
private static void assertRewritten(String carrier, Schema schema) {
assertNotNull("no schema found for carrier " + carrier, schema);
assertEquals("$ref in " + carrier + " must be rewritten to the canonical schema",
diff --git a/modules/openapi-generator/src/test/java/org/openapitools/codegen/OpenAPINormalizerTest.java b/modules/openapi-generator/src/test/java/org/openapitools/codegen/OpenAPINormalizerTest.java
index c213586af2b9..a4399b3e20bd 100644
--- a/modules/openapi-generator/src/test/java/org/openapitools/codegen/OpenAPINormalizerTest.java
+++ b/modules/openapi-generator/src/test/java/org/openapitools/codegen/OpenAPINormalizerTest.java
@@ -874,6 +874,71 @@ public void testFilterWithMethod() {
assertEquals(openAPI.getPaths().get("/person/display/{personId}").getPut().getExtensions().get(X_INTERNAL), true);
}
+ @Test
+ public void testFilterWithMethodCoversQueryAndAdditionalOperations() {
+ // OpenAPI 3.2: `query` and arbitrary `additionalOperations` entries must be
+ // covered by `method:` filtering like the fixed methods
+ OpenAPI openAPI = TestUtils.createOpenAPI();
+ PathItem pathItem = new PathItem()
+ .get(new Operation().operationId("getPets"))
+ .query(new Operation().operationId("queryPets"))
+ .addAdditionalOperation("PURGE", new Operation().operationId("purgePets"));
+ openAPI.path("/pets", pathItem);
+
+ Map options = Map.of("FILTER", "method:get");
+ new OpenAPINormalizer(openAPI, options).normalize();
+
+ assertEquals(pathItem.getGet().getExtensions().get(X_INTERNAL), false);
+ assertEquals(pathItem.getQuery().getExtensions().get(X_INTERNAL), true);
+ assertEquals(pathItem.getAdditionalOperations().get("PURGE").getExtensions().get(X_INTERNAL), true);
+
+ // filtering by the additional method name must keep that operation
+ OpenAPI openAPI2 = TestUtils.createOpenAPI();
+ PathItem pathItem2 = new PathItem()
+ .get(new Operation().operationId("getPets"))
+ .query(new Operation().operationId("queryPets"))
+ .addAdditionalOperation("PURGE", new Operation().operationId("purgePets"));
+ openAPI2.path("/pets", pathItem2);
+
+ Map options2 = Map.of("FILTER", "method:query|purge");
+ new OpenAPINormalizer(openAPI2, options2).normalize();
+
+ assertEquals(pathItem2.getGet().getExtensions().get(X_INTERNAL), true);
+ assertEquals(pathItem2.getQuery().getExtensions().get(X_INTERNAL), false);
+ assertEquals(pathItem2.getAdditionalOperations().get("PURGE").getExtensions().get(X_INTERNAL), false);
+ }
+
+ @Test
+ public void testFilterWithCaseCollidingAdditionalOperation() {
+ // OpenAPI 3.2: an additionalOperations key differing from a fixed method only by
+ // case (e.g. "GET") must not hide the fixed method's operation from FILTER:
+ // both operations have to be evaluated and marked with x-internal
+ OpenAPI openAPI = TestUtils.createOpenAPI();
+ PathItem pathItem = new PathItem()
+ .get(new Operation().operationId("fixedGet"))
+ .addAdditionalOperation("GET", new Operation().operationId("additionalGet"));
+ openAPI.path("/pets", pathItem);
+
+ Map options = Map.of("FILTER", "operationId:additionalGet");
+ new OpenAPINormalizer(openAPI, options).normalize();
+
+ assertEquals(pathItem.getGet().getExtensions().get(X_INTERNAL), true);
+ assertEquals(pathItem.getAdditionalOperations().get("GET").getExtensions().get(X_INTERNAL), false);
+
+ // `method:` filtering matches both operations by the lowercased method name
+ OpenAPI openAPI2 = TestUtils.createOpenAPI();
+ PathItem pathItem2 = new PathItem()
+ .get(new Operation().operationId("fixedGet"))
+ .addAdditionalOperation("GET", new Operation().operationId("additionalGet"));
+ openAPI2.path("/pets", pathItem2);
+
+ Map options2 = Map.of("FILTER", "method:get");
+ new OpenAPINormalizer(openAPI2, options2).normalize();
+
+ assertEquals(pathItem2.getGet().getExtensions().get(X_INTERNAL), false);
+ assertEquals(pathItem2.getAdditionalOperations().get("GET").getExtensions().get(X_INTERNAL), false);
+ }
+
static OpenAPINormalizer.Filter parseOperationsFilter(String filters) {
OpenAPINormalizer.Filter filter = new OpenAPINormalizer.Filter(filters);
filter.parse();
diff --git a/modules/openapi-generator/src/test/java/org/openapitools/codegen/config/CodegenConfiguratorTest.java b/modules/openapi-generator/src/test/java/org/openapitools/codegen/config/CodegenConfiguratorTest.java
index 551c613b1ca0..1240a9cb78b9 100644
--- a/modules/openapi-generator/src/test/java/org/openapitools/codegen/config/CodegenConfiguratorTest.java
+++ b/modules/openapi-generator/src/test/java/org/openapitools/codegen/config/CodegenConfiguratorTest.java
@@ -16,12 +16,18 @@
package org.openapitools.codegen.config;
+import ch.qos.logback.classic.spi.ILoggingEvent;
+import ch.qos.logback.core.filter.Filter;
+import ch.qos.logback.core.read.ListAppender;
+import ch.qos.logback.core.spi.FilterReply;
import io.swagger.v3.oas.models.OpenAPI;
import org.junit.jupiter.api.Assertions;
import org.openapitools.codegen.ClientOptInput;
import org.openapitools.codegen.CodegenConfig;
import org.openapitools.codegen.CodegenConstants;
+import org.openapitools.codegen.SpecValidationException;
import org.openapitools.codegen.testutils.ConfigAssert;
+import org.slf4j.LoggerFactory;
import org.testng.annotations.Test;
import java.io.File;
@@ -29,13 +35,39 @@
import java.nio.file.Files;
import java.nio.file.Paths;
import java.util.HashMap;
+import java.util.List;
import java.util.Map;
+import java.util.stream.Collectors;
+
+import static org.testng.Assert.assertFalse;
+import static org.testng.Assert.assertTrue;
public class CodegenConfiguratorTest {
private void want(ConfigAssert configAssert, String key, Object expected) {
configAssert.assertValue(key, expected);
}
+ // Attaches a ListAppender that records only events logged on the calling thread.
+ // Surefire runs test classes in parallel (pom.xml: classes) and
+ // CodegenConfigurator.LOGGER is a shared static logger, so a plain ListAppender would
+ // also capture events emitted by other test classes' threads -- polluting the MISSING
+ // assertions and letting foreign threads mutate its list while we read it.
+ private ListAppender attachListAppenderCapturingCurrentThread(
+ ch.qos.logback.classic.Logger logger) {
+ final String testThreadName = Thread.currentThread().getName();
+ ListAppender listAppender = new ListAppender<>();
+ listAppender.addFilter(new Filter() {
+ @Override
+ public FilterReply decide(ILoggingEvent event) {
+ return testThreadName.equals(event.getThreadName())
+ ? FilterReply.NEUTRAL : FilterReply.DENY;
+ }
+ });
+ listAppender.start();
+ logger.addAppender(listAppender);
+ return listAppender;
+ }
+
@Test
public void shouldSetConfigProperties() throws IOException {
// This tests that properties we set on CodegenConfigurator make it down into generator properties,
@@ -133,4 +165,193 @@ public void resolvesResponses() {
Assertions.assertNotNull(context.getSpecDocument().getPaths().get("/hello").getGet().getResponses().get("200").getContent());
}
+
+ @Test
+ public void shouldNotCaptureLogEventsFromOtherThreads() throws Exception {
+ ch.qos.logback.classic.Logger logger =
+ (ch.qos.logback.classic.Logger) LoggerFactory.getLogger(CodegenConfigurator.class);
+ ListAppender listAppender = attachListAppenderCapturingCurrentThread(logger);
+
+ try {
+ // surefire is configured with classes, so sibling test
+ // classes can write to this shared static logger while these tests run; events
+ // from any thread other than the one that attached the appender must be ignored
+ Thread foreign = new Thread(
+ () -> CodegenConfigurator.LOGGER.warn("foreign-thread MISSING marker"));
+ foreign.start();
+ foreign.join();
+ CodegenConfigurator.LOGGER.warn("own-thread MISSING marker");
+
+ assertTrue(listAppender.list.stream()
+ .noneMatch(e -> e.getFormattedMessage().contains("foreign-thread MISSING")),
+ "the capturing appender must ignore events logged by other threads");
+ assertTrue(listAppender.list.stream()
+ .anyMatch(e -> e.getFormattedMessage().contains("own-thread MISSING")),
+ "the capturing appender must still record events logged by the test's own thread");
+ } finally {
+ logger.detachAppender(listAppender);
+ }
+ }
+
+ // https://github.com/OpenAPITools/openapi-generator/issues/24212
+ @Test
+ public void shouldWarnAboutDroppedUnrecognizedPathItemOperation() {
+ ch.qos.logback.classic.Logger logger =
+ (ch.qos.logback.classic.Logger) LoggerFactory.getLogger(CodegenConfigurator.class);
+ ListAppender listAppender = attachListAppenderCapturingCurrentThread(logger);
+
+ try {
+ @SuppressWarnings("unchecked") Context context = (Context) new CodegenConfigurator()
+ .setInputSpec("src/test/resources/3_0/issue_24212_unknown_path_item_member.yaml")
+ .setGeneratorName("java")
+ .setValidateSpec(false)
+ .toContext();
+
+ // generation still proceeds: the recognized 'get' operation is present
+ Assertions.assertNotNull(context.getSpecDocument().getPaths().get("/tasks").getGet());
+
+ List missingWarnLogs = listAppender.list.stream()
+ .filter(e -> e.getLevel() == ch.qos.logback.classic.Level.WARN)
+ .filter(e -> e.getFormattedMessage().contains("'query' at path '/tasks'"))
+ .filter(e -> e.getFormattedMessage().contains("MISSING"))
+ .collect(Collectors.toList());
+ assertFalse(missingWarnLogs.isEmpty(),
+ "A WARN log naming the dropped 'query' operation at path '/tasks' as MISSING must be emitted");
+ } finally {
+ logger.detachAppender(listAppender);
+ }
+ }
+
+ // https://github.com/OpenAPITools/openapi-generator/issues/24212
+ @Test
+ public void shouldWarnAboutDroppedCustomMethodContainingDot() {
+ ch.qos.logback.classic.Logger logger =
+ (ch.qos.logback.classic.Logger) LoggerFactory.getLogger(CodegenConfigurator.class);
+ ListAppender listAppender = attachListAppenderCapturingCurrentThread(logger);
+
+ try {
+ @SuppressWarnings("unchecked") Context context = (Context) new CodegenConfigurator()
+ .setInputSpec("src/test/resources/3_0/issue_24212_dotted_custom_method.yaml")
+ .setGeneratorName("java")
+ .setValidateSpec(false)
+ .toContext();
+
+ // generation still proceeds: the recognized 'get' operation is present
+ Assertions.assertNotNull(context.getSpecDocument().getPaths().get("/tasks").getGet());
+
+ // a custom method name may contain a dot (RFC 9110 tchar); the parser reports
+ // it as "attribute paths.'/tasks'.M.FOO is unexpected" and it must surface as
+ // MISSING rather than being silently dropped
+ for (String method : new String[]{"M.FOO", "m.foo"}) {
+ List missingWarnLogs = listAppender.list.stream()
+ .filter(e -> e.getLevel() == ch.qos.logback.classic.Level.WARN)
+ .filter(e -> e.getFormattedMessage().contains("'" + method + "' at path '/tasks'"))
+ .filter(e -> e.getFormattedMessage().contains("MISSING"))
+ .collect(Collectors.toList());
+ assertFalse(missingWarnLogs.isEmpty(),
+ "A WARN log naming the dropped '" + method + "' custom method at path '/tasks' as MISSING must be emitted");
+ }
+ } finally {
+ logger.detachAppender(listAppender);
+ }
+ }
+
+ // https://github.com/OpenAPITools/openapi-generator/issues/24212
+ @Test
+ public void shouldNotFalsePositiveOnNestedPathItemMemberTypo() {
+ ch.qos.logback.classic.Logger logger =
+ (ch.qos.logback.classic.Logger) LoggerFactory.getLogger(CodegenConfigurator.class);
+ ListAppender listAppender = attachListAppenderCapturingCurrentThread(logger);
+
+ try {
+ @SuppressWarnings("unchecked") Context context = (Context) new CodegenConfigurator()
+ .setInputSpec("src/test/resources/3_0/issue_24212_path_item_parameter_typo.yaml")
+ .setGeneratorName("java")
+ .setValidateSpec(false)
+ .toContext();
+
+ // generation still proceeds despite the typo'd nested attribute
+ Assertions.assertNotNull(context.getSpecDocument().getPaths().get("/tasks/{id}").getGet());
+
+ // a typo inside a path-level parameter/server object is not itself a dropped operation
+ List missingWarnLogs = listAppender.list.stream()
+ .filter(e -> e.getLevel() == ch.qos.logback.classic.Level.WARN)
+ .filter(e -> e.getFormattedMessage().contains("MISSING"))
+ .collect(Collectors.toList());
+ assertTrue(missingWarnLogs.isEmpty(),
+ "A nested parameter/server typo must not be reported as a dropped path-item operation");
+ } finally {
+ logger.detachAppender(listAppender);
+ }
+ }
+
+ // https://github.com/OpenAPITools/openapi-generator/issues/24212
+ @Test
+ public void shouldNotFalsePositiveOnNestedQuotedPathItemMemberTypo() {
+ ch.qos.logback.classic.Logger logger =
+ (ch.qos.logback.classic.Logger) LoggerFactory.getLogger(CodegenConfigurator.class);
+ ListAppender listAppender = attachListAppenderCapturingCurrentThread(logger);
+
+ try {
+ @SuppressWarnings("unchecked") Context context = (Context) new CodegenConfigurator()
+ .setInputSpec("src/test/resources/3_0/issue_24212_nested_quoted_path_member_typo.yaml")
+ .setGeneratorName("java")
+ .setValidateSpec(false)
+ .toContext();
+
+ // generation still proceeds despite the typo'd path-item member
+ Assertions.assertNotNull(context.getSpecDocument().getPaths().get("/tasks'.'x-custom").getGet());
+
+ // the parser reports the typo'd member with a second quoted segment nested inside
+ // the path name's quotes ("attribute paths.'/tasks'.'x-custom'.nested is
+ // unexpected"); a typo'd member at that nested location is not a dropped operation
+ List missingWarnLogs = listAppender.list.stream()
+ .filter(e -> e.getLevel() == ch.qos.logback.classic.Level.WARN)
+ .filter(e -> e.getFormattedMessage().contains("MISSING"))
+ .collect(Collectors.toList());
+ assertTrue(missingWarnLogs.isEmpty(),
+ "A typo'd member reported under a nested quoted location must not be reported as a dropped path-item operation");
+ } finally {
+ logger.detachAppender(listAppender);
+ }
+ }
+
+ // https://github.com/OpenAPITools/openapi-generator/issues/24212
+ @Test
+ public void shouldFailWithClearMessageAndNoMisleadingWarningWhenSpecificationIsNull() {
+ ch.qos.logback.classic.Logger logger =
+ (ch.qos.logback.classic.Logger) LoggerFactory.getLogger(CodegenConfigurator.class);
+ ListAppender listAppender = attachListAppenderCapturingCurrentThread(logger);
+
+ try {
+ CodegenConfigurator configurator = new CodegenConfigurator()
+ .setInputSpec("src/test/resources/3_0/issue_24212_unsupported_version.yaml")
+ .setGeneratorName("java")
+ .setValidateSpec(false);
+
+ RuntimeException ex = Assertions.assertThrows(RuntimeException.class, configurator::toContext);
+ assertFalse(ex instanceof SpecValidationException, "expected a plain RuntimeException, not SpecValidationException");
+ assertTrue(ex.getMessage().startsWith("Unable to parse an OpenAPI document"), ex.getMessage());
+
+ // nothing will be generated at all, so no operation should be reported as merely "MISSING"
+ List missingWarnLogs = listAppender.list.stream()
+ .filter(e -> e.getFormattedMessage().contains("MISSING"))
+ .collect(Collectors.toList());
+ assertTrue(missingWarnLogs.isEmpty(),
+ "Must not claim specific operations are 'MISSING' when generation cannot proceed at all");
+ } finally {
+ logger.detachAppender(listAppender);
+ }
+ }
+
+ // https://github.com/OpenAPITools/openapi-generator/issues/24212
+ @Test
+ public void shouldStillThrowSpecValidationExceptionByDefaultForUnknownPathItemOperation() {
+ CodegenConfigurator configurator = new CodegenConfigurator()
+ .setInputSpec("src/test/resources/3_0/issue_24212_unknown_path_item_member.yaml")
+ .setGeneratorName("java");
+ // default validateSpec=true is unchanged by this fix: it still fails fast with the
+ // existing, structured SpecValidationException rather than the new generic message.
+ Assertions.assertThrows(SpecValidationException.class, configurator::toContext);
+ }
}
diff --git a/modules/openapi-generator/src/test/java/org/openapitools/codegen/config/MergedSpecBuilderTest.java b/modules/openapi-generator/src/test/java/org/openapitools/codegen/config/MergedSpecBuilderTest.java
index 744c5e71f219..7f45d1f63994 100644
--- a/modules/openapi-generator/src/test/java/org/openapitools/codegen/config/MergedSpecBuilderTest.java
+++ b/modules/openapi-generator/src/test/java/org/openapitools/codegen/config/MergedSpecBuilderTest.java
@@ -1170,4 +1170,47 @@ public void shouldRejectMalformedVersion() {
}
}
}
+
+ /**
+ * Root-level security must reach OpenAPI 3.2 {@code additionalOperations} too: the propagation
+ * relies on {@code PathItem.readOperations()}, which in the 3.2 model includes
+ * {@code additionalOperations} values — this test pins that contract.
+ */
+ @Test
+ public void shouldPropagateRootSecurityToAdditionalOperations() {
+ io.swagger.v3.oas.models.security.SecurityRequirement req =
+ new io.swagger.v3.oas.models.security.SecurityRequirement().addList("apiKeyAuth");
+
+ OpenAPI spec1 = new OpenAPI().openapi("3.2.0")
+ .info(new Info().title("s1").version("1.0.0"))
+ .security(Collections.singletonList(req));
+ spec1.setPaths(new io.swagger.v3.oas.models.Paths());
+ spec1.setComponents(new Components());
+ PathItem path = new PathItem()
+ .get(new io.swagger.v3.oas.models.Operation().operationId("getA"));
+ path.addAdditionalOperation("REPORT",
+ new io.swagger.v3.oas.models.Operation().operationId("reportA"));
+ spec1.getPaths().addPathItem("/a", path);
+
+ // Same path URL in a second spec forces mergePathItem (the addAdditionalOperation branch)
+ OpenAPI spec2 = new OpenAPI().openapi("3.2.0")
+ .info(new Info().title("s2").version("1.0.0"));
+ spec2.setPaths(new io.swagger.v3.oas.models.Paths());
+ spec2.setComponents(new Components());
+ spec2.getPaths().addPathItem("/a", new PathItem()
+ .post(new io.swagger.v3.oas.models.Operation().operationId("postA")));
+
+ MergedSpecBuilder builder = new MergedSpecBuilder("dummy", "_merged")
+ .withMergeMode(MergedSpecBuilder.MergeMode.DEEP);
+ OpenAPI merged = builder.mergeSpecs(Arrays.asList(spec1, spec2), Collections.emptyList());
+
+ PathItem mergedPath = merged.getPaths().get("/a");
+ assertNotNull(mergedPath.getAdditionalOperations(), "additionalOperations must survive the merge");
+ io.swagger.v3.oas.models.Operation report = mergedPath.getAdditionalOperations().get("REPORT");
+ assertNotNull(report, "REPORT operation must be present after merge");
+ assertNotNull(report.getSecurity(),
+ "Root security must be propagated to additionalOperations");
+ assertTrue(report.getSecurity().stream().anyMatch(r -> r.containsKey("apiKeyAuth")),
+ "apiKeyAuth requirement must be present on the additionalOperation");
+ }
}
\ No newline at end of file
diff --git a/modules/openapi-generator/src/test/java/org/openapitools/codegen/csharpnetcore/CSharpClientCodegenTest.java b/modules/openapi-generator/src/test/java/org/openapitools/codegen/csharpnetcore/CSharpClientCodegenTest.java
index 55e7cb4d720f..b2959a0f436a 100644
--- a/modules/openapi-generator/src/test/java/org/openapitools/codegen/csharpnetcore/CSharpClientCodegenTest.java
+++ b/modules/openapi-generator/src/test/java/org/openapitools/codegen/csharpnetcore/CSharpClientCodegenTest.java
@@ -21,7 +21,9 @@
import io.swagger.v3.oas.models.media.*;
import io.swagger.v3.oas.models.responses.ApiResponse;
import io.swagger.v3.oas.models.responses.ApiResponses;
+import org.apache.commons.io.FileUtils;
import org.openapitools.codegen.*;
+import org.openapitools.codegen.config.CodegenConfigurator;
import org.openapitools.codegen.languages.CSharpClientCodegen;
import org.openapitools.codegen.languages.JavaCXFClientCodegen;
import org.openapitools.codegen.model.OperationMap;
@@ -32,7 +34,9 @@
import java.io.File;
import java.io.IOException;
+import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
+import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.Collections;
import java.util.List;
@@ -709,4 +713,249 @@ private File getGeneratedModel(Map files, File output, String mode
assertNotNull(model, "Could not find generated model: " + path);
return model;
}
+
+ @Test
+ public void testGenerichostOpenApi32OperationsAndQueryStringParam() throws IOException {
+ Path target = Files.createTempDirectory("test");
+ try {
+ final CodegenConfigurator configurator = new CodegenConfigurator()
+ .setGeneratorName("csharp")
+ .setLibrary("generichost")
+ .setInputSpec("src/test/resources/3_2/query-operation.yaml")
+ .setSkipOverwrite(false)
+ .setOutputDir(target.toAbsolutePath().toString().replace("\\", "/"));
+ new DefaultGenerator().opts(configurator.toClientOptInput()).generate();
+ Path apiPath = target.resolve("src/Org.OpenAPITools/Api/DefaultApi.cs");
+ TestUtils.assertFileExists(apiPath);
+ String generated = new String(Files.readAllBytes(apiPath), StandardCharsets.UTF_8);
+ // non-standard methods are emitted verbatim; HttpMethod.Query only
+ // exists on net10+, so QUERY also goes through the literal path
+ for (String method : new String[]{"QUERY", "PURGE", "customMethod", "CHECK&FETCH", "X#Y", "REPORT", "PROPPATCH"}) {
+ Assert.assertTrue(generated.contains("new HttpMethod(\"" + method + "\")"),
+ "expected verbatim HttpMethod literal for " + method);
+ }
+ Assert.assertTrue(generated.contains("httpRequestMessageLocalVar.Method = HttpMethod.Get"),
+ "standard method kept on the static singleton");
+ // `in: querystring` appends verbatim after UriBuilder.Query, not via
+ // ParseQueryString (which would double-encode)
+ Assert.assertTrue(generated.contains("uriBuilderLocalVar.Query + \"&\" + qs"),
+ "querystring param should be appended verbatim");
+ Assert.assertFalse(generated.contains("parseQueryStringLocalVar[\"qs\"]"),
+ "querystring param must not be serialized as a name=value pair");
+ // QUERY may carry a body; a querystring param named `uri` must not
+ // collide with UriBuilder
+ Assert.assertTrue(generated.contains("SearchItemsAsync(string uri, Dictionary requestBody"),
+ "QUERY-with-body operation should take uri + body parameters");
+ Assert.assertTrue(generated.contains("? localVariableQuery :"),
+ "a querystring param named `localVariableQuery` must reach the wire verbatim");
+ } finally {
+ FileUtils.deleteDirectory(target.toFile());
+ }
+ }
+
+ @Test
+ public void testRestsharpSkipsOpenApi32Operations() throws IOException {
+ Path target = Files.createTempDirectory("test");
+ try {
+ final CodegenConfigurator configurator = new CodegenConfigurator()
+ .setGeneratorName("csharp")
+ .setLibrary("restsharp")
+ .setInputSpec("src/test/resources/3_2/query-operation.yaml")
+ .setSkipOverwrite(false)
+ .setOutputDir(target.toAbsolutePath().toString().replace("\\", "/"));
+ new DefaultGenerator().opts(configurator.toClientOptInput()).generate();
+ Path apiPath = target.resolve("src/Org.OpenAPITools/Api/DefaultApi.cs");
+ TestUtils.assertFileExists(apiPath);
+ String generated = new String(Files.readAllBytes(apiPath), StandardCharsets.UTF_8);
+ Assert.assertTrue(generated.contains("ListPets"), "GET operation should be kept");
+ for (String op : new String[]{"QueryPets", "PurgePets", "CustomPets", "CheckFetchPets", "HashPets"}) {
+ Assert.assertFalse(generated.contains(op),
+ "restsharp must skip unsupported 3.2 operation " + op);
+ }
+ } finally {
+ FileUtils.deleteDirectory(target.toFile());
+ }
+ }
+
+ @Test
+ public void testGenerichostSkipsInvalidOrNormalizingMethodToken() throws IOException {
+ Path target = Files.createTempDirectory("test");
+ try {
+ // "MY METHOD" is not a valid RFC 9110 token. (A case-variant like a
+ // lowercase "get" is additionally guarded in CSharpClientCodegen, but
+ // the parser already rejects it as a duplicate fixed-method name.)
+ String spec = "openapi: 3.2.0\n"
+ + "info: {title: t, version: '1'}\n"
+ + "paths:\n"
+ + " /pets:\n"
+ + " get:\n"
+ + " operationId: listPets\n"
+ + " responses: {'200': {description: ok}}\n"
+ + " additionalOperations:\n"
+ + " \"MY METHOD\":\n"
+ + " operationId: badMethod\n"
+ + " responses: {'204': {description: done}}\n";
+ Path specFile = target.resolve("spec.yaml");
+ Files.writeString(specFile, spec);
+ final CodegenConfigurator configurator = new CodegenConfigurator()
+ .setGeneratorName("csharp")
+ .setLibrary("generichost")
+ .setInputSpec(specFile.toString())
+ .setSkipOverwrite(false)
+ .setOutputDir(target.resolve("out").toAbsolutePath().toString().replace("\\", "/"));
+ new DefaultGenerator().opts(configurator.toClientOptInput()).generate();
+ Path apiPath = target.resolve("out/src/Org.OpenAPITools/Api/DefaultApi.cs");
+ TestUtils.assertFileExists(apiPath);
+ String generated = new String(Files.readAllBytes(apiPath), StandardCharsets.UTF_8);
+ Assert.assertTrue(generated.contains("ListPets"), "GET operation should be kept");
+ Assert.assertFalse(generated.contains("BadMethod"),
+ "invalid RFC 9110 method token must be skipped");
+ } finally {
+ FileUtils.deleteDirectory(target.toFile());
+ }
+ }
+
+ @Test
+ public void testGenerichostSkipsCaseVariantOfNormalizedMethod() throws IOException {
+ Path target = Files.createTempDirectory("test");
+ try {
+ // .NET 10 knows HttpMethod.Query, so HttpClient folds "qUeRy" onto
+ // QUERY on the wire (verified on net8 vs net10: net8 sends it
+ // verbatim). Since the generated library is multi-target, the only
+ // uniform-honest behavior is warn+skip. The spec validator rejects
+ // the key as a duplicate fixed method, so validation is bypassed.
+ String spec = "openapi: 3.2.0\n"
+ + "info: {title: t, version: '1'}\n"
+ + "paths:\n"
+ + " /pets:\n"
+ + " additionalOperations:\n"
+ + " \"qUeRy\":\n"
+ + " operationId: caseQueryPets\n"
+ + " responses: {'204': {description: done}}\n";
+ Path specFile = target.resolve("spec.yaml");
+ Files.writeString(specFile, spec);
+ final CodegenConfigurator configurator = new CodegenConfigurator()
+ .setGeneratorName("csharp")
+ .setLibrary("generichost")
+ .setInputSpec(specFile.toString())
+ .setValidateSpec(false)
+ .setSkipOverwrite(false)
+ .setOutputDir(target.resolve("out").toAbsolutePath().toString().replace("\\", "/"));
+ new DefaultGenerator().opts(configurator.toClientOptInput()).generate();
+ Path apiPath = target.resolve("out/src/Org.OpenAPITools/Api/DefaultApi.cs");
+ TestUtils.assertFileExists(apiPath);
+ String generated = new String(Files.readAllBytes(apiPath), StandardCharsets.UTF_8);
+ Assert.assertFalse(generated.contains("CaseQueryPets"),
+ "case-variant of a normalized method must be skipped");
+ } finally {
+ FileUtils.deleteDirectory(target.toFile());
+ }
+ }
+
+ @Test
+ public void testGenerichostWebhookModelGetsPublicCtor() throws IOException {
+ Path target = Files.createTempDirectory("test");
+ try {
+ // the Pet schema is referenced only by a webhook operation; models
+ // reachable only via openAPI.getWebhooks() must still be marked as
+ // operation inputs or their ctor stays `internal`
+ final CodegenConfigurator configurator = new CodegenConfigurator()
+ .setGeneratorName("csharp")
+ .setLibrary("generichost")
+ .setInputSpec("src/test/resources/3_2/webhook-operations.yaml")
+ .setSkipOverwrite(false)
+ .setOutputDir(target.toAbsolutePath().toString().replace("\\", "/"));
+ new DefaultGenerator().opts(configurator.toClientOptInput()).generate();
+ Path modelPath = target.resolve("src/Org.OpenAPITools/Model/Pet.cs");
+ TestUtils.assertFileExists(modelPath);
+ String model = new String(Files.readAllBytes(modelPath), StandardCharsets.UTF_8);
+ Assert.assertTrue(model.contains("public Pet("),
+ "webhook-referenced model must get a public ctor");
+ Assert.assertFalse(model.contains("internal Pet("),
+ "webhook-referenced model must not keep an internal ctor");
+ } finally {
+ FileUtils.deleteDirectory(target.toFile());
+ }
+ }
+
+ /**
+ * End-to-end check: builds the generated generichost client and runs a raw
+ * TcpListener capture, verifying query/additionalOperations methods and
+ * `in: querystring` reach the wire verbatim. Skipped when dotnet is not on
+ * PATH or nuget restore fails.
+ */
+ @Test
+ public void testGenerichostGeneratedClientSendsVerbatimMethods() throws IOException, InterruptedException {
+ if (!isCommandAvailable("dotnet", "--version")) {
+ throw new org.testng.SkipException("dotnet is not on PATH; skipping generated-client verification");
+ }
+ Path target = Files.createTempDirectory("csharp32-verify");
+ try {
+ final CodegenConfigurator configurator = new CodegenConfigurator()
+ .setGeneratorName("csharp")
+ .setLibrary("generichost")
+ .setInputSpec("src/test/resources/3_2/query-operation.yaml")
+ .setSkipOverwrite(false)
+ .setOutputDir(target.toAbsolutePath().toString().replace("\\", "/"))
+ .addAdditionalProperty("targetFramework", "net8.0");
+ new DefaultGenerator().opts(configurator.toClientOptInput()).generate();
+
+ Path captureDir = target.resolve("capture");
+ Files.createDirectories(captureDir);
+ for (String f : new String[]{"Program.cs", "Capture.csproj"}) {
+ Files.copy(Path.of("src/test/resources/3_2/csharp-generichost-capture/" + f),
+ captureDir.resolve(f));
+ }
+
+ // nuget restore is the only network-dependent step; only its failure
+ // is skippable - compile or capture failures must fail the test
+ Path restoreLog = target.resolve("restore.log");
+ Process restore = new ProcessBuilder("dotnet", "restore", "capture/Capture.csproj")
+ .directory(target.toFile())
+ .redirectErrorStream(true)
+ .redirectOutput(restoreLog.toFile())
+ .start();
+ if (!restore.waitFor(180, java.util.concurrent.TimeUnit.SECONDS) || restore.exitValue() != 0) {
+ restore.destroyForcibly();
+ throw new org.testng.SkipException("dotnet restore failed (nuget unreachable?):\n"
+ + new String(Files.readAllBytes(restoreLog), StandardCharsets.UTF_8));
+ }
+
+ Path runLog = target.resolve("run.log");
+ Process p = new ProcessBuilder("dotnet", "run", "--project", "capture/Capture.csproj",
+ "--no-restore")
+ .directory(target.toFile())
+ .redirectErrorStream(true)
+ .redirectOutput(runLog.toFile())
+ .start();
+ boolean finished = p.waitFor(180, java.util.concurrent.TimeUnit.SECONDS);
+ if (!finished) {
+ p.destroyForcibly();
+ }
+ String output = new String(Files.readAllBytes(runLog), StandardCharsets.UTF_8);
+ Assert.assertTrue(finished, "dotnet capture timed out:\n" + output);
+ Assert.assertTrue(output.contains("CAPTURE-PASS"),
+ "generated client did not send verbatim 3.2 methods/querystring:\n" + output);
+ } finally {
+ // bin/obj dirs are large; deleteOnExit cannot remove non-empty dirs
+ FileUtils.deleteDirectory(target.toFile());
+ }
+ }
+
+ private boolean isCommandAvailable(String... command) {
+ try {
+ Process p = new ProcessBuilder(command)
+ .redirectErrorStream(true).start();
+ // wait before draining: a child that never exits would otherwise block
+ // the stream read forever
+ if (!p.waitFor(10, java.util.concurrent.TimeUnit.SECONDS)) {
+ p.destroyForcibly();
+ return false;
+ }
+ p.getInputStream().transferTo(java.io.OutputStream.nullOutputStream());
+ return p.exitValue() == 0;
+ } catch (IOException | InterruptedException e) {
+ return false;
+ }
+ }
}
diff --git a/modules/openapi-generator/src/test/java/org/openapitools/codegen/go/GoClientCodegenTest.java b/modules/openapi-generator/src/test/java/org/openapitools/codegen/go/GoClientCodegenTest.java
index 0abf0a052ec7..59dcf5cdfce7 100644
--- a/modules/openapi-generator/src/test/java/org/openapitools/codegen/go/GoClientCodegenTest.java
+++ b/modules/openapi-generator/src/test/java/org/openapitools/codegen/go/GoClientCodegenTest.java
@@ -19,10 +19,12 @@
import io.swagger.v3.oas.models.OpenAPI;
import io.swagger.v3.oas.models.Operation;
+import org.apache.commons.io.FileUtils;
import org.openapitools.codegen.*;
import org.openapitools.codegen.config.CodegenConfigurator;
import org.openapitools.codegen.languages.GoClientCodegen;
import org.testng.Assert;
+import org.testng.annotations.AfterClass;
import org.testng.annotations.Test;
import java.io.File;
@@ -30,6 +32,7 @@
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
+import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
@@ -37,6 +40,19 @@
public class GoClientCodegenTest {
+ // File.deleteOnExit() cannot remove non-empty directories, so generated
+ // output trees are collected here and deleted recursively after the class
+ private static final List TEMP_DIRS = new ArrayList<>();
+
+ @AfterClass(alwaysRun = true)
+ public static void deleteTempDirs() throws IOException {
+ for (File dir : TEMP_DIRS) {
+ FileUtils.deleteDirectory(dir);
+ }
+ TEMP_DIRS.clear();
+ }
+
+
@Test
public void testInitialConfigValues() throws Exception {
final GoClientCodegen codegen = new GoClientCodegen();
@@ -121,7 +137,7 @@ public void testFilenames() throws Exception {
@Test
public void testPrimitiveTypeInOneOf() throws IOException {
File output = Files.createTempDirectory("test").toFile();
- output.deleteOnExit();
+ TEMP_DIRS.add(output);
final CodegenConfigurator configurator = new CodegenConfigurator()
.setGeneratorName("go")
@@ -131,7 +147,6 @@ public void testPrimitiveTypeInOneOf() throws IOException {
DefaultGenerator generator = new DefaultGenerator();
List files = generator.opts(configurator.toClientOptInput()).generate();
System.out.println(files);
- files.forEach(File::deleteOnExit);
Path modelFile = Paths.get(output + "/model_example.go");
TestUtils.assertFileContains(modelFile, "Child *Child");
@@ -173,7 +188,7 @@ public void testExplodedObjectQueryParameter() throws IOException {
@Test
public void testNullableComposition() throws IOException {
File output = Files.createTempDirectory("test").toFile();
- output.deleteOnExit();
+ TEMP_DIRS.add(output);
final CodegenConfigurator configurator = new CodegenConfigurator()
.setGeneratorName("go")
@@ -182,7 +197,6 @@ public void testNullableComposition() throws IOException {
DefaultGenerator generator = new DefaultGenerator();
List files = generator.opts(configurator.toClientOptInput()).generate();
- files.forEach(File::deleteOnExit);
TestUtils.assertFileContains(Paths.get(output + "/model_example.go"), "Child NullableChild");
}
@@ -190,7 +204,7 @@ public void testNullableComposition() throws IOException {
@Test
public void testMultipleRequiredPropertiesHasSameOneOfObject() throws IOException {
File output = Files.createTempDirectory("test").toFile();
- output.deleteOnExit();
+ TEMP_DIRS.add(output);
final CodegenConfigurator configurator = new CodegenConfigurator()
.setGeneratorName("go")
@@ -200,7 +214,6 @@ public void testMultipleRequiredPropertiesHasSameOneOfObject() throws IOExceptio
DefaultGenerator generator = new DefaultGenerator();
List files = generator.opts(configurator.toClientOptInput()).generate();
System.out.println(files);
- files.forEach(File::deleteOnExit);
Path docFile = Paths.get(output + "/docs/PetAPI.md");
TestUtils.assertFileContains(docFile, "openapiclient.pet{Cat: openapiclient.NewCat(\"Attr_example\")}, openapiclient.pet{Cat: openapiclient.NewCat(\"Attr_example\")}, openapiclient.pet{Cat: openapiclient.NewCat(\"Attr_example\")}");
@@ -212,7 +225,7 @@ public void testStructPrefix() throws IOException {
properties.put(GoClientCodegen.STRUCT_PREFIX, true);
File output = Files.createTempDirectory("test").toFile();
- output.deleteOnExit();
+ TEMP_DIRS.add(output);
final CodegenConfigurator configurator = new CodegenConfigurator()
.setGeneratorName("go")
@@ -222,7 +235,6 @@ public void testStructPrefix() throws IOException {
DefaultGenerator generator = new DefaultGenerator();
List files = generator.opts(configurator.toClientOptInput()).generate();
- files.forEach(File::deleteOnExit);
TestUtils.assertFileContains(Paths.get(output + "/api_pet.go"), "type PetAPIAddPetRequest struct");
}
@@ -234,7 +246,7 @@ public void testEnumUnknownDefaultCaseUsesModelSpecificNamesWhenEnumClassPrefixD
properties.put(CodegenConstants.ENUM_CLASS_PREFIX, false);
File output = Files.createTempDirectory("test").toFile();
- output.deleteOnExit();
+ TEMP_DIRS.add(output);
final CodegenConfigurator configurator = new CodegenConfigurator()
.setGeneratorName("go")
@@ -244,7 +256,6 @@ public void testEnumUnknownDefaultCaseUsesModelSpecificNamesWhenEnumClassPrefixD
DefaultGenerator generator = new DefaultGenerator();
List files = generator.opts(configurator.toClientOptInput()).generate();
- files.forEach(File::deleteOnExit);
Path statusA = Paths.get(output + "/model_status_a.go");
Path statusB = Paths.get(output + "/model_status_b.go");
@@ -270,7 +281,7 @@ public void testAdditionalPropertiesModelFileFolder() throws Exception {
@Test
public void verifyTestFile() throws IOException {
File output = Files.createTempDirectory("test").toFile();
- output.deleteOnExit();
+ TEMP_DIRS.add(output);
final CodegenConfigurator configurator = new CodegenConfigurator()
.setGeneratorName("go")
@@ -279,7 +290,6 @@ public void verifyTestFile() throws IOException {
DefaultGenerator generator = new DefaultGenerator();
List files = generator.opts(configurator.toClientOptInput()).generate();
- files.forEach(File::deleteOnExit);
TestUtils.assertFileExists(Paths.get(output + "/test/api_pet_test.go"));
TestUtils.assertFileContains(Paths.get(output + "/test/api_pet_test.go"),
@@ -289,7 +299,7 @@ public void verifyTestFile() throws IOException {
@Test
public void verifyTestImport() throws IOException {
File output = Files.createTempDirectory("test").toFile();
- output.deleteOnExit();
+ TEMP_DIRS.add(output);
final CodegenConfigurator configurator = new CodegenConfigurator()
.setGeneratorName("go")
@@ -300,7 +310,6 @@ public void verifyTestImport() throws IOException {
DefaultGenerator generator = new DefaultGenerator();
List files = generator.opts(configurator.toClientOptInput()).generate();
- files.forEach(File::deleteOnExit);
TestUtils.assertFileExists(Paths.get(output + "/test/api_pet_test.go"));
TestUtils.assertFileContains(Paths.get(output + "/test/api_pet_test.go"),
@@ -310,7 +319,7 @@ public void verifyTestImport() throws IOException {
@Test
public void verifyFormatErrorMessageInUse() throws IOException {
File output = Files.createTempDirectory("test").toFile();
- output.deleteOnExit();
+ TEMP_DIRS.add(output);
final CodegenConfigurator configurator = new CodegenConfigurator()
.setGeneratorName("go")
@@ -319,7 +328,6 @@ public void verifyFormatErrorMessageInUse() throws IOException {
DefaultGenerator generator = new DefaultGenerator();
List files = generator.opts(configurator.toClientOptInput()).generate();
- files.forEach(File::deleteOnExit);
TestUtils.assertFileExists(Paths.get(output + "/api_pet.go"));
TestUtils.assertFileContains(Paths.get(output + "/api_pet.go"),
@@ -329,7 +337,7 @@ public void verifyFormatErrorMessageInUse() throws IOException {
@Test
public void verifyApiTestWithNullResponse() throws IOException {
File output = Files.createTempDirectory("test").toFile();
- output.deleteOnExit();
+ TEMP_DIRS.add(output);
final CodegenConfigurator configurator = new CodegenConfigurator()
.setGeneratorName("go")
@@ -340,7 +348,6 @@ public void verifyApiTestWithNullResponse() throws IOException {
DefaultGenerator generator = new DefaultGenerator();
List files = generator.opts(configurator.toClientOptInput()).generate();
- files.forEach(File::deleteOnExit);
TestUtils.assertFileExists(Paths.get(output + "/test/api_pet_test.go"));
TestUtils.assertFileNotContains(Paths.get(output + "/test/api_pet_test.go"),
@@ -354,7 +361,7 @@ public void verifyApiTestWithNullResponse() throws IOException {
@Test
public void verifyApiWithAllOfMultipleRefAndDiscriminator() throws IOException {
File output = Files.createTempDirectory("test").toFile();
- output.deleteOnExit();
+ TEMP_DIRS.add(output);
final CodegenConfigurator configurator = new CodegenConfigurator()
.setGeneratorName("go")
@@ -365,7 +372,6 @@ public void verifyApiWithAllOfMultipleRefAndDiscriminator() throws IOException {
DefaultGenerator generator = new DefaultGenerator();
List files = generator.opts(configurator.toClientOptInput()).generate();
- files.forEach(File::deleteOnExit);
TestUtils.assertFileExists(Paths.get(output + "/model_final_item.go"));
TestUtils.assertFileContains(Paths.get(output + "/model_final_item.go"),
@@ -375,7 +381,7 @@ public void verifyApiWithAllOfMultipleRefAndDiscriminator() throws IOException {
@Test
public void testVendorExtensionGenerateUnmarshalJson() throws IOException {
File output = Files.createTempDirectory("test").toFile();
- output.deleteOnExit();
+ TEMP_DIRS.add(output);
final CodegenConfigurator configurator = new CodegenConfigurator()
.setGeneratorName("go")
@@ -386,7 +392,6 @@ public void testVendorExtensionGenerateUnmarshalJson() throws IOException {
DefaultGenerator generator = new DefaultGenerator();
List files = generator.opts(configurator.toClientOptInput()).generate();
- files.forEach(File::deleteOnExit);
TestUtils.assertFileExists(Paths.get(output + "/model_base_item.go"));
TestUtils.assertFileContains(Paths.get(output + "/model_base_item.go"),
@@ -396,7 +401,7 @@ public void testVendorExtensionGenerateUnmarshalJson() throws IOException {
@Test
public void testVendorExtensionSkipGenerateUnmarshalJson() throws IOException {
File output = Files.createTempDirectory("test").toFile();
- output.deleteOnExit();
+ TEMP_DIRS.add(output);
final CodegenConfigurator configurator = new CodegenConfigurator()
.setGeneratorName("go")
@@ -407,7 +412,6 @@ public void testVendorExtensionSkipGenerateUnmarshalJson() throws IOException {
DefaultGenerator generator = new DefaultGenerator();
List files = generator.opts(configurator.toClientOptInput()).generate();
- files.forEach(File::deleteOnExit);
TestUtils.assertFileExists(Paths.get(output + "/model_base_item.go"));
TestUtils.assertFileNotContains(Paths.get(output + "/model_base_item.go"),
@@ -420,7 +424,7 @@ public void testNoImportsWithoutUnmarshal() throws IOException {
properties.put(CodegenConstants.GENERATE_UNMARSHAL_JSON, false);
File output = Files.createTempDirectory("test").toFile();
- output.deleteOnExit();
+ TEMP_DIRS.add(output);
final CodegenConfigurator configurator = new CodegenConfigurator()
.setGeneratorName("go")
@@ -430,7 +434,6 @@ public void testNoImportsWithoutUnmarshal() throws IOException {
DefaultGenerator generator = new DefaultGenerator();
List files = generator.opts(configurator.toClientOptInput()).generate();
- files.forEach(File::deleteOnExit);
TestUtils.assertFileNotContains(Paths.get(output + "/model_pet.go"), "bytes");
}
@@ -438,7 +441,7 @@ public void testNoImportsWithoutUnmarshal() throws IOException {
@Test
public void testAdditionalPropertiesWithGoMod() throws Exception {
File output = Files.createTempDirectory("test").toFile();
- output.deleteOnExit();
+ TEMP_DIRS.add(output);
final CodegenConfigurator configurator = new CodegenConfigurator()
.setGeneratorName("go")
@@ -448,7 +451,6 @@ public void testAdditionalPropertiesWithGoMod() throws Exception {
DefaultGenerator generator = new DefaultGenerator();
List files = generator.opts(configurator.toClientOptInput()).generate();
System.out.println(files);
- files.forEach(File::deleteOnExit);
Path goModFile = Paths.get(output + "/go.mod");
TestUtils.assertFileExists(goModFile);
@@ -459,7 +461,7 @@ public void testAdditionalPropertiesWithGoMod() throws Exception {
@Test
public void testAdditionalPropertiesWithoutGoMod() throws Exception {
File output = Files.createTempDirectory("test").toFile();
- output.deleteOnExit();
+ TEMP_DIRS.add(output);
final CodegenConfigurator configurator = new CodegenConfigurator()
.setGeneratorName("go")
@@ -470,7 +472,6 @@ public void testAdditionalPropertiesWithoutGoMod() throws Exception {
DefaultGenerator generator = new DefaultGenerator();
List files = generator.opts(configurator.toClientOptInput()).generate();
System.out.println(files);
- files.forEach(File::deleteOnExit);
Path goModFile = Paths.get(output + "/go.mod");
TestUtils.assertFileNotExists(goModFile);
@@ -485,7 +486,7 @@ public void testXmlOptionsBeingUsed() throws IOException {
properties.put(GoClientCodegen.WITH_XML, true);
File output = Files.createTempDirectory("test").toFile();
- output.deleteOnExit();
+ TEMP_DIRS.add(output);
final CodegenConfigurator configurator = new CodegenConfigurator()
.setGeneratorName("go")
@@ -495,7 +496,6 @@ public void testXmlOptionsBeingUsed() throws IOException {
DefaultGenerator generator = new DefaultGenerator();
List files = generator.opts(configurator.toClientOptInput()).generate();
- files.forEach(File::deleteOnExit);
TestUtils.assertFileContains(Paths.get(output + "/model_pet.go"), "tags>tag");
}
@@ -503,7 +503,7 @@ public void testXmlOptionsBeingUsed() throws IOException {
@Test
public void testArrayDefaultValue() throws IOException {
File output = Files.createTempDirectory("test").toFile();
- output.deleteOnExit();
+ TEMP_DIRS.add(output);
final CodegenConfigurator configurator = new CodegenConfigurator()
.setGeneratorName("go")
@@ -512,7 +512,6 @@ public void testArrayDefaultValue() throws IOException {
DefaultGenerator generator = new DefaultGenerator();
List files = generator.opts(configurator.toClientOptInput()).generate();
- files.forEach(File::deleteOnExit);
Path apiPath = Paths.get(output + "/api_default.go");
String defaultStringArrayString = "var defaultValue []string = []string{\"test1\", \"test2\"}";
String defaultEnumArrayString = "var defaultValue []ExampleEnum = []ExampleEnum{\"example1\"}";
@@ -525,7 +524,7 @@ public void testArrayDefaultValue() throws IOException {
@Test
public void testEscapingInExamples() throws IOException {
File output = Files.createTempDirectory("test").toFile();
- output.deleteOnExit();
+ TEMP_DIRS.add(output);
final CodegenConfigurator configurator = new CodegenConfigurator()
.setGeneratorName("go")
@@ -534,7 +533,6 @@ public void testEscapingInExamples() throws IOException {
DefaultGenerator generator = new DefaultGenerator();
List files = generator.opts(configurator.toClientOptInput()).generate();
- files.forEach(File::deleteOnExit);
Path docPath = Paths.get(output + "/docs/TestAPI.md");
// Verify that quotes are properly escaped in parameter examples
@@ -551,7 +549,7 @@ public void testOneOfUnmarshalJSONHonorsGenerateUnmarshalJSONFlag() throws IOExc
properties.put(CodegenConstants.GENERATE_UNMARSHAL_JSON, false);
File output = Files.createTempDirectory("test").toFile();
- output.deleteOnExit();
+ TEMP_DIRS.add(output);
final CodegenConfigurator configurator = new CodegenConfigurator()
.setGeneratorName("go")
@@ -561,7 +559,6 @@ public void testOneOfUnmarshalJSONHonorsGenerateUnmarshalJSONFlag() throws IOExc
DefaultGenerator generator = new DefaultGenerator();
List files = generator.opts(configurator.toClientOptInput()).generate();
- files.forEach(File::deleteOnExit);
// With the flag disabled the validator import is not added, so the oneOf model must not
// emit UnmarshalJSON (which would reference the missing validator package and fail to compile).
@@ -574,7 +571,7 @@ public void testOneOfUnmarshalJSONHonorsGenerateUnmarshalJSONFlag() throws IOExc
@Test(description = "with the default generateUnmarshalJSON=true the oneOf UnmarshalJSON and the validator import are still generated")
public void testOneOfUnmarshalJSONGeneratedByDefault() throws IOException {
File output = Files.createTempDirectory("test").toFile();
- output.deleteOnExit();
+ TEMP_DIRS.add(output);
final CodegenConfigurator configurator = new CodegenConfigurator()
.setGeneratorName("go")
@@ -583,7 +580,6 @@ public void testOneOfUnmarshalJSONGeneratedByDefault() throws IOException {
DefaultGenerator generator = new DefaultGenerator();
List files = generator.opts(configurator.toClientOptInput()).generate();
- files.forEach(File::deleteOnExit);
Path oneOfModel = Paths.get(output + "/model_object.go");
TestUtils.assertFileContains(oneOfModel,
@@ -591,4 +587,110 @@ public void testOneOfUnmarshalJSONGeneratedByDefault() throws IOException {
"validator.Validate",
"gopkg.in/validator.v2");
}
+
+ @Test(description = "OpenAPI 3.2 query/additionalOperations and in:querystring generate working Go code")
+ public void testOpenAPI32QueryAndAdditionalOperations() throws IOException {
+ File output = Files.createTempDirectory("test").toFile();
+ TEMP_DIRS.add(output);
+
+ final CodegenConfigurator configurator = new CodegenConfigurator()
+ .setGeneratorName("go")
+ .setInputSpec("src/test/resources/3_2/query-operation.yaml")
+ .setOutputDir(output.getAbsolutePath().replace("\\", "/"));
+
+ DefaultGenerator generator = new DefaultGenerator();
+ List files = generator.opts(configurator.toClientOptInput()).generate();
+
+ Path apiFile = Paths.get(output + "/api_default.go");
+ // non-standard methods are emitted as string literals; net/http has no
+ // MethodQuery/MethodPurge/MethodCustomMethod constants
+ TestUtils.assertFileContains(apiFile,
+ "localVarHTTPMethod = \"QUERY\"",
+ "localVarHTTPMethod = \"PURGE\"",
+ // additionalOperations keys are sent verbatim - no camelize
+ "localVarHTTPMethod = \"customMethod\"",
+ // valid HTTP token punctuation must not be HTML-escaped
+ "localVarHTTPMethod = \"CHECK&FETCH\"");
+ TestUtils.assertFileNotContains(apiFile, "http.MethodQuery");
+ TestUtils.assertFileNotContains(apiFile, "http.MethodPurge");
+ TestUtils.assertFileNotContains(apiFile, "&");
+ // in:querystring appends the raw, already-encoded value to the path
+ TestUtils.assertFileContains(apiFile,
+ "localVarPath = localVarPath + \"?\" + *r.qs");
+ }
+
+ @Test(description = "prepareRequest keeps a path-embedded raw query string verbatim")
+ public void testOpenAPI32QueryStringPreservedInClient() throws IOException {
+ File output = Files.createTempDirectory("test").toFile();
+ TEMP_DIRS.add(output);
+
+ final CodegenConfigurator configurator = new CodegenConfigurator()
+ .setGeneratorName("go")
+ .setInputSpec("src/test/resources/3_2/query-operation.yaml")
+ .setOutputDir(output.getAbsolutePath().replace("\\", "/"));
+
+ DefaultGenerator generator = new DefaultGenerator();
+ List files = generator.opts(configurator.toClientOptInput()).generate();
+
+ Path clientFile = Paths.get(output + "/client.go");
+ // the full verbatim-preservation chain: the path-embedded raw query is
+ // captured before url.Query() merges it, then written back ahead of the
+ // encoded parameters (path-declared query data first)
+ TestUtils.assertFileContains(clientFile,
+ "rawQueryString := url.RawQuery",
+ "url.RawQuery = \"\"",
+ "url.RawQuery = rawQueryString + encodedQuery");
+ // and the capture must precede the merge into url.Query()
+ String client = new String(Files.readAllBytes(clientFile), java.nio.charset.StandardCharsets.UTF_8);
+ Assert.assertTrue(
+ client.indexOf("rawQueryString := url.RawQuery")
+ < client.indexOf("query := url.Query()"),
+ "raw query must be captured before url.Query() reads it");
+ }
+
+ @Test(description = "in:querystring together with a path parameter imports strings only once")
+ public void testOpenAPI32QueryStringWithPathParamImports() throws IOException {
+ File output = Files.createTempDirectory("test").toFile();
+ TEMP_DIRS.add(output);
+
+ final CodegenConfigurator configurator = new CodegenConfigurator()
+ .setGeneratorName("go")
+ .setInputSpec("src/test/resources/3_2/go-querystring-pathparam.yaml")
+ .setOutputDir(output.getAbsolutePath().replace("\\", "/"));
+
+ DefaultGenerator generator = new DefaultGenerator();
+ List files = generator.opts(configurator.toClientOptInput()).generate();
+
+ Path apiFile = Paths.get(output + "/api_default.go");
+ TestUtils.assertFileContains(apiFile, "strings.Contains(localVarPath, \"?\")");
+ // pathParams already pull in "strings"; a duplicate import would not compile
+ String content = new String(Files.readAllBytes(apiFile), java.nio.charset.StandardCharsets.UTF_8);
+ Assert.assertEquals(content.split("\"strings\"", -1).length - 1, 1,
+ "duplicate \"strings\" import in generated Go file");
+ }
+
+ @Test(description = "OpenAPI 3.2 query/additionalOperations in webhooks also emit verbatim method literals")
+ public void testOpenAPI32WebhookOperations() throws IOException {
+ File output = Files.createTempDirectory("test").toFile();
+ TEMP_DIRS.add(output);
+
+ final CodegenConfigurator configurator = new CodegenConfigurator()
+ .setGeneratorName("go")
+ .setInputSpec("src/test/resources/3_2/webhook-operations.yaml")
+ .setOutputDir(output.getAbsolutePath().replace("\\", "/"));
+
+ DefaultGenerator generator = new DefaultGenerator();
+ List files = generator.opts(configurator.toClientOptInput()).generate();
+
+ // webhooks render through the same api.mustache; their non-standard
+ // methods must be literals too, otherwise the output does not compile
+ Path apiFile = Paths.get(output + "/api_default.go");
+ TestUtils.assertFileContains(apiFile,
+ "localVarHTTPMethod = \"QUERY\"",
+ "localVarHTTPMethod = \"customMethod\"");
+ TestUtils.assertFileNotContains(apiFile, "http.MethodQuery");
+ TestUtils.assertFileNotContains(apiFile, "http.MethodCustommethod");
+ // webhook querystring parameters need the strings import as well
+ TestUtils.assertFileContains(apiFile, "\"strings\"");
+ }
}
diff --git a/modules/openapi-generator/src/test/java/org/openapitools/codegen/java/JavaClientCodegenTest.java b/modules/openapi-generator/src/test/java/org/openapitools/codegen/java/JavaClientCodegenTest.java
index 33e78b1e4847..db12811293a1 100644
--- a/modules/openapi-generator/src/test/java/org/openapitools/codegen/java/JavaClientCodegenTest.java
+++ b/modules/openapi-generator/src/test/java/org/openapitools/codegen/java/JavaClientCodegenTest.java
@@ -5428,6 +5428,69 @@ public void testInsecureTlsHookOmittedWhenDisabled(String library) {
.doesNotContain("import java.security.cert.X509Certificate;");
}
+ @Test
+ public void testQueryOperationAndQueryStringParamGeneration() {
+ final Path output = newTempFolder();
+ final CodegenConfigurator configurator = new CodegenConfigurator()
+ .setGeneratorName(JAVA_GENERATOR) // default library okhttp-gson
+ .setInputSpec("src/test/resources/3_2/query-operation.yaml")
+ .setOutputDir(output.toString().replace("\\", "/"));
+
+ new DefaultGenerator().opts(configurator.toClientOptInput()).generate();
+
+ // the 3.2 'query' operation and the additionalOperations 'PURGE' entry
+ // must both be generated, and the in: querystring parameter is emitted
+ // as the whole (already encoded) query string appended to the path
+ assertThat(output.resolve("src/main/java/org/openapitools/client/api/DefaultApi.java"))
+ .content()
+ .contains("queryPetsCall(")
+ .contains("purgePetsCall(")
+ .contains("\"QUERY\"")
+ .contains("\"PURGE\"")
+ .contains("localVarPath = localVarPath + (localVarPath.contains(\"?\") ? \"&\" : \"?\") + qs;");
+ }
+
+ @Test
+ public void testDynamicOperationsWithQueryAndQueryString() {
+ final Path output = newTempFolder();
+ final CodegenConfigurator configurator = new CodegenConfigurator()
+ .setGeneratorName(JAVA_GENERATOR)
+ .setInputSpec("src/test/resources/3_2/query-operation.yaml")
+ .addAdditionalProperty("dynamicOperations", true)
+ .setOutputDir(output.toString().replace("\\", "/"));
+
+ new DefaultGenerator().opts(configurator.toClientOptInput()).generate();
+
+ // dynamic operation lookup must register the 3.2 'query' operation and
+ // additionalOperations entries, and fillParametersFromOperation must
+ // accept `in: querystring`
+ assertThat(output.resolve("src/main/java/org/openapitools/client/ApiClient.java"))
+ .content()
+ .contains("addOperationLookupEntry(path, \"QUERY\", pathItem.getQuery());")
+ .contains("pathItem.getAdditionalOperations()")
+ .contains("case \"querystring\":");
+ }
+
+ @Test
+ public void testDynamicOperationsWithout32Operations() {
+ final Path output = newTempFolder();
+ final CodegenConfigurator configurator = new CodegenConfigurator()
+ .setGeneratorName(JAVA_GENERATOR)
+ .setInputSpec("src/test/resources/3_0/petstore.yaml")
+ .addAdditionalProperty("dynamicOperations", true)
+ .setOutputDir(output.toString().replace("\\", "/"));
+
+ new DefaultGenerator().opts(configurator.toClientOptInput()).generate();
+
+ // specs without `query`/`additionalOperations` must not emit calls to
+ // PathItem.getQuery()/getAdditionalOperations() - the generated client's
+ // pinned swagger-parser release has no such methods and would not compile
+ assertThat(output.resolve("src/main/java/org/openapitools/client/ApiClient.java"))
+ .content()
+ .doesNotContain("pathItem.getQuery()")
+ .doesNotContain("getAdditionalOperations()");
+ }
+
@Test(dataProvider = "jerseyLibraries")
public void testArrayPathParamSerializedAsCollection(String library) {
Path output = newTempFolder();
diff --git a/modules/openapi-generator/src/test/java/org/openapitools/codegen/k6/K6ClientCodegenTest.java b/modules/openapi-generator/src/test/java/org/openapitools/codegen/k6/K6ClientCodegenTest.java
new file mode 100644
index 000000000000..e7ba03313443
--- /dev/null
+++ b/modules/openapi-generator/src/test/java/org/openapitools/codegen/k6/K6ClientCodegenTest.java
@@ -0,0 +1,86 @@
+package org.openapitools.codegen.k6;
+
+import io.swagger.v3.oas.models.OpenAPI;
+import io.swagger.v3.oas.models.Operation;
+import io.swagger.v3.oas.models.PathItem;
+import io.swagger.v3.oas.models.responses.ApiResponse;
+import io.swagger.v3.oas.models.responses.ApiResponses;
+import org.apache.commons.io.FileUtils;
+import org.openapitools.codegen.ClientOptInput;
+import org.openapitools.codegen.DefaultGenerator;
+import org.openapitools.codegen.TestUtils;
+import org.openapitools.codegen.languages.K6ClientCodegen;
+import org.testng.annotations.Test;
+
+import java.io.File;
+import java.io.IOException;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.util.Map;
+
+public class K6ClientCodegenTest {
+
+ @Test
+ public void testQueryOperationIsSkipped() throws IOException {
+ // generators without supportsAdditionalOperations() must skip OpenAPI 3.2
+ // `query` operations: k6's JS http module has no `query` function, so
+ // emitting them would generate a script that fails at runtime with
+ // `TypeError: http.query is not a function`
+ OpenAPI openAPI = TestUtils.createOpenAPI();
+ openAPI.getPaths().addPathItem("/search",
+ new PathItem()
+ .get(new Operation().operationId("getItems")
+ .responses(new ApiResponses().addApiResponse("200", new ApiResponse().description("OK"))))
+ .query(new Operation().operationId("searchItems")
+ .responses(new ApiResponses().addApiResponse("200", new ApiResponse().description("OK")))));
+
+ Path scriptJs = generate(openAPI);
+ try {
+ TestUtils.assertFileContains(scriptJs, "http.get(");
+ TestUtils.assertFileNotContains(scriptJs, "http.query(", "searchItems");
+ } finally {
+ FileUtils.deleteDirectory(scriptJs.getParent().toFile());
+ }
+ }
+
+ @Test
+ public void testQueryOnlyPathLeavesNoEmptyGroup() throws IOException {
+ // a path declaring only a `query` operation must not leave an empty
+ // group() block behind; the dataextract extension on the second path
+ // additionally exercises the substitute-parameter pre-scan against a
+ // spec containing a skipped path
+ OpenAPI openAPI = TestUtils.createOpenAPI();
+ openAPI.getPaths().addPathItem("/queryOnly",
+ new PathItem().query(new Operation().operationId("searchItems")
+ .responses(new ApiResponses().addApiResponse("200", new ApiResponse().description("OK")))));
+
+ Operation getPets = new Operation().operationId("getPets")
+ .responses(new ApiResponses().addApiResponse("200", new ApiResponse().description("OK")));
+ getPets.addExtension("x-k6-openapi-operation-dataextract",
+ Map.of("operationId", "getPets", "valuePath", "id", "parameterName", "petId"));
+ openAPI.getPaths().addPathItem("/pets", new PathItem().get(getPets));
+
+ Path scriptJs = generate(openAPI);
+ try {
+ TestUtils.assertFileContains(scriptJs, "http.get(");
+ TestUtils.assertFileNotContains(scriptJs, "http.query(", "searchItems", "queryOnly");
+ } finally {
+ FileUtils.deleteDirectory(scriptJs.getParent().toFile());
+ }
+ }
+
+ private Path generate(OpenAPI openAPI) throws IOException {
+ File output = Files.createTempDirectory("test").toFile();
+
+ K6ClientCodegen codegen = new K6ClientCodegen();
+ codegen.setOutputDir(output.getAbsolutePath().replace("\\", "/"));
+
+ ClientOptInput input = new ClientOptInput();
+ input.openAPI(openAPI);
+ input.config(codegen);
+
+ new DefaultGenerator().opts(input).generate();
+
+ return output.toPath().resolve("script.js");
+ }
+}
diff --git a/modules/openapi-generator/src/test/java/org/openapitools/codegen/kotlin/KotlinClientCodegenApiTest.java b/modules/openapi-generator/src/test/java/org/openapitools/codegen/kotlin/KotlinClientCodegenApiTest.java
index da4faa356004..cb9e3ee407ad 100644
--- a/modules/openapi-generator/src/test/java/org/openapitools/codegen/kotlin/KotlinClientCodegenApiTest.java
+++ b/modules/openapi-generator/src/test/java/org/openapitools/codegen/kotlin/KotlinClientCodegenApiTest.java
@@ -17,10 +17,20 @@
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
+import java.nio.file.Path;
import java.nio.file.Paths;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collections;
import java.util.List;
+import java.util.Set;
+import java.util.TreeSet;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
import java.util.stream.Collectors;
+import org.openapitools.codegen.config.CodegenConfigurator;
+
import static org.openapitools.codegen.TestUtils.assertFileContains;
import static org.openapitools.codegen.TestUtils.assertFileNotContains;
@@ -281,8 +291,8 @@ public void testExplodedObjectQueryParameterJvmOkhttp() throws IOException {
// rather than replacing a query parameter of the same name
assertFileContains(defaultApi.toPath(),
"is kotlin.collections.Iterable<*> -> value.toList()",
- "}.filterNotNull().map { parameterToString(it) }",
- "put(name, getOrElse(name) { emptyList() } + values)");
+ "}.filterNotNull().map { this@DefaultApi.parameterToString(it) }",
+ "put(localVariableName, getOrElse(localVariableName) { emptyList() } + localVariableValues)");
// deepObject and form without explode both keep a single parameter
assertFileContains(defaultApi.toPath(),
@@ -334,4 +344,948 @@ private KotlinClientCodegen createCodegen(ClientLibrary library) throws IOExcept
codegen.additionalProperties().put(KotlinClientCodegen.DATE_LIBRARY, "kotlinx-datetime");
return codegen;
}
+
+ @Test
+ void testJvmOkhttp4OpenApi32OperationsAndQueryStringParam() throws IOException {
+ Path target = Files.createTempDirectory("kotlin32");
+ try {
+ generate("jvm-okhttp4", "src/test/resources/3_2/query-operation.yaml", target);
+
+ String api = new String(Files.readAllBytes(
+ target.resolve("src/main/kotlin/org/openapitools/client/apis/DefaultApi.kt")), StandardCharsets.UTF_8);
+ // non-standard methods are emitted verbatim via customMethod; there is no RequestMethod.QUERY
+ for (String method : new String[]{"QUERY", "PURGE", "customMethod", "CHECK&FETCH", "X#Y", "A|B", "REPORT", "PROPPATCH"}) {
+ Assert.assertTrue(api.contains("customMethod = \"" + method + "\""),
+ "expected verbatim customMethod literal for " + method);
+ }
+ // '$' must be escaped so the Kotlin string literal keeps it verbatim
+ Assert.assertTrue(api.contains("customMethod = \"A\\$B\""),
+ "expected $-escaped customMethod literal for A$B");
+ Assert.assertTrue(api.contains("method = RequestMethod.GET"),
+ "standard method kept on the RequestMethod enum");
+ // `in: querystring` is wired verbatim, excluded from the name=value query map
+ Assert.assertTrue(api.contains("encodedQueryString = listOfNotNull(qs).joinToString"),
+ "querystring param should be passed verbatim");
+ Assert.assertFalse(api.contains("put(\"qs\""),
+ "querystring param must not be serialized as a name=value pair");
+ // params named like template-internal locals are renamed, wire names stay
+ Assert.assertTrue(api.contains("fun collidePetsRequestConfig(paramLocalVariableQuery"),
+ "colliding param names must be renamed");
+ Assert.assertTrue(api.contains("put(\"localVariableQuery\", listOf(paramLocalVariableQuery.toString()))"),
+ "renamed param must keep its wire name");
+ Assert.assertTrue(api.contains("encodedQueryString = listOfNotNull(paramLocalVariableQuery).joinToString"),
+ "renamed querystring param must still be wired verbatim");
+
+ String requestConfig = new String(Files.readAllBytes(
+ target.resolve("src/main/kotlin/org/openapitools/client/infrastructure/RequestConfig.kt")), StandardCharsets.UTF_8);
+ Assert.assertTrue(requestConfig.contains("val customMethod: String?"),
+ "RequestConfig should carry the verbatim method field");
+ Assert.assertTrue(requestConfig.contains("val encodedQueryString: String?"),
+ "RequestConfig should carry the querystring field");
+
+ String apiClient = new String(Files.readAllBytes(
+ target.resolve("src/main/kotlin/org/openapitools/client/infrastructure/ApiClient.kt")), StandardCharsets.UTF_8);
+ Assert.assertTrue(apiClient.contains("builder.method(requestConfig.customMethod, customBody)"),
+ "ApiClient should dispatch verbatim methods");
+ Assert.assertTrue(apiClient.contains("encodedQuery("),
+ "ApiClient should append the querystring verbatim");
+ // OkHttp 5 rejects a null body for QUERY/REPORT/PROPPATCH, not just QUERY
+ Assert.assertTrue(apiClient.contains("requestConfig.customMethod in REQUIRES_REQUEST_BODY_METHODS"),
+ "body-required methods must be handled as a set, not just QUERY");
+
+ String docs = new String(Files.readAllBytes(target.resolve("docs/DefaultApi.md")), StandardCharsets.UTF_8);
+ Assert.assertTrue(docs.contains("**A\\|B**"), "doc table should escape |");
+ } finally {
+ deleteRecursively(target);
+ }
+ }
+
+ @Test
+ void testJvmOkhttp4DeepObjectCollisionKeepsSpecWireName() throws IOException {
+ Path target = Files.createTempDirectory("kotlin32-deepobj");
+ try {
+ generate("jvm-okhttp4", "src/test/resources/3_2/kotlin-deep-object-collision.yaml", target);
+ String api = new String(Files.readAllBytes(
+ target.resolve("src/main/kotlin/org/openapitools/client/apis/DefaultApi.kt")), StandardCharsets.UTF_8);
+ // the kotlin parameter is renamed to avoid the localVariableBody local,
+ // but the `name[prop]` wire prefix must stay the spec baseName
+ Assert.assertTrue(api.contains("paramLocalVariableBody:"),
+ "colliding param name must be renamed");
+ Assert.assertTrue(api.contains("put(\"localVariableBody[foo]\""),
+ "deepObject wire prefix must use the spec baseName, not the renamed param");
+ Assert.assertFalse(api.contains("put(\"paramLocalVariableBody[foo]\""),
+ "deepObject wire prefix must not leak the renamed param");
+ Assert.assertFalse(api.contains("put(\"foo[foo]\""),
+ "deepObject wire prefix must not collapse to the property name");
+ // two deepObject params share the same cached property instance;
+ // each must keep its own baseName on the wire (no cross-leak)
+ Assert.assertTrue(api.contains("put(\"localVariableQuery[foo]\""),
+ "second deepObject param must keep its own baseName on the wire");
+ Assert.assertTrue(api.contains("paramLocalVariableQuery:"),
+ "second colliding param name must be renamed");
+ // spelling variants normalize to the same internal local names and
+ // must hit the collision guard, keeping spec baseNames on the wire
+ Assert.assertTrue(api.contains("put(\"local_variable_headers\", listOf(paramLocalVariableHeaders.toString()))"),
+ "snake_case variant must be renamed but keep spec wire name");
+ Assert.assertTrue(api.contains("put(\"LocalVariableQuery\", listOf(paramLocalVariableQuery.toString()))"),
+ "PascalCase variant must be renamed but keep spec wire name");
+ } finally {
+ deleteRecursively(target);
+ }
+ }
+
+ /**
+ * Canary for issue #15: spec parameters named after template-internal locals
+ * (the localVar prefix family) must be renamed while keeping their wire
+ * names, and class members must be qualified with `this.` so same-named
+ * parameters cannot shadow them. Covers every kotlin-client library.
+ */
+ @Test
+ void testKotlinLibrariesAvoidTemplateLocalCollisions() throws IOException {
+ String spec = "src/test/resources/3_0/kotlin/kotlin-member-collision.yaml";
+ String[][] libraries = {
+ // {library, api source path, additionalProperties}
+ {"jvm-vertx", "src/main/kotlin/org/openapitools/client/apis/DefaultApi.kt", "serializationLibrary=jackson"},
+ {"jvm-volley", "src/main/java/org/openapitools/client/apis/DefaultApi.kt", "serializationLibrary=gson"},
+ {"jvm-spring-restclient", "src/main/kotlin/org/openapitools/client/apis/DefaultApi.kt", "useSpringBoot3=true", "serializationLibrary=jackson"},
+ {"jvm-spring-webclient", "src/main/kotlin/org/openapitools/client/apis/DefaultApi.kt", "useSpringBoot3=true", "serializationLibrary=jackson"},
+ {"jvm-ktor", "src/main/kotlin/org/openapitools/client/apis/DefaultApi.kt", "serializationLibrary=jackson"},
+ {"jvm-retrofit2", "src/main/kotlin/org/openapitools/client/apis/DefaultApi.kt", "serializationLibrary=jackson"},
+ {"jvm-okhttp4", "src/main/kotlin/org/openapitools/client/apis/DefaultApi.kt", "serializationLibrary=jackson"},
+ {"multiplatform", "src/commonMain/kotlin/org/openapitools/client/apis/DefaultApi.kt", "dateLibrary=kotlinx-datetime"},
+ };
+ // every library: colliding params are renamed but keep their wire names
+ String[][] renames = {
+ {"localVariableAuthNames", "paramLocalVariableAuthNames"},
+ {"local_variable_body", "paramLocalVariableBody"},
+ {"local_variable_query", "paramLocalVariableQuery"},
+ {"localVariableHeaders", "paramLocalVariableHeaders"},
+ {"local_variable_response", "paramLocalVariableResponse"},
+ };
+ for (String[] lib : libraries) {
+ Path target = Files.createTempDirectory("kotlin-collide-" + lib[0]);
+ try {
+ generate(lib[0], spec, target, Arrays.copyOfRange(lib, 2, lib.length));
+ Path apiFile = target.resolve(lib[1]);
+ Assert.assertTrue(Files.exists(apiFile), lib[0] + " must emit " + lib[1]);
+ String api = new String(Files.readAllBytes(apiFile), StandardCharsets.UTF_8);
+ for (String[] rename : renames) {
+ Assert.assertTrue(api.contains(rename[1] + ":"),
+ lib[0] + ": param " + rename[0] + " must be renamed to " + rename[1]);
+ Assert.assertTrue(api.contains("\"" + rename[0] + "\""),
+ lib[0] + ": wire name " + rename[0] + " must be preserved");
+ }
+ switch (lib[0]) {
+ case "jvm-vertx":
+ // member refs must be qualified so same-named params cannot shadow them
+ for (String member : new String[]{"this.vertx", "this.basePath", "this.apiKey",
+ "this.apiKeyPrefix", "this.username", "this.password", "this.accessToken",
+ "this.handleResponse(", "this.responseBody(", "this.encodeURIComponent(",
+ "this.parseDateToQueryString<"}) {
+ Assert.assertTrue(api.contains(member), "jvm-vertx must qualify " + member);
+ }
+ Assert.assertTrue(api.contains("fun basicAuthCollide(username: kotlin.String?"),
+ "jvm-vertx: spec param names must stay public");
+ Assert.assertTrue(api.contains("vertx?.let { localVariableRequest.queryParams().add(\"vertx\""),
+ "jvm-vertx: vertx param must be wired under its own name");
+ Assert.assertTrue(api.contains("localVariableForm.add(\"form\", form)"),
+ "jvm-vertx: form param must reach the form map");
+ break;
+ case "jvm-volley":
+ for (String member : new String[]{"this.requestFactory", "this.basePath",
+ "this.postProcessors", "this.requestQueue"}) {
+ Assert.assertTrue(api.contains(member), "jvm-volley must qualify " + member);
+ }
+ Assert.assertTrue(api.contains("\"form\" to IRequestFactory.parameterToString(form)"),
+ "jvm-volley: form param must reach the form map");
+ Assert.assertTrue(api.contains("\"request\" to IRequestFactory.parameterToString(request)"),
+ "jvm-volley: request param must reach the request");
+ break;
+ case "jvm-spring-restclient":
+ Assert.assertTrue(api.contains("this.request<"),
+ "jvm-spring-restclient: member request() must be qualified");
+ Assert.assertTrue(api.contains("val localVariableResult ="),
+ "jvm-spring-restclient: result local must be prefixed");
+ Assert.assertTrue(api.contains("val localVariableParams ="),
+ "jvm-spring-restclient: params local must be prefixed");
+ assertBareDateConversion(api, "jvm-spring-restclient");
+ break;
+ case "jvm-spring-webclient":
+ Assert.assertTrue(api.contains("this.request<"),
+ "jvm-spring-webclient: member request() must be qualified");
+ Assert.assertTrue(api.contains("val localVariableParams ="),
+ "jvm-spring-webclient: params local must be prefixed");
+ assertBareDateConversion(api, "jvm-spring-webclient");
+ break;
+ case "jvm-okhttp4":
+ // pre-existing locals keep their localVar* spelling
+ Assert.assertTrue(api.contains("localVarResponse") && api.contains("localVarError"),
+ "jvm-okhttp4: localVarResponse/localVarError must remain");
+ // member calls inside apply{} blocks need a labeled receiver
+ Assert.assertTrue(api.contains("this@DefaultApi.parseDateToQueryString(dueDate)"),
+ "jvm-okhttp4: date conversion must reach the api class inside apply{}");
+ Assert.assertTrue(api.contains("this@DefaultApi.encodeURIComponent(path.toString())"),
+ "jvm-okhttp4: path encoding must reach the api class");
+ break;
+ case "jvm-ktor":
+ case "multiplatform":
+ // request()/jsonRequest()/urlEncodedFormRequest() are
+ // inherited ApiClient members; a spec `request` param
+ // must not shadow them
+ for (String member : new String[]{"this.request(", "this.jsonRequest(",
+ "this.urlEncodedFormRequest("}) {
+ Assert.assertTrue(api.contains(member), lib[0] + " must qualify " + member);
+ }
+ Assert.assertTrue(api.contains("request: kotlin.String?"),
+ lib[0] + ": spec param `request` must keep its name");
+ break;
+ default:
+ break;
+ }
+ } finally {
+ deleteRecursively(target);
+ }
+ }
+ }
+
+ /**
+ * Spring's parseDateToQueryString is a top-level function, and Kotlin
+ * resolves a call site to the function even when a value parameter shares
+ * its name — so no qualification is wanted at all. A package-qualified
+ * call would actually break whenever a parameter is named `org` (the
+ * first segment of the default package), which the fixture exercises.
+ */
+ private void assertBareDateConversion(String api, String lib) {
+ Assert.assertTrue(api.contains("listOf(parseDateToQueryString(dueDate))"),
+ lib + ": date conversion must be an unqualified call");
+ Assert.assertFalse(api.contains("infrastructure.parseDateToQueryString"),
+ lib + ": date conversion must not be package-qualified");
+ Assert.assertFalse(api.contains("this.parseDateToQueryString"),
+ lib + ": date conversion must not be this-qualified");
+ }
+
+ /**
+ * Lint guard for issue #15: every `val`/`var` declared at statement level in
+ * an operation template must use the `localVar` prefix, so spec parameters
+ * (which are renamed by the toParamName prefix rule) can never collide with
+ * template-internal locals again.
+ */
+ @Test
+ void testKotlinApiTemplatesUseLocalVariablePrefix() throws IOException {
+ Path libs = Path.of("src/main/resources/kotlin-client/libraries");
+ Pattern localDecl = Pattern.compile("^\\s*(?:val|var)\\s+([a-zA-Z_]\\w*)");
+ List violations = new ArrayList<>();
+ try (var stream = Files.walk(libs)) {
+ for (Path template : stream.filter(p -> p.getFileName().toString().equals("api.mustache")
+ || p.getFileName().toString().matches("(queryParams|queryParam|explodedQueryParam|pathParams|headerParams|bodyParams|formParams|paramJavadoc)\\.mustache")).toList()) {
+ int lineNo = 0;
+ for (String line : Files.readAllLines(template, StandardCharsets.UTF_8)) {
+ lineNo++;
+ Matcher m = localDecl.matcher(line.replaceAll("\\{\\{[^}]*\\}\\}", ""));
+ if (m.find() && !m.group(1).startsWith("localVar")) {
+ violations.add(template + ":" + lineNo + " declares `" + m.group(1) + "`");
+ }
+ }
+ }
+ }
+ Assert.assertTrue(violations.isEmpty(),
+ "operation-scope locals must use the localVar prefix:\n" + String.join("\n", violations));
+ }
+
+ /**
+ * Lint guard (issue #15, G1): every val/var member inherited from the
+ * library's ApiClient must be referenced as `this.`/`this@` inside the
+ * operation body, otherwise a spec parameter with the same name would
+ * silently shadow the member. Member names are extracted mechanically
+ * from the ApiClient constructor (volley: the api class's own header),
+ * and the same set is checked against api.mustache's {{#operation}}
+ * block plus the partial templates spliced into it. Comments and
+ * mustache tags are stripped first; template lambda bindings use the
+ * localVariable prefix so a bare member name is always a violation.
+ */
+ @Test
+ void testKotlinApiTemplatesQualifyInheritedMembers() throws IOException {
+ Path libs = Path.of("src/main/resources/kotlin-client/libraries");
+ String[][] libraries = {
+ {"jvm-ktor", "infrastructure/ApiClient.kt.mustache"},
+ {"jvm-okhttp", "infrastructure/ApiClient.kt.mustache"},
+ {"jvm-retrofit2", "infrastructure/ApiClient.kt.mustache"},
+ {"jvm-spring-restclient", "infrastructure/ApiClient.kt.mustache"},
+ {"jvm-spring-webclient", "infrastructure/ApiClient.kt.mustache"},
+ {"jvm-vertx", "infrastructure/ApiClient.kt.mustache"},
+ {"jvm-volley", "api.mustache"},
+ {"multiplatform", "infrastructure/ApiClient.kt.mustache"},
+ };
+ Pattern operationBlock = Pattern.compile("\\{\\{#operation\\}\\}(.*)\\{\\{/operation\\}\\}", Pattern.DOTALL);
+ Pattern mustacheTag = Pattern.compile("\\{\\{\\{[^}]*\\}\\}\\}|\\{\\{[^}]*\\}\\}");
+ List violations = new ArrayList<>();
+ for (String[] library : libraries) {
+ Path libDir = libs.resolve(library[0]);
+ Set members = constructorMemberNames(libDir.resolve(library[1]));
+ Assert.assertFalse(members.isEmpty(), library[0] + ": no ApiClient constructor members extracted");
+ List targets = new ArrayList<>();
+ try (var stream = Files.list(libDir)) {
+ for (Path p : stream.filter(p -> p.getFileName().toString().equals("api.mustache")
+ || p.getFileName().toString().matches("(queryParams|queryParam|explodedQueryParam|pathParams|headerParams|bodyParams|formParams|paramJavadoc)\\.mustache")).toList()) {
+ targets.add(p);
+ }
+ }
+ for (Path target : targets) {
+ String source = Files.readString(target, StandardCharsets.UTF_8);
+ // restrict api.mustache to the operation block; partials are
+ // operation-scope by construction
+ if (target.getFileName().toString().equals("api.mustache")) {
+ Matcher op = operationBlock.matcher(source);
+ if (!op.find()) {
+ violations.add(target + ": no {{#operation}} block found");
+ continue;
+ }
+ source = op.group(1);
+ }
+ int lineNo = 0;
+ for (String line : source.split("\n", -1)) {
+ lineNo++;
+ String code = mustacheTag.matcher(line).replaceAll("");
+ String trimmed = code.strip();
+ if (trimmed.startsWith("*") || trimmed.startsWith("//")) {
+ continue;
+ }
+ for (String member : members) {
+ if (Pattern.compile("(? constructorMemberNames(Path template) throws IOException {
+ String source = Files.readString(template, StandardCharsets.UTF_8);
+ Matcher cls = Pattern.compile("class\\s+[^\\s(]+\\s*\\(").matcher(source);
+ if (!cls.find()) {
+ return Collections.emptySet();
+ }
+ int depth = 1;
+ int end = cls.end();
+ while (end < source.length() && depth > 0) {
+ char c = source.charAt(end);
+ if (c == '(') {
+ depth++;
+ } else if (c == ')') {
+ depth--;
+ }
+ end++;
+ }
+ String ctor = source.substring(cls.end(), end - 1)
+ .replaceAll("\\{\\{[^}]*\\}\\}", "");
+ Set names = new TreeSet<>();
+ Matcher m = Pattern.compile("\\b(?:val|var)\\s+([a-zA-Z_]\\w*)").matcher(ctor);
+ while (m.find()) {
+ names.add(m.group(1));
+ }
+ return names;
+ }
+
+ /**
+ * Wire names containing `$` (OData-style `$filter`/`$top`) must be emitted
+ * as escaped Kotlin string literals (`"\$filter"`), otherwise Kotlin
+ * string interpolation turns the wire key into the same-named parameter's
+ * value — or fails compilation when no such variable exists. Covers every
+ * kotlin-client library (jvm-okhttp already escaped; the rest did not).
+ */
+ @Test
+ void testKotlinLibrariesEscapeDollarInWireNames() throws IOException {
+ String spec = "src/test/resources/3_0/kotlin/kotlin-dollar-wire-name.yaml";
+ String[][] libraries = {
+ {"jvm-vertx", "src/main/kotlin/org/openapitools/client/apis/DefaultApi.kt", "serializationLibrary=jackson"},
+ {"jvm-volley", "src/main/java/org/openapitools/client/apis/DefaultApi.kt", "serializationLibrary=gson"},
+ {"jvm-spring-restclient", "src/main/kotlin/org/openapitools/client/apis/DefaultApi.kt", "useSpringBoot3=true", "serializationLibrary=jackson"},
+ {"jvm-spring-webclient", "src/main/kotlin/org/openapitools/client/apis/DefaultApi.kt", "useSpringBoot3=true", "serializationLibrary=jackson"},
+ {"jvm-ktor", "src/main/kotlin/org/openapitools/client/apis/DefaultApi.kt", "serializationLibrary=jackson", "dateLibrary=java8"},
+ {"jvm-retrofit2", "src/main/kotlin/org/openapitools/client/apis/DefaultApi.kt", "serializationLibrary=jackson", "dateLibrary=java8"},
+ {"jvm-okhttp4", "src/main/kotlin/org/openapitools/client/apis/DefaultApi.kt", "serializationLibrary=jackson", "dateLibrary=java8"},
+ {"multiplatform", "src/commonMain/kotlin/org/openapitools/client/apis/DefaultApi.kt", "dateLibrary=kotlinx-datetime"},
+ };
+ for (String[] lib : libraries) {
+ Path target = Files.createTempDirectory("kotlin-dollar-" + lib[0]);
+ try {
+ generate(lib[0], spec, target, Arrays.copyOfRange(lib, 2, lib.length));
+ Path apiFile = target.resolve(lib[1]);
+ Assert.assertTrue(Files.exists(apiFile), lib[0] + " must emit " + lib[1]);
+ String api = new String(Files.readAllBytes(apiFile), StandardCharsets.UTF_8);
+ Assert.assertTrue(api.contains("\\$filter"),
+ lib[0] + ": wire name $filter must be escaped in the Kotlin literal");
+ Assert.assertTrue(api.contains("\\$top"),
+ lib[0] + ": wire name $top must be escaped in the Kotlin literal");
+ // "$top" unescaped could only come from the wire key — no `top`
+ // variable exists in scope, so it cannot be a legitimate
+ // interpolation ("$filter" appears legitimately as the
+ // `filter` param's value-side interpolation)
+ Assert.assertFalse(api.contains("\"$top\""),
+ lib[0] + ": bare \"$top\" means the wire key is unescaped");
+ // the static path `/odata/$count` and the `{$id}` path
+ // placeholder are Kotlin string literals too — `$count`/`$id`
+ // would interpolate (or fail compilation) unless escaped.
+ // retrofit2 drops the leading slash, so match `odata/...`
+ Assert.assertTrue(api.contains("odata/\\$count"),
+ lib[0] + ": static path segment $count must be escaped");
+ // `odata/$count"` with a closing quote can only come from an
+ // unescaped string literal — the KDoc comment shows the path
+ // without quotes (`GET /odata/$count`)
+ Assert.assertFalse(api.contains("odata/$count\""),
+ lib[0] + ": bare odata/$count means the path literal is unescaped");
+ Assert.assertTrue(api.contains("{\\$id}"),
+ lib[0] + ": path placeholder {$id} must be escaped");
+ Assert.assertTrue(api.contains("dollarId"),
+ lib[0] + ": path param $id must be renamed to a valid identifier");
+ // exploded-object libraries wire each property by its baseName;
+ // vertx/volley/spring/multiplatform keep `opts` as a single value
+ switch (lib[0]) {
+ case "jvm-okhttp4":
+ case "jvm-ktor":
+ Assert.assertTrue(api.contains("\\$a"),
+ lib[0] + ": exploded wire name $a must be escaped");
+ break;
+ case "jvm-retrofit2":
+ // the @Query string keeps the escaped wire name, while
+ // the identifier is the sanitized property name —
+ // `"\$a"` inside the string vs `dollarA:` outside it
+ Assert.assertTrue(api.contains("@Query(\"\\$a\") dollarA"),
+ "jvm-retrofit2: exploded wire name $a must stay escaped in @Query "
+ + "and the identifier must be the sanitized name");
+ Assert.assertFalse(api.contains("\\$a:"),
+ "jvm-retrofit2: `\\$a` is not a valid Kotlin identifier");
+ break;
+ default:
+ Assert.assertTrue(api.contains("opts"),
+ lib[0] + ": object query param must still be emitted");
+ break;
+ }
+ } finally {
+ deleteRecursively(target);
+ }
+ }
+ }
+
+ /**
+ * multiplatform wraps array/map responses via a receiver lambda
+ * `.map { value }`; a spec parameter named `value` shadows the
+ * receiver's member, so the decoded field must be referenced as
+ * `this.value`.
+ */
+ @Test
+ void testMultiplatformWrapMapUsesExplicitReceiver() throws IOException {
+ Path target = Files.createTempDirectory("kotlin-mp-value");
+ try {
+ generate("multiplatform", "src/test/resources/3_0/kotlin/kotlin-receiver-value.yaml",
+ target, "dateLibrary=kotlinx-datetime");
+ String api = new String(Files.readAllBytes(
+ target.resolve("src/commonMain/kotlin/org/openapitools/client/apis/DefaultApi.kt")),
+ StandardCharsets.UTF_8);
+ Assert.assertTrue(api.contains("`value`: kotlin.String?"),
+ "the spec `value` param must keep its (backticked) name");
+ Assert.assertTrue(api.contains("wrap