diff --git a/openapi-codegen/core/src/main/scala/sttp/tapir/codegen/EndpointGenerator.scala b/openapi-codegen/core/src/main/scala/sttp/tapir/codegen/EndpointGenerator.scala index adae2abb52..bbbf5da5e3 100644 --- a/openapi-codegen/core/src/main/scala/sttp/tapir/codegen/EndpointGenerator.scala +++ b/openapi-codegen/core/src/main/scala/sttp/tapir/codegen/EndpointGenerator.scala @@ -162,7 +162,10 @@ class EndpointGenerator { val maybeTargetFileName = if (useHeadTagForObjectNames) m.tags.flatMap(_.headOption) else None val queryOrPathParamRefs = m.resolvedParameters .collect { case queryParam: OpenapiParameter if queryParam.in == "query" || queryParam.in == "path" => queryParam.schema } - .collect { case ref: OpenapiSchemaRef if ref.isSchema => ref.stripped } + .collect { + case ref: OpenapiSchemaRef if ref.isSchema => ref.stripped + case OpenapiSchemaArray(ref: OpenapiSchemaRef, _) if ref.isSchema => ref.stripped + } .toSet val jsonParamRefs = (m.requestBody.toSeq.flatMap(_.content.map(c => (c.contentType, c.schema))) ++ m.responses.flatMap(_.content.map(c => (c.contentType, c.schema)))) @@ -264,6 +267,12 @@ class EndpointGenerator { streamingImplementation: StreamingImplementation, doc: OpenapiDocument )(implicit location: Location): (String, Option[String], Seq[String]) = { + def toOutType(baseType: String, isArray: Boolean, noOptionWrapper: Boolean) = (isArray, noOptionWrapper) match { + case (true, true) => s"List[$baseType]" + case (true, false) => s"Option[List[$baseType]]" + case (false, true) => baseType + case (false, false) => s"Option[$baseType]" + } def getEnumParamDefn(param: OpenapiParameter, e: OpenapiSchemaEnum, isArray: Boolean) = { val enumName = endpointName.capitalize + strippedToCamelCase(param.name).capitalize val enumParamRefs = if (param.in == "query" || param.in == "path") Set(enumName) else Set.empty[String] @@ -283,12 +292,7 @@ class EndpointGenerator { // 'exploded' params have no distinction between an empty list and an absent value, so don't wrap in 'Option' for them val noOptionWrapper = required || (isArray && param.isExploded) val req = if (noOptionWrapper) tpe else s"Option[$tpe]" - val outType = (isArray, noOptionWrapper) match { - case (true, true) => s"List[$enumName]" - case (true, false) => s"Option[List[$enumName]]" - case (false, true) => enumName - case (false, false) => s"Option[$enumName]" - } + val outType = toOutType(enumName, isArray, noOptionWrapper) def mapToList = if (!isArray) "" else if (noOptionWrapper) s".map(_.values)($arrayType(_))" else s".map(_.map(_.values))(_.map($arrayType(_)))" @@ -320,7 +324,8 @@ class EndpointGenerator { def mapToList = if (noOptionWrapper) s".map(_.values)($arrayType(_))" else s".map(_.map(_.values))(_.map($arrayType(_)))" val desc = param.description.map(d => JavaEscape.escapeString(d)).fold("")(d => s""".description("$d")""") - (s""".in(${param.in}[$req]("${param.name}")$mapToList$desc)""", None, req) + val outType = toOutType(t, true, noOptionWrapper) + (s""".in(${param.in}[$req]("${param.name}")$mapToList$desc)""", None, outType) case e @ OpenapiSchemaEnum(_, _, _) => getEnumParamDefn(param, e, isArray = false) case OpenapiSchemaArray(e: OpenapiSchemaEnum, _) => getEnumParamDefn(param, e, isArray = true) case x => bail(s"Can't create non-simple params to input - found $x") diff --git a/openapi-codegen/sbt-plugin/src/sbt-test/sbt-openapi-codegen/oneOf-json-roundtrip_jsoniter/Expected.scala.txt b/openapi-codegen/sbt-plugin/src/sbt-test/sbt-openapi-codegen/oneOf-json-roundtrip_jsoniter/Expected.scala.txt index 8b8685ab23..573442bfae 100644 --- a/openapi-codegen/sbt-plugin/src/sbt-test/sbt-openapi-codegen/oneOf-json-roundtrip_jsoniter/Expected.scala.txt +++ b/openapi-codegen/sbt-plugin/src/sbt-test/sbt-openapi-codegen/oneOf-json-roundtrip_jsoniter/Expected.scala.txt @@ -45,7 +45,24 @@ object TapirGeneratedEndpoints { support.mapDecode(l => DecodeResult.Value(ExplodedValues(l)))(_.values) } - + case class EnumExtraParamSupport[T <: enumeratum.EnumEntry](enumName: String, T: enumeratum.Enum[T]) extends ExtraParamSupport[T] { + // Case-insensitive mapping + def decode(s: String): sttp.tapir.DecodeResult[T] = + scala.util.Try(T.upperCaseNameValuesToMap(s.toUpperCase)) + .fold( + _ => + sttp.tapir.DecodeResult.Error( + s, + new NoSuchElementException( + s"Could not find value $s for enum ${enumName}, available values: ${T.values.mkString(", ")}" + ) + ), + sttp.tapir.DecodeResult.Value(_) + ) + def encode(t: T): String = t.entryName + } + def extraCodecSupport[T <: enumeratum.EnumEntry](enumName: String, T: enumeratum.Enum[T]): ExtraParamSupport[T] = + EnumExtraParamSupport(enumName, T) sealed trait ADTWithoutDiscriminator sealed trait ADTWithDiscriminator sealed trait ADTWithDiscriminatorNoMapping @@ -81,25 +98,29 @@ object TapirGeneratedEndpoints { case object Foo extends AnEnum case object Bar extends AnEnum case object Baz extends AnEnum + implicit val enumCodecSupportAnEnum: ExtraParamSupport[AnEnum] = + extraCodecSupport[AnEnum]("AnEnum", AnEnum) } - - lazy val putAdtTest = + type PutAdtTestEndpoint = Endpoint[Unit, ADTWithoutDiscriminator, Unit, ADTWithoutDiscriminator, Any] + lazy val putAdtTest: PutAdtTestEndpoint = endpoint .put .in(("adt" / "test")) .in(jsonBody[ADTWithoutDiscriminator]) .out(jsonBody[ADTWithoutDiscriminator].description("successful operation")) - lazy val postAdtTest = + type PostAdtTestEndpoint = Endpoint[Unit, ADTWithDiscriminatorNoMapping, Unit, ADTWithDiscriminator, Any] + lazy val postAdtTest: PostAdtTestEndpoint = endpoint .post .in(("adt" / "test")) .in(jsonBody[ADTWithDiscriminatorNoMapping]) .out(jsonBody[ADTWithDiscriminator].description("successful operation")) - lazy val getOneofOptionTest = + type GetOneofOptionTestEndpoint = Endpoint[Unit, Unit, Unit, Option[AnEnum], Any] + lazy val getOneofOptionTest: GetOneofOptionTestEndpoint = endpoint .get .in(("oneof" / "option" / "test")) @@ -107,10 +128,12 @@ object TapirGeneratedEndpoints { oneOfVariantSingletonMatcher(sttp.model.StatusCode(204), emptyOutput.description("No response"))(None), oneOfVariantValueMatcher(sttp.model.StatusCode(200), jsonBody[Option[AnEnum]].description("An enum")){ case Some(_: AnEnum) => true })) - lazy val postGenericJson = + type PostGenericJsonEndpoint = Endpoint[Unit, (Option[List[AnEnum]], Option[io.circe.Json]), Unit, io.circe.Json, Any] + lazy val postGenericJson: PostGenericJsonEndpoint = endpoint .post .in(("generic" / "json")) + .in(query[Option[CommaSeparatedValues[AnEnum]]]("aTrickyParam").map(_.map(_.values))(_.map(CommaSeparatedValues(_))).description("A very thorough description")) .in(jsonBody[Option[io.circe.Json]]) .out(jsonBody[io.circe.Json].description("anything back")) diff --git a/openapi-codegen/sbt-plugin/src/sbt-test/sbt-openapi-codegen/oneOf-json-roundtrip_jsoniter/build.sbt b/openapi-codegen/sbt-plugin/src/sbt-test/sbt-openapi-codegen/oneOf-json-roundtrip_jsoniter/build.sbt index facaf3bc99..c9351a221e 100644 --- a/openapi-codegen/sbt-plugin/src/sbt-test/sbt-openapi-codegen/oneOf-json-roundtrip_jsoniter/build.sbt +++ b/openapi-codegen/sbt-plugin/src/sbt-test/sbt-openapi-codegen/oneOf-json-roundtrip_jsoniter/build.sbt @@ -3,7 +3,8 @@ lazy val root = (project in file(".")) .settings( scalaVersion := "2.13.16", version := "0.1", - openapiJsonSerdeLib := "jsoniter" + openapiJsonSerdeLib := "jsoniter", + openapiGenerateEndpointTypes := true ) libraryDependencies ++= Seq( diff --git a/openapi-codegen/sbt-plugin/src/sbt-test/sbt-openapi-codegen/oneOf-json-roundtrip_jsoniter/swagger.yaml b/openapi-codegen/sbt-plugin/src/sbt-test/sbt-openapi-codegen/oneOf-json-roundtrip_jsoniter/swagger.yaml index e3f232bf13..661d6063d9 100644 --- a/openapi-codegen/sbt-plugin/src/sbt-test/sbt-openapi-codegen/oneOf-json-roundtrip_jsoniter/swagger.yaml +++ b/openapi-codegen/sbt-plugin/src/sbt-test/sbt-openapi-codegen/oneOf-json-roundtrip_jsoniter/swagger.yaml @@ -51,17 +51,28 @@ paths: $ref: '#/components/schemas/AnEnum' '/generic/json': post: + parameters: + - in: query + name: aTrickyParam + style: form + explode: false + required: false + description: A very thorough description + schema: + type: array + items: + $ref: '#/components/schemas/AnEnum' requestBody: description: anything content: application/json: - schema: {} + schema: { } responses: "200": description: anything back content: application/json: - schema: {} + schema: { } components: schemas: