Skip to content

Commit

Permalink
Merge branch 'main' into kaizen/harden-suite-for-optional-param
Browse files Browse the repository at this point in the history
  • Loading branch information
Kasper Kondzielski committed Mar 21, 2023
2 parents c9796b8 + f686e25 commit c7c938f
Show file tree
Hide file tree
Showing 9 changed files with 315 additions and 6 deletions.
4 changes: 2 additions & 2 deletions .github/workflows/ci.yaml
Expand Up @@ -15,8 +15,8 @@ jobs:
runs-on: ubuntu-20.04
steps:
- uses: actions/checkout@v3
- uses: coursier/cache-action@v5
- uses: coursier/setup-action@v1.1.2
- uses: coursier/cache-action@v6
- uses: coursier/setup-action@v1.3.0
with:
jvm: adopt:11
- name: Check format
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/dependency-graph.yaml
Expand Up @@ -11,7 +11,7 @@ jobs:
steps:
- uses: actions/checkout@v3
- uses: coursier/cache-action@v6
- uses: coursier/setup-action@v1.1.2
- uses: coursier/setup-action@v1.3.0
with:
jvm: adopt:11
- uses: ckipp01/mill-dependency-submission@v1
4 changes: 3 additions & 1 deletion .gitignore
Expand Up @@ -7,4 +7,6 @@ out
mill-bsp.json

project
target
target

