diff --git a/.github/workflows/samples-scala.yaml b/.github/workflows/samples-scala.yaml
index eb1a3ac43cff..5b937b0a42bb 100644
--- a/.github/workflows/samples-scala.yaml
+++ b/.github/workflows/samples-scala.yaml
@@ -23,6 +23,7 @@ jobs:
- 'samples/client/petstore/java/okhttp-gson'
- samples/client/petstore/scalaz
- samples/client/petstore/scala-pekko
+ - samples/client/petstore/scala-http4s
#- samples/client/petstore/scala-sttp # won't pass while the same tests in circleci pass
# servers
- samples/server/petstore/scala-lagom-server
diff --git a/bin/configs/scala-http4s.yaml b/bin/configs/scala-http4s.yaml
new file mode 100644
index 000000000000..eb0479e6d4e2
--- /dev/null
+++ b/bin/configs/scala-http4s.yaml
@@ -0,0 +1,6 @@
+generatorName: scala-http4s
+outputDir: samples/client/petstore/scala-http4s
+inputSpec: modules/openapi-generator/src/test/resources/3_0/scala-http4s/petstore.yaml
+templateDir: modules/openapi-generator/src/main/resources/scala-http4s
+additionalProperties:
+ artifactId: scala-http4s-client
diff --git a/docs/generators/scala-http4s.md b/docs/generators/scala-http4s.md
new file mode 100644
index 000000000000..f4d7a7f648ef
--- /dev/null
+++ b/docs/generators/scala-http4s.md
@@ -0,0 +1,272 @@
+---
+title: Documentation for the scala-http4s Generator
+---
+
+## METADATA
+
+| Property | Value | Notes |
+| -------- | ----- | ----- |
+| generator name | scala-http4s | pass this to the generate command after -g |
+| generator stability | STABLE | |
+| generator type | CLIENT | |
+| generator language | Scala | |
+| generator default templating engine | mustache | |
+| helpTxt | Generates a scala-http4s client. | |
+
+## CONFIG OPTIONS
+These options may be applied as additional-properties (cli) or configOptions (plugins). Refer to [configuration docs](https://openapi-generator.tech/docs/configuration) for more details.
+
+| Option | Description | Values | Default |
+|-------------------------------------------|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| ------ | ------- |
+| allowUnicodeIdentifiers | boolean, toggles whether unicode identifiers are allowed in names or not, default is false | |false|
+| packageName | main package for the generated classes, parent package for api package and model package | |null|
+| artifactId | project name / artifactId for for the generated project | |null|
+| excludeSbt | exclude sbt from generation | |null|
+| excludeApi | exclude apis from generation | |null|
+| prependFormOrBodyParameters | Add form or body parameters to the beginning of the parameter list. | |false|
+| disallowAdditionalPropertiesIfNotPresent | If false, the 'additionalProperties' implementation (set to true by default) is compliant with the OAS and JSON schema specifications. If true (default), keep the old (incorrect) behaviour that 'additionalProperties' is set to false by default. |
**false** The 'additionalProperties' implementation is compliant with the OAS and JSON schema specifications. **true** Keep the old (incorrect) behaviour that 'additionalProperties' is set to false by default. |true|
+| ensureUniqueParams | Whether to ensure parameter names are unique in an operation (rename parameters that are not). | |true|
+| legacyDiscriminatorBehavior | Set to false for generators with better support for discriminators. (Python, Java, Go, PowerShell, C# have this enabled by default). |**true** The mapping in the discriminator includes descendent schemas that allOf inherit from self and the discriminator mapping schemas in the OAS document. **false** The mapping in the discriminator includes any descendent schemas that allOf inherit from self, any oneOf schemas, any anyOf schemas, any x-discriminator-values, and the discriminator mapping schemas in the OAS document AND Codegen validates that oneOf and anyOf schemas contain the required discriminator and throws an error if the discriminator is missing. |true|
+| sortModelPropertiesByRequiredFlag | Sort model properties to place required parameters before optional parameters. | |true|
+| sortParamsByRequiredFlag | Sort method arguments to place required parameters before optional parameters. | |true|
+| sourceFolder | source folder for generated code | |null|
+
+## IMPORT MAPPING
+
+| Type/Alias | Imports |
+| ---------- | ------- |
+|ArrayBuffer|scala.collection.mutable.ArrayBuffer|
+|Date|java.util.Date|
+|File|java.io.File|
+|HashMap|scala.collection.immutable.HashMap|
+|Instant|java.time.Instant|
+|Json|io.circe.Json|
+|LocalDate|java.time.LocalDate|
+|LocalDateTime|java.time.LocalDateTime|
+|LocalTime|java.time.LocalTime|
+|Map|scala.collection.immutable.Map|
+|OffsetDateTime|java.time.OffsetDateTime|
+|Seq|scala.collection.immutable.Seq|
+|Timestamp|java.sql.Timestamp|
+|URI|java.net.URI|
+|UUID|java.util.UUID|
+|ZonedDateTime|java.time.ZonedDateTime|
+
+
+## INSTANTIATION TYPES
+
+| Type/Alias | Instantiated By |
+| ---------- | --------------- |
+|array|Seq|
+|list|List|
+|map|Map|
+|seq|Seq|
+|set|Set|
+
+
+## LANGUAGE PRIMITIVES
+
+
+Any
+AnyRef
+AnyVal
+BigDecimal
+Boolean
+Double
+Float
+Int
+Integer
+Long
+Object
+String
+
+
+## RESERVED WORDS
+
+
+abstract
+assert
+break
+byte
+case
+catch
+char
+class
+const
+continue
+def
+default
+do
+double
+else
+enum
+extends
+false
+final
+finally
+float
+for
+forsome
+goto
+if
+implements
+implicit
+import
+instanceof
+int
+interface
+lazy
+long
+match
+native
+new
+null
+object
+override
+package
+private
+protected
+public
+return
+sealed
+short
+static
+strictfp
+super
+switch
+synchronized
+this
+throw
+throws
+trait
+transient
+true
+try
+type
+val
+var
+void
+volatile
+while
+with
+yield
+
+
+## FEATURE SET
+
+
+### Client Modification Feature
+| Name | Supported | Defined By |
+| ---- | --------- | ---------- |
+|BasePath|✗|ToolingExtension
+|Authorizations|✗|ToolingExtension
+|UserAgent|✗|ToolingExtension
+|MockServer|✗|ToolingExtension
+
+### Data Type Feature
+| Name | Supported | Defined By |
+| ---- | --------- | ---------- |
+|Custom|✗|OAS2,OAS3
+|Int32|✓|OAS2,OAS3
+|Int64|✓|OAS2,OAS3
+|Float|✓|OAS2,OAS3
+|Double|✓|OAS2,OAS3
+|Decimal|✓|ToolingExtension
+|String|✓|OAS2,OAS3
+|Byte|✓|OAS2,OAS3
+|Binary|✓|OAS2,OAS3
+|Boolean|✓|OAS2,OAS3
+|Date|✓|OAS2,OAS3
+|DateTime|✓|OAS2,OAS3
+|Password|✓|OAS2,OAS3
+|File|✓|OAS2
+|Uuid|✗|
+|Array|✓|OAS2,OAS3
+|Null|✗|OAS3
+|AnyType|✗|OAS2,OAS3
+|Object|✓|OAS2,OAS3
+|Maps|✓|ToolingExtension
+|CollectionFormat|✓|OAS2
+|CollectionFormatMulti|✓|OAS2
+|Enum|✓|OAS2,OAS3
+|ArrayOfEnum|✓|ToolingExtension
+|ArrayOfModel|✓|ToolingExtension
+|ArrayOfCollectionOfPrimitives|✓|ToolingExtension
+|ArrayOfCollectionOfModel|✓|ToolingExtension
+|ArrayOfCollectionOfEnum|✓|ToolingExtension
+|MapOfEnum|✓|ToolingExtension
+|MapOfModel|✓|ToolingExtension
+|MapOfCollectionOfPrimitives|✓|ToolingExtension
+|MapOfCollectionOfModel|✓|ToolingExtension
+|MapOfCollectionOfEnum|✓|ToolingExtension
+
+### Documentation Feature
+| Name | Supported | Defined By |
+| ---- | --------- | ---------- |
+|Readme|✗|ToolingExtension
+|Model|✓|ToolingExtension
+|Api|✓|ToolingExtension
+
+### Global Feature
+| Name | Supported | Defined By |
+| ---- | --------- | ---------- |
+|Host|✓|OAS2,OAS3
+|BasePath|✓|OAS2,OAS3
+|Info|✓|OAS2,OAS3
+|Schemes|✗|OAS2,OAS3
+|PartialSchemes|✓|OAS2,OAS3
+|Consumes|✓|OAS2
+|Produces|✓|OAS2
+|ExternalDocumentation|✓|OAS2,OAS3
+|Examples|✓|OAS2,OAS3
+|XMLStructureDefinitions|✗|OAS2,OAS3
+|MultiServer|✗|OAS3
+|ParameterizedServer|✗|OAS3
+|ParameterStyling|✗|OAS3
+|Callbacks|✗|OAS3
+|LinkObjects|✗|OAS3
+
+### Parameter Feature
+| Name | Supported | Defined By |
+| ---- | --------- | ---------- |
+|Path|✓|OAS2,OAS3
+|Query|✓|OAS2,OAS3
+|Header|✓|OAS2,OAS3
+|Body|✓|OAS2
+|FormUnencoded|✓|OAS2
+|FormMultipart|✗|OAS2
+|Cookie|✗|OAS3
+
+### Schema Support Feature
+| Name | Supported | Defined By |
+| ---- | --------- | ---------- |
+|Simple|✓|OAS2,OAS3
+|Composite|✓|OAS2,OAS3
+|Polymorphism|✗|OAS2,OAS3
+|Union|✗|OAS3
+|allOf|✗|OAS2,OAS3
+|anyOf|✗|OAS3
+|oneOf|✗|OAS3
+|not|✗|OAS3
+
+### Security Feature
+| Name | Supported | Defined By |
+| ---- | --------- | ---------- |
+|BasicAuth|✓|OAS2,OAS3
+|ApiKey|✓|OAS2,OAS3
+|OpenIDConnect|✗|OAS3
+|BearerToken|✓|OAS3
+|OAuth2_Implicit|✗|OAS2,OAS3
+|OAuth2_Password|✗|OAS2,OAS3
+|OAuth2_ClientCredentials|✗|OAS2,OAS3
+|OAuth2_AuthorizationCode|✗|OAS2,OAS3
+|SignatureAuth|✗|OAS3
+|AWSV4Signature|✗|ToolingExtension
+
+### Wire Format Feature
+| Name | Supported | Defined By |
+| ---- | --------- | ---------- |
+|JSON|✓|OAS2,OAS3
+|XML|✓|OAS2,OAS3
+|PROTOBUF|✗|ToolingExtension
+|Custom|✓|OAS2,OAS3
diff --git a/modules/openapi-generator/src/main/java/org/openapitools/codegen/CodegenOperation.java b/modules/openapi-generator/src/main/java/org/openapitools/codegen/CodegenOperation.java
index db659a22d541..127853a88fe7 100644
--- a/modules/openapi-generator/src/main/java/org/openapitools/codegen/CodegenOperation.java
+++ b/modules/openapi-generator/src/main/java/org/openapitools/codegen/CodegenOperation.java
@@ -30,7 +30,7 @@ public class CodegenOperation {
hasVersionHeaders = false, hasVersionQueryParams = false,
isResponseBinary = false, isResponseFile = false, isResponseOptional = false, hasReference = false, defaultReturnType = false,
isRestfulIndex, isRestfulShow, isRestfulCreate, isRestfulUpdate, isRestfulDestroy,
- isRestful, isDeprecated, isCallbackRequest, uniqueItems, hasDefaultResponse = false, hasConstantParams = false,
+ isRestful, isDeprecated, isCallbackRequest, uniqueItems, hasDefaultResponse = false, hasOnlyDefaultResponse = false, hasConstantParams = false,
hasErrorResponseObject, // if 4xx, 5xx responses have at least one error object defined
hasSingleParam = false; // if the operation has only one parameter;
public CodegenProperty returnProperty;
@@ -215,6 +215,13 @@ public boolean getHasDefaultResponse() {
return responses.stream().anyMatch(response -> response.isDefault);
}
+ /**
+ * Check if the responses contain only 1 entry and it's default
+ *
+ * @return true if responses contain only 1 entry and it's a default response, false otherwise
+ */
+ public boolean getHasOnlyDefaultResponse() { return responses.size() == 1 && getHasDefaultResponse(); }
+
public boolean getAllResponsesAreErrors() {
return responses.stream().allMatch(response -> response.is4xx || response.is5xx);
}
@@ -351,6 +358,7 @@ public String toString() {
sb.append(", isResponseOptional=").append(isResponseOptional);
sb.append(", hasReference=").append(hasReference);
sb.append(", hasDefaultResponse=").append(hasDefaultResponse);
+ sb.append(", hasOnlyDefaultResponse=").append(hasOnlyDefaultResponse);
sb.append(", hasErrorResponseObject=").append(hasErrorResponseObject);
sb.append(", hasSingleParam=").append(hasSingleParam);
sb.append(", isRestfulIndex=").append(isRestfulIndex);
@@ -432,6 +440,7 @@ public boolean equals(Object o) {
isResponseOptional == that.isResponseOptional &&
hasReference == that.hasReference &&
hasDefaultResponse == that.hasDefaultResponse &&
+ hasOnlyDefaultResponse == that.hasOnlyDefaultResponse &&
hasErrorResponseObject == that.hasErrorResponseObject &&
hasSingleParam == that.hasSingleParam &&
isRestfulIndex == that.isRestfulIndex &&
@@ -496,7 +505,7 @@ public int hashCode() {
return Objects.hash(responseHeaders, hasAuthMethods, hasConsumes, hasProduces, hasParams, hasOptionalParams,
hasRequiredParams, returnTypeIsPrimitive, returnSimpleType, subresourceOperation, isMap,
isArray, isMultipart, isVoid, isResponseBinary, isResponseFile, isResponseOptional, hasReference,
- hasDefaultResponse, isRestfulIndex, isRestfulShow, isRestfulCreate, isRestfulUpdate, isRestfulDestroy,
+ hasDefaultResponse, hasOnlyDefaultResponse, isRestfulIndex, isRestfulShow, isRestfulCreate, isRestfulUpdate, isRestfulDestroy,
isRestful, isDeprecated, isCallbackRequest, uniqueItems, path, operationId, returnType, httpMethod,
returnBaseType, returnContainer, summary, unescapedNotes, notes, baseName, defaultResponse,
discriminator, consumes, produces, prioritizedContentTypes, servers, bodyParam, allParams, bodyParams,
diff --git a/modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/ScalaHttp4sClientCodegen.java b/modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/ScalaHttp4sClientCodegen.java
new file mode 100644
index 000000000000..6c11d384059c
--- /dev/null
+++ b/modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/ScalaHttp4sClientCodegen.java
@@ -0,0 +1,459 @@
+/*
+ * Copyright 2018 OpenAPI-Generator Contributors (https://openapi-generator.tech)
+ * Copyright 2018 SmartBear Software
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.openapitools.codegen.languages;
+
+import io.swagger.v3.oas.models.Operation;
+import io.swagger.v3.oas.models.media.Schema;
+import io.swagger.v3.oas.models.security.SecurityScheme;
+import io.swagger.v3.oas.models.servers.Server;
+import lombok.Getter;
+import org.openapitools.codegen.*;
+import org.openapitools.codegen.meta.features.*;
+import org.openapitools.codegen.model.ModelMap;
+import org.openapitools.codegen.model.OperationMap;
+import org.openapitools.codegen.model.OperationsMap;
+import org.openapitools.codegen.utils.ModelUtils;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.io.File;
+import java.util.*;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+public class ScalaHttp4sClientCodegen extends AbstractScalaCodegen implements CodegenConfig {
+ private final Logger LOGGER = LoggerFactory.getLogger(ScalaHttp4sClientCodegen.class);
+
+ protected String packageName = "org.openapitools.client";
+ protected String groupId = "org.openapitools";
+ protected String artifactId = "scala-http4s-client";
+ protected String artifactVersion = "1.0.0";
+ protected boolean registerNonStandardStatusCodes = true;
+ protected boolean removeOAuthSecurities = true;
+ @Getter
+ protected boolean excludeSbt = false;
+ @Getter
+ protected boolean excludeApi = false;
+ protected static final String EXCLUDE_SBT = "excludeSbt";
+ protected static final String EXCLUDE_API = "excludeApi";
+ protected String sourceFolder = "src" + File.separator + "main" + File.separator + "scala";
+
+ @Override
+ public CodegenType getTag() {
+ return CodegenType.CLIENT;
+ }
+
+ public String getName() {
+ return "scala-http4s";
+ }
+
+ public String getHelp() {
+ return "Generates a scala-http4s client.";
+ }
+
+ public ScalaHttp4sClientCodegen() {
+ super();
+
+ modifyFeatureSet(features -> features
+ .wireFormatFeatures(EnumSet.of(WireFormatFeature.JSON, WireFormatFeature.XML, WireFormatFeature.Custom))
+ .securityFeatures(EnumSet.of(
+ SecurityFeature.BasicAuth,
+ SecurityFeature.ApiKey,
+ SecurityFeature.BearerToken
+ ))
+ .excludeGlobalFeatures(
+ GlobalFeature.XMLStructureDefinitions,
+ GlobalFeature.Callbacks,
+ GlobalFeature.LinkObjects,
+ GlobalFeature.ParameterStyling
+ )
+ .excludeSchemaSupportFeatures(
+ SchemaSupportFeature.Polymorphism,
+ SchemaSupportFeature.not
+ )
+ .excludeParameterFeatures(
+ ParameterFeature.Cookie,
+ ParameterFeature.FormMultipart
+ )
+ );
+
+ useOneOfInterfaces = true;
+ supportsMultipleInheritance = true;
+ supportsInheritance = true;
+ supportsMixins = true;
+ addOneOfInterfaceImports = true;
+
+ embeddedTemplateDir = templateDir = "scala-http4s";
+
+ modelTemplateFiles.put("model.mustache", ".scala");
+ apiTemplateFiles.put("api.mustache", ".scala");
+
+ setApiPackage(packageName + ".apis");
+ setModelPackage(packageName + ".models");
+
+ setReservedWordsLowerCase(
+ Arrays.asList(
+ // Scala
+ "abstract", "case", "catch", "class", "def",
+ "do", "else", "extends", "false", "final",
+ "finally", "for", "forSome", "if", "implicit",
+ "import", "lazy", "match", "new", "null",
+ "object", "override", "package", "private", "protected",
+ "return", "sealed", "super", "this", "throw",
+ "trait", "try", "true", "type", "val",
+ "var", "while", "with", "yield",
+ // Scala-interop languages keywords
+ "abstract", "continue", "switch", "assert",
+ "default", "synchronized", "goto",
+ "break", "double", "implements", "byte",
+ "public", "throws", "enum", "instanceof", "transient",
+ "int", "short", "char", "interface", "static",
+ "void", "finally", "long", "strictfp", "volatile", "const", "float",
+ "native")
+ );
+
+ defaultIncludes = new HashSet<>(
+ Arrays.asList("double",
+ "Int",
+ "Long",
+ "Float",
+ "Double",
+ "char",
+ "float",
+ "String",
+ "boolean",
+ "Boolean",
+ "Double",
+ "Integer",
+ "Long",
+ "Float",
+ "List",
+ "Set",
+ "Map")
+ );
+
+ languageSpecificPrimitives = new HashSet<>(
+ Arrays.asList(
+ "String",
+ "Boolean",
+ "Double",
+ "Int",
+ "Integer",
+ "Long",
+ "Float",
+ "Any",
+ "AnyVal",
+ "AnyRef",
+ "Object",
+ "BigDecimal"
+ )
+ );
+
+ typeMapping = new HashMap<>();
+ typeMapping.put("string", "String");
+ typeMapping.put("boolean", "Boolean");
+ typeMapping.put("integer", "Int");
+ typeMapping.put("long", "Long");
+ typeMapping.put("float", "Float");
+ typeMapping.put("double", "Double");
+ typeMapping.put("number", "BigDecimal");
+ typeMapping.put("decimal", "BigDecimal");
+ typeMapping.put("date", "LocalDate");
+ typeMapping.put("date-time", "Instant");
+ typeMapping.put("offset-date-time", "OffsetDateTime");
+ typeMapping.put("file", "File");
+ typeMapping.put("array", "Seq");
+ typeMapping.put("list", "List");
+ typeMapping.put("map", "Map");
+ typeMapping.put("object", "Json");
+ typeMapping.put("binary", "File");
+ typeMapping.put("Date", "LocalDate");
+ typeMapping.put("DateTime", "Instant");
+ typeMapping.put("OffsetDateTime", "OffsetDateTime");
+ typeMapping.put("uuid", "UUID");
+
+ additionalProperties.put(CodegenConstants.GROUP_ID, groupId);
+ additionalProperties.put(CodegenConstants.ARTIFACT_ID, artifactId);
+ additionalProperties.put(CodegenConstants.ARTIFACT_VERSION, artifactVersion);
+ additionalProperties.put(CodegenConstants.MODEL_PACKAGE, modelPackage());
+ additionalProperties.put(CodegenConstants.API_PACKAGE, apiPackage());
+ additionalProperties.put("infoUrl", "http://org.openapitools");
+ additionalProperties.put("infoEmail", "team@openapitools.org");
+ additionalProperties.put("licenseInfo", "Apache 2.0");
+ additionalProperties.put("licenseUrl", "http://apache.org/licenses/LICENSE-2.0.html");
+
+ importMapping = new HashMap<>();
+ importMapping.put("UUID", "java.util.UUID");
+ importMapping.put("URI", "java.net.URI");
+ importMapping.put("File", "java.io.File");
+ importMapping.put("Json", "io.circe.Json");
+ importMapping.put("Date", "java.util.Date");
+ importMapping.put("Timestamp", "java.sql.Timestamp");
+ importMapping.put("Map", "scala.collection.immutable.Map");
+ importMapping.put("HashMap", "scala.collection.immutable.HashMap");
+ importMapping.put("Seq", "scala.collection.immutable.Seq");
+ importMapping.put("ArrayBuffer", "scala.collection.mutable.ArrayBuffer");
+ importMapping.put("Instant", "java.time.Instant");
+ importMapping.put("LocalDateTime", "java.time.LocalDateTime");
+ importMapping.put("LocalDate", "java.time.LocalDate");
+ importMapping.put("LocalTime", "java.time.LocalTime");
+ importMapping.put("ZonedDateTime", "java.time.ZonedDateTime");
+ importMapping.put("OffsetDateTime", "java.time.OffsetDateTime");
+
+ instantiationTypes.put("array", "Seq");
+ instantiationTypes.put("seq", "Seq");
+ instantiationTypes.put("list", "List");
+ instantiationTypes.put("map", "Map");
+
+ //this option allows inline enums to be separate own models
+ inlineSchemaOption.put("RESOLVE_INLINE_ENUMS", "true");
+
+ }
+
+ @Override
+ public void processOpts() {
+ super.processOpts();
+
+ if (DateLibraries.java8.name().equals(dateLibrary)) {
+ typeMapping.put("date", "LocalDate");
+ typeMapping.put("date-time", "Instant");
+ typeMapping.put("offset-date-time", "OffsetDateTime");
+ typeMapping.put("Date", "LocalDate");
+ typeMapping.put("DateTime", "Instant");
+ typeMapping.put("OffsetDateTime", "OffsetDateTime");
+ this.importMapping.put("LocalDate", "java.time.LocalDate");
+ this.importMapping.put("Instant", "java.time.Instant");
+ this.importMapping.put("OffsetDateTime", "java.time.OffsetDateTime");
+ additionalProperties.put("java8", "true");
+ } else {
+ String error = "DateLibrary " + dateLibrary + " is not supported. Please use java8";
+ LOGGER.error(error);
+ throw new RuntimeException(error);
+ }
+
+ if (additionalProperties.containsKey(CodegenConstants.PACKAGE_NAME)) {
+ packageName = (String) additionalProperties.get(CodegenConstants.PACKAGE_NAME);
+ setApiPackage(packageName + ".apis");
+ setModelPackage(packageName + ".models");
+ additionalProperties.put(CodegenConstants.API_PACKAGE, apiPackage());
+ additionalProperties.put(CodegenConstants.MODEL_PACKAGE, modelPackage());
+ } else {
+ additionalProperties.put(CodegenConstants.PACKAGE_NAME, packageName);
+ }
+ if (additionalProperties.containsKey(CodegenConstants.SOURCE_FOLDER)) {
+ //you can set your own source folder, i.e. target/scala-3.3.3/src_managed/main
+ this.sourceFolder = (String) additionalProperties.get(CodegenConstants.SOURCE_FOLDER);
+ }
+ if (additionalProperties.containsKey(CodegenConstants.ARTIFACT_ID)) {
+ this.artifactId = (String) additionalProperties.get(CodegenConstants.ARTIFACT_ID);
+ }
+
+ additionalProperties.put("fnEnumEntry", new EnumEntryLambda());
+
+ supportingFiles.add(new SupportingFile("failedRequest.mustache", modelFileFolderRelative(), "_FailedRequest.scala"));
+ supportingFiles.add(new SupportingFile("authModel.mustache", modelFileFolderRelative(), "_Authorization.scala"));
+ supportingFiles.add(new SupportingFile("modelsPackage.mustache", modelFileFolderRelative(), "package.scala"));
+
+ if (additionalProperties.containsKey(EXCLUDE_SBT)) {
+ this.excludeSbt = convertPropertyToBoolean(EXCLUDE_SBT);
+ }
+ if (!excludeSbt) {
+ supportingFiles.add(new SupportingFile("build.sbt.mustache", "", "build.sbt"));
+ supportingFiles.add(new SupportingFile("project/build.properties.mustache", "project", "build.properties"));
+ } else {
+ supportingFiles.remove(new SupportingFile("build.sbt.mustache", "", "build.sbt"));
+ supportingFiles.remove(new SupportingFile("project/build.properties.mustache", "project", "build.properties"));
+ }
+ if (additionalProperties.containsKey(EXCLUDE_API)) {
+ this.excludeApi = convertPropertyToBoolean(EXCLUDE_API);
+ }
+ if (!excludeApi) {
+ supportingFiles.add(new SupportingFile("baseClient.mustache", apisFileFolderRelative(), "BaseClient.scala"));
+ supportingFiles.add(new SupportingFile("jsonSupports.mustache", apisFileFolderRelative(), "JsonSupports.scala"));
+ apiTemplateFiles.put("api.mustache", ".scala");
+ } else {
+ supportingFiles.remove(new SupportingFile("baseClient.mustache", apisFileFolderRelative(), "BaseClient.scala"));
+ supportingFiles.remove(new SupportingFile("jsonSupports.mustache", apisFileFolderRelative(), "JsonSupports.scala"));
+ apiTemplateFiles.remove("api.mustache");
+ }
+ }
+
+
+ @Override
+ public String apiFileFolder() {
+ return outputFolder + File.separator + apiFileFolderRelative();
+ }
+
+ private String apiFileFolderRelative() {
+ return sourceFolder + File.separator + apiPackage().replace('.', File.separatorChar);
+ }
+
+ @Override
+ public String modelFileFolder() {
+ return outputFolder + File.separator + modelFileFolderRelative();
+ }
+
+ private String modelFileFolderRelative() {
+ return sourceFolder + File.separator + modelPackage().replace('.', File.separatorChar);
+ }
+
+ private String apisFileFolderRelative() {
+ return sourceFolder + File.separator + apiPackage().replace('.', File.separatorChar);
+ }
+
+ @Override
+ public String escapeReservedWord(String name) {
+ if (this.reservedWordsMappings().containsKey(name)) {
+ return this.reservedWordsMappings().get(name);
+ }
+ return "`" + name + "`";
+ }
+
+ @Override
+ public OperationsMap postProcessOperationsWithModels(OperationsMap objs, List allModels) {
+ if (registerNonStandardStatusCodes) {
+ try {
+ OperationMap opsMap = objs.getOperations();
+ HashSet unknownCodes = new HashSet<>();
+ for (CodegenOperation operation : opsMap.getOperation()) {
+ for (CodegenResponse response : operation.responses) {
+ if ("default".equals(response.code)) {
+ continue;
+ }
+ try {
+ int code = Integer.parseInt(response.code);
+ if (code >= 600) {
+ unknownCodes.add(code);
+ }
+ } catch (NumberFormatException e) {
+ LOGGER.error("Status code is not an integer : response.code", e);
+ }
+ }
+ }
+ if (!unknownCodes.isEmpty()) {
+ additionalProperties.put("unknownStatusCodes", unknownCodes);
+ }
+ } catch (Exception e) {
+ LOGGER.error("Unable to find operations List", e);
+ }
+ }
+ return super.postProcessOperationsWithModels(objs, allModels);
+ }
+
+ @Override
+ public List fromSecurity(Map schemes) {
+ final List codegenSecurities = super.fromSecurity(schemes);
+ if (!removeOAuthSecurities) {
+ return codegenSecurities;
+ }
+
+ // Remove OAuth securities
+ codegenSecurities.removeIf(security -> security.isOAuth);
+ if (codegenSecurities.isEmpty()) {
+ return null;
+ }
+ return codegenSecurities;
+ }
+
+ @Override
+ public String toParamName(String name) {
+ // obtain the name from parameterNameMapping directly if provided
+ if (parameterNameMapping.containsKey(name)) {
+ return parameterNameMapping.get(name);
+ }
+
+ return formatIdentifier(name, false);
+ }
+
+ @Override
+ public String encodePath(String input) {
+ String path = super.encodePath(input);
+
+ // The parameter names in the URI must be converted to the same case as
+ // the method parameter.
+ StringBuilder buf = new StringBuilder(path.length());
+ Matcher matcher = Pattern.compile("[{](.*?)[}]").matcher(path);
+ while (matcher.find()) {
+ matcher.appendReplacement(buf, "\\${" + toParamName(matcher.group(0)) + "}");
+ }
+ matcher.appendTail(buf);
+ return buf.toString();
+ }
+
+ @Override
+ public CodegenOperation fromOperation(String path,
+ String httpMethod,
+ Operation operation,
+ List servers) {
+ CodegenOperation op = super.fromOperation(path, httpMethod, operation, servers);
+ op.path = encodePath(path);
+ return op;
+ }
+
+ @Override
+ public String toEnumName(CodegenProperty property) {
+ return formatIdentifier(property.baseName, true);
+ }
+
+ @Override
+ public String toDefaultValue(Schema p) {
+ if (p.getRequired() != null && p.getRequired().contains(p.getName())) {
+ return "None";
+ }
+
+ if (ModelUtils.isBooleanSchema(p)) {
+ return null;
+ } else if (ModelUtils.isDateSchema(p)) {
+ return null;
+ } else if (ModelUtils.isDateTimeSchema(p)) {
+ return null;
+ } else if (ModelUtils.isNumberSchema(p)) {
+ return null;
+ } else if (ModelUtils.isIntegerSchema(p)) {
+ return null;
+ } else if (ModelUtils.isMapSchema(p)) {
+ String inner = getSchemaType(ModelUtils.getAdditionalProperties(p));
+ return "Map[String, " + inner + "].empty ";
+ } else if (ModelUtils.isArraySchema(p)) {
+ String inner = getSchemaType(ModelUtils.getSchemaItems(p));
+ if (ModelUtils.isSet(p)) {
+ return "Set[" + inner + "].empty ";
+ }
+ return "Seq[" + inner + "].empty ";
+ } else if (ModelUtils.isStringSchema(p)) {
+ return null;
+ } else {
+ return null;
+ }
+ }
+
+
+ private class EnumEntryLambda extends CustomLambda {
+ @Override
+ public String formatFragment(String fragment) {
+ return formatIdentifier(fragment, true);
+ }
+ }
+
+ @Override
+ public String escapeQuotationMark(String input) {
+ // remove " to avoid code injection
+ return input.replace("\"", "");
+ }
+
+}
diff --git a/modules/openapi-generator/src/main/resources/META-INF/services/org.openapitools.codegen.CodegenConfig b/modules/openapi-generator/src/main/resources/META-INF/services/org.openapitools.codegen.CodegenConfig
index 00a51f289abe..fcfde2b56601 100644
--- a/modules/openapi-generator/src/main/resources/META-INF/services/org.openapitools.codegen.CodegenConfig
+++ b/modules/openapi-generator/src/main/resources/META-INF/services/org.openapitools.codegen.CodegenConfig
@@ -124,6 +124,7 @@ org.openapitools.codegen.languages.ScalaPekkoClientCodegen
org.openapitools.codegen.languages.ScalaAkkaHttpServerCodegen
org.openapitools.codegen.languages.ScalaFinchServerCodegen
org.openapitools.codegen.languages.ScalaGatlingCodegen
+org.openapitools.codegen.languages.ScalaHttp4sClientCodegen
org.openapitools.codegen.languages.ScalaHttp4sServerCodegen
org.openapitools.codegen.languages.ScalaLagomServerCodegen
org.openapitools.codegen.languages.ScalaPlayFrameworkServerCodegen
diff --git a/modules/openapi-generator/src/main/resources/scala-http4s/api.mustache b/modules/openapi-generator/src/main/resources/scala-http4s/api.mustache
new file mode 100644
index 000000000000..339ecc9bd37f
--- /dev/null
+++ b/modules/openapi-generator/src/main/resources/scala-http4s/api.mustache
@@ -0,0 +1,60 @@
+{{>licenseInfo}}
+package {{apiPackage}}
+
+import cats.effect.Concurrent
+import io.circe.Encoder
+import org.http4s.Uri
+import org.http4s.client.Client as Http4sClient
+{{#imports}}
+import {{import}}
+{{/imports}}
+import {{modelPackage}}.*
+
+{{#operations}}
+trait {{classname}}Endpoints[F[*]] {
+
+{{#operation}}
+ def {{operationId}}({{>methodParameters}}): F[{{>operationReturnType}}]
+{{/operation}}
+
+}
+{{/operations}}
+
+{{#operations}}
+class {{classname}}EndpointsImpl[F[*]: Concurrent](
+ override val baseUrl: Uri,
+ defaultHeaders: Seq[(String, String)] = Nil,
+ httpClient: Http4sClient[F]
+) extends BaseClient[F](baseUrl, defaultHeaders, httpClient) with {{classname}}Endpoints[F] {
+
+ import JsonSupports.*
+ import io.circe.syntax.EncoderOps
+ import cats.implicits.toFlatMapOps
+
+{{#operation}}
+ override def {{operationId}}({{>methodParameters}}): F[{{>operationReturnType}}] = {
+ val requestHeaders = {{>headerParamCreation}}{{#hasFormParams}}
+ val formParameters = Some(({{#formParams}}
+ {{>paramsCreation}}{{/formParams}}
+ ).toSeq.flatten){{/hasFormParams}}{{#hasQueryParams}}
+ val queryParameters = ({{#queryParams}}
+ {{>paramsCreation}}{{/queryParams}}
+ ).toSeq.flatten{{/hasQueryParams}}
+
+ _executeRequest[{{>bodyParamType}}, {{>operationReturnType}}](
+ method = "{{httpMethod.toUpperCase}}",
+ path = s"{{{path}}}",
+ body = {{#bodyParam}}{{#required}}Some({{paramName}}){{/required}}{{^required}}{{paramName}}{{/required}}{{/bodyParam}}{{^bodyParam}}None{{/bodyParam}},
+ formParameters = {{^hasFormParams}}None,{{/hasFormParams}}{{#hasFormParams}}formParameters,{{/hasFormParams}}
+ queryParameters = {{^hasQueryParams}}Nil,{{/hasQueryParams}}{{#hasQueryParams}}queryParameters,{{/hasQueryParams}}
+ requestHeaders = requestHeaders,
+ auth = {{#authMethods}}Some(auth){{/authMethods}}{{^authMethods}}None{{/authMethods}}) {
+ {{>responseState}}
+ }
+ }
+
+ {{/operation}}
+}
+{{/operations}}
+
+
diff --git a/modules/openapi-generator/src/main/resources/scala-http4s/authModel.mustache b/modules/openapi-generator/src/main/resources/scala-http4s/authModel.mustache
new file mode 100644
index 000000000000..4fbef837d271
--- /dev/null
+++ b/modules/openapi-generator/src/main/resources/scala-http4s/authModel.mustache
@@ -0,0 +1,10 @@
+{{>licenseInfo}}
+package {{modelPackage}}
+
+sealed trait _Authorization
+
+object _Authorization {
+ final case class Basic(username: String, password: Option[String] = None) extends _Authorization
+ final case class ApiKey(name: String, value: String) extends _Authorization
+ final case class Bearer(token: String) extends _Authorization
+}
diff --git a/modules/openapi-generator/src/main/resources/scala-http4s/baseClient.mustache b/modules/openapi-generator/src/main/resources/scala-http4s/baseClient.mustache
new file mode 100644
index 000000000000..aa325edc0a6e
--- /dev/null
+++ b/modules/openapi-generator/src/main/resources/scala-http4s/baseClient.mustache
@@ -0,0 +1,80 @@
+{{>licenseInfo}}
+package {{apiPackage}}
+
+import cats.effect.Concurrent
+import io.circe.Encoder
+import org.http4s.{Header, Headers, Method, Request, Response, Uri, UrlForm}
+import org.http4s.client.Client as Http4sClient
+import org.http4s.QueryParamEncoder.*
+import org.typelevel.ci.CIString
+import java.util.Base64
+import java.nio.charset.StandardCharsets
+import {{modelPackage}}.*
+
+abstract class BaseClient[F[*]: Concurrent](
+ val baseUrl: Uri,
+ defaultHeaders: Seq[(String, String)] = Nil,
+ httpClient: Http4sClient[F]
+) {
+
+ val ApiVersion: String = "{{artifactVersion}}"
+
+ private lazy val defaultApiHeaders = Seq(
+ ("X-Apidoc-Version", ApiVersion)
+ )
+
+ protected def modifyRequest(request: Request[F]): Request[F] = request
+
+ def _executeRequest[T, U](
+ method: String,
+ path: String,
+ body: Option[T] = None,
+ formParameters: Option[Seq[(String, Any)]] = None,
+ queryParameters: Seq[(String, Any)] = Nil,
+ requestHeaders: Seq[(String, String)] = Nil,
+ auth: Option[_Authorization] = None
+ )(handler: Response[F] => F[U])(implicit encoder: Encoder[T]): F[U] = {
+
+ val m = Method.fromString(method) match {
+ case Right(m) => m
+ case Left(e) => sys.error(e.toString)
+ }
+
+ val headers = Headers(
+ (
+ defaultApiHeaders ++
+ defaultHeaders ++
+ requestHeaders
+ ).groupBy(_._1).map { case (k, l) => Header.Raw(CIString(k), l.last._2) }.toList
+ )
+
+ val queryMap = queryParameters.groupBy(_._1).map { case (k, v) => k -> v.map(_._2.toString) }
+ val uri = Uri.unsafeFromString(s"$baseUrl$path").setQueryParams(queryMap)
+
+ val request = Request[F](method = m, uri = uri, headers = headers)
+
+ val reqAndMaybeAuth = auth.fold(request) {
+ case _Authorization.Basic(username, passwordOpt) =>
+ val userpass = s"$username:${passwordOpt.getOrElse("")}"
+ val token = Base64.getEncoder.encodeToString(
+ userpass.getBytes(StandardCharsets.ISO_8859_1)
+ )
+ request.putHeaders(Header.Raw(CIString("Authorization"), s"Basic $token"))
+ case _Authorization.Bearer(token) =>
+ request.putHeaders(Header.Raw(CIString("Authorization"), s"Bearer $token"))
+ case _Authorization.ApiKey(name, value) =>
+ request.putHeaders(Header.Raw(CIString(name), value))
+ }
+ val formBody = formParameters.map { x =>
+ UrlForm(x.groupBy(_._1).map{case (k, v) => (k, v.mkString(","))}.toSeq*)
+ }
+
+ import JsonSupports.*
+ val reqAndMaybeAuthAndBody =
+ if (formBody.nonEmpty) formBody.fold(reqAndMaybeAuth)(reqAndMaybeAuth.withEntity)
+ else body.fold(reqAndMaybeAuth)(reqAndMaybeAuth.withEntity)
+
+ httpClient.run(modifyRequest(reqAndMaybeAuthAndBody)).use(handler)
+ }
+
+}
diff --git a/modules/openapi-generator/src/main/resources/scala-http4s/bodyParamType.mustache b/modules/openapi-generator/src/main/resources/scala-http4s/bodyParamType.mustache
new file mode 100644
index 000000000000..372c5821ada5
--- /dev/null
+++ b/modules/openapi-generator/src/main/resources/scala-http4s/bodyParamType.mustache
@@ -0,0 +1 @@
+{{#hasBodyParam}}{{#allParams}}{{#isBodyParam}}{{dataType}}{{/isBodyParam}}{{/allParams}}{{/hasBodyParam}}{{^hasBodyParam}}Unit{{/hasBodyParam}}
\ No newline at end of file
diff --git a/modules/openapi-generator/src/main/resources/scala-http4s/build.sbt.mustache b/modules/openapi-generator/src/main/resources/scala-http4s/build.sbt.mustache
new file mode 100644
index 000000000000..af5c5a47f385
--- /dev/null
+++ b/modules/openapi-generator/src/main/resources/scala-http4s/build.sbt.mustache
@@ -0,0 +1,34 @@
+scalaVersion := "3.3.3"
+
+version := "{{artifactVersion}}"
+name := "{{artifactId}}"
+organization := "{{groupId}}"
+
+val CirceVersion = "0.14.9"
+val Http4sVersion = "0.23.26"
+
+libraryDependencies ++= Seq(
+ "org.http4s" %% "http4s-ember-client" % Http4sVersion,
+ "org.http4s" %% "http4s-circe" % Http4sVersion,
+ "org.http4s" %% "http4s-dsl" % Http4sVersion,
+ "org.http4s" %% "http4s-client" % Http4sVersion,
+ "io.circe" %% "circe-core" % CirceVersion,
+ "io.circe" %% "circe-generic" % CirceVersion,
+ "io.circe" %% "circe-parser" % CirceVersion,
+ "org.scalatest" %% "scalatest" % "3.2.19" % "test"
+)
+
+scalacOptions := Seq(
+ "-encoding",
+ "UTF-8",
+ "-deprecation",
+ "-unchecked",
+ "-feature",
+ "-language:existentials,experimental.macros,higherKinds,implicitConversions,postfixOps,adhocExtensions",
+ "-Yretain-trees",
+ "-Xmax-inlines:100",
+ "-Ykind-projector:underscores",
+ "-source:future"
+)
+
+Compile / publishArtifact := false
\ No newline at end of file
diff --git a/modules/openapi-generator/src/main/resources/scala-http4s/errorResponsePart.mustache b/modules/openapi-generator/src/main/resources/scala-http4s/errorResponsePart.mustache
new file mode 100644
index 000000000000..b879b7607f69
--- /dev/null
+++ b/modules/openapi-generator/src/main/resources/scala-http4s/errorResponsePart.mustache
@@ -0,0 +1 @@
+{{#dataType}}case r if r.status.code == {{code}} => parseJson[F, {{dataType}}]("{{dataType}}", r).flatMap(res => Concurrent[F].raiseError(_FailedRequest(r.status.code, res.asJson.noSpaces))){{/dataType}}{{^dataType}}case r if r.status.code == {{code}} => Concurrent[F].raiseError(_FailedRequest(r.status.code, r.status.reason)){{/dataType}}
\ No newline at end of file
diff --git a/modules/openapi-generator/src/main/resources/scala-http4s/failedRequest.mustache b/modules/openapi-generator/src/main/resources/scala-http4s/failedRequest.mustache
new file mode 100644
index 000000000000..a2845b037741
--- /dev/null
+++ b/modules/openapi-generator/src/main/resources/scala-http4s/failedRequest.mustache
@@ -0,0 +1,33 @@
+{{>licenseInfo}}
+package {{modelPackage}}
+
+import io.circe.*
+import io.circe.Decoder.*
+import io.circe.Encoder.*
+import io.circe.syntax.*
+
+case class _FailedRequest(code: Int, message: String) extends Exception(s"Server return status code: $code; message: $message")
+
+object _FailedRequest {
+
+ given encoderFailedRequest: Encoder[_FailedRequest] = Encoder.instance { t =>
+ Json.fromFields{
+ Seq(
+ "code" -> t.code.asJson,
+ "message" -> t.message.asJson
+ )
+ }
+ }
+
+ given decodeFailedRequest: Decoder[_FailedRequest] = Decoder.instance { c =>
+ for {
+ code <- c.downField("code").as[Int]
+ message <- c.downField("message").as[String]
+ } yield _FailedRequest(
+ code = code,
+ message = message
+ )
+ }
+
+}
+
diff --git a/modules/openapi-generator/src/main/resources/scala-http4s/headerParamCreation.mustache b/modules/openapi-generator/src/main/resources/scala-http4s/headerParamCreation.mustache
new file mode 100644
index 000000000000..fd92ce93e069
--- /dev/null
+++ b/modules/openapi-generator/src/main/resources/scala-http4s/headerParamCreation.mustache
@@ -0,0 +1,4 @@
+Seq(
+ Some("Content-Type" -> {{#consumes.0}}"{{{mediaType}}}"{{/consumes.0}}{{^consumes}}"application/json"{{/consumes}}){{#hasHeaderParams}},
+ {{#headerParams}}{{#required}}Some("{{baseName}}" -> {{{paramName}}}){{/required}}{{^required}}{{{paramName}}}.map(x => "{{baseName}}" -> x){{/required}}{{^-last}}, {{/-last}}{{/headerParams}}{{/hasHeaderParams}}
+ ).flatten
\ No newline at end of file
diff --git a/modules/openapi-generator/src/main/resources/scala-http4s/jsonSupports.mustache b/modules/openapi-generator/src/main/resources/scala-http4s/jsonSupports.mustache
new file mode 100644
index 000000000000..6b6ae135e041
--- /dev/null
+++ b/modules/openapi-generator/src/main/resources/scala-http4s/jsonSupports.mustache
@@ -0,0 +1,29 @@
+{{>licenseInfo}}
+package {{apiPackage}}
+
+import cats.effect.*
+import cats.implicits.*
+import io.circe.{Decoder, Encoder}
+import org.http4s.{EntityDecoder, EntityEncoder, Response}
+import org.http4s.circe as http4sCirce
+import {{modelPackage}}.*
+
+object JsonSupports {
+
+ implicit def circeJsonEncoder[F[*]: Concurrent, A](implicit encoder: Encoder[A]): EntityEncoder[F, A] =
+ http4sCirce.jsonEncoderOf[F, A]
+ implicit def circeJsonDecoder[F[*]: Concurrent, A](implicit decoder: Decoder[A]): EntityDecoder[F, A] =
+ http4sCirce.jsonOf[F, A]
+
+ def parseJson[F[*]: Concurrent, T](
+ className: String,
+ r: Response[F]
+ )(implicit decoder: Decoder[T]): F[T] = r.attemptAs[T].value.flatMap {
+ case Right(value) => Concurrent[F].pure(value)
+ case Left(error) => Concurrent[F].raiseError(
+ _FailedRequest(r.status.code, s"Invalid json for class[$className]: error $error")
+ )
+ }
+
+}
+
diff --git a/modules/openapi-generator/src/main/resources/scala-http4s/licenseInfo.mustache b/modules/openapi-generator/src/main/resources/scala-http4s/licenseInfo.mustache
new file mode 100644
index 000000000000..b9d17f0cd440
--- /dev/null
+++ b/modules/openapi-generator/src/main/resources/scala-http4s/licenseInfo.mustache
@@ -0,0 +1,10 @@
+/** {{{appName}}}
+ * {{{appDescription}}}
+ *
+ * {{#version}}The version of the OpenAPI document: {{{.}}}{{/version}}
+ * {{#infoEmail}}Contact: {{{.}}}{{/infoEmail}}
+ *
+ * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).
+ * https://openapi-generator.tech
+ * Do not edit the class manually.
+ */
\ No newline at end of file
diff --git a/modules/openapi-generator/src/main/resources/scala-http4s/methodParameters.mustache b/modules/openapi-generator/src/main/resources/scala-http4s/methodParameters.mustache
new file mode 100644
index 000000000000..c80db9ee5675
--- /dev/null
+++ b/modules/openapi-generator/src/main/resources/scala-http4s/methodParameters.mustache
@@ -0,0 +1 @@
+{{#allParams}}{{paramName}}: {{#required}}{{dataType}}{{/required}}{{^required}}Option[{{dataType}}]{{/required}}{{^defaultValue}}{{^required}} = None{{/required}}{{/defaultValue}}{{^-last}}, {{/-last}}{{/allParams}}{{#authMethods.0}}{{#isApiKey}})(implicit auth: _Authorization.ApiKey{{/isApiKey}}{{#isBasic}}{{#isBasicBasic}})(implicit auth: _Authorization.Basic{{/isBasicBasic}}{{#isBasicBearer}})(implicit auth: _Authorization.Bearer{{/isBasicBearer}}{{/isBasic}}{{/authMethods.0}}
\ No newline at end of file
diff --git a/modules/openapi-generator/src/main/resources/scala-http4s/model.mustache b/modules/openapi-generator/src/main/resources/scala-http4s/model.mustache
new file mode 100644
index 000000000000..912925158182
--- /dev/null
+++ b/modules/openapi-generator/src/main/resources/scala-http4s/model.mustache
@@ -0,0 +1,70 @@
+{{>licenseInfo}}
+package {{modelPackage}}
+
+import io.circe.*
+import io.circe.syntax.*
+import io.circe.{Decoder, Encoder}
+
+{{#imports}}
+import {{import}}
+{{/imports}}
+
+{{#models}}
+{{#model}}
+/** {{{description}}}
+{{#vars}}
+ * @param {{name}} {{{description}}}
+{{/vars}}
+ */
+{{#isEnum}}
+enum {{classname}}(val value: String) {
+ {{#allowableValues}}
+ {{#values}}
+ case {{#fnEnumEntry}}{{.}}{{/fnEnumEntry}} extends {{classname}}("{{.}}")
+ {{/values}}
+ {{/allowableValues}}
+}
+
+object {{classname}} {
+ given decoder{{classname}}: Decoder[{{classname}}] =
+ Decoder.decodeString.map(str => {{classname}}.values.find(_.value == str)
+ .getOrElse(throw java.lang.IllegalArgumentException(s"{{classname}} enum case not found: $str"))
+ )
+
+ given encoder{{classname}}: Encoder[{{classname}}] =
+ Encoder.encodeString.contramap[{{classname}}](_.value)
+}
+{{/isEnum}}
+{{^isEnum}}
+case class {{classname}}(
+{{#vars}}
+ {{name}}: {{^required}}Option[{{{dataType}}}] = None{{/required}}{{#required}}{{{dataType}}}{{/required}}{{^-last}},{{/-last}}
+{{/vars}}
+)
+
+object {{classname}} {
+ given encoder{{classname}}: Encoder[{{classname}}] = Encoder.instance { t =>
+ Json.fromFields{
+ Seq(
+ {{#vars}}
+ {{#required}}Some("{{baseName}}" -> t.{{name}}.asJson){{/required}}{{^required}}t.{{name}}.map(v => "{{baseName}}" -> v.asJson){{/required}}{{^-last}},{{/-last}}
+ {{/vars}}
+ ).flatten
+ }
+ }
+ given decoder{{classname}}: Decoder[{{classname}}] = Decoder.instance { c =>
+ for {
+ {{#vars}}
+ {{name}} <- c.downField("{{baseName}}").as[{{^required}}Option[{{{dataType}}}]{{/required}}{{#required}}{{{dataType}}}{{/required}}]
+ {{/vars}}
+ } yield {{classname}}(
+ {{#vars}}
+ {{name}} = {{name}}{{^-last}},{{/-last}}
+ {{/vars}}
+ )
+ }
+}
+{{/isEnum}}
+{{/model}}
+{{/models}}
+
diff --git a/modules/openapi-generator/src/main/resources/scala-http4s/modelsPackage.mustache b/modules/openapi-generator/src/main/resources/scala-http4s/modelsPackage.mustache
new file mode 100644
index 000000000000..5211e1dd5cc7
--- /dev/null
+++ b/modules/openapi-generator/src/main/resources/scala-http4s/modelsPackage.mustache
@@ -0,0 +1,32 @@
+{{>licenseInfo}}
+package {{packageName}}
+
+import io.circe.{Decoder, Encoder}
+
+package object models {
+
+ given decodeUUID: Decoder[_root_.java.util.UUID] =
+ Decoder.decodeString.map(str => _root_.java.util.UUID.fromString(str))
+
+ given encodeUUID: Encoder[_root_.java.util.UUID] =
+ Encoder.encodeString.contramap[_root_.java.util.UUID](uuid => uuid.toString)
+
+ given decodeInstant: Decoder[_root_.java.time.Instant] =
+ Decoder.decodeString.map(str => _root_.java.time.OffsetDateTime.parse(str).toInstant)
+
+ given encodeInstant: Encoder[_root_.java.time.Instant] =
+ Encoder.encodeString.contramap[_root_.java.time.Instant](_.toString)
+
+ given decodeLocalDate: Decoder[_root_.java.time.LocalDate] =
+ Decoder.decodeString.map(str => _root_.java.time.LocalDate.parse(str))
+
+ given encodeLocalDate: Encoder[_root_.java.time.LocalDate] =
+ Encoder.encodeString.contramap[_root_.java.time.LocalDate](_.toString)
+
+ given decodeJson: Decoder[io.circe.Json] =
+ Decoder.decodeString.map(str => io.circe.Json.fromString(str))
+
+ given encodeJson: Encoder[io.circe.Json] =
+ Encoder.encodeString.contramap[io.circe.Json](_.toString)
+
+}
\ No newline at end of file
diff --git a/modules/openapi-generator/src/main/resources/scala-http4s/operationReturnType.mustache b/modules/openapi-generator/src/main/resources/scala-http4s/operationReturnType.mustache
new file mode 100644
index 000000000000..170935cad639
--- /dev/null
+++ b/modules/openapi-generator/src/main/resources/scala-http4s/operationReturnType.mustache
@@ -0,0 +1 @@
+{{{returnType}}}{{^returnType}}Unit{{/returnType}}
\ No newline at end of file
diff --git a/modules/openapi-generator/src/main/resources/scala-http4s/paramsCreation.mustache b/modules/openapi-generator/src/main/resources/scala-http4s/paramsCreation.mustache
new file mode 100644
index 000000000000..361e7e889737
--- /dev/null
+++ b/modules/openapi-generator/src/main/resources/scala-http4s/paramsCreation.mustache
@@ -0,0 +1 @@
+{{#isContainer}}{{#required}}Some({{paramName}}.map("{{baseName}}" -> _)){{/required}}{{^required}}{{paramName}}.map(x => x.map("{{baseName}}" -> _)){{/required}}{{/isContainer}}{{^isContainer}}{{#required}}Some(Seq("{{baseName}}" -> {{{paramName}}})){{/required}}{{^required}}{{paramName}}.map("{{baseName}}" -> _).map(Seq(_)){{/required}}{{/isContainer}}{{^-last}} ++ {{/-last}}
\ No newline at end of file
diff --git a/modules/openapi-generator/src/main/resources/scala-http4s/project/build.properties.mustache b/modules/openapi-generator/src/main/resources/scala-http4s/project/build.properties.mustache
new file mode 100644
index 000000000000..4d5f78cc412a
--- /dev/null
+++ b/modules/openapi-generator/src/main/resources/scala-http4s/project/build.properties.mustache
@@ -0,0 +1 @@
+sbt.version=1.9.9
\ No newline at end of file
diff --git a/modules/openapi-generator/src/main/resources/scala-http4s/responseState.mustache b/modules/openapi-generator/src/main/resources/scala-http4s/responseState.mustache
new file mode 100644
index 000000000000..b8aa1645d66f
--- /dev/null
+++ b/modules/openapi-generator/src/main/resources/scala-http4s/responseState.mustache
@@ -0,0 +1,5 @@
+{{#hasOnlyDefaultResponse}}{{#responses.0}}{{#dataType}}r => parseJson[F, {{>operationReturnType}}]("{{>operationReturnType}}", r){{/dataType}}{{^dataType}}r => Concurrent[F].pure(()){{/dataType}}{{/responses.0}}{{/hasOnlyDefaultResponse}}{{^hasOnlyDefaultResponse}}{{#responses}}{{#is2xx}}
+ {{>successResponsePart}}{{/is2xx}}{{^is2xx}}{{#is3xx}}
+ {{>successResponsePart}}{{/is3xx}}{{^is3xx}}{{^isDefault}}
+ {{>errorResponsePart}}{{/isDefault}}{{#isDefault}}
+ {{#dataType}}case r => parseJson[F, {{dataType}}]("{{dataType}}", r).flatMap(res => Concurrent[F].raiseError(_FailedRequest(r.status.code, res.asJson.noSpaces))){{/dataType}}{{^dataType}}case r => Concurrent[F].raiseError(_FailedRequest(r.status.code, r.status.reason)){{/dataType}}{{/isDefault}}{{/is3xx}}{{/is2xx}}{{/responses}}{{/hasOnlyDefaultResponse}}
\ No newline at end of file
diff --git a/modules/openapi-generator/src/main/resources/scala-http4s/successResponsePart.mustache b/modules/openapi-generator/src/main/resources/scala-http4s/successResponsePart.mustache
new file mode 100644
index 000000000000..75ceace3e8ed
--- /dev/null
+++ b/modules/openapi-generator/src/main/resources/scala-http4s/successResponsePart.mustache
@@ -0,0 +1 @@
+{{#dataType}}case r if r.status.code == {{code}} => parseJson[F, {{>operationReturnType}}]("{{>operationReturnType}}", r){{/dataType}}{{^dataType}}case r if r.status.code == {{code}} => Concurrent[F].pure(()){{/dataType}}
\ No newline at end of file
diff --git a/modules/openapi-generator/src/test/java/org/openapitools/codegen/options/ScalaHttp4sClientCodegenOptionsProvider.java b/modules/openapi-generator/src/test/java/org/openapitools/codegen/options/ScalaHttp4sClientCodegenOptionsProvider.java
new file mode 100644
index 000000000000..bacc31603dab
--- /dev/null
+++ b/modules/openapi-generator/src/test/java/org/openapitools/codegen/options/ScalaHttp4sClientCodegenOptionsProvider.java
@@ -0,0 +1,54 @@
+package org.openapitools.codegen.options;
+
+import org.openapitools.codegen.CodegenConstants;
+import com.google.common.collect.ImmutableMap;
+
+import java.io.File;
+import java.util.Map;
+
+public class ScalaHttp4sClientCodegenOptionsProvider implements OptionsProvider {
+ public static final String ARTIFACT_ID_VALUE = "scala-http4s-client";
+ public static final String PACKAGE_NAME_VALUE = "org.openapitools.client";
+ public static final String MODEL_PACKAGE_VALUE = PACKAGE_NAME_VALUE + ".models";
+ public static final String API_PACKAGE_VALUE = PACKAGE_NAME_VALUE + ".apis";
+ public static final String MODEL_PROPERTY_NAMING = "camelCase";
+ public static final String SOURCE_FOLDER_VALUE = "src" + File.separator + "main" + File.separator + "scala";
+ public static final String SORT_PARAMS_VALUE = "false";
+ public static final String SORT_MODEL_PROPERTIES_VALUE = "false";
+ public static final String ENSURE_UNIQUE_PARAMS_VALUE = "true";
+ public static final String ALLOW_UNICODE_IDENTIFIERS_VALUE = "false";
+ public static final String PREPEND_FORM_OR_BODY_PARAMETERS_VALUE = "true";
+ public static final String DATE_LIBRARY = "java8";
+
+ @Override
+ public String getLanguage() {
+ return "scala-http4s";
+ }
+
+ @Override
+ public Map createOptions() {
+ ImmutableMap.Builder builder = new ImmutableMap.Builder();
+ return builder
+ .put(CodegenConstants.ARTIFACT_ID, ARTIFACT_ID_VALUE)
+ .put(CodegenConstants.PACKAGE_NAME, PACKAGE_NAME_VALUE)
+ .put(CodegenConstants.MODEL_PACKAGE, MODEL_PACKAGE_VALUE)
+ .put(CodegenConstants.API_PACKAGE, API_PACKAGE_VALUE)
+ .put(CodegenConstants.MODEL_PROPERTY_NAMING, MODEL_PROPERTY_NAMING)
+ .put(CodegenConstants.SOURCE_FOLDER, SOURCE_FOLDER_VALUE)
+ .put(CodegenConstants.SORT_PARAMS_BY_REQUIRED_FLAG, SORT_PARAMS_VALUE)
+ .put(CodegenConstants.SORT_MODEL_PROPERTIES_BY_REQUIRED_FLAG, SORT_MODEL_PROPERTIES_VALUE)
+ .put(CodegenConstants.ENSURE_UNIQUE_PARAMS, ENSURE_UNIQUE_PARAMS_VALUE)
+ .put(CodegenConstants.ALLOW_UNICODE_IDENTIFIERS, ALLOW_UNICODE_IDENTIFIERS_VALUE)
+ .put(CodegenConstants.PREPEND_FORM_OR_BODY_PARAMETERS, PREPEND_FORM_OR_BODY_PARAMETERS_VALUE)
+ .put("dateLibrary", DATE_LIBRARY)
+ .put(CodegenConstants.LEGACY_DISCRIMINATOR_BEHAVIOR, "true")
+ .put("excludeSbt", "false")
+ .put("excludeApi", "false")
+ .build();
+ }
+
+ @Override
+ public boolean isServer() {
+ return false;
+ }
+}
\ No newline at end of file
diff --git a/modules/openapi-generator/src/test/java/org/openapitools/codegen/scala/ScalaHttp4sClientCodegenTest.java b/modules/openapi-generator/src/test/java/org/openapitools/codegen/scala/ScalaHttp4sClientCodegenTest.java
new file mode 100644
index 000000000000..419f629819d9
--- /dev/null
+++ b/modules/openapi-generator/src/test/java/org/openapitools/codegen/scala/ScalaHttp4sClientCodegenTest.java
@@ -0,0 +1,157 @@
+package org.openapitools.codegen.scala;
+
+import io.swagger.v3.oas.models.media.DateTimeSchema;
+import io.swagger.v3.oas.models.media.IntegerSchema;
+import io.swagger.v3.oas.models.media.Schema;
+import io.swagger.v3.oas.models.media.StringSchema;
+import io.swagger.v3.parser.util.SchemaTypeUtil;
+import org.jetbrains.annotations.NotNull;
+import org.openapitools.codegen.*;
+import org.openapitools.codegen.config.CodegenConfigurator;
+import org.openapitools.codegen.languages.ScalaHttp4sClientCodegen;
+import org.testng.annotations.Test;
+
+import java.io.File;
+import java.io.IOException;
+import java.nio.file.Files;
+import java.util.List;
+
+import static org.testng.Assert.*;
+
+public class ScalaHttp4sClientCodegenTest {
+
+ @Test(description = "convert a simple java model")
+ public void simpleModelTest() {
+ ScalaHttp4sClientCodegen codegen = new ScalaHttp4sClientCodegen();
+ final Schema model = new Schema()
+ .description("a sample model")
+ .addProperty("id", new IntegerSchema().format(SchemaTypeUtil.INTEGER64_FORMAT))
+ .addProperty("name", new StringSchema())
+ .addProperty("created_at", new DateTimeSchema())
+ .addRequiredItem("id")
+ .addRequiredItem("name");
+ CodegenModel cm = codegen.fromModel("sample", model);
+
+ assertEquals(cm.name, "sample");
+ assertEquals(cm.classname, "Sample");
+ assertEquals(cm.description, "a sample model");
+ assertEquals(cm.vars.size(), 3);
+ assertEquals(cm.vars.stream().filter(CodegenProperty::getRequired).count(), 2);
+ }
+
+ @Test(description = "happy path test")
+ public void happyPathTest() throws IOException {
+ ScalaHttp4sClientCodegen codegen = new ScalaHttp4sClientCodegen();
+ assertEquals(codegen.getName(), "scala-http4s");
+ assertEquals(codegen.getTag(), CodegenType.CLIENT);
+
+ File output = Files.createTempDirectory("test").toFile();
+ output.deleteOnExit();
+
+ final CodegenConfigurator configurator = new CodegenConfigurator()
+ .setGeneratorName(codegen.getName())
+ .setInputSpec("src/test/resources/3_0/scala-http4s/petstore.yaml")
+ .setOutputDir(output.getAbsolutePath().replace("\\", "/"));
+
+ final ClientOptInput clientOptInput = configurator.toClientOptInput();
+ DefaultGenerator generator = getDefaultGenerator();
+
+ List files = generator.opts(clientOptInput).generate();
+
+ TestUtils.ensureContainsFile(files, output, "build.sbt");
+ TestUtils.ensureContainsFile(files, output, "project/build.properties");
+ TestUtils.ensureContainsFile(files, output, "src/main/scala/org/openapitools/client/models/package.scala");
+ TestUtils.ensureContainsFile(files, output, "src/main/scala/org/openapitools/client/models/_Authorization.scala");
+ TestUtils.ensureContainsFile(files, output, "src/main/scala/org/openapitools/client/models/_FailedRequest.scala");
+ TestUtils.ensureContainsFile(files, output, "src/main/scala/org/openapitools/client/models/ApiResponse.scala");
+ TestUtils.ensureContainsFile(files, output, "src/main/scala/org/openapitools/client/models/Category.scala");
+ TestUtils.ensureContainsFile(files, output, "src/main/scala/org/openapitools/client/models/FindPetsByStatusStatusParameterInner.scala");
+ TestUtils.ensureContainsFile(files, output, "src/main/scala/org/openapitools/client/models/Order.scala");
+ TestUtils.ensureContainsFile(files, output, "src/main/scala/org/openapitools/client/models/OrderStatus.scala");
+ TestUtils.ensureContainsFile(files, output, "src/main/scala/org/openapitools/client/models/Pet.scala");
+ TestUtils.ensureContainsFile(files, output, "src/main/scala/org/openapitools/client/models/PetStatus.scala");
+ TestUtils.ensureContainsFile(files, output, "src/main/scala/org/openapitools/client/models/Tag.scala");
+ TestUtils.ensureContainsFile(files, output, "src/main/scala/org/openapitools/client/models/User.scala");
+ TestUtils.ensureContainsFile(files, output, "src/main/scala/org/openapitools/client/apis/BaseClient.scala");
+ TestUtils.ensureContainsFile(files, output, "src/main/scala/org/openapitools/client/apis/JsonSupports.scala");
+ TestUtils.ensureContainsFile(files, output, "src/main/scala/org/openapitools/client/apis/PetApi.scala");
+ TestUtils.ensureContainsFile(files, output, "src/main/scala/org/openapitools/client/apis/StoreApi.scala");
+ TestUtils.ensureContainsFile(files, output, "src/main/scala/org/openapitools/client/apis/UserApi.scala");
+
+ }
+
+ @NotNull
+ private static DefaultGenerator getDefaultGenerator() {
+ DefaultGenerator generator = new DefaultGenerator();
+
+ generator.setGenerateMetadata(false);
+
+ generator.setGeneratorPropertyDefault(CodegenConstants.MODELS, "true");
+ generator.setGeneratorPropertyDefault(CodegenConstants.MODEL_TESTS, "false");
+ generator.setGeneratorPropertyDefault(CodegenConstants.MODEL_DOCS, "false");
+ generator.setGeneratorPropertyDefault(CodegenConstants.APIS, "true");
+ generator.setGeneratorPropertyDefault(CodegenConstants.SUPPORTING_FILES, "true");
+ return generator;
+ }
+
+ @Test(description = "use Instant for date-time")
+ public void dateTimeToInstant() {
+ ScalaHttp4sClientCodegen codegen = new ScalaHttp4sClientCodegen();
+
+ final Schema> schema = new Schema()
+ .description("Schema with date-time");
+ schema.setType("string");
+ schema.setFormat("date-time");
+ String type = codegen.getTypeDeclaration(schema);
+ assertEquals(type, "Instant");
+ }
+
+ @Test
+ public void allowExcludeSbtFiles() {
+ ScalaHttp4sClientCodegen codegen = new ScalaHttp4sClientCodegen();
+ assertFalse(codegen.isExcludeSbt());
+ codegen.additionalProperties().put("excludeSbt", "true");
+ assertEquals(codegen.additionalProperties().get("excludeSbt"), "true");
+ codegen.processOpts();
+ assertTrue(codegen.isExcludeSbt());
+ }
+
+ @Test
+ public void allowExcludeApiFiles() {
+ ScalaHttp4sClientCodegen codegen = new ScalaHttp4sClientCodegen();
+ assertFalse(codegen.isExcludeApi());
+ codegen.additionalProperties().put("excludeApi", "true");
+ assertEquals(codegen.additionalProperties().get("excludeApi"), "true");
+ codegen.processOpts();
+ assertTrue(codegen.isExcludeApi());
+ }
+
+ @Test
+ public void convertVarNameCamelCase() {
+ ScalaHttp4sClientCodegen codegen = new ScalaHttp4sClientCodegen();
+ assertEquals(CodegenConstants.ENUM_PROPERTY_NAMING_TYPE.camelCase.name(), codegen.getModelPropertyNaming());
+ assertEquals(codegen.toVarName("name"), "name");
+ assertEquals(codegen.toVarName("user-name"), "userName");
+ assertEquals(codegen.toVarName("user_name"), "userName");
+ assertEquals(codegen.toVarName("user|name"), "userName");
+ assertEquals(codegen.toVarName("uSername"), "uSername");
+ assertEquals(codegen.toVarName("USERNAME"), "USERNAME");
+ assertEquals(codegen.toVarName("USER123NAME"), "USER123NAME");
+ assertEquals(codegen.toVarName("1"), "`1`");
+ assertEquals(codegen.toVarName("1a"), "`1a`");
+ assertEquals(codegen.toVarName("1A"), "`1A`");
+ assertEquals(codegen.toVarName("1AAAA"), "`1AAAA`");
+ assertEquals(codegen.toVarName("1AAaa"), "`1aAaa`");
+ }
+
+ @Test
+ public void encodePath() {
+ ScalaHttp4sClientCodegen codegen = new ScalaHttp4sClientCodegen();
+ assertEquals(codegen.encodePath("{user_name}"), "${userName}");
+ assertEquals(codegen.encodePath("{userName}"), "${userName}");
+ assertEquals(codegen.encodePath("{UserName}"), "${userName}");
+ assertEquals(codegen.encodePath("user_name"), "user_name");
+ assertEquals(codegen.encodePath("before/{UserName}/after"), "before/${userName}/after");
+ }
+
+}
diff --git a/modules/openapi-generator/src/test/resources/3_0/scala-http4s/petstore.yaml b/modules/openapi-generator/src/test/resources/3_0/scala-http4s/petstore.yaml
new file mode 100644
index 000000000000..947376fc0082
--- /dev/null
+++ b/modules/openapi-generator/src/test/resources/3_0/scala-http4s/petstore.yaml
@@ -0,0 +1,742 @@
+openapi: 3.0.0
+servers:
+ - url: 'https://petstore.swagger.io/v2'
+info:
+ description: >-
+ This is a sample server Petstore server. For this sample, you can use the api key
+ `special-key` to test the authorization filters.
+ version: 1.0.0
+ title: OpenAPI Petstore
+ license:
+ name: Apache-2.0
+ url: 'https://www.apache.org/licenses/LICENSE-2.0.html'
+tags:
+ - name: pet
+ description: Everything about your Pets
+ - name: store
+ description: Access to Petstore orders
+ - name: user
+ description: Operations about user
+paths:
+ /pet:
+ post:
+ tags:
+ - pet
+ summary: Add a new pet to the store
+ description: ''
+ operationId: addPet
+ responses:
+ '200':
+ description: successful operation
+ content:
+ application/xml:
+ schema:
+ $ref: '#/components/schemas/Pet'
+ application/json:
+ schema:
+ $ref: '#/components/schemas/Pet'
+ '405':
+ description: Invalid input
+ security:
+ - petstore_auth:
+ - 'write:pets'
+ - 'read:pets'
+ requestBody:
+ $ref: '#/components/requestBodies/Pet'
+ put:
+ tags:
+ - pet
+ summary: Update an existing pet
+ description: ''
+ operationId: updatePet
+ responses:
+ '200':
+ description: successful operation
+ content:
+ application/xml:
+ schema:
+ $ref: '#/components/schemas/Pet'
+ application/json:
+ schema:
+ $ref: '#/components/schemas/Pet'
+ '400':
+ description: Invalid ID supplied
+ '404':
+ description: Pet not found
+ '405':
+ description: Validation exception
+ security:
+ - petstore_auth:
+ - 'write:pets'
+ - 'read:pets'
+ requestBody:
+ $ref: '#/components/requestBodies/Pet'
+ /pet/findByStatus:
+ get:
+ tags:
+ - pet
+ summary: Finds Pets by status
+ description: Multiple status values can be provided with comma separated strings
+ operationId: findPetsByStatus
+ parameters:
+ - name: status
+ in: query
+ description: Status values that need to be considered for filter
+ required: true
+ style: form
+ explode: false
+ schema:
+ type: array
+ items:
+ type: string
+ enum:
+ - available
+ - pending
+ - sold
+ default: available
+ responses:
+ '200':
+ description: successful operation
+ content:
+ application/xml:
+ schema:
+ type: array
+ items:
+ $ref: '#/components/schemas/Pet'
+ application/json:
+ schema:
+ type: array
+ items:
+ $ref: '#/components/schemas/Pet'
+ '400':
+ description: Invalid status value
+ security:
+ - petstore_auth:
+ - 'read:pets'
+ /pet/findByTags:
+ get:
+ tags:
+ - pet
+ summary: Finds Pets by tags
+ description: >-
+ Multiple tags can be provided with comma separated strings. Use tag1,
+ tag2, tag3 for testing.
+ operationId: findPetsByTags
+ parameters:
+ - name: tags
+ in: query
+ description: Tags to filter by
+ required: true
+ style: form
+ explode: false
+ schema:
+ type: array
+ items:
+ type: string
+ responses:
+ '200':
+ description: successful operation
+ content:
+ application/xml:
+ schema:
+ type: array
+ items:
+ $ref: '#/components/schemas/Pet'
+ application/json:
+ schema:
+ type: array
+ items:
+ $ref: '#/components/schemas/Pet'
+ '400':
+ description: Invalid tag value
+ security:
+ - petstore_auth:
+ - 'read:pets'
+ deprecated: true
+ '/pet/{petId}':
+ get:
+ tags:
+ - pet
+ summary: Find pet by ID
+ description: Returns a single pet
+ operationId: getPetById
+ parameters:
+ - name: petId
+ in: path
+ description: ID of pet to return
+ required: true
+ schema:
+ type: integer
+ format: int64
+ responses:
+ '200':
+ description: successful operation
+ content:
+ application/xml:
+ schema:
+ $ref: '#/components/schemas/Pet'
+ application/json:
+ schema:
+ $ref: '#/components/schemas/Pet'
+ '400':
+ description: Invalid ID supplied
+ '404':
+ description: Pet not found
+ security:
+ - api_key: []
+ post:
+ tags:
+ - pet
+ summary: Updates a pet in the store with form data
+ description: ''
+ operationId: updatePetWithForm
+ parameters:
+ - name: petId
+ in: path
+ description: ID of pet that needs to be updated
+ required: true
+ schema:
+ type: integer
+ format: int64
+ responses:
+ '200':
+ description: successful operation
+ '405':
+ description: Invalid input
+ security:
+ - petstore_auth:
+ - 'write:pets'
+ - 'read:pets'
+ requestBody:
+ content:
+ application/x-www-form-urlencoded:
+ schema:
+ type: object
+ properties:
+ name:
+ description: Updated name of the pet
+ type: string
+ status:
+ description: Updated status of the pet
+ type: string
+ delete:
+ tags:
+ - pet
+ summary: Deletes a pet
+ description: ''
+ operationId: deletePet
+ parameters:
+ - name: api_key
+ in: header
+ required: false
+ schema:
+ type: string
+ - name: petId
+ in: path
+ description: Pet id to delete
+ required: true
+ schema:
+ type: integer
+ format: int64
+ responses:
+ '400':
+ description: Invalid pet value
+ security:
+ - petstore_auth:
+ - 'write:pets'
+ - 'read:pets'
+ '/pet/{petId}/uploadImage':
+ post:
+ tags:
+ - pet
+ summary: uploads an image
+ description: ''
+ operationId: uploadFile
+ parameters:
+ - name: petId
+ in: path
+ description: ID of pet to update
+ required: true
+ schema:
+ type: integer
+ format: int64
+ responses:
+ '200':
+ description: successful operation
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/ApiResponse'
+ security:
+ - petstore_auth:
+ - 'write:pets'
+ - 'read:pets'
+ requestBody:
+ content:
+ multipart/form-data:
+ schema:
+ type: object
+ properties:
+ additionalMetadata:
+ description: Additional data to pass to server
+ type: string
+ file:
+ description: file to upload
+ type: string
+ format: binary
+ /store/inventory:
+ get:
+ tags:
+ - store
+ summary: Returns pet inventories by status
+ description: Returns a map of status codes to quantities
+ operationId: getInventory
+ responses:
+ '200':
+ description: successful operation
+ content:
+ application/json:
+ schema:
+ type: object
+ additionalProperties:
+ type: integer
+ format: int32
+ security:
+ - api_key: []
+ /store/order:
+ post:
+ tags:
+ - store
+ summary: Place an order for a pet
+ description: ''
+ operationId: placeOrder
+ responses:
+ '200':
+ description: successful operation
+ content:
+ application/xml:
+ schema:
+ $ref: '#/components/schemas/Order'
+ application/json:
+ schema:
+ $ref: '#/components/schemas/Order'
+ '400':
+ description: Invalid Order
+ requestBody:
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/Order'
+ description: order placed for purchasing the pet
+ required: true
+ '/store/order/{orderId}':
+ get:
+ tags:
+ - store
+ summary: Find purchase order by ID
+ description: >-
+ For valid response try integer IDs with value <= 5 or > 10. Other values
+ will generate exceptions
+ operationId: getOrderById
+ parameters:
+ - name: orderId
+ in: path
+ description: ID of pet that needs to be fetched
+ required: true
+ schema:
+ type: integer
+ format: int64
+ minimum: 1
+ maximum: 5
+ responses:
+ '200':
+ description: successful operation
+ content:
+ application/xml:
+ schema:
+ $ref: '#/components/schemas/Order'
+ application/json:
+ schema:
+ $ref: '#/components/schemas/Order'
+ '400':
+ description: Invalid ID supplied
+ '404':
+ description: Order not found
+ delete:
+ tags:
+ - store
+ summary: Delete purchase order by ID
+ description: >-
+ For valid response try integer IDs with value < 1000. Anything above
+ 1000 or nonintegers will generate API errors
+ operationId: deleteOrder
+ parameters:
+ - name: orderId
+ in: path
+ description: ID of the order that needs to be deleted
+ required: true
+ schema:
+ type: string
+ responses:
+ '400':
+ description: Invalid ID supplied
+ '404':
+ description: Order not found
+ /user:
+ post:
+ tags:
+ - user
+ summary: Create user
+ description: This can only be done by the logged in user.
+ operationId: createUser
+ responses:
+ default:
+ description: successful operation
+ security:
+ - auth_cookie: []
+ requestBody:
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/User'
+ description: Created user object
+ required: true
+ /user/createWithArray:
+ post:
+ tags:
+ - user
+ summary: Creates list of users with given input array
+ description: ''
+ operationId: createUsersWithArrayInput
+ responses:
+ default:
+ description: successful operation
+ security:
+ - auth_cookie: []
+ requestBody:
+ $ref: '#/components/requestBodies/UserArray'
+ /user/createWithList:
+ post:
+ tags:
+ - user
+ summary: Creates list of users with given input array
+ description: ''
+ operationId: createUsersWithListInput
+ responses:
+ default:
+ description: successful operation
+ security:
+ - auth_cookie: []
+ requestBody:
+ $ref: '#/components/requestBodies/UserArray'
+ /user/login:
+ get:
+ tags:
+ - user
+ summary: Logs user into the system
+ description: ''
+ operationId: loginUser
+ parameters:
+ - name: username
+ in: query
+ description: The user name for login
+ required: true
+ schema:
+ type: string
+ pattern: '^[a-zA-Z0-9]+[a-zA-Z0-9\.\-_]*[a-zA-Z0-9]+$'
+ - name: password
+ in: query
+ description: The password for login in clear text
+ required: true
+ schema:
+ type: string
+ responses:
+ '200':
+ description: successful operation
+ headers:
+ Set-Cookie:
+ description: >-
+ Cookie authentication key for use with the `auth_cookie`
+ apiKey authentication.
+ schema:
+ type: string
+ example: AUTH_KEY=abcde12345; Path=/; HttpOnly
+ X-Rate-Limit:
+ description: calls per hour allowed by the user
+ schema:
+ type: integer
+ format: int32
+ X-Expires-After:
+ description: date in UTC when token expires
+ schema:
+ type: string
+ format: date-time
+ content:
+ application/xml:
+ schema:
+ type: string
+ application/json:
+ schema:
+ type: string
+ '400':
+ description: Invalid username/password supplied
+ /user/logout:
+ get:
+ tags:
+ - user
+ summary: Logs out current logged in user session
+ description: ''
+ operationId: logoutUser
+ responses:
+ default:
+ description: successful operation
+ security:
+ - auth_cookie: []
+ '/user/{username}':
+ get:
+ tags:
+ - user
+ summary: Get user by user name
+ description: ''
+ operationId: getUserByName
+ parameters:
+ - name: username
+ in: path
+ description: The name that needs to be fetched. Use user1 for testing.
+ required: true
+ schema:
+ type: string
+ responses:
+ '200':
+ description: successful operation
+ content:
+ application/xml:
+ schema:
+ $ref: '#/components/schemas/User'
+ application/json:
+ schema:
+ $ref: '#/components/schemas/User'
+ '400':
+ description: Invalid username supplied
+ '404':
+ description: User not found
+ put:
+ tags:
+ - user
+ summary: Updated user
+ description: This can only be done by the logged in user.
+ operationId: updateUser
+ parameters:
+ - name: username
+ in: path
+ description: name that need to be deleted
+ required: true
+ schema:
+ type: string
+ responses:
+ '400':
+ description: Invalid user supplied
+ '404':
+ description: User not found
+ security:
+ - auth_cookie: []
+ requestBody:
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/User'
+ description: Updated user object
+ required: true
+ delete:
+ tags:
+ - user
+ summary: Delete user
+ description: This can only be done by the logged in user.
+ operationId: deleteUser
+ parameters:
+ - name: username
+ in: path
+ description: The name that needs to be deleted
+ required: true
+ schema:
+ type: string
+ responses:
+ '400':
+ description: Invalid username supplied
+ '404':
+ description: User not found
+ security:
+ - auth_cookie: []
+externalDocs:
+ description: Find out more about Swagger
+ url: 'http://swagger.io'
+components:
+ requestBodies:
+ UserArray:
+ content:
+ application/json:
+ schema:
+ type: array
+ items:
+ $ref: '#/components/schemas/User'
+ description: List of user object
+ required: true
+ Pet:
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/Pet'
+ application/xml:
+ schema:
+ $ref: '#/components/schemas/Pet'
+ description: Pet object that needs to be added to the store
+ required: true
+ securitySchemes:
+ petstore_auth:
+ type: oauth2
+ flows:
+ implicit:
+ authorizationUrl: 'http://petstore.swagger.io/api/oauth/dialog'
+ scopes:
+ 'write:pets': modify pets in your account
+ 'read:pets': read your pets
+ api_key:
+ type: apiKey
+ name: api_key
+ in: header
+ auth_cookie:
+ type: apiKey
+ name: AUTH_KEY
+ in: cookie
+ schemas:
+ Order:
+ title: Pet Order
+ description: An order for a pets from the pet store
+ type: object
+ properties:
+ id:
+ type: integer
+ format: int64
+ petId:
+ type: integer
+ format: int64
+ quantity:
+ type: integer
+ format: int32
+ shipDate:
+ type: string
+ format: date-time
+ status:
+ type: string
+ description: Order Status
+ enum:
+ - placed
+ - approved
+ - delivered
+ complete:
+ type: boolean
+ default: false
+ xml:
+ name: Order
+ Category:
+ title: Pet category
+ description: A category for a pet
+ type: object
+ properties:
+ id:
+ type: integer
+ format: int64
+ name:
+ type: string
+ pattern: '^[a-zA-Z0-9]+[a-zA-Z0-9\.\-_]*[a-zA-Z0-9]+$'
+ xml:
+ name: Category
+ User:
+ title: a User
+ description: A User who is purchasing from the pet store
+ type: object
+ properties:
+ id:
+ type: integer
+ format: int64
+ username:
+ type: string
+ firstName:
+ type: string
+ lastName:
+ type: string
+ email:
+ type: string
+ password:
+ type: string
+ phone:
+ type: string
+ userStatus:
+ type: integer
+ format: int32
+ description: User Status
+ xml:
+ name: User
+ Tag:
+ title: Pet Tag
+ description: A tag for a pet
+ type: object
+ properties:
+ id:
+ type: integer
+ format: int64
+ name:
+ type: string
+ xml:
+ name: Tag
+ Pet:
+ title: a Pet
+ description: A pet for sale in the pet store
+ type: object
+ required:
+ - name
+ - photoUrls
+ properties:
+ id:
+ type: integer
+ format: int64
+ category:
+ $ref: '#/components/schemas/Category'
+ name:
+ type: string
+ example: doggie
+ photoUrls:
+ type: array
+ xml:
+ name: photoUrl
+ wrapped: true
+ items:
+ type: string
+ tags:
+ type: array
+ xml:
+ name: tag
+ wrapped: true
+ items:
+ $ref: '#/components/schemas/Tag'
+ status:
+ type: string
+ description: pet status in the store
+ enum:
+ - available
+ - pending
+ - sold
+ xml:
+ name: Pet
+ ApiResponse:
+ title: An uploaded response
+ description: Describes the result of uploading an image resource
+ type: object
+ properties:
+ code:
+ type: integer
+ format: int32
+ type:
+ type: string
+ message:
+ type: string
diff --git a/samples/client/petstore/scala-http4s/.openapi-generator-ignore b/samples/client/petstore/scala-http4s/.openapi-generator-ignore
new file mode 100644
index 000000000000..7484ee590a38
--- /dev/null
+++ b/samples/client/petstore/scala-http4s/.openapi-generator-ignore
@@ -0,0 +1,23 @@
+# OpenAPI Generator Ignore
+# Generated by openapi-generator https://github.com/openapitools/openapi-generator
+
+# Use this file to prevent files from being overwritten by the generator.
+# The patterns follow closely to .gitignore or .dockerignore.
+
+# As an example, the C# client generator defines ApiClient.cs.
+# You can make changes and tell OpenAPI Generator to ignore just this file by uncommenting the following line:
+#ApiClient.cs
+
+# You can match any string of characters against a directory, file or extension with a single asterisk (*):
+#foo/*/qux
+# The above matches foo/bar/qux and foo/baz/qux, but not foo/bar/baz/qux
+
+# You can recursively match patterns against a directory, file or extension with a double asterisk (**):
+#foo/**/qux
+# This matches foo/bar/qux, foo/baz/qux, and foo/bar/baz/qux
+
+# You can also negate patterns with an exclamation (!).
+# For example, you can ignore all files in a docs folder with the file extension .md:
+#docs/*.md
+# Then explicitly reverse the ignore rule for a single file:
+#!docs/README.md
diff --git a/samples/client/petstore/scala-http4s/.openapi-generator/FILES b/samples/client/petstore/scala-http4s/.openapi-generator/FILES
new file mode 100644
index 000000000000..d9a14d04d59f
--- /dev/null
+++ b/samples/client/petstore/scala-http4s/.openapi-generator/FILES
@@ -0,0 +1,19 @@
+build.sbt
+project/build.properties
+src/main/scala/org/openapitools/client/apis/BaseClient.scala
+src/main/scala/org/openapitools/client/apis/JsonSupports.scala
+src/main/scala/org/openapitools/client/apis/PetApi.scala
+src/main/scala/org/openapitools/client/apis/StoreApi.scala
+src/main/scala/org/openapitools/client/apis/UserApi.scala
+src/main/scala/org/openapitools/client/models/ApiResponse.scala
+src/main/scala/org/openapitools/client/models/Category.scala
+src/main/scala/org/openapitools/client/models/FindPetsByStatusStatusParameterInner.scala
+src/main/scala/org/openapitools/client/models/Order.scala
+src/main/scala/org/openapitools/client/models/OrderStatus.scala
+src/main/scala/org/openapitools/client/models/Pet.scala
+src/main/scala/org/openapitools/client/models/PetStatus.scala
+src/main/scala/org/openapitools/client/models/Tag.scala
+src/main/scala/org/openapitools/client/models/User.scala
+src/main/scala/org/openapitools/client/models/_Authorization.scala
+src/main/scala/org/openapitools/client/models/_FailedRequest.scala
+src/main/scala/org/openapitools/client/models/package.scala
diff --git a/samples/client/petstore/scala-http4s/.openapi-generator/VERSION b/samples/client/petstore/scala-http4s/.openapi-generator/VERSION
new file mode 100644
index 000000000000..17f2442ff3bc
--- /dev/null
+++ b/samples/client/petstore/scala-http4s/.openapi-generator/VERSION
@@ -0,0 +1 @@
+7.9.0-SNAPSHOT
diff --git a/samples/client/petstore/scala-http4s/build.sbt b/samples/client/petstore/scala-http4s/build.sbt
new file mode 100644
index 000000000000..c74943d3ae78
--- /dev/null
+++ b/samples/client/petstore/scala-http4s/build.sbt
@@ -0,0 +1,34 @@
+scalaVersion := "3.3.3"
+
+version := "1.0.0"
+name := "scala-http4s-client"
+organization := "org.openapitools"
+
+val CirceVersion = "0.14.9"
+val Http4sVersion = "0.23.26"
+
+libraryDependencies ++= Seq(
+ "org.http4s" %% "http4s-ember-client" % Http4sVersion,
+ "org.http4s" %% "http4s-circe" % Http4sVersion,
+ "org.http4s" %% "http4s-dsl" % Http4sVersion,
+ "org.http4s" %% "http4s-client" % Http4sVersion,
+ "io.circe" %% "circe-core" % CirceVersion,
+ "io.circe" %% "circe-generic" % CirceVersion,
+ "io.circe" %% "circe-parser" % CirceVersion,
+ "org.scalatest" %% "scalatest" % "3.2.19" % "test"
+)
+
+scalacOptions := Seq(
+ "-encoding",
+ "UTF-8",
+ "-deprecation",
+ "-unchecked",
+ "-feature",
+ "-language:existentials,experimental.macros,higherKinds,implicitConversions,postfixOps,adhocExtensions",
+ "-Yretain-trees",
+ "-Xmax-inlines:100",
+ "-Ykind-projector:underscores",
+ "-source:future"
+)
+
+Compile / publishArtifact := false
\ No newline at end of file
diff --git a/samples/client/petstore/scala-http4s/project/build.properties b/samples/client/petstore/scala-http4s/project/build.properties
new file mode 100644
index 000000000000..4d5f78cc412a
--- /dev/null
+++ b/samples/client/petstore/scala-http4s/project/build.properties
@@ -0,0 +1 @@
+sbt.version=1.9.9
\ No newline at end of file
diff --git a/samples/client/petstore/scala-http4s/src/main/scala/org/openapitools/client/apis/BaseClient.scala b/samples/client/petstore/scala-http4s/src/main/scala/org/openapitools/client/apis/BaseClient.scala
new file mode 100644
index 000000000000..aafc08df2b37
--- /dev/null
+++ b/samples/client/petstore/scala-http4s/src/main/scala/org/openapitools/client/apis/BaseClient.scala
@@ -0,0 +1,89 @@
+/** OpenAPI Petstore
+ * This is a sample server Petstore server. For this sample, you can use the api key `special-key` to test the authorization filters.
+ *
+ * The version of the OpenAPI document: 1.0.0
+ * Contact: team@openapitools.org
+ *
+ * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).
+ * https://openapi-generator.tech
+ * Do not edit the class manually.
+ */
+package org.openapitools.client.apis
+
+import cats.effect.Concurrent
+import io.circe.Encoder
+import org.http4s.{Header, Headers, Method, Request, Response, Uri, UrlForm}
+import org.http4s.client.Client as Http4sClient
+import org.http4s.QueryParamEncoder.*
+import org.typelevel.ci.CIString
+import java.util.Base64
+import java.nio.charset.StandardCharsets
+import org.openapitools.client.models.*
+
+abstract class BaseClient[F[*]: Concurrent](
+ val baseUrl: Uri,
+ defaultHeaders: Seq[(String, String)] = Nil,
+ httpClient: Http4sClient[F]
+) {
+
+ val ApiVersion: String = "1.0.0"
+
+ private lazy val defaultApiHeaders = Seq(
+ ("X-Apidoc-Version", ApiVersion)
+ )
+
+ protected def modifyRequest(request: Request[F]): Request[F] = request
+
+ def _executeRequest[T, U](
+ method: String,
+ path: String,
+ body: Option[T] = None,
+ formParameters: Option[Seq[(String, Any)]] = None,
+ queryParameters: Seq[(String, Any)] = Nil,
+ requestHeaders: Seq[(String, String)] = Nil,
+ auth: Option[_Authorization] = None
+ )(handler: Response[F] => F[U])(implicit encoder: Encoder[T]): F[U] = {
+
+ val m = Method.fromString(method) match {
+ case Right(m) => m
+ case Left(e) => sys.error(e.toString)
+ }
+
+ val headers = Headers(
+ (
+ defaultApiHeaders ++
+ defaultHeaders ++
+ requestHeaders
+ ).groupBy(_._1).map { case (k, l) => Header.Raw(CIString(k), l.last._2) }.toList
+ )
+
+ val queryMap = queryParameters.groupBy(_._1).map { case (k, v) => k -> v.map(_._2.toString) }
+ val uri = Uri.unsafeFromString(s"$baseUrl$path").setQueryParams(queryMap)
+
+ val request = Request[F](method = m, uri = uri, headers = headers)
+
+ val reqAndMaybeAuth = auth.fold(request) {
+ case _Authorization.Basic(username, passwordOpt) =>
+ val userpass = s"$username:${passwordOpt.getOrElse("")}"
+ val token = Base64.getEncoder.encodeToString(
+ userpass.getBytes(StandardCharsets.ISO_8859_1)
+ )
+ request.putHeaders(Header.Raw(CIString("Authorization"), s"Basic $token"))
+ case _Authorization.Bearer(token) =>
+ request.putHeaders(Header.Raw(CIString("Authorization"), s"Bearer $token"))
+ case _Authorization.ApiKey(name, value) =>
+ request.putHeaders(Header.Raw(CIString(name), value))
+ }
+ val formBody = formParameters.map { x =>
+ UrlForm(x.groupBy(_._1).map{case (k, v) => (k, v.mkString(","))}.toSeq*)
+ }
+
+ import JsonSupports.*
+ val reqAndMaybeAuthAndBody =
+ if (formBody.nonEmpty) formBody.fold(reqAndMaybeAuth)(reqAndMaybeAuth.withEntity)
+ else body.fold(reqAndMaybeAuth)(reqAndMaybeAuth.withEntity)
+
+ httpClient.run(modifyRequest(reqAndMaybeAuthAndBody)).use(handler)
+ }
+
+}
diff --git a/samples/client/petstore/scala-http4s/src/main/scala/org/openapitools/client/apis/JsonSupports.scala b/samples/client/petstore/scala-http4s/src/main/scala/org/openapitools/client/apis/JsonSupports.scala
new file mode 100644
index 000000000000..70dd487ea314
--- /dev/null
+++ b/samples/client/petstore/scala-http4s/src/main/scala/org/openapitools/client/apis/JsonSupports.scala
@@ -0,0 +1,38 @@
+/** OpenAPI Petstore
+ * This is a sample server Petstore server. For this sample, you can use the api key `special-key` to test the authorization filters.
+ *
+ * The version of the OpenAPI document: 1.0.0
+ * Contact: team@openapitools.org
+ *
+ * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).
+ * https://openapi-generator.tech
+ * Do not edit the class manually.
+ */
+package org.openapitools.client.apis
+
+import cats.effect.*
+import cats.implicits.*
+import io.circe.{Decoder, Encoder}
+import org.http4s.{EntityDecoder, EntityEncoder, Response}
+import org.http4s.circe as http4sCirce
+import org.openapitools.client.models.*
+
+object JsonSupports {
+
+ implicit def circeJsonEncoder[F[*]: Concurrent, A](implicit encoder: Encoder[A]): EntityEncoder[F, A] =
+ http4sCirce.jsonEncoderOf[F, A]
+ implicit def circeJsonDecoder[F[*]: Concurrent, A](implicit decoder: Decoder[A]): EntityDecoder[F, A] =
+ http4sCirce.jsonOf[F, A]
+
+ def parseJson[F[*]: Concurrent, T](
+ className: String,
+ r: Response[F]
+ )(implicit decoder: Decoder[T]): F[T] = r.attemptAs[T].value.flatMap {
+ case Right(value) => Concurrent[F].pure(value)
+ case Left(error) => Concurrent[F].raiseError(
+ _FailedRequest(r.status.code, s"Invalid json for class[$className]: error $error")
+ )
+ }
+
+}
+
diff --git a/samples/client/petstore/scala-http4s/src/main/scala/org/openapitools/client/apis/PetApi.scala b/samples/client/petstore/scala-http4s/src/main/scala/org/openapitools/client/apis/PetApi.scala
new file mode 100644
index 000000000000..e562c0edfb3c
--- /dev/null
+++ b/samples/client/petstore/scala-http4s/src/main/scala/org/openapitools/client/apis/PetApi.scala
@@ -0,0 +1,217 @@
+/** OpenAPI Petstore
+ * This is a sample server Petstore server. For this sample, you can use the api key `special-key` to test the authorization filters.
+ *
+ * The version of the OpenAPI document: 1.0.0
+ * Contact: team@openapitools.org
+ *
+ * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).
+ * https://openapi-generator.tech
+ * Do not edit the class manually.
+ */
+package org.openapitools.client.apis
+
+import cats.effect.Concurrent
+import io.circe.Encoder
+import org.http4s.Uri
+import org.http4s.client.Client as Http4sClient
+import org.openapitools.client.models.ApiResponse
+import java.io.File
+import org.openapitools.client.models.FindPetsByStatusStatusParameterInner
+import org.openapitools.client.models.Pet
+import scala.collection.immutable.Seq
+import org.openapitools.client.models.*
+
+trait PetApiEndpoints[F[*]] {
+
+ def addPet(pet: Pet): F[Pet]
+ def deletePet(petId: Long, apiKey: Option[String] = None): F[Unit]
+ def findPetsByStatus(status: Seq[FindPetsByStatusStatusParameterInner]): F[Seq[Pet]]
+ def findPetsByTags(tags: Seq[String]): F[Seq[Pet]]
+ def getPetById(petId: Long)(implicit auth: _Authorization.ApiKey): F[Pet]
+ def updatePet(pet: Pet): F[Pet]
+ def updatePetWithForm(petId: Long, name: Option[String] = None, status: Option[String] = None): F[Unit]
+ def uploadFile(petId: Long, additionalMetadata: Option[String] = None, file: Option[File] = None): F[ApiResponse]
+
+}
+
+class PetApiEndpointsImpl[F[*]: Concurrent](
+ override val baseUrl: Uri,
+ defaultHeaders: Seq[(String, String)] = Nil,
+ httpClient: Http4sClient[F]
+) extends BaseClient[F](baseUrl, defaultHeaders, httpClient) with PetApiEndpoints[F] {
+
+ import JsonSupports.*
+ import io.circe.syntax.EncoderOps
+ import cats.implicits.toFlatMapOps
+
+ override def addPet(pet: Pet): F[Pet] = {
+ val requestHeaders = Seq(
+ Some("Content-Type" -> "application/json")
+ ).flatten
+
+ _executeRequest[Pet, Pet](
+ method = "POST",
+ path = s"/pet",
+ body = Some(pet),
+ formParameters = None,
+ queryParameters = Nil,
+ requestHeaders = requestHeaders,
+ auth = None) {
+
+ case r if r.status.code == 200 => parseJson[F, Pet]("Pet", r)
+ case r if r.status.code == 405 => Concurrent[F].raiseError(_FailedRequest(r.status.code, r.status.reason))
+ }
+ }
+
+ override def deletePet(petId: Long, apiKey: Option[String] = None): F[Unit] = {
+ val requestHeaders = Seq(
+ Some("Content-Type" -> "application/json"),
+ apiKey.map(x => "api_key" -> x)
+ ).flatten
+
+ _executeRequest[Unit, Unit](
+ method = "DELETE",
+ path = s"/pet/${petId}",
+ body = None,
+ formParameters = None,
+ queryParameters = Nil,
+ requestHeaders = requestHeaders,
+ auth = None) {
+
+ case r if r.status.code == 400 => Concurrent[F].raiseError(_FailedRequest(r.status.code, r.status.reason))
+ }
+ }
+
+ override def findPetsByStatus(status: Seq[FindPetsByStatusStatusParameterInner]): F[Seq[Pet]] = {
+ val requestHeaders = Seq(
+ Some("Content-Type" -> "application/json")
+ ).flatten
+ val queryParameters = (
+ Some(status.map("status" -> _))
+ ).toSeq.flatten
+
+ _executeRequest[Unit, Seq[Pet]](
+ method = "GET",
+ path = s"/pet/findByStatus",
+ body = None,
+ formParameters = None,
+ queryParameters = queryParameters,
+ requestHeaders = requestHeaders,
+ auth = None) {
+
+ case r if r.status.code == 200 => parseJson[F, Seq[Pet]]("Seq[Pet]", r)
+ case r if r.status.code == 400 => Concurrent[F].raiseError(_FailedRequest(r.status.code, r.status.reason))
+ }
+ }
+
+ override def findPetsByTags(tags: Seq[String]): F[Seq[Pet]] = {
+ val requestHeaders = Seq(
+ Some("Content-Type" -> "application/json")
+ ).flatten
+ val queryParameters = (
+ Some(tags.map("tags" -> _))
+ ).toSeq.flatten
+
+ _executeRequest[Unit, Seq[Pet]](
+ method = "GET",
+ path = s"/pet/findByTags",
+ body = None,
+ formParameters = None,
+ queryParameters = queryParameters,
+ requestHeaders = requestHeaders,
+ auth = None) {
+
+ case r if r.status.code == 200 => parseJson[F, Seq[Pet]]("Seq[Pet]", r)
+ case r if r.status.code == 400 => Concurrent[F].raiseError(_FailedRequest(r.status.code, r.status.reason))
+ }
+ }
+
+ override def getPetById(petId: Long)(implicit auth: _Authorization.ApiKey): F[Pet] = {
+ val requestHeaders = Seq(
+ Some("Content-Type" -> "application/json")
+ ).flatten
+
+ _executeRequest[Unit, Pet](
+ method = "GET",
+ path = s"/pet/${petId}",
+ body = None,
+ formParameters = None,
+ queryParameters = Nil,
+ requestHeaders = requestHeaders,
+ auth = Some(auth)) {
+
+ case r if r.status.code == 200 => parseJson[F, Pet]("Pet", r)
+ case r if r.status.code == 400 => Concurrent[F].raiseError(_FailedRequest(r.status.code, r.status.reason))
+ case r if r.status.code == 404 => Concurrent[F].raiseError(_FailedRequest(r.status.code, r.status.reason))
+ }
+ }
+
+ override def updatePet(pet: Pet): F[Pet] = {
+ val requestHeaders = Seq(
+ Some("Content-Type" -> "application/json")
+ ).flatten
+
+ _executeRequest[Pet, Pet](
+ method = "PUT",
+ path = s"/pet",
+ body = Some(pet),
+ formParameters = None,
+ queryParameters = Nil,
+ requestHeaders = requestHeaders,
+ auth = None) {
+
+ case r if r.status.code == 200 => parseJson[F, Pet]("Pet", r)
+ case r if r.status.code == 400 => Concurrent[F].raiseError(_FailedRequest(r.status.code, r.status.reason))
+ case r if r.status.code == 404 => Concurrent[F].raiseError(_FailedRequest(r.status.code, r.status.reason))
+ case r if r.status.code == 405 => Concurrent[F].raiseError(_FailedRequest(r.status.code, r.status.reason))
+ }
+ }
+
+ override def updatePetWithForm(petId: Long, name: Option[String] = None, status: Option[String] = None): F[Unit] = {
+ val requestHeaders = Seq(
+ Some("Content-Type" -> "application/x-www-form-urlencoded")
+ ).flatten
+ val formParameters = Some((
+ name.map("name" -> _).map(Seq(_)) ++
+ status.map("status" -> _).map(Seq(_))
+ ).toSeq.flatten)
+
+ _executeRequest[Unit, Unit](
+ method = "POST",
+ path = s"/pet/${petId}",
+ body = None,
+ formParameters = formParameters,
+ queryParameters = Nil,
+ requestHeaders = requestHeaders,
+ auth = None) {
+
+ case r if r.status.code == 200 => Concurrent[F].pure(())
+ case r if r.status.code == 405 => Concurrent[F].raiseError(_FailedRequest(r.status.code, r.status.reason))
+ }
+ }
+
+ override def uploadFile(petId: Long, additionalMetadata: Option[String] = None, file: Option[File] = None): F[ApiResponse] = {
+ val requestHeaders = Seq(
+ Some("Content-Type" -> "multipart/form-data")
+ ).flatten
+ val formParameters = Some((
+ additionalMetadata.map("additionalMetadata" -> _).map(Seq(_)) ++
+ file.map("file" -> _).map(Seq(_))
+ ).toSeq.flatten)
+
+ _executeRequest[Unit, ApiResponse](
+ method = "POST",
+ path = s"/pet/${petId}/uploadImage",
+ body = None,
+ formParameters = formParameters,
+ queryParameters = Nil,
+ requestHeaders = requestHeaders,
+ auth = None) {
+
+ case r if r.status.code == 200 => parseJson[F, ApiResponse]("ApiResponse", r)
+ }
+ }
+
+}
+
+
diff --git a/samples/client/petstore/scala-http4s/src/main/scala/org/openapitools/client/apis/StoreApi.scala b/samples/client/petstore/scala-http4s/src/main/scala/org/openapitools/client/apis/StoreApi.scala
new file mode 100644
index 000000000000..a0c33365cc0a
--- /dev/null
+++ b/samples/client/petstore/scala-http4s/src/main/scala/org/openapitools/client/apis/StoreApi.scala
@@ -0,0 +1,117 @@
+/** OpenAPI Petstore
+ * This is a sample server Petstore server. For this sample, you can use the api key `special-key` to test the authorization filters.
+ *
+ * The version of the OpenAPI document: 1.0.0
+ * Contact: team@openapitools.org
+ *
+ * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).
+ * https://openapi-generator.tech
+ * Do not edit the class manually.
+ */
+package org.openapitools.client.apis
+
+import cats.effect.Concurrent
+import io.circe.Encoder
+import org.http4s.Uri
+import org.http4s.client.Client as Http4sClient
+import org.openapitools.client.models.Order
+import org.openapitools.client.models.*
+
+trait StoreApiEndpoints[F[*]] {
+
+ def deleteOrder(orderId: String): F[Unit]
+ def getInventory()(implicit auth: _Authorization.ApiKey): F[Map[String, Int]]
+ def getOrderById(orderId: Long): F[Order]
+ def placeOrder(order: Order): F[Order]
+
+}
+
+class StoreApiEndpointsImpl[F[*]: Concurrent](
+ override val baseUrl: Uri,
+ defaultHeaders: Seq[(String, String)] = Nil,
+ httpClient: Http4sClient[F]
+) extends BaseClient[F](baseUrl, defaultHeaders, httpClient) with StoreApiEndpoints[F] {
+
+ import JsonSupports.*
+ import io.circe.syntax.EncoderOps
+ import cats.implicits.toFlatMapOps
+
+ override def deleteOrder(orderId: String): F[Unit] = {
+ val requestHeaders = Seq(
+ Some("Content-Type" -> "application/json")
+ ).flatten
+
+ _executeRequest[Unit, Unit](
+ method = "DELETE",
+ path = s"/store/order/${orderId}",
+ body = None,
+ formParameters = None,
+ queryParameters = Nil,
+ requestHeaders = requestHeaders,
+ auth = None) {
+
+ case r if r.status.code == 400 => Concurrent[F].raiseError(_FailedRequest(r.status.code, r.status.reason))
+ case r if r.status.code == 404 => Concurrent[F].raiseError(_FailedRequest(r.status.code, r.status.reason))
+ }
+ }
+
+ override def getInventory()(implicit auth: _Authorization.ApiKey): F[Map[String, Int]] = {
+ val requestHeaders = Seq(
+ Some("Content-Type" -> "application/json")
+ ).flatten
+
+ _executeRequest[Unit, Map[String, Int]](
+ method = "GET",
+ path = s"/store/inventory",
+ body = None,
+ formParameters = None,
+ queryParameters = Nil,
+ requestHeaders = requestHeaders,
+ auth = Some(auth)) {
+
+ case r if r.status.code == 200 => parseJson[F, Map[String, Int]]("Map[String, Int]", r)
+ }
+ }
+
+ override def getOrderById(orderId: Long): F[Order] = {
+ val requestHeaders = Seq(
+ Some("Content-Type" -> "application/json")
+ ).flatten
+
+ _executeRequest[Unit, Order](
+ method = "GET",
+ path = s"/store/order/${orderId}",
+ body = None,
+ formParameters = None,
+ queryParameters = Nil,
+ requestHeaders = requestHeaders,
+ auth = None) {
+
+ case r if r.status.code == 200 => parseJson[F, Order]("Order", r)
+ case r if r.status.code == 400 => Concurrent[F].raiseError(_FailedRequest(r.status.code, r.status.reason))
+ case r if r.status.code == 404 => Concurrent[F].raiseError(_FailedRequest(r.status.code, r.status.reason))
+ }
+ }
+
+ override def placeOrder(order: Order): F[Order] = {
+ val requestHeaders = Seq(
+ Some("Content-Type" -> "application/json")
+ ).flatten
+
+ _executeRequest[Order, Order](
+ method = "POST",
+ path = s"/store/order",
+ body = Some(order),
+ formParameters = None,
+ queryParameters = Nil,
+ requestHeaders = requestHeaders,
+ auth = None) {
+
+ case r if r.status.code == 200 => parseJson[F, Order]("Order", r)
+ case r if r.status.code == 400 => Concurrent[F].raiseError(_FailedRequest(r.status.code, r.status.reason))
+ }
+ }
+
+}
+
+
diff --git a/samples/client/petstore/scala-http4s/src/main/scala/org/openapitools/client/apis/UserApi.scala b/samples/client/petstore/scala-http4s/src/main/scala/org/openapitools/client/apis/UserApi.scala
new file mode 100644
index 000000000000..749cb93d4b80
--- /dev/null
+++ b/samples/client/petstore/scala-http4s/src/main/scala/org/openapitools/client/apis/UserApi.scala
@@ -0,0 +1,196 @@
+/** OpenAPI Petstore
+ * This is a sample server Petstore server. For this sample, you can use the api key `special-key` to test the authorization filters.
+ *
+ * The version of the OpenAPI document: 1.0.0
+ * Contact: team@openapitools.org
+ *
+ * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).
+ * https://openapi-generator.tech
+ * Do not edit the class manually.
+ */
+package org.openapitools.client.apis
+
+import cats.effect.Concurrent
+import io.circe.Encoder
+import org.http4s.Uri
+import org.http4s.client.Client as Http4sClient
+import java.time.Instant
+import scala.collection.immutable.Seq
+import org.openapitools.client.models.User
+import org.openapitools.client.models.*
+
+trait UserApiEndpoints[F[*]] {
+
+ def createUser(user: User)(implicit auth: _Authorization.ApiKey): F[Unit]
+ def createUsersWithArrayInput(user: Seq[User])(implicit auth: _Authorization.ApiKey): F[Unit]
+ def createUsersWithListInput(user: Seq[User])(implicit auth: _Authorization.ApiKey): F[Unit]
+ def deleteUser(username: String)(implicit auth: _Authorization.ApiKey): F[Unit]
+ def getUserByName(username: String): F[User]
+ def loginUser(username: String, password: String): F[String]
+ def logoutUser()(implicit auth: _Authorization.ApiKey): F[Unit]
+ def updateUser(username: String, user: User)(implicit auth: _Authorization.ApiKey): F[Unit]
+
+}
+
+class UserApiEndpointsImpl[F[*]: Concurrent](
+ override val baseUrl: Uri,
+ defaultHeaders: Seq[(String, String)] = Nil,
+ httpClient: Http4sClient[F]
+) extends BaseClient[F](baseUrl, defaultHeaders, httpClient) with UserApiEndpoints[F] {
+
+ import JsonSupports.*
+ import io.circe.syntax.EncoderOps
+ import cats.implicits.toFlatMapOps
+
+ override def createUser(user: User)(implicit auth: _Authorization.ApiKey): F[Unit] = {
+ val requestHeaders = Seq(
+ Some("Content-Type" -> "application/json")
+ ).flatten
+
+ _executeRequest[User, Unit](
+ method = "POST",
+ path = s"/user",
+ body = Some(user),
+ formParameters = None,
+ queryParameters = Nil,
+ requestHeaders = requestHeaders,
+ auth = Some(auth)) {
+ r => Concurrent[F].pure(())
+ }
+ }
+
+ override def createUsersWithArrayInput(user: Seq[User])(implicit auth: _Authorization.ApiKey): F[Unit] = {
+ val requestHeaders = Seq(
+ Some("Content-Type" -> "application/json")
+ ).flatten
+
+ _executeRequest[Seq[User], Unit](
+ method = "POST",
+ path = s"/user/createWithArray",
+ body = Some(user),
+ formParameters = None,
+ queryParameters = Nil,
+ requestHeaders = requestHeaders,
+ auth = Some(auth)) {
+ r => Concurrent[F].pure(())
+ }
+ }
+
+ override def createUsersWithListInput(user: Seq[User])(implicit auth: _Authorization.ApiKey): F[Unit] = {
+ val requestHeaders = Seq(
+ Some("Content-Type" -> "application/json")
+ ).flatten
+
+ _executeRequest[Seq[User], Unit](
+ method = "POST",
+ path = s"/user/createWithList",
+ body = Some(user),
+ formParameters = None,
+ queryParameters = Nil,
+ requestHeaders = requestHeaders,
+ auth = Some(auth)) {
+ r => Concurrent[F].pure(())
+ }
+ }
+
+ override def deleteUser(username: String)(implicit auth: _Authorization.ApiKey): F[Unit] = {
+ val requestHeaders = Seq(
+ Some("Content-Type" -> "application/json")
+ ).flatten
+
+ _executeRequest[Unit, Unit](
+ method = "DELETE",
+ path = s"/user/${username}",
+ body = None,
+ formParameters = None,
+ queryParameters = Nil,
+ requestHeaders = requestHeaders,
+ auth = Some(auth)) {
+
+ case r if r.status.code == 400 => Concurrent[F].raiseError(_FailedRequest(r.status.code, r.status.reason))
+ case r if r.status.code == 404 => Concurrent[F].raiseError(_FailedRequest(r.status.code, r.status.reason))
+ }
+ }
+
+ override def getUserByName(username: String): F[User] = {
+ val requestHeaders = Seq(
+ Some("Content-Type" -> "application/json")
+ ).flatten
+
+ _executeRequest[Unit, User](
+ method = "GET",
+ path = s"/user/${username}",
+ body = None,
+ formParameters = None,
+ queryParameters = Nil,
+ requestHeaders = requestHeaders,
+ auth = None) {
+
+ case r if r.status.code == 200 => parseJson[F, User]("User", r)
+ case r if r.status.code == 400 => Concurrent[F].raiseError(_FailedRequest(r.status.code, r.status.reason))
+ case r if r.status.code == 404 => Concurrent[F].raiseError(_FailedRequest(r.status.code, r.status.reason))
+ }
+ }
+
+ override def loginUser(username: String, password: String): F[String] = {
+ val requestHeaders = Seq(
+ Some("Content-Type" -> "application/json")
+ ).flatten
+ val queryParameters = (
+ Some(Seq("username" -> username)) ++
+ Some(Seq("password" -> password))
+ ).toSeq.flatten
+
+ _executeRequest[Unit, String](
+ method = "GET",
+ path = s"/user/login",
+ body = None,
+ formParameters = None,
+ queryParameters = queryParameters,
+ requestHeaders = requestHeaders,
+ auth = None) {
+
+ case r if r.status.code == 200 => parseJson[F, String]("String", r)
+ case r if r.status.code == 400 => Concurrent[F].raiseError(_FailedRequest(r.status.code, r.status.reason))
+ }
+ }
+
+ override def logoutUser()(implicit auth: _Authorization.ApiKey): F[Unit] = {
+ val requestHeaders = Seq(
+ Some("Content-Type" -> "application/json")
+ ).flatten
+
+ _executeRequest[Unit, Unit](
+ method = "GET",
+ path = s"/user/logout",
+ body = None,
+ formParameters = None,
+ queryParameters = Nil,
+ requestHeaders = requestHeaders,
+ auth = Some(auth)) {
+ r => Concurrent[F].pure(())
+ }
+ }
+
+ override def updateUser(username: String, user: User)(implicit auth: _Authorization.ApiKey): F[Unit] = {
+ val requestHeaders = Seq(
+ Some("Content-Type" -> "application/json")
+ ).flatten
+
+ _executeRequest[User, Unit](
+ method = "PUT",
+ path = s"/user/${username}",
+ body = Some(user),
+ formParameters = None,
+ queryParameters = Nil,
+ requestHeaders = requestHeaders,
+ auth = Some(auth)) {
+
+ case r if r.status.code == 400 => Concurrent[F].raiseError(_FailedRequest(r.status.code, r.status.reason))
+ case r if r.status.code == 404 => Concurrent[F].raiseError(_FailedRequest(r.status.code, r.status.reason))
+ }
+ }
+
+}
+
+
diff --git a/samples/client/petstore/scala-http4s/src/main/scala/org/openapitools/client/models/ApiResponse.scala b/samples/client/petstore/scala-http4s/src/main/scala/org/openapitools/client/models/ApiResponse.scala
new file mode 100644
index 000000000000..6d46c256b8ae
--- /dev/null
+++ b/samples/client/petstore/scala-http4s/src/main/scala/org/openapitools/client/models/ApiResponse.scala
@@ -0,0 +1,51 @@
+/** OpenAPI Petstore
+ * This is a sample server Petstore server. For this sample, you can use the api key `special-key` to test the authorization filters.
+ *
+ * The version of the OpenAPI document: 1.0.0
+ * Contact: team@openapitools.org
+ *
+ * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).
+ * https://openapi-generator.tech
+ * Do not edit the class manually.
+ */
+package org.openapitools.client.models
+
+import io.circe.*
+import io.circe.syntax.*
+import io.circe.{Decoder, Encoder}
+
+
+/** Describes the result of uploading an image resource
+ * @param code
+ * @param `type`
+ * @param message
+ */
+case class ApiResponse(
+ code: Option[Int] = None,
+ `type`: Option[String] = None,
+ message: Option[String] = None
+)
+
+object ApiResponse {
+ given encoderApiResponse: Encoder[ApiResponse] = Encoder.instance { t =>
+ Json.fromFields{
+ Seq(
+ t.code.map(v => "code" -> v.asJson),
+ t.`type`.map(v => "type" -> v.asJson),
+ t.message.map(v => "message" -> v.asJson)
+ ).flatten
+ }
+ }
+ given decoderApiResponse: Decoder[ApiResponse] = Decoder.instance { c =>
+ for {
+ code <- c.downField("code").as[Option[Int]]
+ `type` <- c.downField("type").as[Option[String]]
+ message <- c.downField("message").as[Option[String]]
+ } yield ApiResponse(
+ code = code,
+ `type` = `type`,
+ message = message
+ )
+ }
+}
+
diff --git a/samples/client/petstore/scala-http4s/src/main/scala/org/openapitools/client/models/Category.scala b/samples/client/petstore/scala-http4s/src/main/scala/org/openapitools/client/models/Category.scala
new file mode 100644
index 000000000000..79e91d6e1fe0
--- /dev/null
+++ b/samples/client/petstore/scala-http4s/src/main/scala/org/openapitools/client/models/Category.scala
@@ -0,0 +1,46 @@
+/** OpenAPI Petstore
+ * This is a sample server Petstore server. For this sample, you can use the api key `special-key` to test the authorization filters.
+ *
+ * The version of the OpenAPI document: 1.0.0
+ * Contact: team@openapitools.org
+ *
+ * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).
+ * https://openapi-generator.tech
+ * Do not edit the class manually.
+ */
+package org.openapitools.client.models
+
+import io.circe.*
+import io.circe.syntax.*
+import io.circe.{Decoder, Encoder}
+
+
+/** A category for a pet
+ * @param id
+ * @param name
+ */
+case class Category(
+ id: Option[Long] = None,
+ name: Option[String] = None
+)
+
+object Category {
+ given encoderCategory: Encoder[Category] = Encoder.instance { t =>
+ Json.fromFields{
+ Seq(
+ t.id.map(v => "id" -> v.asJson),
+ t.name.map(v => "name" -> v.asJson)
+ ).flatten
+ }
+ }
+ given decoderCategory: Decoder[Category] = Decoder.instance { c =>
+ for {
+ id <- c.downField("id").as[Option[Long]]
+ name <- c.downField("name").as[Option[String]]
+ } yield Category(
+ id = id,
+ name = name
+ )
+ }
+}
+
diff --git a/samples/client/petstore/scala-http4s/src/main/scala/org/openapitools/client/models/FindPetsByStatusStatusParameterInner.scala b/samples/client/petstore/scala-http4s/src/main/scala/org/openapitools/client/models/FindPetsByStatusStatusParameterInner.scala
new file mode 100644
index 000000000000..e2534970a07e
--- /dev/null
+++ b/samples/client/petstore/scala-http4s/src/main/scala/org/openapitools/client/models/FindPetsByStatusStatusParameterInner.scala
@@ -0,0 +1,35 @@
+/** OpenAPI Petstore
+ * This is a sample server Petstore server. For this sample, you can use the api key `special-key` to test the authorization filters.
+ *
+ * The version of the OpenAPI document: 1.0.0
+ * Contact: team@openapitools.org
+ *
+ * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).
+ * https://openapi-generator.tech
+ * Do not edit the class manually.
+ */
+package org.openapitools.client.models
+
+import io.circe.*
+import io.circe.syntax.*
+import io.circe.{Decoder, Encoder}
+
+
+/**
+ */
+enum FindPetsByStatusStatusParameterInner(val value: String) {
+ case Available extends FindPetsByStatusStatusParameterInner("available")
+ case Pending extends FindPetsByStatusStatusParameterInner("pending")
+ case Sold extends FindPetsByStatusStatusParameterInner("sold")
+}
+
+object FindPetsByStatusStatusParameterInner {
+ given decoderFindPetsByStatusStatusParameterInner: Decoder[FindPetsByStatusStatusParameterInner] =
+ Decoder.decodeString.map(str => FindPetsByStatusStatusParameterInner.values.find(_.value == str)
+ .getOrElse(throw java.lang.IllegalArgumentException(s"FindPetsByStatusStatusParameterInner enum case not found: $str"))
+ )
+
+ given encoderFindPetsByStatusStatusParameterInner: Encoder[FindPetsByStatusStatusParameterInner] =
+ Encoder.encodeString.contramap[FindPetsByStatusStatusParameterInner](_.value)
+}
+
diff --git a/samples/client/petstore/scala-http4s/src/main/scala/org/openapitools/client/models/Order.scala b/samples/client/petstore/scala-http4s/src/main/scala/org/openapitools/client/models/Order.scala
new file mode 100644
index 000000000000..a9649cc85cab
--- /dev/null
+++ b/samples/client/petstore/scala-http4s/src/main/scala/org/openapitools/client/models/Order.scala
@@ -0,0 +1,67 @@
+/** OpenAPI Petstore
+ * This is a sample server Petstore server. For this sample, you can use the api key `special-key` to test the authorization filters.
+ *
+ * The version of the OpenAPI document: 1.0.0
+ * Contact: team@openapitools.org
+ *
+ * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).
+ * https://openapi-generator.tech
+ * Do not edit the class manually.
+ */
+package org.openapitools.client.models
+
+import io.circe.*
+import io.circe.syntax.*
+import io.circe.{Decoder, Encoder}
+
+import java.time.Instant
+
+/** An order for a pets from the pet store
+ * @param id
+ * @param petId
+ * @param quantity
+ * @param shipDate
+ * @param status
+ * @param complete
+ */
+case class Order(
+ id: Option[Long] = None,
+ petId: Option[Long] = None,
+ quantity: Option[Int] = None,
+ shipDate: Option[Instant] = None,
+ status: Option[OrderStatus] = None,
+ complete: Option[Boolean] = None
+)
+
+object Order {
+ given encoderOrder: Encoder[Order] = Encoder.instance { t =>
+ Json.fromFields{
+ Seq(
+ t.id.map(v => "id" -> v.asJson),
+ t.petId.map(v => "petId" -> v.asJson),
+ t.quantity.map(v => "quantity" -> v.asJson),
+ t.shipDate.map(v => "shipDate" -> v.asJson),
+ t.status.map(v => "status" -> v.asJson),
+ t.complete.map(v => "complete" -> v.asJson)
+ ).flatten
+ }
+ }
+ given decoderOrder: Decoder[Order] = Decoder.instance { c =>
+ for {
+ id <- c.downField("id").as[Option[Long]]
+ petId <- c.downField("petId").as[Option[Long]]
+ quantity <- c.downField("quantity").as[Option[Int]]
+ shipDate <- c.downField("shipDate").as[Option[Instant]]
+ status <- c.downField("status").as[Option[OrderStatus]]
+ complete <- c.downField("complete").as[Option[Boolean]]
+ } yield Order(
+ id = id,
+ petId = petId,
+ quantity = quantity,
+ shipDate = shipDate,
+ status = status,
+ complete = complete
+ )
+ }
+}
+
diff --git a/samples/client/petstore/scala-http4s/src/main/scala/org/openapitools/client/models/OrderStatus.scala b/samples/client/petstore/scala-http4s/src/main/scala/org/openapitools/client/models/OrderStatus.scala
new file mode 100644
index 000000000000..96a360806ea3
--- /dev/null
+++ b/samples/client/petstore/scala-http4s/src/main/scala/org/openapitools/client/models/OrderStatus.scala
@@ -0,0 +1,35 @@
+/** OpenAPI Petstore
+ * This is a sample server Petstore server. For this sample, you can use the api key `special-key` to test the authorization filters.
+ *
+ * The version of the OpenAPI document: 1.0.0
+ * Contact: team@openapitools.org
+ *
+ * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).
+ * https://openapi-generator.tech
+ * Do not edit the class manually.
+ */
+package org.openapitools.client.models
+
+import io.circe.*
+import io.circe.syntax.*
+import io.circe.{Decoder, Encoder}
+
+
+/** Order Status
+ */
+enum OrderStatus(val value: String) {
+ case Placed extends OrderStatus("placed")
+ case Approved extends OrderStatus("approved")
+ case Delivered extends OrderStatus("delivered")
+}
+
+object OrderStatus {
+ given decoderOrderStatus: Decoder[OrderStatus] =
+ Decoder.decodeString.map(str => OrderStatus.values.find(_.value == str)
+ .getOrElse(throw java.lang.IllegalArgumentException(s"OrderStatus enum case not found: $str"))
+ )
+
+ given encoderOrderStatus: Encoder[OrderStatus] =
+ Encoder.encodeString.contramap[OrderStatus](_.value)
+}
+
diff --git a/samples/client/petstore/scala-http4s/src/main/scala/org/openapitools/client/models/Pet.scala b/samples/client/petstore/scala-http4s/src/main/scala/org/openapitools/client/models/Pet.scala
new file mode 100644
index 000000000000..2800dcbdb4bb
--- /dev/null
+++ b/samples/client/petstore/scala-http4s/src/main/scala/org/openapitools/client/models/Pet.scala
@@ -0,0 +1,67 @@
+/** OpenAPI Petstore
+ * This is a sample server Petstore server. For this sample, you can use the api key `special-key` to test the authorization filters.
+ *
+ * The version of the OpenAPI document: 1.0.0
+ * Contact: team@openapitools.org
+ *
+ * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).
+ * https://openapi-generator.tech
+ * Do not edit the class manually.
+ */
+package org.openapitools.client.models
+
+import io.circe.*
+import io.circe.syntax.*
+import io.circe.{Decoder, Encoder}
+
+import scala.collection.immutable.Seq
+
+/** A pet for sale in the pet store
+ * @param id
+ * @param category
+ * @param name
+ * @param photoUrls
+ * @param tags
+ * @param status
+ */
+case class Pet(
+ id: Option[Long] = None,
+ category: Option[Category] = None,
+ name: String,
+ photoUrls: Seq[String],
+ tags: Option[Seq[Tag]] = None,
+ status: Option[PetStatus] = None
+)
+
+object Pet {
+ given encoderPet: Encoder[Pet] = Encoder.instance { t =>
+ Json.fromFields{
+ Seq(
+ t.id.map(v => "id" -> v.asJson),
+ t.category.map(v => "category" -> v.asJson),
+ Some("name" -> t.name.asJson),
+ Some("photoUrls" -> t.photoUrls.asJson),
+ t.tags.map(v => "tags" -> v.asJson),
+ t.status.map(v => "status" -> v.asJson)
+ ).flatten
+ }
+ }
+ given decoderPet: Decoder[Pet] = Decoder.instance { c =>
+ for {
+ id <- c.downField("id").as[Option[Long]]
+ category <- c.downField("category").as[Option[Category]]
+ name <- c.downField("name").as[String]
+ photoUrls <- c.downField("photoUrls").as[Seq[String]]
+ tags <- c.downField("tags").as[Option[Seq[Tag]]]
+ status <- c.downField("status").as[Option[PetStatus]]
+ } yield Pet(
+ id = id,
+ category = category,
+ name = name,
+ photoUrls = photoUrls,
+ tags = tags,
+ status = status
+ )
+ }
+}
+
diff --git a/samples/client/petstore/scala-http4s/src/main/scala/org/openapitools/client/models/PetStatus.scala b/samples/client/petstore/scala-http4s/src/main/scala/org/openapitools/client/models/PetStatus.scala
new file mode 100644
index 000000000000..5300aff9dc6d
--- /dev/null
+++ b/samples/client/petstore/scala-http4s/src/main/scala/org/openapitools/client/models/PetStatus.scala
@@ -0,0 +1,35 @@
+/** OpenAPI Petstore
+ * This is a sample server Petstore server. For this sample, you can use the api key `special-key` to test the authorization filters.
+ *
+ * The version of the OpenAPI document: 1.0.0
+ * Contact: team@openapitools.org
+ *
+ * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).
+ * https://openapi-generator.tech
+ * Do not edit the class manually.
+ */
+package org.openapitools.client.models
+
+import io.circe.*
+import io.circe.syntax.*
+import io.circe.{Decoder, Encoder}
+
+
+/** pet status in the store
+ */
+enum PetStatus(val value: String) {
+ case Available extends PetStatus("available")
+ case Pending extends PetStatus("pending")
+ case Sold extends PetStatus("sold")
+}
+
+object PetStatus {
+ given decoderPetStatus: Decoder[PetStatus] =
+ Decoder.decodeString.map(str => PetStatus.values.find(_.value == str)
+ .getOrElse(throw java.lang.IllegalArgumentException(s"PetStatus enum case not found: $str"))
+ )
+
+ given encoderPetStatus: Encoder[PetStatus] =
+ Encoder.encodeString.contramap[PetStatus](_.value)
+}
+
diff --git a/samples/client/petstore/scala-http4s/src/main/scala/org/openapitools/client/models/Tag.scala b/samples/client/petstore/scala-http4s/src/main/scala/org/openapitools/client/models/Tag.scala
new file mode 100644
index 000000000000..a6a7cd083c5d
--- /dev/null
+++ b/samples/client/petstore/scala-http4s/src/main/scala/org/openapitools/client/models/Tag.scala
@@ -0,0 +1,46 @@
+/** OpenAPI Petstore
+ * This is a sample server Petstore server. For this sample, you can use the api key `special-key` to test the authorization filters.
+ *
+ * The version of the OpenAPI document: 1.0.0
+ * Contact: team@openapitools.org
+ *
+ * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).
+ * https://openapi-generator.tech
+ * Do not edit the class manually.
+ */
+package org.openapitools.client.models
+
+import io.circe.*
+import io.circe.syntax.*
+import io.circe.{Decoder, Encoder}
+
+
+/** A tag for a pet
+ * @param id
+ * @param name
+ */
+case class Tag(
+ id: Option[Long] = None,
+ name: Option[String] = None
+)
+
+object Tag {
+ given encoderTag: Encoder[Tag] = Encoder.instance { t =>
+ Json.fromFields{
+ Seq(
+ t.id.map(v => "id" -> v.asJson),
+ t.name.map(v => "name" -> v.asJson)
+ ).flatten
+ }
+ }
+ given decoderTag: Decoder[Tag] = Decoder.instance { c =>
+ for {
+ id <- c.downField("id").as[Option[Long]]
+ name <- c.downField("name").as[Option[String]]
+ } yield Tag(
+ id = id,
+ name = name
+ )
+ }
+}
+
diff --git a/samples/client/petstore/scala-http4s/src/main/scala/org/openapitools/client/models/User.scala b/samples/client/petstore/scala-http4s/src/main/scala/org/openapitools/client/models/User.scala
new file mode 100644
index 000000000000..a1cedbc53f92
--- /dev/null
+++ b/samples/client/petstore/scala-http4s/src/main/scala/org/openapitools/client/models/User.scala
@@ -0,0 +1,76 @@
+/** OpenAPI Petstore
+ * This is a sample server Petstore server. For this sample, you can use the api key `special-key` to test the authorization filters.
+ *
+ * The version of the OpenAPI document: 1.0.0
+ * Contact: team@openapitools.org
+ *
+ * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).
+ * https://openapi-generator.tech
+ * Do not edit the class manually.
+ */
+package org.openapitools.client.models
+
+import io.circe.*
+import io.circe.syntax.*
+import io.circe.{Decoder, Encoder}
+
+
+/** A User who is purchasing from the pet store
+ * @param id
+ * @param username
+ * @param firstName
+ * @param lastName
+ * @param email
+ * @param password
+ * @param phone
+ * @param userStatus User Status
+ */
+case class User(
+ id: Option[Long] = None,
+ username: Option[String] = None,
+ firstName: Option[String] = None,
+ lastName: Option[String] = None,
+ email: Option[String] = None,
+ password: Option[String] = None,
+ phone: Option[String] = None,
+ userStatus: Option[Int] = None
+)
+
+object User {
+ given encoderUser: Encoder[User] = Encoder.instance { t =>
+ Json.fromFields{
+ Seq(
+ t.id.map(v => "id" -> v.asJson),
+ t.username.map(v => "username" -> v.asJson),
+ t.firstName.map(v => "firstName" -> v.asJson),
+ t.lastName.map(v => "lastName" -> v.asJson),
+ t.email.map(v => "email" -> v.asJson),
+ t.password.map(v => "password" -> v.asJson),
+ t.phone.map(v => "phone" -> v.asJson),
+ t.userStatus.map(v => "userStatus" -> v.asJson)
+ ).flatten
+ }
+ }
+ given decoderUser: Decoder[User] = Decoder.instance { c =>
+ for {
+ id <- c.downField("id").as[Option[Long]]
+ username <- c.downField("username").as[Option[String]]
+ firstName <- c.downField("firstName").as[Option[String]]
+ lastName <- c.downField("lastName").as[Option[String]]
+ email <- c.downField("email").as[Option[String]]
+ password <- c.downField("password").as[Option[String]]
+ phone <- c.downField("phone").as[Option[String]]
+ userStatus <- c.downField("userStatus").as[Option[Int]]
+ } yield User(
+ id = id,
+ username = username,
+ firstName = firstName,
+ lastName = lastName,
+ email = email,
+ password = password,
+ phone = phone,
+ userStatus = userStatus
+ )
+ }
+}
+
diff --git a/samples/client/petstore/scala-http4s/src/main/scala/org/openapitools/client/models/_Authorization.scala b/samples/client/petstore/scala-http4s/src/main/scala/org/openapitools/client/models/_Authorization.scala
new file mode 100644
index 000000000000..9c711757a899
--- /dev/null
+++ b/samples/client/petstore/scala-http4s/src/main/scala/org/openapitools/client/models/_Authorization.scala
@@ -0,0 +1,19 @@
+/** OpenAPI Petstore
+ * This is a sample server Petstore server. For this sample, you can use the api key `special-key` to test the authorization filters.
+ *
+ * The version of the OpenAPI document: 1.0.0
+ * Contact: team@openapitools.org
+ *
+ * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).
+ * https://openapi-generator.tech
+ * Do not edit the class manually.
+ */
+package org.openapitools.client.models
+
+sealed trait _Authorization
+
+object _Authorization {
+ final case class Basic(username: String, password: Option[String] = None) extends _Authorization
+ final case class ApiKey(name: String, value: String) extends _Authorization
+ final case class Bearer(token: String) extends _Authorization
+}
diff --git a/samples/client/petstore/scala-http4s/src/main/scala/org/openapitools/client/models/_FailedRequest.scala b/samples/client/petstore/scala-http4s/src/main/scala/org/openapitools/client/models/_FailedRequest.scala
new file mode 100644
index 000000000000..e883a521dc2f
--- /dev/null
+++ b/samples/client/petstore/scala-http4s/src/main/scala/org/openapitools/client/models/_FailedRequest.scala
@@ -0,0 +1,42 @@
+/** OpenAPI Petstore
+ * This is a sample server Petstore server. For this sample, you can use the api key `special-key` to test the authorization filters.
+ *
+ * The version of the OpenAPI document: 1.0.0
+ * Contact: team@openapitools.org
+ *
+ * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).
+ * https://openapi-generator.tech
+ * Do not edit the class manually.
+ */
+package org.openapitools.client.models
+
+import io.circe.*
+import io.circe.Decoder.*
+import io.circe.Encoder.*
+import io.circe.syntax.*
+
+case class _FailedRequest(code: Int, message: String) extends Exception(s"Server return status code: $code; message: $message")
+
+object _FailedRequest {
+
+ given encoderFailedRequest: Encoder[_FailedRequest] = Encoder.instance { t =>
+ Json.fromFields{
+ Seq(
+ "code" -> t.code.asJson,
+ "message" -> t.message.asJson
+ )
+ }
+ }
+
+ given decodeFailedRequest: Decoder[_FailedRequest] = Decoder.instance { c =>
+ for {
+ code <- c.downField("code").as[Int]
+ message <- c.downField("message").as[String]
+ } yield _FailedRequest(
+ code = code,
+ message = message
+ )
+ }
+
+}
+
diff --git a/samples/client/petstore/scala-http4s/src/main/scala/org/openapitools/client/models/package.scala b/samples/client/petstore/scala-http4s/src/main/scala/org/openapitools/client/models/package.scala
new file mode 100644
index 000000000000..b3a8d920c308
--- /dev/null
+++ b/samples/client/petstore/scala-http4s/src/main/scala/org/openapitools/client/models/package.scala
@@ -0,0 +1,41 @@
+/** OpenAPI Petstore
+ * This is a sample server Petstore server. For this sample, you can use the api key `special-key` to test the authorization filters.
+ *
+ * The version of the OpenAPI document: 1.0.0
+ * Contact: team@openapitools.org
+ *
+ * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).
+ * https://openapi-generator.tech
+ * Do not edit the class manually.
+ */
+package org.openapitools.client
+
+import io.circe.{Decoder, Encoder}
+
+package object models {
+
+ given decodeUUID: Decoder[_root_.java.util.UUID] =
+ Decoder.decodeString.map(str => _root_.java.util.UUID.fromString(str))
+
+ given encodeUUID: Encoder[_root_.java.util.UUID] =
+ Encoder.encodeString.contramap[_root_.java.util.UUID](uuid => uuid.toString)
+
+ given decodeInstant: Decoder[_root_.java.time.Instant] =
+ Decoder.decodeString.map(str => _root_.java.time.OffsetDateTime.parse(str).toInstant)
+
+ given encodeInstant: Encoder[_root_.java.time.Instant] =
+ Encoder.encodeString.contramap[_root_.java.time.Instant](_.toString)
+
+ given decodeLocalDate: Decoder[_root_.java.time.LocalDate] =
+ Decoder.decodeString.map(str => _root_.java.time.LocalDate.parse(str))
+
+ given encodeLocalDate: Encoder[_root_.java.time.LocalDate] =
+ Encoder.encodeString.contramap[_root_.java.time.LocalDate](_.toString)
+
+ given decodeJson: Decoder[io.circe.Json] =
+ Decoder.decodeString.map(str => io.circe.Json.fromString(str))
+
+ given encodeJson: Encoder[io.circe.Json] =
+ Encoder.encodeString.contramap[io.circe.Json](_.toString)
+
+}
\ No newline at end of file