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.scalacontaining the HTTP api description using https://tapir.softwaremill.com/en/latest/CatsHttpEndpoints.scalawhich 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@d43d9b7
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]]