-
Notifications
You must be signed in to change notification settings - Fork 427
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
codegen: Fix issues with jsoniter in scala3 (#3963)
- Loading branch information
1 parent
81c9a76
commit 9916314
Showing
11 changed files
with
461 additions
and
11 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
101 changes: 101 additions & 0 deletions
101
.../src/sbt-test/sbt-openapi-codegen/oneOf-json-roundtrip_jsoniter_scala3/Expected.scala.txt
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,101 @@ | ||
package sttp.tapir.generated | ||
|
||
object TapirGeneratedEndpoints { | ||
|
||
import sttp.tapir._ | ||
import sttp.tapir.model._ | ||
import sttp.tapir.generic.auto._ | ||
import sttp.tapir.json.jsoniter._ | ||
import com.github.plokhotnyuk.jsoniter_scala.macros._ | ||
import com.github.plokhotnyuk.jsoniter_scala.core._ | ||
|
||
import sttp.tapir.generated.TapirGeneratedEndpointsJsonSerdes._ | ||
import TapirGeneratedEndpointsSchemas._ | ||
|
||
|
||
case class CommaSeparatedValues[T](values: List[T]) | ||
case class ExplodedValues[T](values: List[T]) | ||
trait ExtraParamSupport[T] { | ||
def decode(s: String): sttp.tapir.DecodeResult[T] | ||
def encode(t: T): String | ||
} | ||
implicit def makePathCodecFromSupport[T](implicit support: ExtraParamSupport[T]): sttp.tapir.Codec[String, T, sttp.tapir.CodecFormat.TextPlain] = { | ||
sttp.tapir.Codec.string.mapDecode(support.decode)(support.encode) | ||
} | ||
implicit def makeQueryCodecFromSupport[T](implicit support: ExtraParamSupport[T]): sttp.tapir.Codec[List[String], T, sttp.tapir.CodecFormat.TextPlain] = { | ||
sttp.tapir.Codec.listHead[String, String, sttp.tapir.CodecFormat.TextPlain] | ||
.mapDecode(support.decode)(support.encode) | ||
} | ||
implicit def makeQueryOptCodecFromSupport[T](implicit support: ExtraParamSupport[T]): sttp.tapir.Codec[List[String], Option[T], sttp.tapir.CodecFormat.TextPlain] = { | ||
sttp.tapir.Codec.listHeadOption[String, String, sttp.tapir.CodecFormat.TextPlain] | ||
.mapDecode(maybeV => DecodeResult.sequence(maybeV.toSeq.map(support.decode)).map(_.headOption))(_.map(support.encode)) | ||
} | ||
implicit def makeUnexplodedQuerySeqCodecFromListHead[T](implicit support: sttp.tapir.Codec[List[String], T, sttp.tapir.CodecFormat.TextPlain]): sttp.tapir.Codec[List[String], CommaSeparatedValues[T], sttp.tapir.CodecFormat.TextPlain] = { | ||
sttp.tapir.Codec.listHead[String, String, sttp.tapir.CodecFormat.TextPlain] | ||
.mapDecode(values => DecodeResult.sequence(values.split(',').toSeq.map(e => support.rawDecode(List(e)))).map(s => CommaSeparatedValues(s.toList)))(_.values.map(support.encode).mkString(",")) | ||
} | ||
implicit def makeUnexplodedQueryOptSeqCodecFromListHead[T](implicit support: sttp.tapir.Codec[List[String], T, sttp.tapir.CodecFormat.TextPlain]): sttp.tapir.Codec[List[String], Option[CommaSeparatedValues[T]], sttp.tapir.CodecFormat.TextPlain] = { | ||
sttp.tapir.Codec.listHeadOption[String, String, sttp.tapir.CodecFormat.TextPlain] | ||
.mapDecode{ | ||
case None => DecodeResult.Value(None) | ||
case Some(values) => DecodeResult.sequence(values.split(',').toSeq.map(e => support.rawDecode(List(e)))).map(r => Some(CommaSeparatedValues(r.toList))) | ||
}(_.map(_.values.map(support.encode).mkString(","))) | ||
} | ||
implicit def makeExplodedQuerySeqCodecFromListSeq[T](implicit support: sttp.tapir.Codec[List[String], List[T], sttp.tapir.CodecFormat.TextPlain]): sttp.tapir.Codec[List[String], ExplodedValues[T], sttp.tapir.CodecFormat.TextPlain] = { | ||
support.mapDecode(l => DecodeResult.Value(ExplodedValues(l)))(_.values) | ||
} | ||
|
||
|
||
sealed trait ADTWithoutDiscriminator | ||
sealed trait ADTWithDiscriminator | ||
sealed trait ADTWithDiscriminatorNoMapping | ||
case class SubtypeWithoutD1 ( | ||
s: String, | ||
i: Option[Int] = None, | ||
a: Seq[String], | ||
absent: Option[String] = None | ||
) extends ADTWithoutDiscriminator | ||
case class SubtypeWithD1 ( | ||
s: String, | ||
i: Option[Int] = None, | ||
d: Option[Double] = None | ||
) extends ADTWithDiscriminator with ADTWithDiscriminatorNoMapping | ||
case class SubtypeWithoutD3 ( | ||
s: String, | ||
i: Option[Int] = None, | ||
e: Option[AnEnum] = None, | ||
absent: Option[String] = None | ||
) extends ADTWithoutDiscriminator | ||
case class SubtypeWithoutD2 ( | ||
a: Seq[String], | ||
absent: Option[String] = None | ||
) extends ADTWithoutDiscriminator | ||
case class SubtypeWithD2 ( | ||
s: String, | ||
a: Option[Seq[String]] = None | ||
) extends ADTWithDiscriminator with ADTWithDiscriminatorNoMapping | ||
|
||
enum AnEnum { | ||
case Foo, Bar, Baz | ||
} | ||
|
||
|
||
|
||
lazy val putAdtTest = | ||
endpoint | ||
.put | ||
.in(("adt" / "test")) | ||
.in(jsonBody[ADTWithoutDiscriminator]) | ||
.out(jsonBody[ADTWithoutDiscriminator].description("successful operation")) | ||
|
||
lazy val postAdtTest = | ||
endpoint | ||
.post | ||
.in(("adt" / "test")) | ||
.in(jsonBody[ADTWithDiscriminatorNoMapping]) | ||
.out(jsonBody[ADTWithDiscriminator].description("successful operation")) | ||
|
||
|
||
lazy val generatedEndpoints = List(putAdtTest, postAdtTest) | ||
|
||
} |
38 changes: 38 additions & 0 deletions
38
...bt-plugin/src/sbt-test/sbt-openapi-codegen/oneOf-json-roundtrip_jsoniter_scala3/build.sbt
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,38 @@ | ||
lazy val root = (project in file(".")) | ||
.enablePlugins(OpenapiCodegenPlugin) | ||
.settings( | ||
scalaVersion := "3.3.3", | ||
version := "0.1", | ||
openapiJsonSerdeLib := "jsoniter" | ||
) | ||
|
||
libraryDependencies ++= Seq( | ||
"com.softwaremill.sttp.tapir" %% "tapir-jsoniter-scala" % "1.10.0", | ||
"com.softwaremill.sttp.tapir" %% "tapir-openapi-docs" % "1.10.0", | ||
"com.softwaremill.sttp.apispec" %% "openapi-circe-yaml" % "0.8.0", | ||
"com.beachape" %% "enumeratum" % "1.7.4", | ||
"com.github.plokhotnyuk.jsoniter-scala" %% "jsoniter-scala-core" % "2.30.7", | ||
"com.github.plokhotnyuk.jsoniter-scala" %% "jsoniter-scala-macros" % "2.30.7" % "compile-internal", | ||
"org.scalatest" %% "scalatest" % "3.2.19" % Test, | ||
"com.softwaremill.sttp.tapir" %% "tapir-sttp-stub-server" % "1.10.0" % Test | ||
) | ||
|
||
import sttp.tapir.sbt.OpenapiCodegenPlugin.autoImport.{openapiJsonSerdeLib, openapiUseHeadTagForObjectName} | ||
|
||
import scala.io.Source | ||
|
||
TaskKey[Unit]("check") := { | ||
val generatedCode = | ||
Source.fromFile("target/scala-3.3.3/src_managed/main/sbt-openapi-codegen/TapirGeneratedEndpoints.scala").getLines.mkString("\n") | ||
val expected = Source.fromFile("Expected.scala.txt").getLines.mkString("\n") | ||
val generatedTrimmed = | ||
generatedCode.linesIterator.zipWithIndex.filterNot(_._1.forall(_.isWhitespace)).map { case (a, i) => a.trim -> i }.toSeq | ||
val expectedTrimmed = expected.linesIterator.filterNot(_.forall(_.isWhitespace)).map(_.trim).toSeq | ||
if (generatedTrimmed.size != expectedTrimmed.size) | ||
sys.error(s"expected ${expectedTrimmed.size} non-empty lines, found ${generatedTrimmed.size}") | ||
generatedTrimmed.zip(expectedTrimmed).foreach { case ((a, i), b) => | ||
if (a != b) sys.error(s"Generated code did not match (expected '$b' on line $i, found '$a')") | ||
} | ||
println("Skipping swagger roundtrip for petstore") | ||
() | ||
} |
1 change: 1 addition & 0 deletions
1
...bt-test/sbt-openapi-codegen/oneOf-json-roundtrip_jsoniter_scala3/project/build.properties
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
sbt.version=1.10.1 |
11 changes: 11 additions & 0 deletions
11
...src/sbt-test/sbt-openapi-codegen/oneOf-json-roundtrip_jsoniter_scala3/project/plugins.sbt
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,11 @@ | ||
{ | ||
val pluginVersion = System.getProperty("plugin.version") | ||
if (pluginVersion == null) | ||
throw new RuntimeException("""| | ||
| | ||
|The system property 'plugin.version' is not defined. | ||
|Specify this property using the scriptedLaunchOpts -D. | ||
| | ||
|""".stripMargin) | ||
else addSbtPlugin("com.softwaremill.sttp.tapir" % "sbt-openapi-codegen" % pluginVersion) | ||
} |
12 changes: 12 additions & 0 deletions
12
...t-test/sbt-openapi-codegen/oneOf-json-roundtrip_jsoniter_scala3/src/main/scala/Main.scala
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,12 @@ | ||
object Main extends App { | ||
import sttp.apispec.openapi.circe.yaml._ | ||
import sttp.tapir.generated._ | ||
import sttp.tapir.docs.openapi._ | ||
|
||
val docs = OpenAPIDocsInterpreter().toOpenAPI(TapirGeneratedEndpoints.generatedEndpoints, "My Bookshop", "1.0") | ||
|
||
import java.nio.file.{Paths, Files} | ||
import java.nio.charset.StandardCharsets | ||
|
||
Files.write(Paths.get("target/swagger.yaml"), docs.toYaml.getBytes(StandardCharsets.UTF_8)) | ||
} |
147 changes: 147 additions & 0 deletions
147
...t-openapi-codegen/oneOf-json-roundtrip_jsoniter_scala3/src/test/scala/JsonRoundtrip.scala
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,147 @@ | ||
import com.github.plokhotnyuk.jsoniter_scala.core.writeToString | ||
import io.circe.parser.parse | ||
import org.scalatest.freespec.AnyFreeSpec | ||
import org.scalatest.matchers.should.Matchers | ||
import sttp.client3.UriContext | ||
import sttp.client3.testing.SttpBackendStub | ||
import sttp.tapir.generated.{TapirGeneratedEndpoints, TapirGeneratedEndpointsJsonSerdes} | ||
import sttp.tapir.generated.TapirGeneratedEndpoints.* | ||
import sttp.tapir.generated.TapirGeneratedEndpointsSchemas.* | ||
import TapirGeneratedEndpointsJsonSerdes._ | ||
import sttp.tapir.server.stub.TapirStubInterpreter | ||
|
||
import scala.concurrent.duration.DurationInt | ||
import scala.concurrent.{Await, Future} | ||
import scala.concurrent.ExecutionContext.Implicits.global | ||
|
||
class JsonRoundtrip extends AnyFreeSpec with Matchers { | ||
"oneOf without discriminator can be round-tripped by generated serdes" in { | ||
val route = TapirGeneratedEndpoints.putAdtTest.serverLogic[Future]({ | ||
case foo: SubtypeWithoutD1 => | ||
Future successful Right[Unit, ADTWithoutDiscriminator](SubtypeWithoutD1(foo.s + "+SubtypeWithoutD1", foo.i, foo.a)) | ||
case foo: SubtypeWithoutD2 => Future successful Right[Unit, ADTWithoutDiscriminator](SubtypeWithoutD2(foo.a :+ "+SubtypeWithoutD2")) | ||
case foo: SubtypeWithoutD3 => | ||
Future successful Right[Unit, ADTWithoutDiscriminator](SubtypeWithoutD3(foo.s + "+SubtypeWithoutD3", foo.i, foo.e)) | ||
}) | ||
|
||
val stub = TapirStubInterpreter(SttpBackendStub.asynchronousFuture) | ||
.whenServerEndpoint(route) | ||
.thenRunLogic() | ||
.backend() | ||
|
||
def normalise(json: String): String = parse(json).toTry.get.noSpacesSortKeys | ||
locally { | ||
val reqBody = SubtypeWithoutD1("a string", Some(123), Seq("string 1", "string 2")) | ||
val reqJsonBody = writeToString(reqBody) | ||
val respBody = SubtypeWithoutD1("a string+SubtypeWithoutD1", Some(123), Seq("string 1", "string 2")) | ||
val respJsonBody = writeToString(respBody) | ||
reqJsonBody shouldEqual """{"s":"a string","i":123,"a":["string 1","string 2"]}""" | ||
respJsonBody shouldEqual """{"s":"a string+SubtypeWithoutD1","i":123,"a":["string 1","string 2"]}""" | ||
Await.result( | ||
sttp.client3.basicRequest | ||
.put(uri"http://test.com/adt/test") | ||
.body(reqJsonBody) | ||
.send(stub) | ||
.map { resp => | ||
resp.code.code === 200 | ||
resp.body shouldEqual Right(respJsonBody) | ||
}, | ||
1.second | ||
) | ||
} | ||
|
||
locally { | ||
val reqBody = SubtypeWithoutD2(Seq("string 1", "string 2")) | ||
val reqJsonBody = writeToString(reqBody) | ||
val respBody = SubtypeWithoutD2(Seq("string 1", "string 2", "+SubtypeWithoutD2")) | ||
val respJsonBody = writeToString(respBody) | ||
reqJsonBody shouldEqual """{"a":["string 1","string 2"]}""" | ||
respJsonBody shouldEqual """{"a":["string 1","string 2","+SubtypeWithoutD2"]}""" | ||
Await.result( | ||
sttp.client3.basicRequest | ||
.put(uri"http://test.com/adt/test") | ||
.body(reqJsonBody) | ||
.send(stub) | ||
.map { resp => | ||
resp.body shouldEqual Right(respJsonBody) | ||
resp.code.code === 200 | ||
}, | ||
1.second | ||
) | ||
} | ||
|
||
locally { | ||
val reqBody = SubtypeWithoutD3("a string", Some(123), Some(AnEnum.Foo)) | ||
val reqJsonBody = writeToString(reqBody) | ||
val respBody = SubtypeWithoutD3("a string+SubtypeWithoutD3", Some(123), Some(AnEnum.Foo)) | ||
val respJsonBody = writeToString(respBody) | ||
reqJsonBody shouldEqual """{"s":"a string","i":123,"e":"Foo"}""" | ||
respJsonBody shouldEqual """{"s":"a string+SubtypeWithoutD3","i":123,"e":"Foo"}""" | ||
Await.result( | ||
sttp.client3.basicRequest | ||
.put(uri"http://test.com/adt/test") | ||
.body(reqJsonBody) | ||
.send(stub) | ||
.map { resp => | ||
resp.body shouldEqual Right(respJsonBody) | ||
resp.code.code === 200 | ||
}, | ||
1.second | ||
) | ||
} | ||
} | ||
"oneOf with discriminator can be round-tripped by generated serdes" in { | ||
val route = TapirGeneratedEndpoints.postAdtTest.serverLogic[Future]({ | ||
case foo: SubtypeWithD1 => Future successful Right[Unit, ADTWithDiscriminator](SubtypeWithD1(foo.s + "+SubtypeWithD1", foo.i, foo.d)) | ||
case foo: SubtypeWithD2 => Future successful Right[Unit, ADTWithDiscriminator](SubtypeWithD2(foo.s + "+SubtypeWithD2", foo.a)) | ||
}) | ||
|
||
val stub = TapirStubInterpreter(SttpBackendStub.asynchronousFuture) | ||
.whenServerEndpoint(route) | ||
.thenRunLogic() | ||
.backend() | ||
|
||
def normalise(json: String): String = parse(json).toTry.get.noSpacesSortKeys | ||
|
||
locally { | ||
val reqBody: ADTWithDiscriminatorNoMapping = SubtypeWithD1("a string", Some(123), Some(23.4)) | ||
val reqJsonBody = writeToString(reqBody) | ||
val respBody: ADTWithDiscriminator = SubtypeWithD1("a string+SubtypeWithD1", Some(123), Some(23.4)) | ||
val respJsonBody = writeToString(respBody) | ||
reqJsonBody shouldEqual """{"type":"SubtypeWithD1","s":"a string","i":123,"d":23.4}""" | ||
respJsonBody shouldEqual """{"type":"SubA","s":"a string+SubtypeWithD1","i":123,"d":23.4}""" | ||
Await.result( | ||
sttp.client3.basicRequest | ||
.post(uri"http://test.com/adt/test") | ||
.body(reqJsonBody) | ||
.send(stub) | ||
.map { resp => | ||
resp.code.code === 200 | ||
resp.body shouldEqual Right(respJsonBody) | ||
}, | ||
1.second | ||
) | ||
} | ||
|
||
locally { | ||
val reqBody: ADTWithDiscriminatorNoMapping = SubtypeWithD2("a string", Some(Seq("string 1", "string 2"))) | ||
val reqJsonBody = writeToString(reqBody) | ||
val respBody: ADTWithDiscriminator = SubtypeWithD2("a string+SubtypeWithD2", Some(Seq("string 1", "string 2"))) | ||
val respJsonBody = writeToString(respBody) | ||
reqJsonBody shouldEqual """{"type":"SubtypeWithD2","s":"a string","a":["string 1","string 2"]}""" | ||
respJsonBody shouldEqual """{"type":"SubB","s":"a string+SubtypeWithD2","a":["string 1","string 2"]}""" | ||
Await.result( | ||
sttp.client3.basicRequest | ||
.post(uri"http://test.com/adt/test") | ||
.body(reqJsonBody) | ||
.send(stub) | ||
.map { resp => | ||
resp.code.code === 200 | ||
resp.body shouldEqual Right(respJsonBody) | ||
}, | ||
1.second | ||
) | ||
} | ||
|
||
} | ||
} |
Oops, something went wrong.