Skip to content

Commit

Permalink
test the json roundtrip
Browse files Browse the repository at this point in the history
  • Loading branch information
hughsimpson committed Apr 4, 2024
1 parent c6ae2ed commit 73cba8b
Show file tree
Hide file tree
Showing 14 changed files with 950 additions and 44 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,9 @@ object TapirGeneratedEndpoints {
sttp.tapir.DecodeResult.Value(_)
)
)(_.map(_.entryName))
sealed trait ADTWithoutDiscriminator
sealed trait ADTWithDiscriminator
sealed trait ADTWithDiscriminatorNoMapping
case class Pet (
status: Option[PetStatus] = None,
tags: Option[Seq[Tag]] = None,
Expand All @@ -69,6 +72,28 @@ object TapirGeneratedEndpoints {
state: Option[String] = None,
zip: Option[String] = None
)
case class SubtypeWithoutD1 (
s: String,
i: Option[Int] = None,
a: Seq[String]
) 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,
d: Option[Double] = None
) extends ADTWithoutDiscriminator
case class SubtypeWithoutD2 (
a: Seq[String]
) extends ADTWithoutDiscriminator
case class SubtypeWithD2 (
s: String,
a: Option[Seq[String]] = None
) extends ADTWithDiscriminator with ADTWithDiscriminatorNoMapping
case class Tag (
id: Option[Long] = None,
name: Option[String] = None
Expand Down Expand Up @@ -195,6 +220,20 @@ object TapirGeneratedEndpoints {
.tags(List("user"))
.attribute[SwaggerRouterControllerExtension](swaggerRouterControllerExtensionKey, "UserController")

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 getUserByName =
endpoint
.get
Expand Down Expand Up @@ -293,7 +332,7 @@ object TapirGeneratedEndpoints {
makeExplodedQueryCodecForEnum("FindPetsByStatusStatus", FindPetsByStatusStatus)
}

lazy val generatedEndpoints: List[sttp.tapir.AnyEndpoint] = List(placeOrder, findPetsByTags, updatePet, addPet, getOrderById, deleteOrder, logoutUser, getInventory, createUsersWithListInput, getUserByName, updateUser, deleteUser, loginUser, createUser, getPetById, updatePetWithForm, deletePet, uploadFile, findPetsByStatus)

lazy val generatedEndpoints: List[sttp.tapir.AnyEndpoint] = List(placeOrder, findPetsByTags, updatePet, addPet, getOrderById, deleteOrder, logoutUser, getInventory, createUsersWithListInput, putAdtTest, postAdtTest, getUserByName, updateUser, deleteUser, loginUser, createUser, getPetById, updatePetWithForm, deletePet, uploadFile, findPetsByStatus)

}
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,9 @@ libraryDependencies ++= Seq(
"com.softwaremill.sttp.apispec" %% "openapi-circe-yaml" % "0.8.0",
"io.circe" %% "circe-generic" % "0.14.6",
"com.beachape" %% "enumeratum" % "1.7.3",
"com.beachape" %% "enumeratum-circe" % "1.7.3"
"com.beachape" %% "enumeratum-circe" % "1.7.3",
"org.scalatest" %% "scalatest" % "3.2.18" % Test,
"com.softwaremill.sttp.tapir" %% "tapir-sttp-stub-server" % "1.10.0" % Test
)

import scala.io.Source
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,144 @@
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, TapirGeneratedEndpointsExtra1}
import sttp.tapir.generated.TapirGeneratedEndpoints._
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.d))
})

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 = TapirGeneratedEndpointsExtra1.aDTWithoutDiscriminatorJsonEncoder(reqBody).noSpacesSortKeys
val respBody = SubtypeWithoutD1("a string+SubtypeWithoutD1", Some(123), Seq("string 1", "string 2"))
val respJsonBody = TapirGeneratedEndpointsExtra1.aDTWithoutDiscriminatorJsonEncoder(respBody).noSpacesSortKeys
reqJsonBody shouldEqual """{"a":["string 1","string 2"],"i":123,"s":"a string"}"""
respJsonBody shouldEqual """{"a":["string 1","string 2"],"i":123,"s":"a string+SubtypeWithoutD1"}"""
Await.result(
sttp.client3.basicRequest
.put(uri"http://test.com/adt/test")
.body(reqJsonBody)
.send(stub)
.map { resp =>
resp.code.code === 200
resp.body.map(normalise) shouldEqual Right(respJsonBody)
},
1.second
)
}

