diff --git a/modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/ScalaSttpClientCodegen.java b/modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/ScalaSttpClientCodegen.java index 0d3c45fd9b8..e9d9039220c 100644 --- a/modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/ScalaSttpClientCodegen.java +++ b/modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/ScalaSttpClientCodegen.java @@ -113,6 +113,10 @@ public class ScalaSttpClientCodegen extends AbstractScalaCodegen implements Code apiTemplateFiles.put("api.mustache", ".scala"); embeddedTemplateDir = templateDir = "scala-sttp"; + String jsonLibrary = JSON_LIBRARY_PROPERTY.getValue(additionalProperties); + + String jsonValueClass = jsonLibrary == "circe" ? "io.circe.Json" : "org.json4s.JValue"; + additionalProperties.put(CodegenConstants.GROUP_ID, groupId); additionalProperties.put(CodegenConstants.ARTIFACT_ID, artifactId); additionalProperties.put(CodegenConstants.ARTIFACT_VERSION, artifactVersion); @@ -148,6 +152,7 @@ public class ScalaSttpClientCodegen extends AbstractScalaCodegen implements Code typeMapping.put("number", "Double"); typeMapping.put("decimal", "BigDecimal"); typeMapping.put("ByteArray", "Array[Byte]"); + typeMapping.put("AnyType", jsonValueClass); instantiationTypes.put("array", "ListBuffer"); instantiationTypes.put("map", "Map"); @@ -170,6 +175,7 @@ public class ScalaSttpClientCodegen extends AbstractScalaCodegen implements Code supportingFiles.add(new SupportingFile("build.sbt.mustache", "", "build.sbt")); final String invokerFolder = (sourceFolder + File.separator + invokerPackage).replace(".", File.separator); supportingFiles.add(new SupportingFile("jsonSupport.mustache", invokerFolder, "JsonSupport.scala")); + supportingFiles.add(new SupportingFile("additionalTypeSerializers.mustache", invokerFolder, "AdditionalTypeSerializers.scala")); supportingFiles.add(new SupportingFile("project/build.properties.mustache", "project", "build.properties")); supportingFiles.add(new SupportingFile("dateSerializers.mustache", invokerFolder, "DateSerializers.scala")); } diff --git a/modules/openapi-generator/src/main/resources/scala-sttp/additionalTypeSerializers.mustache b/modules/openapi-generator/src/main/resources/scala-sttp/additionalTypeSerializers.mustache new file mode 100644 index 00000000000..9a2c169cc85 --- /dev/null +++ b/modules/openapi-generator/src/main/resources/scala-sttp/additionalTypeSerializers.mustache @@ -0,0 +1,45 @@ +package {{invokerPackage}} + +import java.net.{ URI, URISyntaxException } + +{{#json4s}} +object AdditionalTypeSerializers { + import org.json4s.{Serializer, CustomSerializer, JNull, MappingException} + import org.json4s.JsonAST.JString + case object URISerializer extends CustomSerializer[URI]( _ => ( { + case JString(s) => + try new URI(s) + catch { + case _: URISyntaxException => + throw new MappingException("String could not be parsed as a URI reference, it violates RFC 2396.") + case _: NullPointerException => + throw new MappingException("String is null.") + } + case JNull => null + }, { + case uri: URI => + JString(uri.toString()) + })) + + def all: Seq[Serializer[_]] = Seq[Serializer[_]]() :+ URISerializer +} +{{/json4s}} +{{#circe}} +trait AdditionalTypeSerializers { + import io.circe._ + + implicit final lazy val URIDecoder: Decoder[URI] = Decoder.decodeString.emap(string => + try Right(new URI(string)) + catch { + case _: URISyntaxException => + Left("String could not be parsed as a URI reference, it violates RFC 2396.") + case _: NullPointerException => + Left("String is null.") + } + ) + + implicit final lazy val URIEncoder: Encoder[URI] = new Encoder[URI] { + final def apply(a: URI): Json = Json.fromString(a.toString) + } +} +{{/circe}} diff --git a/modules/openapi-generator/src/main/resources/scala-sttp/dateSerializers.mustache b/modules/openapi-generator/src/main/resources/scala-sttp/dateSerializers.mustache index 779e7a428de..f47d2addab3 100644 --- a/modules/openapi-generator/src/main/resources/scala-sttp/dateSerializers.mustache +++ b/modules/openapi-generator/src/main/resources/scala-sttp/dateSerializers.mustache @@ -1,9 +1,8 @@ package {{invokerPackage}} {{#java8}} -import java.time.{LocalDate, LocalDateTime, OffsetDateTime, ZoneId} +import java.time.{LocalDate, OffsetDateTime} import java.time.format.DateTimeFormatter -import scala.util.Try {{/java8}} {{#joda}} import org.joda.time.DateTime @@ -15,6 +14,9 @@ object DateSerializers { import org.json4s.{Serializer, CustomSerializer, JNull} import org.json4s.JsonAST.JString {{#java8}} + import scala.util.Try + import java.time.{LocalDateTime, ZoneId} + case object DateTimeSerializer extends CustomSerializer[OffsetDateTime](_ => ( { case JString(s) => Try(OffsetDateTime.parse(s, DateTimeFormatter.ISO_OFFSET_DATE_TIME)) orElse diff --git a/modules/openapi-generator/src/main/resources/scala-sttp/jsonSupport.mustache b/modules/openapi-generator/src/main/resources/scala-sttp/jsonSupport.mustache index cfa33bd834a..c6d1d06eef5 100644 --- a/modules/openapi-generator/src/main/resources/scala-sttp/jsonSupport.mustache +++ b/modules/openapi-generator/src/main/resources/scala-sttp/jsonSupport.mustache @@ -10,23 +10,23 @@ import sttp.client3.json4s.SttpJson4sApi import scala.reflect.ClassTag object JsonSupport extends SttpJson4sApi { - def enumSerializers: Seq[Serializer[_]] = Seq[Serializer[_]](){{#models}}{{#model}}{{#hasEnums}}{{#vars}}{{#isEnum}} :+ - new EnumNameSerializer({{classname}}Enums.{{datatypeWithEnum}}){{/isEnum}}{{/vars}}{{/hasEnums}}{{/model}}{{/models}} + def enumSerializers: Seq[Serializer[_]] = Seq[Serializer[_]](){{#models}}{{#model}}{{#isEnum}} :+ + new EnumNameSerializer({{classname}}){{/isEnum}}{{/model}}{{/models}} - private class EnumNameSerializer[E <: Enumeration: ClassTag](enum: E) extends Serializer[E#Value] { + private class EnumNameSerializer[E <: Enumeration: ClassTag](enumeration: E) extends Serializer[E#Value] { import JsonDSL._ val EnumerationClass: Class[E#Value] = classOf[E#Value] def deserialize(implicit format: Formats): PartialFunction[(TypeInfo, JValue), E#Value] = { case (t @ TypeInfo(EnumerationClass, _), json) if isValid(json) => json match { - case JString(value) => enum.withName(value) + case JString(value) => enumeration.withName(value) case value => throw new MappingException(s"Can't convert $value to $EnumerationClass") } } private[this] def isValid(json: JValue) = json match { - case JString(value) if enum.values.exists(_.toString == value) => true + case JString(value) if enumeration.values.exists(_.toString == value) => true case _ => false } @@ -35,7 +35,7 @@ object JsonSupport extends SttpJson4sApi { } } - implicit val format: Formats = DefaultFormats ++ enumSerializers ++ DateSerializers.all + implicit val format: Formats = DefaultFormats ++ enumSerializers ++ DateSerializers.all ++ AdditionalTypeSerializers.all implicit val serialization: org.json4s.Serialization = org.json4s.jackson.Serialization } {{/json4s}} @@ -44,10 +44,15 @@ import io.circe.{Decoder, Encoder} import io.circe.generic.AutoDerivation import sttp.client3.circe.SttpCirceApi -object JsonSupport extends SttpCirceApi with AutoDerivation with DateSerializers { -{{#models}}{{#model}}{{#hasEnums}}{{#vars}}{{#isEnum}} - implicit val {{classname}}{{datatypeWithEnum}}Decoder: Decoder[{{classname}}Enums.{{datatypeWithEnum}}] = Decoder.decodeEnumeration({{classname}}Enums.{{datatypeWithEnum}}) - implicit val {{classname}}{{datatypeWithEnum}}Encoder: Encoder[{{classname}}Enums.{{datatypeWithEnum}}] = Encoder.encodeEnumeration({{classname}}Enums.{{datatypeWithEnum}}) -{{/isEnum}}{{/vars}}{{/hasEnums}}{{/model}}{{/models}} +object JsonSupport extends SttpCirceApi with AutoDerivation with DateSerializers with AdditionalTypeSerializers { + +{{#models}} +{{#model}} +{{#isEnum}} + implicit val {{classname}}Decoder: Decoder[{{classname}}.{{classname}}] = Decoder.decodeEnumeration({{classname}}) + implicit val {{classname}}Encoder: Encoder[{{classname}}.{{classname}}] = Encoder.encodeEnumeration({{classname}}) +{{/isEnum}} +{{/model}} +{{/models}} } {{/circe}} diff --git a/samples/client/petstore/scala-sttp/.openapi-generator/FILES b/samples/client/petstore/scala-sttp/.openapi-generator/FILES index aeb15657fb9..93bed4b07d2 100644 --- a/samples/client/petstore/scala-sttp/.openapi-generator/FILES +++ b/samples/client/petstore/scala-sttp/.openapi-generator/FILES @@ -4,6 +4,7 @@ project/build.properties src/main/scala/org/openapitools/client/api/PetApi.scala src/main/scala/org/openapitools/client/api/StoreApi.scala src/main/scala/org/openapitools/client/api/UserApi.scala +src/main/scala/org/openapitools/client/core/AdditionalTypeSerializers.scala src/main/scala/org/openapitools/client/core/DateSerializers.scala src/main/scala/org/openapitools/client/core/JsonSupport.scala src/main/scala/org/openapitools/client/model/ApiResponse.scala diff --git a/samples/client/petstore/scala-sttp/src/main/scala/org/openapitools/client/core/AdditionalTypeSerializers.scala b/samples/client/petstore/scala-sttp/src/main/scala/org/openapitools/client/core/AdditionalTypeSerializers.scala new file mode 100644 index 00000000000..2ac2a4b1927 --- /dev/null +++ b/samples/client/petstore/scala-sttp/src/main/scala/org/openapitools/client/core/AdditionalTypeSerializers.scala @@ -0,0 +1,24 @@ +package org.openapitools.client.core + +import java.net.{ URI, URISyntaxException } + +object AdditionalTypeSerializers { + import org.json4s.{Serializer, CustomSerializer, JNull, MappingException} + import org.json4s.JsonAST.JString + case object URISerializer extends CustomSerializer[URI]( _ => ( { + case JString(s) => + try new URI(s) + catch { + case _: URISyntaxException => + throw new MappingException("String could not be parsed as a URI reference, it violates RFC 2396.") + case _: NullPointerException => + throw new MappingException("String is null.") + } + case JNull => null + }, { + case uri: URI => + JString(uri.toString()) + })) + + def all: Seq[Serializer[_]] = Seq[Serializer[_]]() :+ URISerializer +} diff --git a/samples/client/petstore/scala-sttp/src/main/scala/org/openapitools/client/core/DateSerializers.scala b/samples/client/petstore/scala-sttp/src/main/scala/org/openapitools/client/core/DateSerializers.scala index cb2d1d2aa43..e869bf889cf 100644 --- a/samples/client/petstore/scala-sttp/src/main/scala/org/openapitools/client/core/DateSerializers.scala +++ b/samples/client/petstore/scala-sttp/src/main/scala/org/openapitools/client/core/DateSerializers.scala @@ -1,12 +1,14 @@ package org.openapitools.client.core -import java.time.{LocalDate, LocalDateTime, OffsetDateTime, ZoneId} +import java.time.{LocalDate, OffsetDateTime} import java.time.format.DateTimeFormatter -import scala.util.Try object DateSerializers { import org.json4s.{Serializer, CustomSerializer, JNull} import org.json4s.JsonAST.JString + import scala.util.Try + import java.time.{LocalDateTime, ZoneId} + case object DateTimeSerializer extends CustomSerializer[OffsetDateTime](_ => ( { case JString(s) => Try(OffsetDateTime.parse(s, DateTimeFormatter.ISO_OFFSET_DATE_TIME)) orElse diff --git a/samples/client/petstore/scala-sttp/src/main/scala/org/openapitools/client/core/JsonSupport.scala b/samples/client/petstore/scala-sttp/src/main/scala/org/openapitools/client/core/JsonSupport.scala index 69c83df3f42..767c0b4b9a6 100644 --- a/samples/client/petstore/scala-sttp/src/main/scala/org/openapitools/client/core/JsonSupport.scala +++ b/samples/client/petstore/scala-sttp/src/main/scala/org/openapitools/client/core/JsonSupport.scala @@ -17,24 +17,22 @@ import sttp.client3.json4s.SttpJson4sApi import scala.reflect.ClassTag object JsonSupport extends SttpJson4sApi { - def enumSerializers: Seq[Serializer[_]] = Seq[Serializer[_]]() :+ - new EnumNameSerializer(OrderEnums.Status) :+ - new EnumNameSerializer(PetEnums.Status) + def enumSerializers: Seq[Serializer[_]] = Seq[Serializer[_]]() - private class EnumNameSerializer[E <: Enumeration: ClassTag](enum: E) extends Serializer[E#Value] { + private class EnumNameSerializer[E <: Enumeration: ClassTag](enumeration: E) extends Serializer[E#Value] { import JsonDSL._ val EnumerationClass: Class[E#Value] = classOf[E#Value] def deserialize(implicit format: Formats): PartialFunction[(TypeInfo, JValue), E#Value] = { case (t @ TypeInfo(EnumerationClass, _), json) if isValid(json) => json match { - case JString(value) => enum.withName(value) + case JString(value) => enumeration.withName(value) case value => throw new MappingException(s"Can't convert $value to $EnumerationClass") } } private[this] def isValid(json: JValue) = json match { - case JString(value) if enum.values.exists(_.toString == value) => true + case JString(value) if enumeration.values.exists(_.toString == value) => true case _ => false } @@ -43,6 +41,6 @@ object JsonSupport extends SttpJson4sApi { } } - implicit val format: Formats = DefaultFormats ++ enumSerializers ++ DateSerializers.all + implicit val format: Formats = DefaultFormats ++ enumSerializers ++ DateSerializers.all ++ AdditionalTypeSerializers.all implicit val serialization: org.json4s.Serialization = org.json4s.jackson.Serialization }