Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -108,10 +108,6 @@ public ScalaSttpClientCodegen() {
apiTemplateFiles.put("api.mustache", ".scala");
embeddedTemplateDir = templateDir = "scala-sttp";

String jsonLibrary = JSON_LIBRARY_PROPERTY.getValue(additionalProperties);

String jsonValueClass = "circe".equals(jsonLibrary) ? "io.circe.Json" : "org.json4s.JValue";

additionalProperties.put(CodegenConstants.GROUP_ID, groupId);
additionalProperties.put(CodegenConstants.ARTIFACT_ID, artifactId);
additionalProperties.put(CodegenConstants.ARTIFACT_VERSION, artifactVersion);
Expand Down Expand Up @@ -147,7 +143,7 @@ public ScalaSttpClientCodegen() {
typeMapping.put("number", "Double");
typeMapping.put("decimal", "BigDecimal");
typeMapping.put("ByteArray", "Array[Byte]");
typeMapping.put("AnyType", jsonValueClass);
typeMapping.put("AnyType", "Any");

instantiationTypes.put("array", "ListBuffer");
instantiationTypes.put("map", "Map");
Expand All @@ -166,6 +162,11 @@ public void processOpts() {
apiPackage = PACKAGE_PROPERTY.getApiPackage(additionalProperties);
modelPackage = PACKAGE_PROPERTY.getModelPackage(additionalProperties);

String jsonLibrary = JSON_LIBRARY_PROPERTY.getValue(additionalProperties);
String jsonValueClass = "circe".equals(jsonLibrary) ? "io.circe.Json" : "org.json4s.JValue";
typeMapping.put("object", jsonValueClass);
typeMapping.put("AnyType", jsonValueClass);

supportingFiles.add(new SupportingFile("README.mustache", "", "README.md"));
supportingFiles.add(new SupportingFile("build.sbt.mustache", "", "build.sbt"));
final String invokerFolder = (sourceFolder + File.separator + invokerPackage).replace(".", File.separator);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ object AdditionalTypeSerializers {
import org.json4s.{Serializer, CustomSerializer, JNull, MappingException}
import org.json4s.JsonAST.JString
case object URISerializer extends CustomSerializer[URI]( _ => ( {
case JString(s) =>
case JString(s) =>
try new URI(s)
catch {
case _: URISyntaxException =>
Expand All @@ -25,21 +25,51 @@ object AdditionalTypeSerializers {
}
{{/json4s}}
{{#circe}}
import java.io.File
import java.nio.file.Files

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)
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)
}

implicit final lazy val FileDecoder: Decoder[File] = Decoder[Array[Byte]].emap { bytes =>
try {
val tmpFile = File.createTempFile("download", ".tmp")
Files.write(tmpFile.toPath, bytes)
Right(tmpFile)
} catch {
case e: Exception => Left(s"Failed to write binary content to file: ${e.getMessage}")
}
}

implicit final lazy val FileEncoder: Encoder[File] = Encoder[Array[Byte]].contramap(
f => Files.readAllBytes(f.toPath)
)

implicit final lazy val AnyDecoder: Decoder[Any] = Decoder[Json].map(_.asInstanceOf[Any])

implicit final lazy val AnyEncoder: Encoder[Any] = Encoder.instance {
case json: Json => json
case b: Boolean => Json.fromBoolean(b)
case n: Int => Json.fromInt(n)
case n: Long => Json.fromLong(n)
case n: Double => Json.fromDoubleOrNull(n)
case n: BigDecimal => Json.fromBigDecimal(n)
case s: String => Json.fromString(s)
case other => Json.fromString(other.toString)
}
}
{{/circe}}
Original file line number Diff line number Diff line change
Expand Up @@ -42,10 +42,9 @@ object JsonSupport extends SttpJson4sApi {
{{/json4s}}
{{#circe}}
import io.circe.{Decoder, Encoder}
import io.circe.generic.AutoDerivation
import sttp.client3.circe.SttpCirceApi

object JsonSupport extends SttpCirceApi with AutoDerivation with DateSerializers with AdditionalTypeSerializers {
object JsonSupport extends SttpCirceApi with DateSerializers with AdditionalTypeSerializers {

{{#models}}
{{#model}}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,11 @@ package {{package}}
{{#imports}}
import {{import}}
{{/imports}}
{{#circe}}
import io.circe.{Decoder, Encoder, Json}
import io.circe.syntax._
import {{invokerPackage}}.JsonSupport._
{{/circe}}

{{#models}}
{{#model}}
Expand All @@ -24,6 +29,40 @@ case class {{classname}}(
{{{name}}}: {{^required}}Option[{{/required}}{{^isEnum}}{{dataType}}{{/isEnum}}{{#isEnum}}{{^isArray}}{{classname}}Enums.{{datatypeWithEnum}}{{/isArray}}{{#isArray}}Seq[{{classname}}Enums.{{datatypeWithEnum}}]{{/isArray}}{{/isEnum}}{{^required}}] = None{{/required}}{{^-last}},{{/-last}}
{{/vars}}
)
{{#circe}}
object {{classname}} {
{{#hasVars}}
implicit val 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
}
}
implicit val decoder{{classname}}: Decoder[{{classname}}] = Decoder.instance { c =>
for {
Comment thread
nikhilsu marked this conversation as resolved.
{{#vars}}
{{{name}}} <- c.downField("{{baseName}}").as[{{^required}}Option[{{/required}}{{^isEnum}}{{dataType}}{{/isEnum}}{{#isEnum}}{{^isArray}}{{classname}}Enums.{{datatypeWithEnum}}{{/isArray}}{{#isArray}}Seq[{{classname}}Enums.{{datatypeWithEnum}}]{{/isArray}}{{/isEnum}}{{^required}}]{{/required}}]
{{/vars}}
} yield {{classname}}(
{{#vars}}
{{{name}}} = {{{name}}}{{^-last}},{{/-last}}
{{/vars}}
)
}
{{/hasVars}}
{{^hasVars}}
implicit val encoder{{classname}}: Encoder[{{classname}}] = Encoder.instance { _ =>
Json.fromFields(Seq.empty)
}
implicit val decoder{{classname}}: Decoder[{{classname}}] = Decoder.instance { _ =>
Right({{classname}}())
}
{{/hasVars}}
}
{{/circe}}
{{/isEnum}}

{{#isEnum}}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,69 @@ public void verifyApiKeyLocations() throws IOException {
assertFileContains(path, ".cookie(\"apikey\", apiKeyCookie)");
}

@Test
public void verifyCirceSerdeWithMixedCaseFields() throws IOException {
File output = Files.createTempDirectory("test").toFile().getCanonicalFile();
output.deleteOnExit();
String outputPath = output.getAbsolutePath().replace('\\', '/');

OpenAPI openAPI = new OpenAPIParser()
.readLocation("src/test/resources/3_0/scala/mixed-case-fields.yaml", null, new ParseOptions()).getOpenAPI();

ScalaSttpClientCodegen codegen = new ScalaSttpClientCodegen();
codegen.setOutputDir(output.getAbsolutePath());
codegen.additionalProperties().put("jsonLibrary", "circe");

ClientOptInput input = new ClientOptInput();
input.openAPI(openAPI);
input.config(codegen);

DefaultGenerator generator = new DefaultGenerator();

generator.setGeneratorPropertyDefault(CodegenConstants.MODELS, "true");
generator.setGeneratorPropertyDefault(CodegenConstants.MODEL_TESTS, "false");
generator.setGeneratorPropertyDefault(CodegenConstants.MODEL_DOCS, "false");
generator.setGeneratorPropertyDefault(CodegenConstants.APIS, "false");
generator.setGeneratorPropertyDefault(CodegenConstants.SUPPORTING_FILES, "true");
generator.opts(input).generate();

Path mixedCaseModelPath = Paths.get(outputPath + "/src/main/scala/org/openapitools/client/model/MixedCaseModel.scala");

assertFileContains(mixedCaseModelPath, "firstName");
assertFileContains(mixedCaseModelPath, "phoneNumber");
assertFileContains(mixedCaseModelPath, "lastName");
assertFileContains(mixedCaseModelPath, "zipCode");
assertFileContains(mixedCaseModelPath, "address");

assertFileContains(mixedCaseModelPath, "\"first-name\"");
assertFileContains(mixedCaseModelPath, "\"phone_number\"");
assertFileContains(mixedCaseModelPath, "\"lastName\"");
assertFileContains(mixedCaseModelPath, "\"ZipCode\"");
assertFileContains(mixedCaseModelPath, "\"address\"");

assertFileContains(mixedCaseModelPath, "c.downField(\"first-name\")");
assertFileContains(mixedCaseModelPath, "c.downField(\"phone_number\")");
assertFileContains(mixedCaseModelPath, "c.downField(\"ZipCode\")");

assertFileContains(mixedCaseModelPath, "object MixedCaseModel");
assertFileContains(mixedCaseModelPath, "implicit val encoderMixedCaseModel");
assertFileContains(mixedCaseModelPath, "implicit val decoderMixedCaseModel");

Path binaryModelPath = Paths.get(outputPath + "/src/main/scala/org/openapitools/client/model/BinaryPayload.scala");
assertFileContains(binaryModelPath, "data: Option[File]");
assertFileContains(binaryModelPath, "metadata: Option[io.circe.Json]");
assertFileContains(binaryModelPath, "c.downField(\"data\")");
assertFileContains(binaryModelPath, "c.downField(\"metadata\")");
assertFileContains(binaryModelPath, "implicit val encoderBinaryPayload");
assertFileContains(binaryModelPath, "implicit val decoderBinaryPayload");

Path additionalSerializersPath = Paths.get(outputPath + "/src/main/scala/org/openapitools/client/core/AdditionalTypeSerializers.scala");
assertFileContains(additionalSerializersPath, "FileDecoder");
assertFileContains(additionalSerializersPath, "FileEncoder");
assertFileContains(additionalSerializersPath, "AnyDecoder");
assertFileContains(additionalSerializersPath, "AnyEncoder");
}

@Test
public void headerSerialization() throws IOException {
File output = Files.createTempDirectory("test").toFile().getCanonicalFile();
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
openapi: 3.0.0
info:
title: Mixed Case Test
version: 1.0.0
paths:
/test:
get:
operationId: getTest
responses:
'200':
description: OK
content:
application/json:
schema:
$ref: '#/components/schemas/MixedCaseModel'
components:
schemas:
MixedCaseModel:
type: object
properties:
first-name:
type: string
phone_number:
type: string
lastName:
type: string
ZipCode:
type: string
address:
type: string
BinaryPayload:
type: object
properties:
data:
type: string
format: binary
metadata:
type: object
Original file line number Diff line number Diff line change
Expand Up @@ -2,20 +2,50 @@ package org.openapitools.client.core

import java.net.{ URI, URISyntaxException }

import java.io.File
import java.nio.file.Files

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)
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)
}

implicit final lazy val FileDecoder: Decoder[File] = Decoder[Array[Byte]].emap { bytes =>
try {
val tmpFile = File.createTempFile("download", ".tmp")
Files.write(tmpFile.toPath, bytes)
Right(tmpFile)
} catch {
case e: Exception => Left(s"Failed to write binary content to file: ${e.getMessage}")
}
}

implicit final lazy val FileEncoder: Encoder[File] = Encoder[Array[Byte]].contramap(
f => Files.readAllBytes(f.toPath)
)

implicit final lazy val AnyDecoder: Decoder[Any] = Decoder[Json].map(_.asInstanceOf[Any])

implicit final lazy val AnyEncoder: Encoder[Any] = Encoder.instance {
case json: Json => json
case b: Boolean => Json.fromBoolean(b)
case n: Int => Json.fromInt(n)
case n: Long => Json.fromLong(n)
case n: Double => Json.fromDoubleOrNull(n)
case n: BigDecimal => Json.fromBigDecimal(n)
case s: String => Json.fromString(s)
case other => Json.fromString(other.toString)
Comment thread
nikhilsu marked this conversation as resolved.
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -13,10 +13,9 @@ package org.openapitools.client.core

import org.openapitools.client.model._
import io.circe.{Decoder, Encoder}
import io.circe.generic.AutoDerivation
import sttp.client3.circe.SttpCirceApi

object JsonSupport extends SttpCirceApi with AutoDerivation with DateSerializers with AdditionalTypeSerializers {
object JsonSupport extends SttpCirceApi with DateSerializers with AdditionalTypeSerializers {

implicit val EnumTestSearchDecoder: Decoder[EnumTestEnums.Search] = Decoder.decodeEnumeration(EnumTestEnums.Search)
implicit val EnumTestSearchEncoder: Encoder[EnumTestEnums.Search] = Encoder.encodeEnumeration(EnumTestEnums.Search)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,9 @@
*/
package org.openapitools.client.model

import io.circe.{Decoder, Encoder, Json}
import io.circe.syntax._
import org.openapitools.client.core.JsonSupport._

/**
* An uploaded response
Expand All @@ -21,4 +24,26 @@ case class ApiResponse(
`type`: Option[String] = None,
message: Option[String] = None
)
object ApiResponse {
implicit val 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
}
}
implicit val 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
)
}
}

Loading
Loading