locally {
val reqBody = SubtypeWithoutD2(Seq("string 1", "string 2"))
val reqJsonBody = TapirGeneratedEndpointsExtra1.aDTWithoutDiscriminatorJsonEncoder(reqBody).noSpacesSortKeys
val respBody = SubtypeWithoutD2(Seq("string 1", "string 2", "+SubtypeWithoutD2"))
val respJsonBody = TapirGeneratedEndpointsExtra1.aDTWithoutDiscriminatorJsonEncoder(respBody).noSpacesSortKeys
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.map(normalise) shouldEqual Right(respJsonBody)
resp.code.code === 200
},
1.second
)
}

locally {
val reqBody = SubtypeWithoutD3("a string", Some(123), Some(23.4))
val reqJsonBody = TapirGeneratedEndpointsExtra1.aDTWithoutDiscriminatorJsonEncoder(reqBody).noSpacesSortKeys
val respBody = SubtypeWithoutD3("a string+SubtypeWithoutD3", Some(123), Some(23.4))
val respJsonBody = TapirGeneratedEndpointsExtra1.aDTWithoutDiscriminatorJsonEncoder(respBody).noSpacesSortKeys
reqJsonBody shouldEqual """{"d":23.4,"i":123,"s":"a string"}"""
respJsonBody shouldEqual """{"d":23.4,"i":123,"s":"a string+SubtypeWithoutD3"}"""
Await.result(
sttp.client3.basicRequest
.put(uri"http://test.com/adt/test")
.body(reqJsonBody)
.send(stub)
.map { resp =>
resp.body.map(normalise) 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 = SubtypeWithD1("a string", Some(123), Some(23.4))
val reqJsonBody = TapirGeneratedEndpointsExtra1.aDTWithDiscriminatorNoMappingJsonEncoder(reqBody).noSpacesSortKeys
val respBody = SubtypeWithD1("a string+SubtypeWithD1", Some(123), Some(23.4))
val respJsonBody = TapirGeneratedEndpointsExtra1.aDTWithDiscriminatorJsonEncoder(respBody).noSpacesSortKeys
reqJsonBody shouldEqual """{"d":23.4,"i":123,"s":"a string","type":"SubtypeWithD1"}"""
respJsonBody shouldEqual """{"d":23.4,"i":123,"s":"a string+SubtypeWithD1","type":"SubA"}"""
Await.result(
sttp.client3.basicRequest
.post(uri"http://test.com/adt/test")
.body(reqJsonBody)
.send(stub)
.map { resp =>
resp.code.code === 200
resp.body.map(normalise) shouldEqual Right(respJsonBody)
},
1.second
)
}

locally {
val reqBody = SubtypeWithD2("a string", Some(Seq("string 1", "string 2")))
val reqJsonBody = TapirGeneratedEndpointsExtra1.aDTWithDiscriminatorNoMappingJsonEncoder(reqBody).noSpacesSortKeys
val respBody = SubtypeWithD2("a string+SubtypeWithD2", Some(Seq("string 1", "string 2")))
val respJsonBody = TapirGeneratedEndpointsExtra1.aDTWithDiscriminatorJsonEncoder(respBody).noSpacesSortKeys
reqJsonBody shouldEqual """{"a":["string 1","string 2"],"s":"a string","type":"SubtypeWithD2"}"""
respJsonBody shouldEqual """{"a":["string 1","string 2"],"s":"a string+SubtypeWithD2","type":"SubB"}"""
Await.result(
sttp.client3.basicRequest
.post(uri"http://test.com/adt/test")
.body(reqJsonBody)
.send(stub)
.map { resp =>
resp.code.code === 200
resp.body.map(normalise) shouldEqual Right(respJsonBody)
},
1.second
)
}

}
}
Original file line number Diff line number Diff line change
Expand Up @@ -217,7 +217,7 @@ paths:
'404':
description: Pet not found
security:
- api_key: []
- api_key: [ ]
# - petstore_auth:
# - 'write:pets'
# - 'read:pets'
Expand Down Expand Up @@ -336,7 +336,7 @@ paths:
type: integer
format: int32
security:
- api_key: []
- api_key: [ ]
/store/order:
post:
tags:
Expand Down Expand Up @@ -525,7 +525,7 @@ paths:
summary: Logs out current logged in user session
description: ''
operationId: logoutUser
parameters: []
parameters: [ ]
responses:
default:
description: successful operation
Expand Down Expand Up @@ -604,11 +604,127 @@ paths:
description: Invalid username supplied
'404':
description: User not found
'/adt/test':
post:
responses:
'200':
description: successful operation
content:
application/json:
schema:
$ref: '#/components/schemas/ADTWithDiscriminator'
requestBody:
required: true
description: Update an existent user in the store
content:
application/json:
schema:
$ref: '#/components/schemas/ADTWithDiscriminatorNoMapping'
put:
responses:
'200':
description: successful operation
content:
application/json:
schema:
$ref: '#/components/schemas/ADTWithoutDiscriminator'
requestBody:
required: true
description: Update an existent user in the store
content:
application/json:
schema:
$ref: '#/components/schemas/ADTWithoutDiscriminator'

