) 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..b370c5aeb0ea 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
+ protected 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 27e6ab27ffab..52830b24ae60 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;
@@ -589,6 +592,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
+ protected 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) {
@@ -596,7 +623,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 6a3481fad6f4..8e43c43fe4d3 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,91 @@ 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
+ protected 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("#", "\\#"));
+ }
+ }
+
+ /**
+ * For libraries without querystring support the verbatim-concat template path
+ * must not run; clearing the flag degrades the parameter to an ordinary
+ * name=value query parameter (same shape as generators without 3.2 support).
+ */
+ private void degradeQueryStringParams(List operationList) {
+ if (supportsQueryStringParameters()) {
+ return;
+ }
+ for (CodegenOperation op : operationList) {
+ // queryParams holds copies (p.copy()), so the flag must be cleared
+ // on both lists or the parameter vanishes from generated code entirely
+ for (CodegenParameter p : op.allParams) {
+ if (p.isQueryStringParam) {
+ p.isQueryStringParam = false;
+ p.isQueryParam = true;
+ }
+ }
+ for (CodegenParameter p : op.queryParams) {
+ if (p.isQueryStringParam) {
+ p.isQueryStringParam = false;
+ p.isQueryParam = true;
+ }
+ }
+ }
+ }
+
+ @Override
+ public WebhooksMap postProcessWebhooksWithModels(WebhooksMap objs, List allModels) {
+ WebhooksMap map = super.postProcessWebhooksWithModels(objs, allModels);
+ flagVerbatimHttpMethods(map.getWebhooks().getOperation());
+ degradeQueryStringParams(map.getWebhooks().getOperation());
+ return map;
+ }
+
@Override
public OperationsMap postProcessOperationsWithModels(OperationsMap objs, List allModels) {
objs = super.postProcessOperationsWithModels(objs, allModels);
@@ -610,6 +696,8 @@ public OperationsMap postProcessOperationsWithModels(OperationsMap objs, List processedModelMaps = new HashMap<>();
List operationList = operations.getOperation();
+ flagVerbatimHttpMethods(operationList);
+ degradeQueryStringParams(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 e9d42056baec..daf13d1d61f6 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;
@@ -780,11 +781,98 @@ 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
+ protected 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);
+ // `|` is a valid RFC 9110 tchar but breaks markdown tables in doc templates
+ operation.vendorExtensions.put("x-rust-http-method-doc", method.replace("|", "\\|"));
+ }
+ }
+
+ @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`.
@@ -919,6 +1007,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
+ protected 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("|", "\\|"));
+ 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;
+ }
+ 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);
+ }
+ // 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);
@@ -1564,6 +1641,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;
@@ -1634,6 +1712,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 9519ce13c75e..53fbdcae94e2 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 c0fea7cafc3d..39796aefc02d 100644
--- a/modules/openapi-generator/src/main/resources/go/client.mustache
+++ b/modules/openapi-generator/src/main/resources/go/client.mustache
@@ -388,6 +388,11 @@ 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 append it verbatim after encoding.
+ rawQueryString := url.RawQuery
+ url.RawQuery = ""
query := url.Query()
for k, v := range queryParams {
for _, iv := range v {
@@ -396,11 +401,18 @@ 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 != "" {
+ encodedQuery += "&"
+ }
+ encodedQuery += rawQueryString
+ }
+ 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..12f292db1321 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())
+ 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}},
+ "{{{path}}}"{{#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-okhttp/api.mustache b/modules/openapi-generator/src/main/resources/kotlin-client/libraries/jvm-okhttp/api.mustache
index 529bfc926130..04bc1ed1b816 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,17 +209,18 @@ import {{packageName}}.infrastructure.Serializer
{{/hasQueryParams}}{{#hasQueryParams}}mutableMapOf>()
.apply {
{{#queryParams}}
+ {{^isQueryStringParam}}
{{^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}}
@@ -229,12 +230,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}}
@@ -242,15 +243,16 @@ 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}}
+ {{/isQueryStringParam}}
{{/queryParams}}
}
{{/hasQueryParams}}
@@ -262,9 +264,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 = "{{{path}}}"{{#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..6cf5be1aba6d 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.",
@@ -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/explodedQueryParam.mustache b/modules/openapi-generator/src/main/resources/kotlin-client/libraries/jvm-retrofit2/explodedQueryParam.mustache
index 3aaa4e67fbf4..ac18b37a3d6e 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}}") {{#lambda.escapeDollar}}{{{baseName}}}{{/lambda.escapeDollar}}: {{#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..3b6cfd7da3a9 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,
+ 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..a0bed1afa692 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,
+ 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..43a08323497a 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,87 @@ 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}{{{path}}}"{{#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}}) }
+ {{{paramName}}}{{^required}}?{{/required}}.let { localVariableForm.{{#isMultipart}}attribute{{/isMultipart}}{{^isMultipart}}add{{/isMultipart}}("{{#lambda.escapeDollar}}{{{baseName}}}{{/lambda.escapeDollar}}", {{{paramName}}}{{#isEnum}}{{^required}}?{{/required}}.value{{/isEnum}}{{^isString}}.toString(){{/isString}}) }
{{/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..f8ba57fa727d 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 = "{{{path}}}"{{#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..2a1a4664828e 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}},
+ "{{{path}}}"{{#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/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 514be3b69a85..44904a3d7e84 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):
@@ -349,6 +350,7 @@ https://github.com/OpenAPITools/openapi-generator/blob/c84b949df1a9ec04ba75989cb
{{^isDateTime}}{{^isDate}}
_query_params.append(('{{baseName}}', {{paramName}}{{#isEnumRef}}.value{{/isEnumRef}}))
{{/isDate}}{{/isDateTime}}
+{{/isQueryStringParam}}
{{/queryParams}}
# process the header parameters
{{#headerParams}}
@@ -426,9 +428,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 874d9cd3efef..7d2f41f93028 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..0dde824d1d75 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.upper() in self._STANDARD_METHODS:
+ return self.pool_manager.request(method, url, **kwargs)
+ if method 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,8 +212,13 @@ class RESTClientObject:
timeout. It can also be a pair (tuple) of
(connection, read) timeouts.
"""
- method = method.upper()
- assert method in [
+ # OpenAPI 3.2 allows arbitrary HTTP method names (query operations,
+ # additionalOperations keys). Keep the historic upper()+whitelist for
+ # the standard set; anything else is validated as an HTTP token
+ # (RFC 9110 tchar) and sent verbatim so casing like 'customMethod'
+ # survives
+ _upper_method = method.upper()
+ if _upper_method in [
'GET',
'HEAD',
'DELETE',
@@ -204,7 +226,10 @@ class RESTClientObject:
'PUT',
'PATCH',
'OPTIONS'
- ]
+ ]:
+ method = _upper_method
+ else:
+ assert re.fullmatch(r"[!#$%&'*+\-.^_`|~0-9A-Za-z]+", method) is not None
if post_params and body:
raise ApiValueError(
@@ -228,8 +253,9 @@ class RESTClientObject:
)
try:
- # For `POST`, `PUT`, `PATCH`, `OPTIONS`, `DELETE`
- if method in ['POST', 'PUT', 'PATCH', 'OPTIONS', 'DELETE']:
+ # For `POST`, `PUT`, `PATCH`, `OPTIONS`, `DELETE` - or any
+ # OpenAPI 3.2 method that actually carries a body/form data
+ if method in ['POST', 'PUT', 'PATCH', 'OPTIONS', 'DELETE'] or body is not None or post_params:
content_type = headers.get('Content-Type')
is_json = (
@@ -252,7 +278,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 +287,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 +303,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 +316,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 +326,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 +341,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 97f38c704126..1e1b964a6ee6 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
@@ -183,8 +195,10 @@ module {{moduleName}}
query_params[:'{{{baseName}}}'] = {{#collectionFormat}}@api_client.build_collection_param({{{paramName}}}, :{{{collectionFormat}}}){{/collectionFormat}}{{^collectionFormat}}{{{paramName}}}{{/collectionFormat}}
{{/queryIsJsonMimeType}}
{{/required}}
+ {{/isQueryStringParam}}
{{/queryParams}}
{{#queryParams}}
+ {{^isQueryStringParam}}
{{^required}}
{{#queryIsJsonMimeType}}
query_params[:'{{{baseName}}}'] = opts[:'{{{paramName}}}'].to_json if !opts[:'{{{paramName}}}'].nil?
@@ -193,6 +207,7 @@ module {{moduleName}}
query_params[:'{{{baseName}}}'] = {{#collectionFormat}}@api_client.build_collection_param(opts[:'{{{paramName}}}'], :{{{collectionFormat}}}){{/collectionFormat}}{{^collectionFormat}}opts[:'{{{paramName}}}']{{/collectionFormat}} if !opts[:'{{{paramName}}}'].nil?
{{/queryIsJsonMimeType}}
{{/required}}
+ {{/isQueryStringParam}}
{{/queryParams}}
# header parameters
@@ -251,7 +266,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 5224b0c4d9b5..afcb2f362790 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}}" {
@@ -290,6 +307,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 7fa306995134..263aa0ad96e0 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}}
@@ -217,6 +218,7 @@ export class {{classname}} extends runtime.BaseAPI {
}
{{/isArray}}
+ {{/isQueryStringParam}}
{{/queryParams}}
const headerParameters: runtime.HTTPHeaders = {};
@@ -338,6 +340,17 @@ 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
+ if (requestParameters['{{paramName}}'] != null && requestParameters['{{paramName}}'] !== '') {
+ urlPath += (urlPath.includes('?') ? '&' : '?') + requestParameters['{{paramName}}'];
+ }
+ {{/isQueryStringParam}}
+ {{/queryParams}}
+ {{/vendorExtensions.x-ts-has-querystring-param}}
{{#hasContentTypeRequestVariants}}
{{! `contentType` selects the body's type, so narrow on it before serialising }}
@@ -362,7 +375,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,
@@ -372,7 +390,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 ec58f037d84e..af1a61103092 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,7 @@ 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);
+ url += (url.includes('?') ? '&' : '?') + this.configuration.queryParamsStringify(context.query);
}
const headers = Object.assign({}, this.configuration.headers, context.headers);
@@ -310,7 +310,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 e94a6d5e5e72..48187ec28298 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,93 @@ 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 testOriginalOpenApiDocumentVersion() {
// Test with OAS 2.0 document.
@@ -2140,6 +2228,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 +2670,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..891c68fc7fdf 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,66 @@ 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/"));
+ }
+
@Test
public void testInlineSchemaNameMapping() {
OpenAPI openAPI = TestUtils.parseSpec("src/test/resources/3_0/inline_model_resolver.yaml");
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..d50969cb6c4f 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,40 @@ 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);
+ }
+
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..e782c03df6a0 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,16 @@
package org.openapitools.codegen.config;
+import ch.qos.logback.classic.spi.ILoggingEvent;
+import ch.qos.logback.core.read.ListAppender;
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,7 +33,12 @@
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) {
@@ -133,4 +142,107 @@ public void resolvesResponses() {
Assertions.assertNotNull(context.getSpecDocument().getPaths().get("/hello").getGet().getResponses().get("200").getContent());
}
+
+ // 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 = new ListAppender<>();
+ listAppender.start();
+ logger.addAppender(listAppender);
+
+ 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 shouldNotFalsePositiveOnNestedPathItemMemberTypo() {
+ ch.qos.logback.classic.Logger logger =
+ (ch.qos.logback.classic.Logger) LoggerFactory.getLogger(CodegenConfigurator.class);
+ ListAppender listAppender = new ListAppender<>();
+ listAppender.start();
+ logger.addAppender(listAppender);
+
+ 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 shouldFailWithClearMessageAndNoMisleadingWarningWhenSpecificationIsNull() {
+ ch.qos.logback.classic.Logger logger =
+ (ch.qos.logback.classic.Logger) LoggerFactory.getLogger(CodegenConfigurator.class);
+ ListAppender listAppender = new ListAppender<>();
+ listAppender.start();
+ logger.addAppender(listAppender);
+
+ 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/csharpnetcore/CSharpClientCodegenTest.java b/modules/openapi-generator/src/test/java/org/openapitools/codegen/csharpnetcore/CSharpClientCodegenTest.java
index f9de3e8af006..221d9c33e054 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;
@@ -637,4 +641,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/go-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 6b04bf8e8703..0ceedc76d2a9 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");
@@ -144,7 +159,7 @@ public void testPrimitiveTypeInOneOf() 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")
@@ -153,7 +168,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");
}
@@ -161,7 +175,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")
@@ -171,7 +185,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\")}");
@@ -183,7 +196,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")
@@ -193,7 +206,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");
}
@@ -205,7 +217,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")
@@ -215,7 +227,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");
@@ -241,7 +252,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")
@@ -250,7 +261,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"),
@@ -260,7 +270,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")
@@ -271,7 +281,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"),
@@ -281,7 +290,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")
@@ -290,7 +299,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"),
@@ -300,7 +308,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")
@@ -311,7 +319,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"),
@@ -325,7 +332,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")
@@ -336,7 +343,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"),
@@ -346,7 +352,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")
@@ -357,7 +363,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"),
@@ -367,7 +372,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")
@@ -378,7 +383,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"),
@@ -391,7 +395,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")
@@ -401,7 +405,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");
}
@@ -409,7 +412,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")
@@ -419,7 +422,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);
@@ -430,7 +432,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")
@@ -441,7 +443,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);
@@ -456,7 +457,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")
@@ -466,7 +467,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");
}
@@ -474,7 +474,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")
@@ -483,7 +483,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\"}";
@@ -496,7 +495,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")
@@ -505,7 +504,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
@@ -522,7 +520,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")
@@ -532,7 +530,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).
@@ -545,7 +542,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")
@@ -554,7 +551,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,
@@ -562,4 +558,98 @@ 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");
+ TestUtils.assertFileContains(clientFile, "rawQueryString := url.RawQuery");
+ }
+
+ @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/go-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 4b5f72bf9fb1..e51fac451030 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
@@ -5259,6 +5259,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()");
+ }
+
private static Path generateJerseyClient(String library, Boolean generateInsecureTlsHook) {
Path output = newTempFolder();
CodegenConfigurator configurator = new CodegenConfigurator()
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 1bad07e5acf0..bdf207b9e6aa 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
@@ -15,11 +15,22 @@
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.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;
@@ -292,4 +303,787 @@ 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");
+ } 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().map { this.value }"),
+ "array response must dereference the wrapper's value via explicit receiver");
+ Assert.assertTrue(api.contains("wrap().map { this.value }"),
+ "map response must dereference the wrapper's value via explicit receiver");
+ Assert.assertFalse(api.contains(".map { value }"),
+ "bare `value` inside map{} resolves to the spec param, not the wrapper field");
+ } finally {
+ deleteRecursively(target);
+ }
+ }
+
+ /**
+ * ktor/multiplatform build form bodies inside `ParametersBuilder().also {}`,
+ * whose implicit `it` is the builder. A form field named `it` shadows the
+ * implicit parameter and `it.append(...)` resolves against the String
+ * parameter instead of the builder — a compile error. The builder must be
+ * bound to an explicit localVariable name.
+ */
+ @Test
+ void testKtorMultiplatformFormBuilderDoesNotUseImplicitIt() throws IOException {
+ String spec = "src/test/resources/3_0/kotlin/kotlin-form-it-param.yaml";
+ String[][] libraries = {
+ {"jvm-ktor", "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-formit-" + lib[0]);
+ try {
+ generate(lib[0], spec, target, Arrays.copyOfRange(lib, 2, lib.length));
+ String api = new String(Files.readAllBytes(target.resolve(lib[1])), StandardCharsets.UTF_8);
+ Assert.assertTrue(api.contains("`it`: kotlin.String?"),
+ lib[0] + ": the spec `it` param must keep its (backticked) name");
+ Assert.assertTrue(api.contains("localVariableBuilder.append(\"it\""),
+ lib[0] + ": form field `it` must be appended via the named builder");
+ Assert.assertFalse(api.contains("it.append("),
+ lib[0] + ": implicit `it` is shadowed by the spec param inside also{}");
+ } finally {
+ deleteRecursively(target);
+ }
+ }
+ }
+
+ /**
+ * File parts in a multipart body are appended as whole FormPart values.
+ * multiplatform embedded the wire name as a bare expression
+ * (`append(my-file)` — a syntax error), and jvm-ktor's array loop
+ * appended the undefined implicit `it` instead of the loop variable.
+ * The whole FormPart carries its own key, so append the parameter
+ * (or loop variable) itself.
+ */
+ @Test
+ void testKtorMultiplatformMultipartFileAppendUsesParameter() throws IOException {
+ String spec = "src/test/resources/3_0/kotlin/kotlin-multipart-file.yaml";
+ String[][] libraries = {
+ {"jvm-ktor", "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-mpfile-" + lib[0]);
+ try {
+ generate(lib[0], spec, target, Arrays.copyOfRange(lib, 2, lib.length));
+ String api = new String(Files.readAllBytes(target.resolve(lib[1])), StandardCharsets.UTF_8);
+ Assert.assertTrue(api.contains("append(myFile)"),
+ lib[0] + ": file part must append the FormPart parameter");
+ Assert.assertFalse(api.contains("append(my-file)"),
+ lib[0] + ": wire name must not be embedded as a bare expression");
+ if (lib[0].equals("jvm-ktor")) {
+ Assert.assertTrue(api.contains("for (x in files ?: listOf()) {\n append(x)"),
+ "jvm-ktor: file array must append the loop variable");
+ }
+ if (lib[0].equals("multiplatform")) {
+ Assert.assertTrue(api.contains("files?.onEach {\n append(it)"),
+ "multiplatform: file array appends each FormPart element");
+ }
+ } finally {
+ deleteRecursively(target);
+ }
+ }
+ }
+
+ @Test
+ void testNonOkhttpLibrariesSkipOpenApi32Operations() throws IOException {
+ Path target = Files.createTempDirectory("kotlin32-skip");
+ try {
+ generate("jvm-ktor", "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);
+ Assert.assertTrue(api.contains("listPets"), "GET operation should be kept");
+ for (String op : new String[]{"queryPets", "purgePets", "customPets", "checkFetchPets", "hashPets", "pipePets", "dollarPets", "reportItems", "propPatch", "searchItems"}) {
+ Assert.assertFalse(api.contains(op + "RequestConfig"),
+ "jvm-ktor must skip unsupported 3.2 operation " + op);
+ }
+ } finally {
+ deleteRecursively(target);
+ }
+ }
+
+ @Test
+ void testJvmOkhttp4SkipsInvalidMethodToken() throws IOException {
+ Path target = Files.createTempDirectory("kotlin32-invalid");
+ try {
+ // "MY METHOD" is not a valid RFC 9110 token; okhttp does not validate
+ // tokens itself, so the generator must reject it
+ 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);
+ generate("jvm-okhttp4", specFile.toString(), target.resolve("out"));
+ String api = new String(Files.readAllBytes(
+ target.resolve("out/src/main/kotlin/org/openapitools/client/apis/DefaultApi.kt")), StandardCharsets.UTF_8);
+ Assert.assertTrue(api.contains("listPets"), "GET operation should be kept");
+ Assert.assertFalse(api.contains("badMethod"),
+ "invalid RFC 9110 method token must be skipped");
+ } finally {
+ deleteRecursively(target);
+ }
+ }
+
+ /**
+ * End-to-end check: compiles the generated jvm-okhttp4 client with kotlinc and
+ * runs a raw ServerSocket capture, verifying query/additionalOperations methods
+ * and `in: querystring` reach the wire verbatim. Skipped when kotlinc is not on
+ * PATH or the dependency jars cannot be located.
+ */
+ @Test
+ void testJvmOkhttp4GeneratedClientSendsVerbatimMethods() throws IOException, InterruptedException {
+ Path kotlinc = requireKotlinc();
+ List jars = captureDepJars(kotlinc);
+
+ Path target = Files.createTempDirectory("kotlin32-verify");
+ try {
+ generate("jvm-okhttp4", "src/test/resources/3_2/query-operation.yaml", target);
+ Path srcDir = target.resolve("src/main/kotlin");
+ Path capture = target.resolve("Capture.kt");
+ Files.copy(Path.of("src/test/resources/3_2/kotlin-okhttp-capture/Capture.kt"), capture);
+
+ List sources = Files.walk(srcDir)
+ .filter(p -> p.toString().endsWith(".kt"))
+ .map(Path::toString)
+ .collect(Collectors.toList());
+ sources.add(capture.toString());
+
+ String classPath = String.join(File.pathSeparator, jars);
+ Path classesDir = target.resolve("classes");
+ List compile = new ArrayList<>(List.of(
+ kotlinc.toString(), "-cp", classPath, "-d", classesDir.toString(), "-jvm-target", "17"));
+ compile.addAll(sources);
+ runProcess(target, "kotlinc.log", 300, compile.toArray(new String[0]));
+
+ String javaBin = Path.of(System.getProperty("java.home"), "bin", "java").toString();
+ String output = runProcess(target, "run.log", 120,
+ javaBin, "-cp", classesDir + File.pathSeparator + classPath, "CaptureKt");
+ Assert.assertTrue(output.contains("CAPTURE-PASS"),
+ "generated client did not send verbatim 3.2 methods/querystring:\n" + output);
+ } finally {
+ deleteRecursively(target);
+ }
+ }
+
+ /**
+ * Issue #15 regression: a spec parameter named `org` breaks a
+ * package-qualified parseDateToQueryString call (the qualifier resolves
+ * against the parameter, not the package). The call is intentionally
+ * unqualified — Kotlin prefers the function over a value parameter at a
+ * call site — and both spring libraries must compile as-is with kotlinc,
+ * alongside colliding parameters (`org`, `parseDateToQueryString`,
+ * `request`, ...). Skipped when kotlinc or the copied deps are missing.
+ */
+ @Test
+ void testJvmSpringGeneratedClientsCompileWithCollidingParams() throws IOException, InterruptedException {
+ Path kotlinc = requireKotlinc();
+ List jars = captureDepJars(kotlinc);
+ String classPath = String.join(File.pathSeparator, jars);
+
+ String[][] libraries = {
+ {"jvm-spring-restclient", "serializationLibrary=jackson", "useSpringBoot3=true"},
+ {"jvm-spring-webclient", "serializationLibrary=jackson", "useSpringBoot3=true"},
+ };
+ for (String[] lib : libraries) {
+ Path target = Files.createTempDirectory("kotlin-spring-compile-" + lib[0]);
+ try {
+ generate(lib[0], "src/test/resources/3_0/kotlin/kotlin-member-collision.yaml",
+ target, Arrays.copyOfRange(lib, 1, lib.length));
+ List sources = Files.walk(target.resolve("src/main/kotlin"))
+ .filter(p -> p.toString().endsWith(".kt"))
+ .map(Path::toString)
+ .collect(Collectors.toList());
+ Assert.assertFalse(sources.isEmpty(), lib[0] + " produced no kotlin sources");
+ List compile = new ArrayList<>(List.of(
+ kotlinc.toString(), "-cp", classPath, "-d",
+ target.resolve("classes").toString(), "-jvm-target", "17"));
+ compile.addAll(sources);
+ runProcess(target, "kotlinc.log", 300, compile.toArray(new String[0]));
+ } finally {
+ deleteRecursively(target);
+ }
+ }
+ }
+
+ /**
+ * Issue #17 wire check: jvm-vertx filled a `localVariableForm` MultiMap but
+ * never attached it to the request — form fields were silently dropped.
+ * Compiles the generated vertx client with kotlinc and captures the raw
+ * HTTP request, asserting the urlencoded form body reaches the wire.
+ * Skipped when kotlinc or the copied vertx deps are unavailable.
+ */
+ @Test
+ void testJvmVertxGeneratedClientSendsFormFields() throws IOException, InterruptedException {
+ Path kotlinc = requireKotlinc();
+ List jars = vertxCaptureDepJars(kotlinc);
+
+ Path target = Files.createTempDirectory("kotlin-vertx-form");
+ try {
+ generate("jvm-vertx", "src/test/resources/3_0/kotlin/kotlin-form-it-param.yaml",
+ target, "serializationLibrary=jackson");
+ List sources = Files.walk(target.resolve("src/main/kotlin"))
+ .filter(p -> p.toString().endsWith(".kt"))
+ .map(Path::toString)
+ .collect(Collectors.toList());
+ Path capture = target.resolve("Capture.kt");
+ Files.copy(Path.of("src/test/resources/3_0/kotlin-vertx-capture/Capture.kt"), capture);
+ sources.add(capture.toString());
+
+ String classPath = String.join(File.pathSeparator, jars);
+ Path classesDir = target.resolve("classes");
+ List compile = new ArrayList<>(List.of(
+ kotlinc.toString(), "-cp", classPath, "-d", classesDir.toString(), "-jvm-target", "17"));
+ compile.addAll(sources);
+ runProcess(target, "kotlinc.log", 300, compile.toArray(new String[0]));
+
+ String javaBin = Path.of(System.getProperty("java.home"), "bin", "java").toString();
+ String output = runProcess(target, "run.log", 120,
+ javaBin, "-cp", classesDir + File.pathSeparator + classPath, "CaptureKt");
+ Assert.assertTrue(output.contains("CAPTURE-PASS"),
+ "generated vertx client did not send the form fields:\n" + output);
+ } finally {
+ deleteRecursively(target);
+ }
+ }
+
+ /**
+ * vertx jars are not part of the build's dependency graph, so they are
+ * located directly in the local repository (~/.m2) — matching the
+ * environment note in issue #17. Skips when absent.
+ */
+ private static List vertxCaptureDepJars(Path kotlinc) throws IOException {
+ Path m2 = Path.of(System.getProperty("user.home"), ".m2", "repository");
+ List jars = new ArrayList<>();
+ for (String group : new String[]{"io/vertx", "io/netty", "com/fasterxml/jackson",
+ "org/jetbrains/kotlinx/kotlinx-coroutines-core-jvm"}) {
+ Path dir = m2.resolve(group);
+ if (!Files.isDirectory(dir)) {
+ continue;
+ }
+ try (var stream = Files.walk(dir)) {
+ stream.filter(p -> p.toString().endsWith(".jar"))
+ .filter(p -> !p.getFileName().toString().contains("sources"))
+ .filter(p -> !p.getFileName().toString().contains("javadoc"))
+ .forEach(p -> jars.add(p.toAbsolutePath().toString()));
+ }
+ }
+ if (jars.size() < 15 || jars.stream().noneMatch(j -> j.contains("vertx-web-client"))) {
+ throw new org.testng.SkipException("vertx jars not found in " + m2 + ", found " + jars.size());
+ }
+ Path kotlincLib = kotlinc.getParent().getParent().resolve("lib");
+ Path stdlib = kotlincLib.resolve("kotlin-stdlib.jar");
+ if (!Files.exists(stdlib)) {
+ throw new org.testng.SkipException("kotlinc lib dir lacks kotlin-stdlib.jar: " + kotlincLib);
+ }
+ jars.add(stdlib.toAbsolutePath().toString());
+ return jars;
+ }
+
+ private static Path requireKotlinc() throws IOException {
+ Path kotlinc = findOnPath("kotlinc");
+ if (kotlinc == null) {
+ throw new org.testng.SkipException("kotlinc is not on PATH; skipping generated-client verification");
+ }
+ // PATH may hold a symlink into the install; resolve it so lib/ is found correctly
+ return kotlinc.toRealPath();
+ }
+
+ private static List captureDepJars(Path kotlinc) {
+ // okhttp5/moshi/spring jars are copied out-of-band by
+ // maven-dependency-plugin (kotlin-capture-deps): they carry Kotlin
+ // 1.8+/2.x metadata that the embedded 1.6 compiler in KotlinTestUtils
+ // cannot read, so they must never sit on the shared test classpath
+ Path depDir = Path.of("target/kotlin-capture-deps");
+ List jars;
+ try (var stream = Files.list(depDir)) {
+ jars = stream.filter(p -> p.toString().endsWith(".jar"))
+ .map(p -> p.toAbsolutePath().toString())
+ .collect(Collectors.toCollection(ArrayList::new));
+ } catch (IOException e) {
+ throw new org.testng.SkipException("kotlin-capture-deps missing (dependency:copy did not run): " + e);
+ }
+ if (jars.size() < 5) {
+ throw new org.testng.SkipException("expected okhttp/okio/moshi/spring jars in " + depDir + ", found " + jars);
+ }
+ // use the stdlib/reflect bundled with the detected kotlinc so versions match
+ // the compiler (the module's own test classpath pins an older kotlin.version)
+ Path kotlincLib = kotlinc.getParent().getParent().resolve("lib");
+ for (String name : new String[]{"kotlin-stdlib.jar", "kotlin-reflect.jar"}) {
+ Path jar = kotlincLib.resolve(name);
+ if (!Files.exists(jar)) {
+ throw new org.testng.SkipException("kotlinc lib dir lacks " + name + ": " + kotlincLib);
+ }
+ jars.add(jar.toString());
+ }
+ return jars;
+ }
+
+ private static void generate(String library, String spec, Path outputDir, String... additionalProperties) {
+ final CodegenConfigurator configurator = new CodegenConfigurator()
+ .setGeneratorName("kotlin")
+ .setLibrary(library)
+ .setInputSpec(spec)
+ .setSkipOverwrite(false)
+ .setOutputDir(outputDir.toAbsolutePath().toString().replace("\\", "/"));
+ for (String kv : additionalProperties) {
+ int eq = kv.indexOf('=');
+ configurator.addAdditionalProperty(kv.substring(0, eq), kv.substring(eq + 1));
+ }
+ new DefaultGenerator().opts(configurator.toClientOptInput()).generate();
+ }
+
+ private static Path findOnPath(String executable) {
+ for (String dir : System.getenv("PATH").split(File.pathSeparator)) {
+ for (String name : new String[]{executable, executable + ".bat", executable + ".exe"}) {
+ Path candidate = Path.of(dir, name);
+ if (Files.isExecutable(candidate)) {
+ return candidate;
+ }
+ }
+ }
+ return null;
+ }
+
+ private static String runProcess(Path workDir, String logName, long timeoutSeconds, String... command)
+ throws IOException, InterruptedException {
+ Path log = workDir.resolve(logName);
+ Process p = new ProcessBuilder(command)
+ .directory(workDir.toFile())
+ .redirectErrorStream(true)
+ .redirectOutput(log.toFile())
+ .start();
+ if (!p.waitFor(timeoutSeconds, java.util.concurrent.TimeUnit.SECONDS)) {
+ p.destroyForcibly();
+ Assert.fail("process timed out: " + String.join(" ", command)
+ + "\n" + new String(Files.readAllBytes(log), StandardCharsets.UTF_8));
+ }
+ String output = new String(Files.readAllBytes(log), StandardCharsets.UTF_8);
+ Assert.assertEquals(p.exitValue(), 0, "process failed: " + String.join(" ", command) + "\n" + output);
+ return output;
+ }
+
+ private static void deleteRecursively(Path dir) throws IOException {
+ if (Files.exists(dir)) {
+ try (var stream = Files.walk(dir)) {
+ stream.sorted(java.util.Comparator.reverseOrder())
+ .forEach(p -> p.toFile().delete());
+ }
+ }
+ }
}
diff --git a/modules/openapi-generator/src/test/java/org/openapitools/codegen/php/PhpClientCodegenTest.java b/modules/openapi-generator/src/test/java/org/openapitools/codegen/php/PhpClientCodegenTest.java
index 7ffd48a1fd09..ef975c7614b7 100644
--- a/modules/openapi-generator/src/test/java/org/openapitools/codegen/php/PhpClientCodegenTest.java
+++ b/modules/openapi-generator/src/test/java/org/openapitools/codegen/php/PhpClientCodegenTest.java
@@ -23,14 +23,19 @@
import io.swagger.v3.oas.models.media.ObjectSchema;
import io.swagger.v3.oas.models.media.StringSchema;
import io.swagger.v3.parser.core.models.ParseOptions;
+import org.apache.commons.io.FileUtils;
import org.openapitools.codegen.*;
+import org.openapitools.codegen.config.CodegenConfigurator;
import org.openapitools.codegen.languages.PhpClientCodegen;
import org.testng.Assert;
import org.testng.annotations.BeforeMethod;
import org.testng.annotations.Test;
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.util.List;
import java.util.Map;
import java.util.function.Function;
@@ -192,4 +197,254 @@ public void testDateTimeLengthValidationIsNotGenerated() throws Exception {
Assert.assertTrue(modelPhp.contains("mb_strlen($this->container['title']) > 10"), modelPhp);
Assert.assertTrue(modelPhp.contains("mb_strlen($title) > 10"), modelPhp);
}
+
+ @Test
+ public void testGuzzleOpenApi32OperationsAndQueryStringParam() throws IOException {
+ Path target = Files.createTempDirectory("test");
+ try {
+ final CodegenConfigurator configurator = new CodegenConfigurator()
+ .setGeneratorName("php")
+ .setLibrary("guzzle")
+ .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("lib/Api/DefaultApi.php");
+ TestUtils.assertFileExists(apiPath);
+ String generated = new String(Files.readAllBytes(apiPath), StandardCharsets.UTF_8);
+ // non-standard methods go through a verbatim subclass so psr7's
+ // strtoupper cannot mangle them
+ for (String method : new String[]{"QUERY", "PURGE", "customMethod", "CHECK&FETCH", "X#Y"}) {
+ Assert.assertTrue(generated.contains("'" + method + "'"),
+ "expected verbatim method literal for " + method);
+ }
+ Assert.assertTrue(generated.contains("new class("), "verbatim Request subclass expected");
+ Assert.assertTrue(generated.contains("'GET'"), "standard method kept on plain Request");
+ // `in: querystring` appends verbatim with ?/& handling, not via query params;
+ // the accumulator is $__-prefixed so a parameter named `uri` cannot shadow it
+ Assert.assertTrue(generated.contains("(str_contains($__requestUri, '?') ? '&' : '?') . $qs"),
+ "querystring param should be appended verbatim");
+ Assert.assertTrue(generated.contains(". $uri;"),
+ "a querystring parameter literally named `uri` must still reach the wire");
+ Assert.assertFalse(generated.contains("toQueryValue(\n $qs"),
+ "querystring param must not be serialized as a name=value pair");
+ // params named after function-scope internal variables are renamed so
+ // the caller's value cannot be shadowed by e.g. `$query = buildQuery()`
+ Assert.assertTrue(generated.contains("function shadowQueryParam($param_query"),
+ "param `query` must be renamed off the internal $query local");
+ Assert.assertTrue(generated.contains("'?') . $param_query"),
+ "renamed `query` param must still be appended verbatim");
+ Assert.assertTrue(generated.contains("function shadowHeadersParam($param_headers"),
+ "param `headers` must be renamed off the internal $headers local");
+ Assert.assertTrue(generated.contains("function shadowMultipartParam($param_multipart"),
+ "param `multipart` must be renamed off the internal $multipart local");
+ Assert.assertTrue(generated.contains("function shadowBodyParam($param_headers"),
+ "a body param named `headers` must be renamed too");
+ Assert.assertFalse(generated.contains("function shadowQueryParam($query"),
+ "the colliding spec name must not leak into the signature");
+ // QUERY may carry a body; REPORT/PROPPATCH need one on some stacks
+ Assert.assertTrue(generated.contains("'REPORT'"), "REPORT should emit a verbatim literal");
+ Assert.assertTrue(generated.contains("'PROPPATCH'"), "PROPPATCH should emit a verbatim literal");
+ Path composerPath = target.resolve("composer.json");
+ String composer = new String(Files.readAllBytes(composerPath), StandardCharsets.UTF_8);
+ // psr7 < 2.10 Utils::modifyRequest() rebuilds a plain Request and
+ // drops the verbatim subclass - pin the floor high enough
+ Assert.assertTrue(composer.contains("\"guzzlehttp/psr7\": \"^2.10\""),
+ "psr7 constraint must exclude versions that rebuild plain Requests");
+ } finally {
+ FileUtils.deleteDirectory(target.toFile());
+ }
+ }
+
+ @Test
+ public void testPsr18SkipsOpenApi32Operations() throws IOException {
+ Path target = Files.createTempDirectory("test");
+ try {
+ final CodegenConfigurator configurator = new CodegenConfigurator()
+ .setGeneratorName("php")
+ .setLibrary("psr-18")
+ .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("lib/Api/DefaultApi.php");
+ TestUtils.assertFileExists(apiPath);
+ String generated = new String(Files.readAllBytes(apiPath), StandardCharsets.UTF_8);
+ Assert.assertTrue(generated.contains("function listPets"), "GET operation should be kept");
+ for (String op : new String[]{"queryPets", "purgePets", "customPets", "checkFetchPets", "hashPets"}) {
+ Assert.assertFalse(generated.contains("function " + op),
+ "psr-18 must skip unsupported 3.2 operation " + op);
+ }
+ } finally {
+ FileUtils.deleteDirectory(target.toFile());
+ }
+ }
+
+ @Test
+ public void testGuzzleSkipsInvalidHttpMethodToken() throws IOException {
+ Path target = Files.createTempDirectory("test");
+ try {
+ final CodegenConfigurator configurator = new CodegenConfigurator()
+ .setGeneratorName("php")
+ .setLibrary("guzzle")
+ .setInputSpec("src/test/resources/3_2/rust-invalid-method.yaml")
+ .setSkipOverwrite(false)
+ .setOutputDir(target.toAbsolutePath().toString().replace("\\", "/"));
+ new DefaultGenerator().opts(configurator.toClientOptInput()).generate();
+ Path apiPath = target.resolve("lib/Api/DefaultApi.php");
+ TestUtils.assertFileExists(apiPath);
+ String generated = new String(Files.readAllBytes(apiPath), StandardCharsets.UTF_8);
+ Assert.assertTrue(generated.contains("function 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 testGuzzleWebhookOperationsEmitVerbatimMethods() throws IOException {
+ Path target = Files.createTempDirectory("test");
+ try {
+ final CodegenConfigurator configurator = new CodegenConfigurator()
+ .setGeneratorName("php")
+ .setLibrary("guzzle")
+ .setInputSpec("src/test/resources/3_2/go-webhook-operations.yaml")
+ .setSkipOverwrite(false)
+ .setOutputDir(target.toAbsolutePath().toString().replace("\\", "/"));
+ new DefaultGenerator().opts(configurator.toClientOptInput()).generate();
+ Path apiPath = target.resolve("lib/Api/DefaultApi.php");
+ TestUtils.assertFileExists(apiPath);
+ String generated = new String(Files.readAllBytes(apiPath), StandardCharsets.UTF_8);
+ Assert.assertTrue(generated.contains("'QUERY'"),
+ "webhook query operation should emit a verbatim QUERY method");
+ Assert.assertTrue(generated.contains("'customMethod'"),
+ "webhook additionalOperations should emit a verbatim method");
+ } finally {
+ FileUtils.deleteDirectory(target.toFile());
+ }
+ }
+
+ @Test
+ public void testPrependFormOrBodyParametersCollisionStaysUnique() throws IOException {
+ // regression pin: a form field `param_query` plus a query param `query`
+ // (renamed to `param_query` by the internal-variable collision guard)
+ // must not produce a duplicate signature when form params are prepended
+ Path target = Files.createTempDirectory("test");
+ try {
+ final CodegenConfigurator configurator = new CodegenConfigurator()
+ .setGeneratorName("php")
+ .setLibrary("guzzle")
+ .setInputSpec("src/test/resources/3_0/form-prepend-collision.yaml")
+ .addAdditionalProperty("prependFormOrBodyParameters", true)
+ .setSkipOverwrite(false)
+ .setOutputDir(target.toAbsolutePath().toString().replace("\\", "/"));
+ new DefaultGenerator().opts(configurator.toClientOptInput()).generate();
+ Path apiPath = target.resolve("lib/Api/DefaultApi.php");
+ TestUtils.assertFileExists(apiPath);
+ String generated = new String(Files.readAllBytes(apiPath), StandardCharsets.UTF_8);
+ java.util.regex.Matcher m = java.util.regex.Pattern
+ .compile("function prependCollide\\(([^)]*)\\)")
+ .matcher(generated);
+ Assert.assertTrue(m.find(), "prependCollide signature should exist");
+ String signature = m.group(1);
+ Assert.assertTrue(signature.contains("$param_query"),
+ "collision-renamed query param should be in the signature: " + signature);
+ Assert.assertTrue(signature.contains("$param_query2"),
+ "form param must be de-duplicated to param_query2: " + signature);
+ } finally {
+ FileUtils.deleteDirectory(target.toFile());
+ }
+ }
+
+ /**
+ * End-to-end check: runs the generated guzzle client against a raw TCP
+ * capture listener, verifying query/additionalOperations methods and
+ * `in: querystring` reach the wire verbatim. Skipped when php or composer
+ * is unavailable, or when packagist cannot be reached.
+ */
+ @Test
+ public void testGuzzleGeneratedClientSendsVerbatimMethods() throws IOException, InterruptedException {
+ if (!isCommandAvailable("php", "--version")) {
+ throw new org.testng.SkipException("php is not on PATH; skipping generated-client verification");
+ }
+ Path target = Files.createTempDirectory("php32-verify");
+ try {
+ final CodegenConfigurator configurator = new CodegenConfigurator()
+ .setGeneratorName("php")
+ .setLibrary("guzzle")
+ .setInputSpec("src/test/resources/3_2/query-operation.yaml")
+ .setSkipOverwrite(false)
+ .setOutputDir(target.toAbsolutePath().toString().replace("\\", "/"));
+ new DefaultGenerator().opts(configurator.toClientOptInput()).generate();
+
+ ProcessBuilder composer = composerInstallCommand(target.toFile());
+ if (composer == null || runAndCheck(composer, 300) != 0) {
+ throw new org.testng.SkipException(
+ "composer install failed (composer missing or packagist unreachable)");
+ }
+
+ Path capture = target.resolve("capture.php");
+ Files.copy(Path.of("src/test/resources/3_2/php-guzzle-capture/capture.php"), capture);
+
+ Process p = new ProcessBuilder("php", "capture.php",
+ target.toAbsolutePath().toString())
+ .directory(target.toFile())
+ .redirectErrorStream(true)
+ .start();
+ boolean finished = p.waitFor(90, java.util.concurrent.TimeUnit.SECONDS);
+ if (!finished) {
+ p.destroyForcibly();
+ }
+ String output = new String(p.getInputStream().readAllBytes(), StandardCharsets.UTF_8);
+ Assert.assertTrue(finished, "php capture timed out:\n" + output);
+ Assert.assertTrue(output.contains("CAPTURE-PASS"),
+ "generated client did not send verbatim 3.2 methods/querystring:\n" + output);
+ } finally {
+ // composer's vendor dir is tens of MB; deleteOnExit cannot remove non-empty dirs
+ FileUtils.deleteDirectory(target.toFile());
+ }
+ }
+
+ private ProcessBuilder composerInstallCommand(File dir) {
+ if (isCommandAvailable("composer", "--version")) {
+ return new ProcessBuilder("composer", "install", "--quiet", "--no-interaction",
+ "--no-dev", "--prefer-dist")
+ .directory(dir).redirectErrorStream(true);
+ }
+ String phar = System.getenv("COMPOSER_PHAR");
+ if (phar != null && Files.isRegularFile(Path.of(phar))) {
+ return new ProcessBuilder("php", phar, "install", "--quiet", "--no-interaction",
+ "--no-dev", "--prefer-dist")
+ .directory(dir).redirectErrorStream(true);
+ }
+ return null;
+ }
+
+ private int runAndCheck(ProcessBuilder pb, long timeoutSeconds) throws IOException, InterruptedException {
+ Process p = pb.start();
+ if (!p.waitFor(timeoutSeconds, java.util.concurrent.TimeUnit.SECONDS)) {
+ p.destroyForcibly();
+ return -1;
+ }
+ p.getInputStream().transferTo(java.io.OutputStream.nullOutputStream());
+ return p.exitValue();
+ }
+
+ 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;
+ }
+ }
}
\ No newline at end of file
diff --git a/modules/openapi-generator/src/test/java/org/openapitools/codegen/python/PythonClientCodegenTest.java b/modules/openapi-generator/src/test/java/org/openapitools/codegen/python/PythonClientCodegenTest.java
index 9787717eb07f..027adb1fbe04 100644
--- a/modules/openapi-generator/src/test/java/org/openapitools/codegen/python/PythonClientCodegenTest.java
+++ b/modules/openapi-generator/src/test/java/org/openapitools/codegen/python/PythonClientCodegenTest.java
@@ -1533,4 +1533,84 @@ public void testIndependentImplicitClientsPreserveDefaultsWhenDisabled() throws
"def close(self) -> None:");
TestUtils.assertFileNotContains(rest, "def close(self) -> None:");
}
+
+ @Test(description = "OpenAPI 3.2 query/additionalOperations and in:querystring generate working Python code")
+ public void testOpenAPI32QueryAndAdditionalOperations() throws IOException {
+ File output = Files.createTempDirectory("test").toFile();
+ output.deleteOnExit();
+
+ final CodegenConfigurator configurator = new CodegenConfigurator()
+ .setGeneratorName("python")
+ .setInputSpec("src/test/resources/3_2/query-operation.yaml")
+ .setOutputDir(output.getAbsolutePath().replace("\\", "/"));
+
+ DefaultGenerator generator = new DefaultGenerator();
+ List files = generator.opts(configurator.toClientOptInput()).generate();
+ files.forEach(File::deleteOnExit);
+
+ Path apiFile = Paths.get(output + "/openapi_client/api/default_api.py");
+ // non-standard methods are emitted as unescaped double-quoted literals;
+ // '...' single-quoted escaping would corrupt CHECK&FETCH to &
+ assertFileContains(apiFile,
+ "method=\"QUERY\"",
+ "method=\"PURGE\"",
+ // additionalOperations keys are sent verbatim - no case folding
+ "method=\"customMethod\"",
+ // valid HTTP token punctuation must not be HTML-escaped
+ "method=\"CHECK&FETCH\"");
+ TestUtils.assertFileNotContains(apiFile, "&");
+ // in:querystring appends the raw, already-encoded value to the path
+ assertFileContains(apiFile,
+ "_resource_path += ('&' if '?' in _resource_path else '?') + qs");
+
+ Path restFile = Paths.get(output + "/openapi_client/rest.py");
+ // urllib3's request() uppercases method; non-standard methods must be
+ // dispatched through the encode helpers to keep verbatim casing
+ assertFileContains(restFile, "_pool_request", "request_encode_body");
+ }
+
+ @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();
+ output.deleteOnExit();
+
+ final CodegenConfigurator configurator = new CodegenConfigurator()
+ .setGeneratorName("python")
+ .setInputSpec("src/test/resources/3_2/go-webhook-operations.yaml")
+ .setOutputDir(output.getAbsolutePath().replace("\\", "/"));
+
+ DefaultGenerator generator = new DefaultGenerator();
+ List files = generator.opts(configurator.toClientOptInput()).generate();
+ files.forEach(File::deleteOnExit);
+
+ Path apiFile = Paths.get(output + "/openapi_client/api/default_api.py");
+ assertFileContains(apiFile,
+ "method=\"QUERY\"",
+ "method=\"customMethod\"");
+ }
+
+ @Test(description = "non-urllib3 python libraries cannot preserve method casing and must skip 3.2 operations")
+ public void testOpenAPI32UnsupportedLibrarySkips() throws IOException {
+ File output = Files.createTempDirectory("test").toFile();
+ output.deleteOnExit();
+
+ final CodegenConfigurator configurator = new CodegenConfigurator()
+ .setGeneratorName("python")
+ .setLibrary("asyncio")
+ .setInputSpec("src/test/resources/3_2/query-operation.yaml")
+ .setOutputDir(output.getAbsolutePath().replace("\\", "/"));
+
+ DefaultGenerator generator = new DefaultGenerator();
+ List files = generator.opts(configurator.toClientOptInput()).generate();
+ files.forEach(File::deleteOnExit);
+
+ // aiohttp uppercases method names internally, so QUERY/customMethod
+ // cannot be sent verbatim - the ops must be omitted, not broken,
+ // while the ordinary GET operation still generates
+ Path apiFile = Paths.get(output + "/openapi_client/api/default_api.py");
+ assertFileContains(apiFile, "def list_pets");
+ TestUtils.assertFileNotContains(apiFile, "query_pets");
+ TestUtils.assertFileNotContains(apiFile, "purge_pets");
+ TestUtils.assertFileNotContains(apiFile, "custom_pets");
+ }
}
diff --git a/modules/openapi-generator/src/test/java/org/openapitools/codegen/ruby/RubyClientCodegenTest.java b/modules/openapi-generator/src/test/java/org/openapitools/codegen/ruby/RubyClientCodegenTest.java
index ed256377bc80..1ad5d5217125 100644
--- a/modules/openapi-generator/src/test/java/org/openapitools/codegen/ruby/RubyClientCodegenTest.java
+++ b/modules/openapi-generator/src/test/java/org/openapitools/codegen/ruby/RubyClientCodegenTest.java
@@ -22,6 +22,7 @@
import io.swagger.v3.oas.models.media.Schema;
import org.apache.commons.io.FileUtils;
import org.openapitools.codegen.*;
+import org.openapitools.codegen.config.CodegenConfigurator;
import org.openapitools.codegen.languages.RubyClientCodegen;
import org.openapitools.codegen.model.ModelMap;
import org.openapitools.codegen.model.OperationMap;
@@ -30,8 +31,10 @@
import org.testng.annotations.Test;
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.util.*;
import java.util.stream.Collectors;
@@ -762,4 +765,214 @@ public void testQueryParamJsonSerializationSetsQueryIsJsonMimeType() {
assertTrue(op.queryParams.stream().allMatch(p -> p.queryIsJsonMimeType),
"All content:application/json query params should have queryIsJsonMimeType=true");
}
+
+ @Test
+ public void testHttpxOpenApi32OperationsAndQueryStringParam() throws IOException {
+ Path target = Files.createTempDirectory("test");
+ try {
+ final CodegenConfigurator configurator = new CodegenConfigurator()
+ .setGeneratorName("ruby")
+ .setLibrary("httpx")
+ .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("lib/openapi_client/api/default_api.rb");
+ TestUtils.assertFileExists(apiPath);
+ String generated = new String(Files.readAllBytes(apiPath), StandardCharsets.UTF_8);
+ // methods are emitted as quoted symbols so arbitrary tokens stay valid Ruby
+ for (String method : new String[]{"QUERY", "PURGE", "customMethod", "CHECK&FETCH"}) {
+ Assert.assertTrue(generated.contains("call_api(:\"" + method + "\""),
+ "expected verbatim symbol literal for " + method);
+ }
+ Assert.assertTrue(generated.contains("call_api(:\"GET\""), "standard method kept");
+ // `#` must be escaped inside the :"..." literal or Ruby interpolates #$/#@/#{
+ Assert.assertTrue(generated.contains("call_api(:\"X\\#Y\""),
+ "# in method token must be escaped in the symbol literal");
+ // `in: querystring` appends verbatim with ?/& handling, not via query_params
+ Assert.assertTrue(generated.contains("+ qs.to_s"),
+ "querystring param should be appended verbatim");
+ Assert.assertFalse(generated.contains("query_params[:'qs']"),
+ "querystring param must not be serialized as a name=value pair");
+ // QUERY may carry a body (OpenAPI 3.2): it must reach opts[:body]
+ Assert.assertTrue(generated.contains("def search_items(uri, request_body, opts = {})"),
+ "QUERY-with-body operation should take the body parameter");
+ Assert.assertTrue(generated.contains("call_api(:\"REPORT\""),
+ "REPORT additionalOperation should emit a verbatim symbol");
+ Path clientPath = target.resolve("lib/openapi_client/api_client.rb");
+ String client = new String(Files.readAllBytes(clientPath), StandardCharsets.UTF_8);
+ Assert.assertTrue(client.contains("instance_variable_set(:@verb, http_method.to_s)"),
+ "httpx verbatim verb path should be present");
+ } finally {
+ FileUtils.deleteDirectory(target.toFile());
+ }
+ }
+
+ @Test
+ public void testTyphoeusSkipsOpenApi32Operations() throws IOException {
+ Path target = Files.createTempDirectory("test");
+ try {
+ final CodegenConfigurator configurator = new CodegenConfigurator()
+ .setGeneratorName("ruby")
+ .setLibrary("typhoeus")
+ .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("lib/openapi_client/api/default_api.rb");
+ TestUtils.assertFileExists(apiPath);
+ String generated = new String(Files.readAllBytes(apiPath), StandardCharsets.UTF_8);
+ Assert.assertTrue(generated.contains("def list_pets"), "GET operation should be kept");
+ for (String op : new String[]{"query_pets", "purge_pets", "custom_pets", "check_fetch_pets", "hash_pets"}) {
+ Assert.assertFalse(generated.contains("def " + op),
+ "typhoeus must skip unsupported 3.2 operation " + op);
+ }
+ // `in: querystring` on a standard method degrades to a name=value
+ // parameter - the verbatim-append path is httpx-only
+ Assert.assertTrue(generated.contains("def find_pets"),
+ "GET+querystring operation should be kept");
+ Assert.assertTrue(generated.contains("query_params[:'q'] = q"),
+ "unsupported library must degrade querystring to name=value");
+ Assert.assertFalse(generated.contains("+ q.to_s"),
+ "unsupported library must not emit the verbatim querystring append");
+ } finally {
+ FileUtils.deleteDirectory(target.toFile());
+ }
+ }
+
+ @Test
+ public void testFaradayDegradesQueryStringParam() throws IOException {
+ Path target = Files.createTempDirectory("test");
+ try {
+ final CodegenConfigurator configurator = new CodegenConfigurator()
+ .setGeneratorName("ruby")
+ .setLibrary("faraday")
+ .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("lib/openapi_client/api/default_api.rb");
+ TestUtils.assertFileExists(apiPath);
+ String generated = new String(Files.readAllBytes(apiPath), StandardCharsets.UTF_8);
+ Assert.assertTrue(generated.contains("def find_pets"),
+ "GET+querystring operation should be kept");
+ Assert.assertTrue(generated.contains("query_params[:'q'] = q"),
+ "faraday must degrade querystring to an ordinary name=value pair");
+ Assert.assertFalse(generated.contains("+ q.to_s"),
+ "faraday must not emit the verbatim querystring append");
+ } finally {
+ FileUtils.deleteDirectory(target.toFile());
+ }
+ }
+
+ @Test
+ public void testHttpxSkipsInvalidHttpMethodToken() throws IOException {
+ Path target = Files.createTempDirectory("test");
+ try {
+ final CodegenConfigurator configurator = new CodegenConfigurator()
+ .setGeneratorName("ruby")
+ .setLibrary("httpx")
+ .setInputSpec("src/test/resources/3_2/rust-invalid-method.yaml")
+ .setSkipOverwrite(false)
+ .setOutputDir(target.toAbsolutePath().toString().replace("\\", "/"));
+ new DefaultGenerator().opts(configurator.toClientOptInput()).generate();
+ Path apiPath = target.resolve("lib/openapi_client/api/default_api.rb");
+ TestUtils.assertFileExists(apiPath);
+ String generated = new String(Files.readAllBytes(apiPath), StandardCharsets.UTF_8);
+ Assert.assertTrue(generated.contains("def list_pets"), "GET operation should be kept");
+ Assert.assertFalse(generated.contains("bad_method"),
+ "invalid RFC 9110 method token must be skipped");
+ } finally {
+ FileUtils.deleteDirectory(target.toFile());
+ }
+ }
+
+ @Test
+ public void testHttpxWebhookOperationsEmitVerbatimSymbols() throws IOException {
+ Path target = Files.createTempDirectory("test");
+ try {
+ final CodegenConfigurator configurator = new CodegenConfigurator()
+ .setGeneratorName("ruby")
+ .setLibrary("httpx")
+ .setInputSpec("src/test/resources/3_2/go-webhook-operations.yaml")
+ .setSkipOverwrite(false)
+ .setOutputDir(target.toAbsolutePath().toString().replace("\\", "/"));
+ new DefaultGenerator().opts(configurator.toClientOptInput()).generate();
+ // webhooks render through the same api.mustache; their non-standard
+ // methods must be symbol literals too, otherwise the verb is lost
+ Path apiPath = target.resolve("lib/openapi_client/api/default_api.rb");
+ TestUtils.assertFileExists(apiPath);
+ String generated = new String(Files.readAllBytes(apiPath), StandardCharsets.UTF_8);
+ Assert.assertTrue(generated.contains("call_api(:\"QUERY\""),
+ "webhook query operation should emit a verbatim QUERY symbol");
+ Assert.assertTrue(generated.contains("call_api(:\"customMethod\""),
+ "webhook additionalOperations should emit a verbatim symbol");
+ Assert.assertFalse(generated.contains("call_api(:\"\""),
+ "no operation may emit an empty method symbol");
+ } finally {
+ FileUtils.deleteDirectory(target.toFile());
+ }
+ }
+
+ /**
+ * End-to-end check: runs the generated httpx client with a raw TCP capture
+ * listener, verifying QUERY/additionalOperations methods and
+ * `in: querystring` reach the wire verbatim. Skipped when ruby or the httpx
+ * gem is unavailable.
+ */
+ @Test
+ public void testHttpxGeneratedClientSendsVerbatimMethods() throws IOException, InterruptedException {
+ if (!isCommandAvailable("ruby", "--version")) {
+ throw new org.testng.SkipException("ruby is not on PATH; skipping generated-client verification");
+ }
+ if (!isCommandAvailable("ruby", "-e", "require 'httpx'")) {
+ throw new org.testng.SkipException("httpx gem not installed; skipping generated-client verification");
+ }
+ Path target = Files.createTempDirectory("ruby32-verify");
+ try {
+ final CodegenConfigurator configurator = new CodegenConfigurator()
+ .setGeneratorName("ruby")
+ .setLibrary("httpx")
+ .setInputSpec("src/test/resources/3_2/query-operation.yaml")
+ .setSkipOverwrite(false)
+ .setOutputDir(target.toAbsolutePath().toString().replace("\\", "/"));
+ new DefaultGenerator().opts(configurator.toClientOptInput()).generate();
+
+ Path capture = target.resolve("capture.rb");
+ Files.copy(Path.of("src/test/resources/3_2/ruby-httpx-capture/capture.rb"), capture);
+
+ Process p = new ProcessBuilder("ruby", "capture.rb",
+ target.resolve("lib").toAbsolutePath().toString())
+ .directory(target.toFile())
+ .redirectErrorStream(true)
+ .start();
+ boolean finished = p.waitFor(60, java.util.concurrent.TimeUnit.SECONDS);
+ if (!finished) {
+ p.destroyForcibly();
+ }
+ String output = new String(p.getInputStream().readAllBytes(), StandardCharsets.UTF_8);
+ Assert.assertTrue(finished, "ruby capture timed out:\n" + output);
+ Assert.assertTrue(output.contains("CAPTURE-PASS"),
+ "generated client did not send verbatim 3.2 methods/querystring:\n" + output);
+ } finally {
+ 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/rust/RustClientCodegenTest.java b/modules/openapi-generator/src/test/java/org/openapitools/codegen/rust/RustClientCodegenTest.java
index f4a92edebb6c..f4fb98fcbd74 100644
--- a/modules/openapi-generator/src/test/java/org/openapitools/codegen/rust/RustClientCodegenTest.java
+++ b/modules/openapi-generator/src/test/java/org/openapitools/codegen/rust/RustClientCodegenTest.java
@@ -18,6 +18,7 @@
package org.openapitools.codegen.rust;
import io.swagger.v3.oas.models.media.IntegerSchema;
+import org.apache.commons.io.FileUtils;
import org.openapitools.codegen.CodegenConstants;
import org.openapitools.codegen.DefaultGenerator;
import org.openapitools.codegen.TestUtils;
@@ -29,9 +30,14 @@
import java.io.File;
import java.io.IOException;
import java.math.BigDecimal;
+import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
+import java.util.ArrayList;
+import java.util.Arrays;
import java.util.List;
+import java.util.Objects;
+import java.util.concurrent.TimeUnit;
import static org.openapitools.codegen.TestUtils.linearize;
@@ -282,79 +288,318 @@ public void testWithIntegerFittingAndPreferUnsigned() {
@Test
public void testMultipleArrayTypesEnum() throws IOException {
Path target = Files.createTempDirectory("test");
- final CodegenConfigurator configurator = new CodegenConfigurator()
- .setGeneratorName("rust")
- .setInputSpec("src/test/resources/3_1/issue_18527.yaml")
- .setSkipOverwrite(false)
- .setOutputDir(target.toAbsolutePath().toString().replace("\\", "/"));
- List files = new DefaultGenerator().opts(configurator.toClientOptInput()).generate();
- files.forEach(File::deleteOnExit);
- Path outputPath = Path.of(target.toString(), "/src/models/option1_or_option2_options.rs");
- String enumSpec = linearize("pub enum Option1OrOption2Options { " +
- "ArrayVecString(Vec), " +
- "ArrayVeci32(Vec)," +
- "}");
- TestUtils.assertFileExists(outputPath);
- TestUtils.assertFileContains(outputPath, enumSpec);
+ try {
+ final CodegenConfigurator configurator = new CodegenConfigurator()
+ .setGeneratorName("rust")
+ .setInputSpec("src/test/resources/3_1/issue_18527.yaml")
+ .setSkipOverwrite(false)
+ .setOutputDir(target.toAbsolutePath().toString().replace("\\", "/"));
+ new DefaultGenerator().opts(configurator.toClientOptInput()).generate();
+ Path outputPath = Path.of(target.toString(), "/src/models/option1_or_option2_options.rs");
+ String enumSpec = linearize("pub enum Option1OrOption2Options { " +
+ "ArrayVecString(Vec), " +
+ "ArrayVeci32(Vec)," +
+ "}");
+ TestUtils.assertFileExists(outputPath);
+ TestUtils.assertFileContains(outputPath, enumSpec);
+ } finally {
+ FileUtils.deleteDirectory(target.toFile());
+ }
}
@Test
public void testIntegerPropertyEnum() throws IOException {
Path target = Files.createTempDirectory("test");
- target.toFile().deleteOnExit();
- final CodegenConfigurator configurator = new CodegenConfigurator()
- .setGeneratorName("rust")
- .setInputSpec("src/test/resources/3_1/rust_integer_property_enum.yaml")
- .setSkipOverwrite(false)
- .setOutputDir(target.toAbsolutePath().toString().replace("\\", "/"));
- new DefaultGenerator().opts(configurator.toClientOptInput()).generate();
- Path outputPath = Path.of(target.toString(), "/src/models/signed_message_signature.rs");
- TestUtils.assertFileExists(outputPath);
- // The integer-enum property must use serde_repr (not string rename)
- TestUtils.assertFileContains(outputPath, "use serde_repr::{Serialize_repr,Deserialize_repr}");
- TestUtils.assertFileContains(outputPath, "Serialize_repr, Deserialize_repr");
- TestUtils.assertFileContains(outputPath, "= 0");
- TestUtils.assertFileContains(outputPath, "= 1");
- // Must NOT contain a string rename for an integer variant
- TestUtils.assertFileNotContains(outputPath, linearize("#[serde(rename = \"0\")]"));
+ try {
+ final CodegenConfigurator configurator = new CodegenConfigurator()
+ .setGeneratorName("rust")
+ .setInputSpec("src/test/resources/3_1/rust_integer_property_enum.yaml")
+ .setSkipOverwrite(false)
+ .setOutputDir(target.toAbsolutePath().toString().replace("\\", "/"));
+ new DefaultGenerator().opts(configurator.toClientOptInput()).generate();
+ Path outputPath = Path.of(target.toString(), "/src/models/signed_message_signature.rs");
+ TestUtils.assertFileExists(outputPath);
+ // The integer-enum property must use serde_repr (not string rename)
+ TestUtils.assertFileContains(outputPath, "use serde_repr::{Serialize_repr,Deserialize_repr}");
+ TestUtils.assertFileContains(outputPath, "Serialize_repr, Deserialize_repr");
+ TestUtils.assertFileContains(outputPath, "= 0");
+ TestUtils.assertFileContains(outputPath, "= 1");
+ // Must NOT contain a string rename for an integer variant
+ TestUtils.assertFileNotContains(outputPath, linearize("#[serde(rename = \"0\")]"));
+ } finally {
+ FileUtils.deleteDirectory(target.toFile());
+ }
}
@Test
public void testArrayWithObjectEnumValues() throws IOException {
Path target = Files.createTempDirectory("test");
- target.toFile().deleteOnExit();
- final CodegenConfigurator configurator = new CodegenConfigurator()
- .setGeneratorName("rust")
- .setInputSpec("src/test/resources/3_1/issue_23278.yaml")
- .setSkipOverwrite(false)
- .setOutputDir(target.toAbsolutePath().toString().replace("\\", "/"));
- new DefaultGenerator().opts(configurator.toClientOptInput()).generate();
- Path outputPath = Path.of(target.toString(), "/src/models/object_arrays_options.rs");
- String enumSpec = linearize("pub enum ObjectArraysOptions { " +
- "ArrayVecTestObject(Vec), " +
- "ArrayVecTestArray(Vec)," +
- "}");
- TestUtils.assertFileExists(outputPath);
- TestUtils.assertFileContains(outputPath, enumSpec);
+ try {
+ final CodegenConfigurator configurator = new CodegenConfigurator()
+ .setGeneratorName("rust")
+ .setInputSpec("src/test/resources/3_1/issue_23278.yaml")
+ .setSkipOverwrite(false)
+ .setOutputDir(target.toAbsolutePath().toString().replace("\\", "/"));
+ new DefaultGenerator().opts(configurator.toClientOptInput()).generate();
+ Path outputPath = Path.of(target.toString(), "/src/models/object_arrays_options.rs");
+ String enumSpec = linearize("pub enum ObjectArraysOptions { " +
+ "ArrayVecTestObject(Vec), " +
+ "ArrayVecTestArray(Vec