Typed schema is an http service definition DSL, currently translating to akka-http Routes and OpenApi 3.0 definition inspired by the haskell-servant library.
libraryDependencies += "ru.tinkoff" %% "typed-schema" % "0.10.6"
We the People building services using modern scala often struggling to satisfy following requirements
- Service implementation should be checked to be compatible with OpenApi 3.0 specifications at the compile time
- Service definition should be detachable from the implementation and exportable as mere specification
- There should be an easy way to migrate all the services to different effect\future\task implementation without changing any definition
- There should be some way to migrate all the service to another framework without reimplementing them
import akka.actor.ActorSystem
import akka.http.scaladsl.Http
import akka.stream.{ActorMaterializer, Materializer}
import io.circe.Printer
import ru.tinkoff.tschema.akkaHttp.MkRoute
import io.circe.syntax._
import ru.tinkoff.tschema.swagger._
object ExampleDefinition {
import ru.tinkoff.tschema.syntax._
def api = get |> operation('hello) |> capture[String]('name) |> $$[String]
}
object ExampleSwagger {}
object Example extends App {
import ExampleDefinition.api
import akka.http.scaladsl.server.Directives._
// building service
object handler {
def hello(name: String): String = s"Hello, $name"
}
val apiRoute = MkRoute(api)(handler)
//building swagger
val apiSwagger = MkSwagger(api)(()).make(OpenApiInfo("example"))
val printer = Printer.spaces2.copy(dropNullValues = true)
val swaggerRoute = path("swagger")(complete(apiSwagger.asJson.pretty(printer)))
//typical akka http boilerplate
implicit val system: ActorSystem = ActorSystem("tschema-example")
implicit val materializer: Materializer = ActorMaterializer()
//run the server
Http().bindAndHandle(apiRoute ~ swaggerRoute, "localhost", 8080)
}
More examples see in subproject examples
Your schemes definitions consist of elements from ru.tinkoff.tschema.syntax._
and maybe custom directives
def api = get |> operation('hello) |> capture[String]('name) |> $$[String]
This may be read as a sequence:
- check HTTP method is GET
- check path prefix is "hello" and mark the following definition as part of
hello
operation - capture segment of uri path as
name
parameter - return String
Your definition could have some branching, a common prefix will be applied as the prefix of all branches.
Branching is done with the <>
operator:
def api =
get |> prefix('greeting) |> capture[String]('name) |> ((
operation('hello) |> $$[String]
) <> (
operation('aloha) |> queryParam[Int]('age) |> $$[String]
))
Note that now you must implement aloha
method in your handler
or compile error will be raised in the MkRoute
application
All definition elements are functions with almost no implementation, returning types from the
ru.tinkoff.tschema.typeDSL._
package, or created by yourself.
Those types are the definition.
All interpreters just traversing the tree type and building construction using information from types only
Type tree will consist of type constructors, subtypes of DSlAtom
at it branches, and DSLRes
at leafs.
Each DSLAtom
describes single step like parameter matching, filter or anything you'd like to do inside
When you are ready to build your source, you now can execute route building.
MkRoute(api)(handler)
will take the type of api
parameter and create corresponding tree.
using directive definitions given by implicit
instances of:
trait Serve.Aux[T, In]{type Out}
where:T
- your DSLAtom or DSLResIn
- input parameters collected by precedingServe
instances and tagged by namesOut
- parameters, that will be provided for subtree
trait ResultIn[In, Res, Out]
where:T
- your DSLResRes
- result type, returned by corresponding method in handlerOut
- result type, defined in the API definition
You generally will need following instances:
ToResponseMarshaller
for returning type of your methodFromEntityUnmarshaller
for type, used in yourbody
directiveFromParam
for any type in parameter directives likequeryParam
When you need to create OpenApi 3.0 object, you can just apply MkSwagger(api)(())
This will create SwaggerBuilder
object.
These builders could be concatenated with ++
to collect definitions from several modules
To successfully run you'll need following implicits:
SwaggerTypeable
for any mentioned typeSwaggerMapper
for any custom directive
When you final SwaggerBuilder
is ready, apply it's make
method supplying some top-level info,
this will create OpenApi
object that has the circe Encoder instance, which you can serve any way you want.