externalDocs:
description: Find out more about Swagger
url: 'http://swagger.io'
components:
schemas:
ADTWithDiscriminator:
type: object
oneOf:
- $ref: '#/components/schemas/SubtypeWithD1'
- $ref: '#/components/schemas/SubtypeWithD2'
discriminator:
propertyName: type
mapping:
'SubA': '#/components/schemas/SubtypeWithD1'
'SubB': '#/components/schemas/SubtypeWithD2'
# This has the same members as ADTWithDiscriminator, to test that we can extend multiple sealed traits in our ADT mappings
ADTWithDiscriminatorNoMapping:
type: object
oneOf:
- $ref: '#/components/schemas/SubtypeWithD1'
- $ref: '#/components/schemas/SubtypeWithD2'
discriminator:
propertyName: type
SubtypeWithD1:
type: object
required:
- s
properties:
s:
type: string
i:
type: integer
d:
type: number
format: double
SubtypeWithD2:
type: object
required:
- s
properties:
s:
type: string
a:
type: array
items:
type: string
ADTWithoutDiscriminator:
type: object
oneOf:
## A 'SubtypeWithoutD1' with only 'a' and 'd' fields set could be decoded as either a SubtypeWithoutD2 or SubtypeWithoutD3,
## and so must be defined first here, or else we'd fail validation
- $ref: '#/components/schemas/SubtypeWithoutD1'
- $ref: '#/components/schemas/SubtypeWithoutD2'
- $ref: '#/components/schemas/SubtypeWithoutD3'
SubtypeWithoutD1:
type: object
required:
- s
- a
properties:
s:
type: string
i:
type: integer
a:
type: array
items:
type: string
SubtypeWithoutD2:
type: object
required:
- a
properties:
a:
type: array
items:
type: string
SubtypeWithoutD3:
type: object
required:
- s
properties:
s:
type: string
i:
type: integer
d:
type: number
format: double
Order:
x-swagger-router-model: io.swagger.petstore.model.Order
properties:
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
> clean
> generateTapirDefinitions
> run
> test
> check

Loading

0 comments on commit 73cba8b

Please sign in to comment.