Introduction
Tapiro parses your Scala controllers to generate HTTP endpoints.
A Scala controller is a trait defined as follows:
import scala.annotation.StaticAnnotation
class path(val path: String) extends StaticAnnotation
class query extends StaticAnnotation
class command extends StaticAnnotation
case class Cat(name: String)
case class Error(msg: String)
@path("Cats")
trait Cats[F[_], AuthToken] {
@query //translate this to a GET
def findCutestCat(): F[Either[Error, Cat]]
@command //translate this to a POST
def doSomethingWithTheCat(catId: Int, token: AuthToken): F[Either[Error, Unit]]
}
For each controller tapiro generates two files:
CatsTapirEndpoints.scala
containing the HTTP api description using https://tapir.softwaremill.com/en/latest/CatsHttpEndpoints.scala
which injects the api logic defined in the controller into the tapir endpoints
Complete Example
Here you have an example implementation of the Cats
controller definied in the previous section:
import cats.effect._
object Cats {
def create[F[_]](implicit F: Sync[F]) = new Cats[F, String] {
override def findCutestCat(): F[Either[Error, Cat]] =
F.delay(Right(Cat("Cheshire")))
override def doSomethingWithTheCat(catId: Int, token: String): F[Either[Error, Unit]] =
F.delay(Right(()))
}
}
Here you have the autogenerated magic fromo tapiro (This is the content of CatsHttpEndpoints.scala
that will be autogenerated).
import org.http4s.HttpRoutes
// ---- begins autogenerated code
object CatsHttpEndpoints {
def routes(controller: Cats[IO, String]): HttpRoutes[IO] = ???
}
Here is how to run the server:
import org.http4s.blaze.server._
import org.http4s.implicits._
object Main extends IOApp {
val catsImpl = Cats.create[IO]
val routes = CatsHttpEndpoints.routes(catsImpl)
override def run(args: List[String]): IO[ExitCode] =
BlazeServerBuilder[IO]
.bindHttp(8080, "localhost")
.withHttpApp(routes.orNotFound)
.serve
.compile
.drain
.as(ExitCode.Success)
}
The resulting server can be queried as follows:
/GET /Cats/findCutestCat
/POST /Cats/doSomethingWithTheCat -d '{ "catId": 1 }'
Authentication
An Auth
type argument is expected in each controller and is added as authorization header.
trait Cats[F[_], Auth]
The actual implementation of the Auth
is left to the user. All tapiro requires is a proper tapir PlainCodec
such as:
import sttp.tapir._
import sttp.tapir.Codec._
case class CustomAuth(token: String)
def decodeAuth(s: String): DecodeResult[CustomAuth] = {
val TokenPattern = "Token token=(.+)".r
s match {
case TokenPattern(token) => DecodeResult.Value(CustomAuth(token))
case _ => DecodeResult.Error(s, new Exception("token not found"))
}
}
def encodeAuth(auth: CustomAuth): String = auth.token
implicit val authCodec: PlainCodec[CustomAuth] = Codec.string
.mapDecode(decodeAuth)(encodeAuth)
// authCodec: PlainCodec[CustomAuth] = sttp.tapir.Codec$$anon$1@ffbe470
The user will find the decoded token as the last argument of the method in the trait.
@command //translate this to a POST
def doSomethingWithTheCat(catId: Int, token: Auth): F[Either[Error, Unit]]