.bloop
2 changes: 1 addition & 1 deletion .mill-version
@@ -1 +1 @@
0.10.5
0.10.10
2 changes: 1 addition & 1 deletion README.md
Expand Up @@ -79,7 +79,7 @@ resolvers ++= Seq(
Add the following dependency:

```
ivy"io.iohk.armadillo::armadillo-core::0.0.10"
ivy"io.iohk.armadillo::armadillo-core::0.1.0"
```
and IOG nexus repository:
```scala
Expand Down
39 changes: 39 additions & 0 deletions build.sc
Expand Up @@ -216,6 +216,45 @@ object example extends CommonModule {
ivy"io.circe::circe-literal::${Version.Circe}"
)
}

object circeOpenRpc extends CommonModule {
override def moduleDeps = Seq(core, json.circe, server.tapir, openrpc, openrpc.circe)
override def ivyDeps = Agg(
ivy"com.softwaremill.sttp.tapir::tapir-core::${Version.Tapir}",
ivy"io.circe::circe-core::${Version.Circe}",
ivy"io.circe::circe-generic::${Version.Circe}",
ivy"io.circe::circe-literal::${Version.Circe}",
ivy"org.typelevel::cats-effect::${Version.CatsEffect}"
)
}

object json4sOpenRpc extends CommonModule {
override def moduleDeps = Seq(core, json.json4s, server.tapir, openrpc, openrpc.circe)
override def ivyDeps = Agg(
ivy"com.softwaremill.sttp.tapir::tapir-core::${Version.Tapir}",
ivy"io.circe::circe-core::${Version.Circe}",
ivy"org.json4s::json4s-core::${Version.Json4s}",
ivy"org.json4s::json4s-jackson:${Version.Json4s}",
ivy"org.typelevel::cats-effect::${Version.CatsEffect}"
)
}

object testing extends CommonModule {
override def moduleDeps = Seq(core, json.circe, server.tapir, openrpc, openrpc.circe)

object test extends Tests with CommonTestModule {
override def moduleDeps = Seq(json.circe, server.stub)
override def ivyDeps = Agg(
WeaverDep,
ivy"com.softwaremill.sttp.client3::cats::${Version.Sttp}",
ivy"com.softwaremill.sttp.client3::core::${Version.Sttp}",
ivy"com.softwaremill.sttp.client3::circe::${Version.Sttp}",
ivy"com.softwaremill.sttp.tapir::tapir-sttp-stub-server::${Version.Tapir}",
ivy"io.circe::circe-literal::${Version.Circe}",
ivy"org.typelevel::cats-effect::${Version.CatsEffect}"
)
}
}
}

object trace4cats extends CommonModule with ArmadilloPublishModule {
Expand Down
@@ -0,0 +1,97 @@
package io.iohk.armadillo.example

import cats.effect.{ExitCode, IO, IOApp}
import io.circe.generic.semiauto._
import io.circe.literal._
import io.circe.syntax.EncoderOps
import io.circe.{Decoder, Encoder}
import io.iohk.armadillo._
import io.iohk.armadillo.json.circe._
import io.iohk.armadillo.openrpc.OpenRpcDocsInterpreter
import io.iohk.armadillo.openrpc.circe.ArmadilloOpenRpcCirce
import io.iohk.armadillo.openrpc.circe.yaml.RichOpenRpcDocument
import io.iohk.armadillo.openrpc.model.OpenRpcInfo
import sttp.apispec.AnySchema
import sttp.tapir.{Schema, Validator}

object ExampleCirce extends IOApp {

case class DescriptionResponse(msg: String)

implicit val rpcBlockResponseEncoder: Encoder[DescriptionResponse] = deriveEncoder
implicit val rpcBlockResponseDecoder: Decoder[DescriptionResponse] = deriveDecoder
implicit val rpcBlockResponseSchema: Schema[DescriptionResponse] = Schema.derived

// Note that only the name is required. Validations, description and example(s) are optionals.
private val nameParam = param[String]("name")
.description("Your name.")
.examples(Set("John Doe", "Jane Doe")) // Multiple examples can be provided
.validate(Validator.minLength(1).and(Validator.maxLength(100)))

private val ageParam = param[Int]("age")
.example(42)
.description("Your age.")
.validate(Validator.positive)

private val descriptionResult = result[DescriptionResponse]("description")
.description("A personalized greeting message.")
.examples(
Set(
DescriptionResponse("Hello John Doe, you are 42 years old"),
DescriptionResponse("Hello Jane Doe, you are 42 years old")
)
)

val describeMeEndpoint: JsonRpcServerEndpoint[IO] = jsonRpcEndpoint(m"describe_me")
.description("Returns a description of a person based on a name and an age.") // An endpoint can also have a description
.summary("A short summary.")
.in(nameParam.and(ageParam))
.out(descriptionResult)
.serverLogic[IO] { case (name, age) =>
IO(Right(DescriptionResponse(s"Hello $name, you are $age years old")))
}

case object CustomArmadilloOpenRpcCirce extends ArmadilloOpenRpcCirce {
override val anyObjectEncoding: AnySchema.Encoding = AnySchema.Encoding.Boolean

override def openApi30: Boolean = true
}

override def run(args: List[String]): IO[ExitCode] = {
val info: OpenRpcInfo = OpenRpcInfo("1.0.0", "Describe me!")
val document = OpenRpcDocsInterpreter().toOpenRpc(info, List(describeMeEndpoint.endpoint))

// Render the OpenRpcDocument in yaml and json.
// Please note the import used, that will allow to render the specification in different version of OpenAPI
for {
_ <- displayStepMessage("Json - Recent version of OpenAPI")
_ <- {
import io.iohk.armadillo.openrpc.circe._
IO.println(document.asJson)
}

_ <- displayStepMessage("Json - Version 3.0 of OpenAPI")
_ <- {
import CustomArmadilloOpenRpcCirce._
IO.println(document.asJson)
}

_ <- displayStepMessage("Yaml - Recent version of OpenAPI")
_ <- {
import io.iohk.armadillo.openrpc.circe._
IO.println(document.toYaml)
}

_ <- displayStepMessage("Yaml - Version 3.0 of OpenAPI")
_ <- {
import CustomArmadilloOpenRpcCirce._
IO.println(document.toYaml)
}
} yield ExitCode.Success
}

private def displayStepMessage(step: String): IO[Unit] = {
val separator = IO.println("_" * 50)
separator >> IO.println(step) >> separator
}
}
@@ -0,0 +1,96 @@
package io.iohk.armadillo.example

import cats.effect.{ExitCode, IO, IOApp}
import io.circe.syntax.EncoderOps
import io.iohk.armadillo._
import io.iohk.armadillo.json.json4s.{Json4sSupport, jsonRpcCodec}
import io.iohk.armadillo.openrpc.OpenRpcDocsInterpreter
import io.iohk.armadillo.openrpc.circe.ArmadilloOpenRpcCirce
import io.iohk.armadillo.openrpc.circe.yaml.RichOpenRpcDocument
import io.iohk.armadillo.openrpc.model.OpenRpcInfo
import org.json4s.{Formats, NoTypeHints, Serialization}
import sttp.apispec.AnySchema
import sttp.tapir.{Schema, Validator}

object ExampleJson4s extends IOApp {

case class DescriptionResponse(msg: String)

implicit val rpcBlockResponseSchema: Schema[DescriptionResponse] = Schema.derived
implicit val serialization: Serialization = org.json4s.jackson.Serialization
implicit val formats: Formats = org.json4s.jackson.Serialization.formats(NoTypeHints)
implicit val json4sSupport: Json4sSupport = Json4sSupport(org.json4s.jackson.parseJson(_), org.json4s.jackson.compactJson)

// Note that only the name is required. Validations, description and example(s) are optionals.
private val nameParam = param[String]("name")
.description("Your name.")
.examples(Set("John Doe", "Jane Doe")) // Multiple examples can be provided
.validate(Validator.minLength(1).and(Validator.maxLength(100)))

private val ageParam = param[Int]("age")
.example(42)
.description("Your age.")
.validate(Validator.positive)

private val descriptionResult = result[DescriptionResponse]("description")
.description("A personalized greeting message.")
.examples(
Set(
DescriptionResponse("Hello John Doe, you are 42 years old"),
DescriptionResponse("Hello Jane Doe, you are 42 years old")
)
)

val describeMeEndpoint: JsonRpcServerEndpoint[IO] = jsonRpcEndpoint(m"describe_me")
.description("Returns a description of a person based on a name and an age.") // An endpoint can also have a description
.summary("A short summary.")
.in(nameParam.and(ageParam))
.out(descriptionResult)
.serverLogic[IO] { case (name, age) =>
IO(Right(DescriptionResponse(s"Hello $name, you are $age years old")))
}

case object CustomArmadilloOpenRpcCirce extends ArmadilloOpenRpcCirce {
override val anyObjectEncoding: AnySchema.Encoding = AnySchema.Encoding.Boolean

override def openApi30: Boolean = true
}

override def run(args: List[String]): IO[ExitCode] = {
val info: OpenRpcInfo = OpenRpcInfo("1.0.0", "Describe me!")
val document = OpenRpcDocsInterpreter().toOpenRpc(info, List(describeMeEndpoint.endpoint))

// Render the OpenRpcDocument in yaml and json.
// Please note the import used, that will allow to render the specification in different version of OpenAPI
for {
_ <- displayStepMessage("Json - Recent version of OpenAPI")
_ <- {
import io.iohk.armadillo.openrpc.circe._
IO.println(document.asJson)
}

_ <- displayStepMessage("Json - Version 3.0 of OpenAPI")
_ <- {
import CustomArmadilloOpenRpcCirce._
IO.println(document.asJson)
}

_ <- displayStepMessage("Yaml - Recent version of OpenAPI")
_ <- {
import io.iohk.armadillo.openrpc.circe._
IO.println(document.toYaml)
}

_ <- displayStepMessage("Yaml - Version 3.0 of OpenAPI")
_ <- {
import CustomArmadilloOpenRpcCirce._
IO.println(document.toYaml)
}
} yield ExitCode.Success
}

private def displayStepMessage(step: String): IO[Unit] = {
val separator = IO.println("_" * 50)
separator >> IO.println(step) >> separator
}
}
@@ -0,0 +1,75 @@
package io.iohk.armadillo.example

import cats.effect.IO
import io.circe.Json
import io.circe.generic.auto._
import io.circe.literal._
import io.iohk.armadillo._
import io.iohk.armadillo.json.circe._
import io.iohk.armadillo.server.ServerInterpreter
import io.iohk.armadillo.server.stub.ArmadilloStubInterpreter
import sttp.client3.HttpError
import sttp.client3.circe._
import sttp.client3.impl.cats.CatsMonadError
import sttp.client3.testing.SttpBackendStub
import sttp.model.StatusCode
import sttp.model.Uri._
import weaver.SimpleIOSuite

object ArmadilloStubInterpreterExample extends SimpleIOSuite {

private val stubInterpreter: ArmadilloStubInterpreter[IO, Json, Nothing] =
ArmadilloStubInterpreter(SttpBackendStub(new CatsMonadError()), new CirceJsonSupport)

private val describeMeEndpoint = jsonRpcEndpoint(m"describe_me")
.in(param[String]("name").and(param[Int]("age")))
.out[String]("description")
.errorOut(errorNoData)

private val stubbedResponse = "Hello John Doe, you are 42 years old"

private val InvalidParamsError: JsonRpcError[Unit] =
JsonRpcError(ServerInterpreter.InvalidParams.code, ServerInterpreter.InvalidParams.message, None)

test("should return a value") {
val backendStub = stubInterpreter
.whenEndpoint(describeMeEndpoint)
.assertInputs(("John Doe", 42))
.thenRespond(stubbedResponse)
.backend()

val expected = Right(JsonRpcResponse.v2(stubbedResponse, 1))

backendStub
.send(
sttp.client3.basicRequest
.post(uri"http://localhost:1234")
.body(JsonRpcRequest.v2("describe_me", json"""[ "John Doe", 42 ]""", 1))
.response(asJson[JsonRpcSuccessResponse[String]])
)
.map(r => expect.same(expected, r.body))
}

test("should fail because age is not provided") {
val backendStub = stubInterpreter
.whenEndpoint(describeMeEndpoint)
.thenRespondError(InvalidParamsError)
.backend()

val expected = Left(
HttpError(
JsonRpcErrorResponse("2.0", json"""{"code": -32602, "message": "Invalid params"}""", Some(1)),
StatusCode(400)
)
)

backendStub
.send(
sttp.client3.basicRequest
.post(uri"http://localhost:1234")
.body(JsonRpcRequest.v2("describe_me", json"""[ "John Doe" ]""", 1))
.response(asJsonEither[JsonRpcErrorResponse[Json], JsonRpcSuccessResponse[Json]])
)
.map(r => expect.same(expected, r.body))
}
}

0 comments on commit c7c938f

Please sign in to comment.