Typedapi

Define your API on the type-level

servant logo

https://github.com/haskell-servant/servant
google search
Servant to scala
myself

let's have an example


              // GET /users/:name?minAge=[age] -> List[User]

              final case class User(name: String, age: Int)
            

what we want


              (name: String, minAge: Int) => F[List[User]]
            

api as a type - path


              // type Path = "users".type - no can do

              val usersW = shapeless.Witness("users")

              sealed trait Path[P]

              type users = Path[usersW.T]
            

api as a type - segment


              val nameW = shapeless.Witness('name)

              sealed trait Segment[K, V]

              type name = Segment[nameW.T, String]
            

api as a type - query and method


              // query
              val minAgeW = shapeless.Witness('minAge)

              sealed trait Query[K, V]

              type filter = Query[minAgeW.T, Int]

              // method
              sealed trait Get[A]
            

api as a type - put it all together


              val Api = api(Get[List[User]],
                            Root / "users" / Segment[String]('name), 
                            Queries.add(Query[Int]('minAge)))

              //Api: ApiTypeCarrier[Get[List[User]] :: name :: users :: ... :: HNil]
            

let's derive a client - type representation

  • expected input: KIn <: HList and VIn <: HList
  • output: O
  • method: M
  • elements of our api: El <: HList

what we will have


              type KIn = nameW.T :: minAgeW.T :: HNil
              type VIn = String :: Int :: HNil
              type El  = usersW.T :: SegmentInput :: QueryInput :: HNil
              type M   = GetCall
              type O   = List[User]
            

fold over types


              trait FoldLeftFunction[In, Agg] { type Out }

              implicit def queryFold
                  [K <: Symbol, V, El <: HList, KIn <: HList, VIn <: HList, M, Out] = 
                FoldLeftFunction[Query[K, V], (El, KIn, VIn, M, Out)] { 
                  type Out = (QueryInput :: El, K :: KIn, V :: VIn, M, Out) 
                }

              // the rest ...
            

fold over types


              trait TypeLevelFoldLeft[H <: HList, Agg] { type Out }

              implicit def returnCase[Agg] = new TypeLevelFoldLeft[HNil, Agg] {
                type Out = Agg
              }

              implicit def foldCase[H, T <: HList, Agg, FfOut, FOut]
                (implicit f: FoldLeftFunction.Aux[H, Agg, FfOut], 
                          next: Lazy[TypeLevelFoldLeft.Aux[T, FfOut, FOut]]) = 
                new TypeLevelFoldLeft[H :: T, Agg] { type Out = FOut }
            

collect the request data


              type Uri     = List[String]
              type Queries = Map[String, List[String]]
 
              VIn => (Uri, Queries)
            

collect the request data


              trait RequestDataBuilder[El <: HList, KIn <: HList, VIn <: HList] {
                def apply(inputs: VIn, 
                          uri: Uri, 
                          queries: Queries): (Uri, Queries)
              }
            

collect the request data


              implicit def queryBuilder
                  [K <: Symbol, V, T <: HList, KIn <: HList, VIn <: HList, M, O]
                  (implicit wit: Witness.Aux[K], next: RequestDataBuilder[T, KIn, VIn]) = 
                new RequestDataBuilder[QueryInput :: T, K :: KIn, V :: VIn] {
                  def apply(inputs: V :: VIn, uri: Uri, queries: Queries): (Uri, Queries) =
                    next(
                      inputs.tail, 
                      uri, 
                      Map(wit.value.name -> List(inputs.head.toString())) ++ queries
                    )
                }
            

what we got so far


              "joe" :: 42 :: HNil => (List("users", "joe"), Map("minAge" -> List("42")))
            

do the request


              trait ApiRequest[M, F[_], C, Out] {

                def apply(data: (Uri, Queries), client: C): F[Out]
              }
            

wrap it up


              // transform -> request data -> request
              "joe" :: 42 :: HNil => IO(List(User("joe", 42), ...))
            

make it nice


              // shapeless.ops.function.FnFromProduct
              ("joe", 42) => IO(List(User("joe", 42), ...))
            

Typedapi - what you saw and more


              val Api = api(
                Get[List[User]], 
                Root / "users" / Segment[String]('name), 
                Queries add Query[Int]('minAge)
              )

              val get = derive(Api)

              get("joe", 42).run[IO](client)
            

Typedapi - what you saw and more


              val endpoints = derive[IO](Api).from((name, age) => ???)

              val server = mount(sm, endpoints)

              server.unsafeRunSync()
            

Typedapi - what you saw and more


              val AllTheAPis =
                api(Get[List[User]], Root / "users" / Segment[String]('name), ...) :|:
                apiWithBody(Put[Unit], User, Root / "store" / "users")

              val (get, put) = deriveAll(AllTheApis)
            

Typedapi - what you saw and more


              val ServantStyle = := "users" :> 
                                    Segment[String]('name) :> 
                                    Query[Int]('minAge) :> 
                                    Get[List[User]]
            

Typedapi - what you saw and more

Support for http4s

What's next

  • support for akka-http and finagle
  • derive swagger documentation from types

Questions?