Repository: pheymann/typedapi Branch: master Commit: c13558a527f2 Files: 81 Total size: 247.9 KB Directory structure: gitextract_555nqngw/ ├── .gitignore ├── .travis.yml ├── CHANGELOG.md ├── LICENSE ├── README.md ├── akka-http-client/ │ └── src/ │ └── main/ │ └── scala/ │ └── typedapi/ │ └── client/ │ └── akkahttp/ │ └── package.scala ├── akka-http-server/ │ └── src/ │ └── main/ │ └── scala/ │ └── typedapi/ │ └── server/ │ └── akkahttp/ │ └── package.scala ├── ammonite-client-support/ │ └── src/ │ └── main/ │ └── scala/ │ └── typedapi/ │ └── client/ │ └── package.scala ├── build.sbt ├── client/ │ └── src/ │ ├── main/ │ │ └── scala/ │ │ └── typedapi/ │ │ └── client/ │ │ ├── ApiRequest.scala │ │ ├── ClientManager.scala │ │ ├── ExecutableDerivation.scala │ │ ├── ExecutablesFromHList.scala │ │ ├── FilterServerElements.scala │ │ ├── RequestDataBuilder.scala │ │ ├── package.scala │ │ └── test/ │ │ ├── RequestInput.scala │ │ └── package.scala │ └── test/ │ └── scala/ │ └── typedapi/ │ └── client/ │ ├── ClientManagerSpec.scala │ └── RequestDataBuilderSpec.scala ├── docs/ │ ├── ApiDefinition.md │ ├── ClientCreation.md │ ├── ExtendIt.md │ ├── ServerCreation.md │ └── example/ │ ├── ammonite_client_example.sc │ ├── build.sbt │ ├── client-js/ │ │ ├── index.html │ │ └── src/ │ │ └── main/ │ │ └── scala/ │ │ └── Client.scala │ ├── client-jvm/ │ │ └── src/ │ │ └── main/ │ │ └── scala/ │ │ └── Client.scala │ ├── project/ │ │ ├── build.properties │ │ └── plugins.sbt │ ├── server/ │ │ └── src/ │ │ └── main/ │ │ └── scala/ │ │ └── Server.scala │ └── shared/ │ └── src/ │ └── main/ │ └── scala/ │ ├── Apis.scala │ └── User.scala ├── http-support-tests/ │ └── src/ │ └── test/ │ └── scala/ │ └── http/ │ └── support/ │ └── tests/ │ ├── User.scala │ ├── client/ │ │ ├── AkkaHttpClientSupportSpec.scala │ │ ├── Http4sClientSupportSpec.scala │ │ ├── ScalajHttpClientSupportSpec.scala │ │ └── TestServer.scala │ ├── package.scala │ └── server/ │ ├── AkkaHttpServerSupportSpec.scala │ ├── Http4sServerSupportSpec.scala │ └── ServerSupportSpec.scala ├── http4s-client/ │ └── src/ │ └── main/ │ └── scala/ │ └── typedapi/ │ └── client/ │ └── http4s/ │ └── package.scala ├── http4s-server/ │ └── src/ │ └── main/ │ └── scala/ │ └── typedapi/ │ └── server/ │ └── htt4ps/ │ └── package.scala ├── js-client/ │ └── src/ │ └── main/ │ └── scala/ │ └── typedapi/ │ └── client/ │ └── js/ │ └── package.scala ├── project/ │ ├── build.properties │ ├── build.scala │ └── plugins.sbt ├── scalaj-http-client/ │ └── src/ │ └── main/ │ └── scala/ │ └── typedapi/ │ └── client/ │ └── scalajhttp/ │ └── package.scala ├── server/ │ └── src/ │ ├── main/ │ │ └── scala/ │ │ └── typedapi/ │ │ └── server/ │ │ ├── Endpoint.scala │ │ ├── EndpointComposition.scala │ │ ├── EndpointExecutor.scala │ │ ├── FilterClientElements.scala │ │ ├── RouteExtractor.scala │ │ ├── Serve.scala │ │ ├── ServeToList.scala │ │ ├── ServerHeaderExtractor.scala │ │ ├── ServerManager.scala │ │ ├── StatusCodes.scala │ │ └── package.scala │ └── test/ │ └── scala/ │ └── typedapi/ │ └── server/ │ ├── ApiToEndpointLinkSpec.scala │ ├── RouteExtractorSpec.scala │ └── ServeAndMountSpec.scala └── shared/ └── src/ ├── main/ │ └── scala/ │ └── typedapi/ │ ├── dsl/ │ │ ├── ApiDsl.scala │ │ └── package.scala │ ├── package.scala │ ├── shared/ │ │ ├── ApiElement.scala │ │ ├── ApiList.scala │ │ ├── ApiTransformer.scala │ │ ├── MediaTypes.scala │ │ ├── TypeCarrier.scala │ │ ├── TypeLevelFoldLeft.scala │ │ ├── WitnessToString.scala │ │ └── package.scala │ └── util/ │ ├── Decoder.scala │ └── Encoder.scala └── test/ └── scala/ └── typedapi/ ├── ApiDefinitionSpec.scala ├── SpecUtil.scala ├── dsl/ │ └── ApiDslSpec.scala └── shared/ └── ApiTransformerSpec.scala ================================================ FILE CONTENTS ================================================ ================================================ FILE: .gitignore ================================================ # ensime *.*~ .ensime .ensime_cache project/ensime-plugin.sbt # sbt target/ project/target/ project/project/ ================================================ FILE: .travis.yml ================================================ language: scala scala: - 2.11.11 - 2.12.3 jdk: - oraclejdk8 script: - sbt ++$TRAVIS_SCALA_VERSION clean test - sbt "project sharedJS" "fastOptJS" - sbt "project js-client" "fastOptJS" ================================================ FILE: CHANGELOG.md ================================================ ### 0.2.0 - `StatusCodes` and `MediaTypes` are in distinct `object`s, thus, have to be imported explicitly - improved Ammonite integration - internal: separated decoded and raw requests with `RawApiRequest` and `ApiRequest` - fixed `implicitNotFound` message for `ApiRequest` - implemented [#31](https://github.com/pheymann/typedapi/issues/31) [#35](https://github.com/pheymann/typedapi/issues/35) [#36](https://github.com/pheymann/typedapi/issues/36) - server endpoints have to define a response code: ```Scala derive[IO](Api) { input => success(/*...*/) ^ } ``` ### 0.1.0 - internal cleanups and refactorings - extended example project and added ScalaJS client - centralized http-support specs - added akka-http support on server and client-side - added scalaj-http support on the client-side - added ScalaJS compilation support for shared and client code - implemented basic ScalaJS client - added body encoding types and made them mandatory (several hundred Mediatypes supported) ```Scala := :> ReqBody[Json, User] :> Get[Json, User] _______________^__________________^ ``` - `RawHeaders` was removed - fixed headers were added; a fixed header is a statically known key-value pair, therefore, no input is required ```Scala // dsl := :> Header("Access-Control-Allow-Origin", "*") :> Get[Json, User] // function api(Get[Json, User], headers = Headers add("Access-Control-Allow-Origin", "*")) ``` - changes to the server API: - `NoReqBodyExecutor` and `ReqBodyExecutor` now expect a `MethodType`: ```Scala new NoReqBodyExecutor[El, KIn, VIn, M, F, FOut] { ____________________________________^ new ReqBodyExecutor[El, KIn, VIn, Bd, M, ROut, POut, F, FOut] { ______________________________________^ ``` - fixed header only sent by the server ```Scala := :> Server.Send("Access-Control-Allow-Origin", "*") :> Get[Json, User] api(Get[Json, User], Headers.serverSend("Access-Control-Allow-Origin", "*")) ``` - extract headers which have keys that match a `String` ```Scala := :> Server.Match[String]("Control") :> Get[Json, User] api(Get[Json, User], Headers.serverMatch[String]("Control")) ``` - changes to the client API: - new encoding types add `Content-Type` and `Accept` headers - fixed header only sent by the client ```Scala := :> Client.Header("Access-Control-Allow-Origin", "*") :> Get[Json, User] api(Get[Json, User], Headers.client("Access-Control-Allow-Origin", "*")) ``` - send dynamic header ignore it on the server-side ```Scala := :> Client.Header[String]("Foo") :> Get[Json, User] api(Get[Json, User], Headers.client[String]("Foo")) ``` ### 0.1.0-RC5 / Almost there - changes to the client API: ```Scala val ApiList = (:= :> Get[Foo]) :|: (:= :> RequestBody[Foo] :> Put[Foo]) // `:|:` removed for API compositions val (get, put) = deriveAll(ApiList) ``` - changes to the server API: ```Scala // same for endpoint compositions val e = deriveAll[IO](ApiList).from(get, put) ``` ### 0.1.0-RC4 / Towards a stable API - changes to the client API: ```Scala val Api = := :> Get[Foo] val ApiList = (:= :> Get[Foo]) :|: (:= :> RequestBody[Foo] :> Put[Foo]) // not `compile`, but val foo = derive(Api) val (foo2 :|: bar :|: =:) = deriveAll(ApiList) ... // explicitly pass ClientManager foo().run[IO](cm) _______________^ ``` - changes to the server API ```Scala // not `link.to`, but val endpoint = derive[IO](Api).from(...) val endpoints = deriveAll[IO](ApiList).from(...) ``` - major changes were applied to the internal code to reach a stable state (see [this PR](https://github.com/pheymann/typedapi/pull/13)) ================================================ FILE: LICENSE ================================================ MIT License Copyright (c) 2018 Paul Heymann Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ================================================ FILE: README.md ================================================ [![Build Status](https://travis-ci.org/pheymann/typedapi.svg?branch=master)](https://travis-ci.org/pheymann/typedapi) [![Maven Central](https://maven-badges.herokuapp.com/maven-central/com.github.pheymann/typedapi-client_2.12/badge.svg)](https://maven-badges.herokuapp.com/maven-central/com.github.pheymann/typedapi-shared_2.12) [![Gitter](https://badges.gitter.im/Join%20Chat.svg)](https://gitter.im/pheymann/Lobby) [![Scala.js](https://www.scala-js.org/assets/badges/scalajs-0.6.17.svg)](https://www.scala-js.org/) *experimental project*: see issues [#39](https://github.com/pheymann/typedapi/issues/39) and [#41](https://github.com/pheymann/typedapi/issues/41) # Typedapi Define type safe APIs and let the Scala compiler do the rest: ### Api definition ```Scala import typedapi._ val MyApi = // GET {body: User} /fetch/user?{name: String} api(method = Get[MT.`application/json`, User], path = Root / "fetch" / "user", queries = Queries add Query[String]('name)) :|: // POST {body: User} /create/user apiWithBody(method = Post[MT.`application/json`, User], body = ReqBody[Json, User], path = Root / "create" / "user") ``` And for the Servant lovers: ```Scala import typedapi.dsl._ val MyApi = // GET {body: User} /fetch/user?{name: String} (:= :> "fetch" :> "user" :> Query[String]('name) :> Get[MT.`application/json`, User]) :|: // POST {body: User} /create/user (:= :> "create" :> "user" :> ReqBody[Json, User] :> Post[MT.`application/json`, User]) ``` ### Client side ```Scala import typedapi.client._ val (fetch, create) = deriveAll(MyApi) import typedapi.client.http4s._; import cats.effect.IO; import org.http4s.client.blaze.Http1Client val cm = ClientManager(Http1Client[IO]().unsafeRunSync, "http://my-host", 8080) fetch("joe").run[IO](cm): IO[User] ``` ### Server side ```Scala import typedapi.server._ val fetch: String => IO[Result[User]] = name => findUserIO(name).map(success) val create: User => IO[Result[User]] = user => createUserIO(user).map(success) val endpoints = deriveAll[IO](MyApi).from(fetch, create) import typedapi.server.http4s._; import cats.effect.IO; import org.http4s.server.blaze.BlazeBuilder val sm = ServerManager(BlazeBuilder[IO], "http://my-host", 8080) val server = mount(sm, endpoints) server.unsafeRunSync() ``` This is all you have to do to define an API with multiple endpoints and to create a working client and server for them. You can find the above code as a complete project [here](https://github.com/pheymann/typedapi/tree/master/docs/example). ## Motivation This library is the result of the following questions: > How much can we encode on the type level? Are we able to describe a whole API and generate the call functions from that without using Macros? It is inspired by [Servant](https://github.com/haskell-servant/servant) and it provides an API layer which is independent of the underlying server/client implementation. Right now Typedapi supports: - [http4s](https://github.com/http4s/http4s) - [akka-http](https://github.com/akka/akka-http) - [scalaj-http](https://github.com/scalaj/scalaj-http) on the client-side - ScalaJS on the client-side If you need something else take a look at this [doc](https://github.com/pheymann/typedapi/blob/master/docs/ExtendIt.md#write-your-own-client-backend). ## Get this library It is available for Scala 2.11, 2.12 and ScalaJS and can be downloaded as Maven artifact: ``` // dsl "com.github.pheymann" %% "typedapi-client" % "com.github.pheymann" %% "typedapi-server" % // http4s support "com.github.pheymann" %% "typedapi-http4s-client" % "com.github.pheymann" %% "typedapi-http4s-server" % // akka-http support "com.github.pheymann" %% "typedapi-akka-http-client" % "com.github.pheymann" %% "typedapi-akka-http-server" % // Scalaj-Http client support "com.github.pheymann" %% "typedapi-scalaj-http-client" % // ScalaJS client support "com.github.pheymann" %% "typedapi-js-client" % ``` You can also build it on your machine: ``` git clone https://github.com/pheymann/typedapi.git cd typedapi sbt "+ publishLocal" ``` ## Ammonite Typedapi also offers an improved experience for [Ammonite](http://ammonite.io/#Ammonite-REPL) and [ScalaScripts](http://ammonite.io/#ScalaScripts): ```Scala import $ivy.`com.github.pheymann::typedapi-ammonite-client:` import typedapi._ import client._ import amm._ val Readme = api(Get[MT.`text/html`, String], Root / "pheymann" / "typedapi" / "master" / "README.md") val readme = derive(Readme) // gives you the raw scalaj-http response val cm = clientManager("https://raw.githubusercontent.com") val response = get().run[Id].raw(cm) response.body response.headers ... ``` In case Ammonite cannot resolve `com.dwijnand:sbt-compat:1.0.0`, follow [this](https://github.com/pheymann/typedapi/blob/master/docs/ClientCreation.md#ammonite) solution. ## Documentation The documentation is located in [docs](https://github.com/pheymann/typedapi/blob/master/docs) and covers the following topics so far: - [How to define an API](https://github.com/pheymann/typedapi/blob/master/docs/ApiDefinition.md) - [How to create a client](https://github.com/pheymann/typedapi/blob/master/docs/ClientCreation.md) - [How to create a server](https://github.com/pheymann/typedapi/blob/master/docs/ServerCreation.md) - [Extend the library](https://github.com/pheymann/typedapi/blob/master/docs/ExtendIt.md) - Typelevel Summit 2018 Berlin Talk [Typedapi: Define your API on the type-level](https://github.com/pheymann/typelevel-summit-2018) - and a [post](https://typelevel.org/blog/2018/06/15/typedapi.html) on the Typelevel Blog describing the basic concept behind this library. ## Dependencies - [shapeless 2.3.3](https://github.com/milessabin/shapeless/) ## Contribution Contributions are highly appreciated. If you find a bug or you are missing the support for a specific client/server library consider opening a PR with your solution. ================================================ FILE: akka-http-client/src/main/scala/typedapi/client/akkahttp/package.scala ================================================ package typedapi.client import akka.http.scaladsl.HttpExt import akka.http.scaladsl.model.RequestEntity import akka.http.scaladsl.model.headers.RawHeader import akka.http.scaladsl.model.{ HttpMethods, HttpRequest, HttpResponse, Uri } import akka.http.scaladsl.unmarshalling.{Unmarshal, FromEntityUnmarshaller} import akka.http.scaladsl.marshalling.{Marshal, ToEntityMarshaller} import akka.stream.Materializer import scala.concurrent.duration.FiniteDuration import scala.concurrent.{ ExecutionContext, Future } package object akkahttp { private def mkRequest(uri: String, queries: Map[String, List[String]], headers: Map[String, String]): HttpRequest = HttpRequest( uri = Uri(uri).withQuery(Uri.Query(queries.mapValues(_.mkString(",")))), headers = headers.map { case (key, value) => RawHeader(key, value) }(collection.breakOut) ) private def execRequest(client: HttpExt, request: HttpRequest, bodyConsumerTimeout: FiniteDuration) (implicit ec: ExecutionContext, mat: Materializer): Future[HttpResponse] = client.singleRequest(request) .flatMap { response => response.toStrict(bodyConsumerTimeout) } def rawGetRequest(bodyConsumerTimeout: FiniteDuration)(implicit ec: ExecutionContext, mat: Materializer) = new RawGetRequest[HttpExt, Future] { type Resp = HttpResponse def apply(uri: List[String], queries: Map[String, List[String]], headers: Map[String, String], cm: ClientManager[HttpExt]): Future[Resp] = { val request = mkRequest(deriveUriString(cm, uri), queries, headers).copy(HttpMethods.GET) execRequest(cm.client, request, bodyConsumerTimeout) } } implicit def rawGetRequestImpl(implicit bodyConsumerTimeout: FiniteDuration, ec: ExecutionContext, mat: Materializer): RawGetRequest[HttpExt, Future] = rawGetRequest(bodyConsumerTimeout) def getRequest[A](bodyConsumerTimeout: FiniteDuration)(implicit decoder: FromEntityUnmarshaller[A], ec: ExecutionContext, mat: Materializer) = new GetRequest[HttpExt, Future, A] { private val raw = rawGetRequest(bodyConsumerTimeout) def apply(uri: List[String], queries: Map[String, List[String]], headers: Map[String, String], cm: ClientManager[HttpExt]): Future[A] = raw(uri, queries, headers, cm).flatMap { response => Unmarshal(response.entity).to[A] } } implicit def getRequestImpl[A](implicit bodyConsumerTimeout: FiniteDuration, decoder: FromEntityUnmarshaller[A], ec: ExecutionContext, mat: Materializer): GetRequest[HttpExt, Future, A] = getRequest(bodyConsumerTimeout) def rawPutRequest(bodyConsumerTimeout: FiniteDuration)(implicit ec: ExecutionContext, mat: Materializer) = new RawPutRequest[HttpExt, Future] { type Resp = HttpResponse def apply(uri: List[String], queries: Map[String, List[String]], headers: Map[String, String], cm: ClientManager[HttpExt]): Future[Resp] = { val request = mkRequest(deriveUriString(cm, uri), queries, headers).copy(HttpMethods.PUT) execRequest(cm.client, request, bodyConsumerTimeout) } } implicit def rawPutRequestImpl(implicit bodyConsumerTimeout: FiniteDuration, ec: ExecutionContext, mat: Materializer): RawPutRequest[HttpExt, Future] = rawPutRequest(bodyConsumerTimeout) def putRequest[A](bodyConsumerTimeout: FiniteDuration)(implicit decoder: FromEntityUnmarshaller[A], ec: ExecutionContext, mat: Materializer) = new PutRequest[HttpExt, Future, A] { private val raw = rawPutRequest(bodyConsumerTimeout) def apply(uri: List[String], queries: Map[String, List[String]], headers: Map[String, String], cm: ClientManager[HttpExt]): Future[A] = raw(uri, queries, headers, cm).flatMap { response => Unmarshal(response.entity).to[A] } } implicit def putRequestImpl[A](implicit bodyConsumerTimeout: FiniteDuration, decoder: FromEntityUnmarshaller[A], ec: ExecutionContext, mat: Materializer): PutRequest[HttpExt, Future, A] = putRequest(bodyConsumerTimeout) def rawPutBodyRequest[Bd](bodyConsumerTimeout: FiniteDuration)(implicit encoder: ToEntityMarshaller[Bd], ec: ExecutionContext, mat: Materializer) = new RawPutWithBodyRequest[HttpExt, Future, Bd] { type Resp = HttpResponse def apply(uri: List[String], queries: Map[String, List[String]], headers: Map[String, String], body: Bd, cm: ClientManager[HttpExt]): Future[Resp] = { Marshal(body).to[RequestEntity].flatMap { marshalledBody => val request = mkRequest(deriveUriString(cm, uri), queries, headers - "Content-Type").copy(HttpMethods.PUT, entity = marshalledBody) execRequest(cm.client, request, bodyConsumerTimeout) } } } implicit def rawPutBodyRequestImpl[Bd](implicit bodyConsumerTimeout: FiniteDuration, encoder: ToEntityMarshaller[Bd], ec: ExecutionContext, mat: Materializer): RawPutWithBodyRequest[HttpExt, Future, Bd] = rawPutBodyRequest(bodyConsumerTimeout) def putBodyRequest[Bd, A](bodyConsumerTimeout: FiniteDuration)(implicit encoder: ToEntityMarshaller[Bd], decoder: FromEntityUnmarshaller[A], ec: ExecutionContext, mat: Materializer) = new PutWithBodyRequest[HttpExt, Future, Bd, A] { private val raw = rawPutBodyRequest(bodyConsumerTimeout) def apply(uri: List[String], queries: Map[String, List[String]], headers: Map[String, String], body: Bd, cm: ClientManager[HttpExt]): Future[A] = raw(uri, queries, headers, body, cm).flatMap { response => Unmarshal(response.entity).to[A] } } implicit def putBodyRequestImpl[Bd, A](implicit bodyConsumerTimeout: FiniteDuration, encoder: ToEntityMarshaller[Bd], decoder: FromEntityUnmarshaller[A], ec: ExecutionContext, mat: Materializer): PutWithBodyRequest[HttpExt, Future, Bd, A] = putBodyRequest(bodyConsumerTimeout) def rawPostRequest(bodyConsumerTimeout: FiniteDuration)(implicit ec: ExecutionContext, mat: Materializer) = new RawPostRequest[HttpExt, Future] { type Resp = HttpResponse def apply(uri: List[String], queries: Map[String, List[String]], headers: Map[String, String], cm: ClientManager[HttpExt]): Future[Resp] = { val request = mkRequest(deriveUriString(cm, uri), queries, headers).copy(HttpMethods.POST) execRequest(cm.client, request, bodyConsumerTimeout) } } implicit def rawPostRequestImpl(implicit bodyConsumerTimeout: FiniteDuration, ec: ExecutionContext, mat: Materializer): RawPostRequest[HttpExt, Future] = rawPostRequest(bodyConsumerTimeout) def postRequest[A](bodyConsumerTimeout: FiniteDuration)(implicit decoder: FromEntityUnmarshaller[A], ec: ExecutionContext, mat: Materializer) = new PostRequest[HttpExt, Future, A] { private val raw = rawPostRequest(bodyConsumerTimeout) def apply(uri: List[String], queries: Map[String, List[String]], headers: Map[String, String], cm: ClientManager[HttpExt]): Future[A] = raw(uri, queries, headers, cm).flatMap { response => Unmarshal(response.entity).to[A] } } implicit def postRequestImpl[A](implicit bodyConsumerTimeout: FiniteDuration, decoder: FromEntityUnmarshaller[A], ec: ExecutionContext, mat: Materializer): PostRequest[HttpExt, Future, A] = postRequest(bodyConsumerTimeout) def rawPostBodyRequest[Bd](bodyConsumerTimeout: FiniteDuration)(implicit encoder: ToEntityMarshaller[Bd], ec: ExecutionContext, mat: Materializer) = new RawPostWithBodyRequest[HttpExt, Future, Bd] { type Resp = HttpResponse def apply(uri: List[String], queries: Map[String, List[String]], headers: Map[String, String], body: Bd, cm: ClientManager[HttpExt]): Future[Resp] = { Marshal(body).to[RequestEntity].flatMap { marshalledBody => val request = mkRequest(deriveUriString(cm, uri), queries, headers - "Content-Type").copy(HttpMethods.POST, entity = marshalledBody) execRequest(cm.client, request, bodyConsumerTimeout) } } } implicit def rawPostBodyRequestImpl[Bd](implicit bodyConsumerTimeout: FiniteDuration, encoder: ToEntityMarshaller[Bd], ec: ExecutionContext, mat: Materializer): RawPostWithBodyRequest[HttpExt, Future, Bd] = rawPostBodyRequest(bodyConsumerTimeout) def postBodyRequest[Bd, A](bodyConsumerTimeout: FiniteDuration)(implicit encoder: ToEntityMarshaller[Bd], decoder: FromEntityUnmarshaller[A], ec: ExecutionContext, mat: Materializer) = new PostWithBodyRequest[HttpExt, Future, Bd, A] { private val raw = rawPostBodyRequest(bodyConsumerTimeout) def apply(uri: List[String], queries: Map[String, List[String]], headers: Map[String, String], body: Bd, cm: ClientManager[HttpExt]): Future[A] = raw(uri, queries, headers, body, cm).flatMap { response => Unmarshal(response.entity).to[A] } } implicit def postBodyRequestImpl[Bd, A](implicit bodyConsumerTimeout: FiniteDuration, encoder: ToEntityMarshaller[Bd], decoder: FromEntityUnmarshaller[A], ec: ExecutionContext, mat: Materializer): PostWithBodyRequest[HttpExt, Future, Bd, A] = postBodyRequest(bodyConsumerTimeout) def rawDeleteRequest(bodyConsumerTimeout: FiniteDuration)(implicit ec: ExecutionContext, mat: Materializer) = new RawDeleteRequest[HttpExt, Future] { type Resp = HttpResponse def apply(uri: List[String], queries: Map[String, List[String]], headers: Map[String, String], cm: ClientManager[HttpExt]): Future[Resp] = { val request = mkRequest(deriveUriString(cm, uri), queries, headers).copy(HttpMethods.DELETE) execRequest(cm.client, request, bodyConsumerTimeout) } } implicit def rawDeleteRequestImpl(implicit bodyConsumerTimeout: FiniteDuration, ec: ExecutionContext, mat: Materializer): RawDeleteRequest[HttpExt, Future] = rawDeleteRequest(bodyConsumerTimeout) def deleteRequest[A](bodyConsumerTimeout: FiniteDuration)(implicit decoder: FromEntityUnmarshaller[A], ec: ExecutionContext, mat: Materializer) = new DeleteRequest[HttpExt, Future, A] { private val raw = rawDeleteRequest(bodyConsumerTimeout) def apply(uri: List[String], queries: Map[String, List[String]], headers: Map[String, String], cm: ClientManager[HttpExt]): Future[A] = raw(uri, queries, headers, cm).flatMap { response => Unmarshal(response.entity).to[A] } } implicit def deleteRequestImpl[A](implicit bodyConsumerTimeout: FiniteDuration, decoder: FromEntityUnmarshaller[A], ec: ExecutionContext, mat: Materializer): DeleteRequest[HttpExt, Future, A] = deleteRequest(bodyConsumerTimeout) } ================================================ FILE: akka-http-server/src/main/scala/typedapi/server/akkahttp/package.scala ================================================ package typedapi.server import typedapi.shared.MethodType import shapeless._ import shapeless.ops.hlist.Prepend import akka.http.scaladsl.{HttpExt, Http} import akka.http.scaladsl.model._ import akka.http.scaladsl.unmarshalling.{Unmarshal, FromEntityUnmarshaller} import akka.http.scaladsl.marshalling.{Marshal, ToEntityMarshaller} import akka.stream.Materializer import akka.stream.scaladsl.Sink import scala.collection.mutable.Builder import scala.concurrent.{Future, ExecutionContext} import scala.annotation.tailrec package object akkahttp { case class AkkaHttpHeaderParseException(msg: String) extends Exception(msg) private def getHeaders(raw: Map[String, String]): List[HttpHeader] = raw.map { case (key, value) => HttpHeader.parse(key, value) match { case HttpHeader.ParsingResult.Ok(header, _) => header case HttpHeader.ParsingResult.Error(cause) => throw AkkaHttpHeaderParseException(cause.formatPretty) }}(collection.breakOut) implicit def noReqBodyExecutor[El <: HList, KIn <: HList, VIn <: HList, M <: MethodType, FOut](implicit encoder: ToEntityMarshaller[FOut], ec: ExecutionContext) = new NoReqBodyExecutor[El, KIn, VIn, M, Future, FOut] { type R = HttpRequest type Out = Future[HttpResponse] def apply(req: R, eReq: EndpointRequest, endpoint: Endpoint[El, KIn, VIn, M, VIn, Future, FOut]): Either[ExtractionError, Out] = { extract(eReq, endpoint).right.map { extracted => execute(extracted, endpoint).flatMap { case Right((code, response)) => Marshal(response).to[ResponseEntity].map { marshalledBody => HttpResponse(status = StatusCode.int2StatusCode(code.statusCode), entity = marshalledBody, headers = getHeaders(endpoint.headers)) } case Left(HttpError(code, msg)) => Future.successful(HttpResponse(status = StatusCode.int2StatusCode(code.statusCode), entity = msg, headers = getHeaders(endpoint.headers))) } } } } implicit def withReqBodyExecutor[El <: HList, KIn <: HList, VIn <: HList, Bd, M <: MethodType, ROut <: HList, POut <: HList, FOut] (implicit encoder: ToEntityMarshaller[FOut], decoder: FromEntityUnmarshaller[Bd], _prepend: Prepend.Aux[ROut, Bd :: HNil, POut], _eqProof: POut =:= VIn, mat: Materializer, ec: ExecutionContext) = new ReqBodyExecutor[El, KIn, VIn, Bd, M, ROut, POut, Future, FOut] { type R = HttpRequest type Out = Future[HttpResponse] implicit val prepend = _prepend implicit val eqProof = _eqProof def apply(req: R, eReq: EndpointRequest, endpoint: Endpoint[El, KIn, VIn, M, (BodyType[Bd], ROut), Future, FOut]): Either[ExtractionError, Out] = { extract(eReq, endpoint).right.map { case (_, extracted) => for { body <- Unmarshal(req.entity).to[Bd] response <- execute(extracted, body, endpoint).flatMap { case Right((code, response)) => Marshal(response).to[ResponseEntity].map { marshalledBody => HttpResponse(status = StatusCode.int2StatusCode(code.statusCode), entity = marshalledBody, headers = getHeaders(endpoint.headers)) } case Left(HttpError(code, msg)) => Future.successful(HttpResponse(status = StatusCode.int2StatusCode(code.statusCode), entity = msg, headers = getHeaders(endpoint.headers))) } } yield response } } } implicit def mountEndpoints(implicit mat: Materializer) = new MountEndpoints[HttpExt, HttpRequest, Future[HttpResponse]] { type Out = Future[Http.ServerBinding] def apply(server: ServerManager[HttpExt], endpoints: List[Serve[HttpRequest, Future[HttpResponse]]]): Out = { val service: HttpRequest => Future[HttpResponse] = request => { def execute(eps: List[Serve[HttpRequest, Future[HttpResponse]]], eReq: EndpointRequest): Future[HttpResponse] = eps match { case collection.immutable.::(endpoint, tail) => endpoint(request, eReq) match { case Right(response) => response case Left(RouteNotFound) => execute(tail, eReq) case Left(BadRouteRequest(msg)) => Future.successful(HttpResponse(400, entity = msg)) } case Nil => Future.successful(HttpResponse(404, entity = "uri = " + request.uri)) } @tailrec def toListPath(path: Uri.Path, agg: Builder[String, List[String]]): List[String] = path match { case Uri.Path.Slash(tail) => toListPath(tail, agg) case Uri.Path.Segment(p, tail) => toListPath(tail, agg += p) case Uri.Path.Empty => agg.result() } val eReq = EndpointRequest( request.method.name, toListPath(request.uri.path, List.newBuilder), request.uri.query().toMultiMap, request.headers.map(header => header.lowercaseName -> header.value)(collection.breakOut) ) if (request.method.name == "OPTIONS") { Future.successful(HttpResponse(headers = getHeaders(optionsHeaders(endpoints, eReq)))) } else execute(endpoints, eReq) } server.server.bind(server.host, server.port).to(Sink.foreach { connection => connection.handleWithAsyncHandler(service) }).run() } } } ================================================ FILE: ammonite-client-support/src/main/scala/typedapi/client/package.scala ================================================ package typedapi.client import typedapi.util._ import scalaj.http._ package object amm { type Id[A] = scalajhttp.Id[A] type Blocking[A] = scalajhttp.Blocking[A] def clientManager(host: String, port: Int): ClientManager[Http.type] = ClientManager(Http, host, port) def clientManager(host: String): ClientManager[Http.type] = ClientManager(Http, host) implicit def rawGetRequestAmm = scalajhttp.rawGetRequest implicit def getRequestAmm[A](implicit decoder: Decoder[Id, A]) = scalajhttp.getRequest[A] implicit def rawPutRequestAmm = scalajhttp.rawPutRequest implicit def putRequestAmm[A](implicit decoder: Decoder[Id, A]) = scalajhttp.putRequest[A] implicit def rawPutBodyRequestAmm[Bd](implicit encoder: Encoder[Id, Bd]) = scalajhttp.rawPutBodyRequest[Bd] implicit def putBodyRequestAmm[Bd, A](implicit encoder: Encoder[Id, Bd], decoder: Decoder[Id, A]) = scalajhttp.putBodyRequest[Bd, A] implicit def rawPostRequestAmm = scalajhttp.rawPostRequest implicit def postRequest[A](implicit decoder: Decoder[Id, A]) = scalajhttp.postRequest[A] implicit def rawPostBodyRequestAm[Bd](implicit encoder: Encoder[Id, Bd]) = scalajhttp.rawPostBodyRequest[Bd] implicit def postBodyRequestAmm[Bd, A](implicit encoder: Encoder[Id, Bd], decoder: Decoder[Id, A]) = scalajhttp.postBodyRequest[Bd, A] implicit def rawDeleteRequestAmm = scalajhttp.rawDeleteRequest implicit def deleteRequestAmm[A](implicit decoder: Decoder[Id, A]) = scalajhttp.deleteRequest[A] } ================================================ FILE: build.sbt ================================================ import sbt.Keys._ val `compiler-2.12` = Seq( "-deprecation", "-encoding", "utf-8", "-explaintypes", "-feature", "-unchecked", "-Xfatal-warnings", "-Xfuture", "-Xlint:inaccessible", "-Xlint:infer-any", "-Xlint:missing-interpolator", "-Xlint:option-implicit", "-Xlint:type-parameter-shadow", "-Xlint:unsound-match", "-Ywarn-dead-code", "-Ywarn-inaccessible", "-Ywarn-infer-any", "-Ywarn-numeric-widen", // "-Ywarn-unused:implicits", -> get errors for implicit evidence "-Ywarn-unused:imports", // "-Ywarn-unused:locals", "-Ywarn-unused:privates" ) val `compiler-2.11` = Seq( "-deprecation", "-encoding", "utf-8", "-explaintypes", "-feature", "-unchecked", "-Xfatal-warnings", "-Xfuture", "-Xlint:inaccessible", "-Xlint:infer-any", "-Xlint:missing-interpolator", "-Xlint:option-implicit", "-Xlint:type-parameter-shadow", "-Xlint:unsound-match", "-Ywarn-dead-code", "-Ywarn-numeric-widen", "-Ywarn-unused", "-Ywarn-inaccessible", "-Ywarn-infer-any" ) lazy val commonSettings = Seq( organization := "com.github.pheymann", version := "0.2.0", crossScalaVersions := Seq("2.11.11", "2.12.4"), scalaVersion := "2.12.4", scalacOptions ++= { CrossVersion.partialVersion(scalaVersion.value) match { case Some((2, 12)) => `compiler-2.12` case Some((2, 11)) => `compiler-2.11` case _ => Seq.empty[String] }}, publishTo := sonatypePublishTo.value ) lazy val mavenSettings = Seq( sonatypeProfileName := "pheymann", publishMavenStyle := true, pomExtra in Global := { https://github.com/pheymann/typedapi MIT https://github.com/pheymann/typedapi/blob/master/LICENSE scm:git:github.com/pheymann/typedapi scm:git:git@github.com:pheymann/typedapi github.com/pheymann/typedapi pheymann Paul Heymann https://github.com/pheymann } ) lazy val typedapi = project .in(file(".")) .settings(commonSettings: _*) .aggregate( `shared-js`, `shared-jvm`, `client-js`, `client-jvm`, server, `http4s-client`, `http4s-server`, `akka-http-client`, `akka-http-server`, `js-client`, `scalaj-http-client`, `http-support-tests`, `ammonite-client-support` ) lazy val shared = crossProject.crossType(CrossType.Pure) .in(file("shared")) .settings( commonSettings, mavenSettings, name := "typedapi-shared", libraryDependencies ++= Dependencies.shared ) lazy val `shared-js` = shared.js lazy val `shared-jvm` = shared.jvm lazy val client = crossProject.crossType(CrossType.Pure) .in(file("client")) .settings( commonSettings, mavenSettings, name := "typedapi-client", libraryDependencies ++= Dependencies.client ) .dependsOn(shared) lazy val `client-js` = client.js lazy val `client-jvm` = client.jvm lazy val server = project .in(file("server")) .settings( commonSettings, mavenSettings, name := "typedapi-server", libraryDependencies ++= Dependencies.server ) .dependsOn(`shared-jvm`) lazy val `http4s-client` = project .in(file("http4s-client")) .settings( commonSettings, mavenSettings, name := "typedapi-http4s-client", libraryDependencies ++= Dependencies.http4sClient, ) .dependsOn(`client-jvm`) lazy val `http4s-server` = project .in(file("http4s-server")) .settings( commonSettings, mavenSettings, name := "typedapi-http4s-server", libraryDependencies ++= Dependencies.http4sServer, ) .dependsOn(server) lazy val `akka-http-client` = project .in(file("akka-http-client")) .settings( commonSettings, mavenSettings, name := "typedapi-akka-http-client", libraryDependencies ++= Dependencies.akkaHttpClient ) .dependsOn(`client-jvm`) lazy val `akka-http-server` = project .in(file("akka-http-server")) .settings( commonSettings, mavenSettings, name := "typedapi-akka-http-server", libraryDependencies ++= Dependencies.akkaHttpServer ) .dependsOn(server) lazy val `js-client` = project .in(file("js-client")) .enablePlugins(ScalaJSPlugin) .settings( commonSettings, mavenSettings, name := "typedapi-js-client", libraryDependencies ++= Seq( "org.scala-js" %%% "scalajs-dom" % "0.9.6" % Compile ) ) .dependsOn(`client-js`) lazy val `scalaj-http-client` = project .in(file("scalaj-http-client")) .settings( commonSettings, mavenSettings, name := "typedapi-scalaj-http-client", libraryDependencies ++= Dependencies.scalajHttpClient ) .dependsOn(`client-jvm`) lazy val `http-support-tests` = project .in(file("http-support-tests")) .settings( commonSettings, parallelExecution in Test := false, libraryDependencies ++= Dependencies.httpSupportTests ) .dependsOn(`http4s-client`, `http4s-server`, `akka-http-client`, `akka-http-server`, `scalaj-http-client`) lazy val `ammonite-client-support` = project .in(file("ammonite-client-support")) .settings( commonSettings, mavenSettings, name := "typedapi-ammonite-client", libraryDependencies ++= Dependencies.ammoniteSupport ) .dependsOn(`scalaj-http-client`) ================================================ FILE: client/src/main/scala/typedapi/client/ApiRequest.scala ================================================ package typedapi.client import RequestDataBuilder.{Data, DataWithBody} import typedapi.shared._ import shapeless._ import scala.language.higherKinds import scala.annotation.implicitNotFound /** Basic api request element. Provides a function to create an effect representing the actual request. */ @implicitNotFound("""Cannot find RawApiRequest instance for ${M}. context: ${F}""") trait RawApiRequest[M <: MethodType, D <: HList, C, F[_]] { type Resp def apply(data: D, cm: ClientManager[C]): F[Resp] } @implicitNotFound("""Cannot find ApiRequest instance for ${M}. Do you miss some implicit value e.g. encoders/decoders? ouput: ${Out} context: ${F}""") trait ApiRequest[M <: MethodType, D <: HList, C, F[_], Out] { def apply(data: D, cm: ClientManager[C]): F[Out] } trait RawApiWithoutBodyRequest[M <: MethodType, C, F[_]] extends RawApiRequest[M, Data, C, F] { def apply(data: Data, cm: ClientManager[C]): F[Resp] = { val (uri :: queries :: headers :: HNil): Data = data apply(uri, queries, headers, cm) } def apply(uri: List[String], queries: Map[String, List[String]], headers: Map[String, String], cm: ClientManager[C]): F[Resp] } trait ApiWithoutBodyRequest[M <: MethodType, C, F[_], Out] extends ApiRequest[M, Data, C, F, Out] { def apply(data: Data, cm: ClientManager[C]): F[Out] = { val (uri :: queries :: headers :: HNil): Data = data apply(uri, queries, headers, cm) } def apply(uri: List[String], queries: Map[String, List[String]], headers: Map[String, String], cm: ClientManager[C]): F[Out] } trait RawApiWithBodyRequest[M <: MethodType, C, F[_], Bd] extends RawApiRequest[M, DataWithBody[Bd], C, F] { def apply(data: DataWithBody[Bd], cm: ClientManager[C]): F[Resp] = { val (uri :: queries :: headers :: body :: HNil): DataWithBody[Bd] = data apply(uri, queries, headers, body, cm) } def apply(uri: List[String], queries: Map[String, List[String]], headers: Map[String, String], body: Bd, cm: ClientManager[C]): F[Resp] } trait ApiWithBodyRequest[M <: MethodType, C, F[_], Bd, Out] extends ApiRequest[M, DataWithBody[Bd], C, F, Out] { def apply(data: DataWithBody[Bd], cm: ClientManager[C]): F[Out] = { val (uri :: queries :: headers :: body :: HNil): DataWithBody[Bd] = data apply(uri, queries, headers, body, cm) } def apply(uri: List[String], queries: Map[String, List[String]], headers: Map[String, String], body: Bd, cm: ClientManager[C]): F[Out] } trait RawGetRequest[C, F[_]] extends RawApiWithoutBodyRequest[GetCall, C, F] trait GetRequest[C, F[_], A] extends ApiWithoutBodyRequest[GetCall, C, F, A] trait RawPutRequest[C, F[_]] extends RawApiWithoutBodyRequest[PutCall, C, F] trait PutRequest[C, F[_], A] extends ApiWithoutBodyRequest[PutCall, C, F, A] trait RawPutWithBodyRequest[C, F[_], Bd] extends RawApiWithBodyRequest[PutWithBodyCall, C, F, Bd] trait PutWithBodyRequest[C, F[_], Bd, A] extends ApiWithBodyRequest[PutWithBodyCall, C, F, Bd, A] trait RawPostRequest[C, F[_]] extends RawApiWithoutBodyRequest[PostCall, C, F] trait PostRequest[C, F[_], A] extends ApiWithoutBodyRequest[PostCall, C, F, A] trait RawPostWithBodyRequest[C, F[_], Bd] extends RawApiWithBodyRequest[PostWithBodyCall, C, F, Bd] trait PostWithBodyRequest[C, F[_], Bd, A] extends ApiWithBodyRequest[PostWithBodyCall, C, F, Bd, A] trait RawDeleteRequest[C, F[_]] extends RawApiWithoutBodyRequest[DeleteCall, C, F] trait DeleteRequest[C, F[_], A] extends ApiWithoutBodyRequest[DeleteCall, C, F, A] ================================================ FILE: client/src/main/scala/typedapi/client/ClientManager.scala ================================================ package typedapi.client /** Provides a supported client instance and some basic configuration. */ final case class ClientManager[C](client: C, host: String, portO: Option[Int]) { val base = portO match { case Some(p) => s"$host:$p" case None => host } } object ClientManager { def apply[C](client: C, host: String, port: Int): ClientManager[C] = ClientManager(client, host, Some(port)) def apply[C](client: C, host: String): ClientManager[C] = ClientManager(client, host, None) } ================================================ FILE: client/src/main/scala/typedapi/client/ExecutableDerivation.scala ================================================ package typedapi.client import typedapi.shared.{MethodType, MediaType} import shapeless._ import shapeless.labelled.FieldType import scala.language.higherKinds /** Helper class to match the [[RequestDataBuilder]] with an [[ApiRequest]] instance. */ final class ExecutableDerivation[El <: HList, KIn <: HList, VIn <: HList, M <: MethodType, MT <: MediaType, O, D <: HList] (builder: RequestDataBuilder.Aux[El, KIn, VIn, M, FieldType[MT, O], D], input: VIn) { final class Derivation[F[_]] { def apply[C](cm: ClientManager[C])(implicit req: ApiRequest[M, D, C, F, O]): F[O] = { val data = builder(input, List.newBuilder, Map.empty, Map.empty) req(data, cm) } def raw[C](cm: ClientManager[C])(implicit req: RawApiRequest[M, D, C, F]): F[req.Resp] = { val data = builder(input, List.newBuilder, Map.empty, Map.empty) req(data, cm) } } def run[F[_]]: Derivation[F] = new Derivation[F] } ================================================ FILE: client/src/main/scala/typedapi/client/ExecutablesFromHList.scala ================================================ package typedapi.client import typedapi.shared.{MethodType, MediaType} import shapeless._ import shapeless.labelled.FieldType import shapeless.ops.function.FnFromProduct /** Derives executables from list of RequestBuilders. */ sealed trait ExecutablesFromHList[H <: HList] { type Out <: HList def apply(h: H): Out } object ExecutablesFromHList extends ExecutablesFromHListLowPrio { type Aux[H <: HList, Out0 <: HList] = ExecutablesFromHList[H] { type Out = Out0 } } trait ExecutablesFromHListLowPrio { implicit val noExecutable = new ExecutablesFromHList[HNil] { type Out = HNil def apply(h: HNil): Out = HNil } implicit def deriveExcutable[El <: HList, KIn <: HList, VIn <: HList, M <: MethodType, MT <: MediaType, O, D <: HList, T <: HList] (implicit next: ExecutablesFromHList[T], vinToFn: FnFromProduct[VIn => ExecutableDerivation[El, KIn, VIn, M, MT, O, D]]) = new ExecutablesFromHList[RequestDataBuilder.Aux[El, KIn, VIn, M, FieldType[MT, O], D] :: T] { type Out = vinToFn.Out :: next.Out def apply(comps: RequestDataBuilder.Aux[El, KIn, VIn, M, FieldType[MT, O], D] :: T): Out = { val fn = vinToFn.apply(input => new ExecutableDerivation[El, KIn, VIn, M, MT, O, D](comps.head, input)) fn :: next(comps.tail) } } } ================================================ FILE: client/src/main/scala/typedapi/client/FilterServerElements.scala ================================================ package typedapi.client import typedapi.shared._ import shapeless._ //TODO replace with Typelevelfoldleft sealed trait FilterServerElements[H <: HList] { type Out <: HList } sealed trait FilterServerElementsLowPrio { implicit val filterServerResult = new FilterServerElements[HNil] { type Out = HNil } implicit def filterServerKeep[El, T <: HList](implicit next: FilterServerElements[T]) = new FilterServerElements[El :: T] { type Out = El :: next.Out } } object FilterServerElements extends FilterServerElementsLowPrio { type Aux[H <: HList, Out0 <: HList] = FilterServerElements[H] { type Out = Out0 } implicit def filterServerHeaderMatch[K, V, T <: HList](implicit next: FilterServerElements[T]) = new FilterServerElements[ServerHeaderMatchParam[K, V] :: T] { type Out = next.Out } implicit def filterServerHeaderSend[K, V, T <: HList](implicit next: FilterServerElements[T]) = new FilterServerElements[ServerHeaderSendElement[K, V] :: T] { type Out = next.Out } } sealed trait FilterServerElementsList[H <: HList] { type Out <: HList } object FilterServerElementsList { type Aux[H <: HList, Out0 <: HList] = FilterServerElementsList[H] { type Out = Out0 } implicit val filterServerListResult = new FilterServerElementsList[HNil] { type Out = HNil } implicit def filterServerListStep[Api <: HList, T <: HList](implicit filtered: FilterServerElements[Api], next: FilterServerElementsList[T]) = new FilterServerElementsList[Api :: T] { type Out = filtered.Out :: next.Out } } ================================================ FILE: client/src/main/scala/typedapi/client/RequestDataBuilder.scala ================================================ package typedapi.client import typedapi.shared._ import shapeless._ import shapeless.labelled.FieldType import scala.collection.mutable.Builder import scala.annotation.implicitNotFound /** Compiles type level api description into a function returning request data (uri, query, header, body) and return-type `A` which are used for a request. */ @implicitNotFound("""Woops, you shouldn't be here. We cannot find the RequestDataBuilder. This seems to be a bug. elements: ${El} input keys: ${KIn} input values: ${VIn} method: ${M} expected result: ${O}""") trait RequestDataBuilder[El <: HList, KIn <: HList, VIn <: HList, M <: MethodType, O] { type Out <: HList def apply(inputs: VIn, uri: Builder[String, List[String]], queries: Map[String, List[String]], headers: Map[String, String]): Out } object RequestDataBuilder extends RequestDataBuilderMediumPrio { type Aux[El <: HList, KIn <: HList, VIn <: HList, M <: MethodType, O, Out0 <: HList] = RequestDataBuilder[El, KIn, VIn, M, O] { type Out = Out0 } } trait RequestDataBuilderLowPrio { implicit def pathCompiler[S, T <: HList, KIn <: HList, VIn <: HList, M <: MethodType, O](implicit wit: Witness.Aux[S], compiler: RequestDataBuilder[T, KIn, VIn, M, O]) = new RequestDataBuilder[S :: T, KIn, VIn, M, O] { type Out = compiler.Out def apply(inputs: VIn, uri: Builder[String, List[String]], queries: Map[String, List[String]], headers: Map[String, String]): Out = { compiler(inputs, uri += wit.value.toString(), queries, headers) } } implicit def segmentInputCompiler[T <: HList, K, V, KIn <: HList, VIn <: HList, M <: MethodType, O](implicit compiler: RequestDataBuilder[T, KIn, VIn, M, O]) = new RequestDataBuilder[SegmentInput :: T, K :: KIn, V :: VIn, M, O] { type Out = compiler.Out def apply(inputs: V :: VIn, uri: Builder[String, List[String]], queries: Map[String, List[String]], headers: Map[String, String]): Out = { val segValue = inputs.head compiler(inputs.tail, uri += segValue.toString(), queries, headers) } } implicit def queryInputCompiler[T <: HList, K, V, KIn <: HList, VIn <: HList, M <: MethodType, O] (implicit wit: Witness.Aux[K], show: WitnessToString[K], compiler: RequestDataBuilder[T, KIn, VIn, M, O]) = new RequestDataBuilder[QueryInput :: T, K :: KIn, V :: VIn, M, O] { type Out = compiler.Out def apply(inputs: V :: VIn, uri: Builder[String, List[String]], queries: Map[String, List[String]], headers: Map[String, String]): Out = { val queryName = show.show(wit.value) val queryValue = inputs.head compiler(inputs.tail, uri, Map((queryName, List(queryValue.toString()))) ++ queries, headers) } } implicit def headerInputCompiler[T <: HList, K, V, KIn <: HList, VIn <: HList, M <: MethodType, O] (implicit wit: Witness.Aux[K], show: WitnessToString[K], compiler: RequestDataBuilder[T, KIn, VIn, M, O]) = new RequestDataBuilder[HeaderInput :: T, K :: KIn, V :: VIn, M, O] { type Out = compiler.Out def apply(inputs: V :: VIn, uri: Builder[String, List[String]], queries: Map[String, List[String]], headers: Map[String, String]): Out = { val headerName = show.show(wit.value) val headerValue = inputs.head compiler(inputs.tail, uri, queries, Map((headerName, headerValue.toString())) ++ headers) } } implicit def fixedHeaderCompiler[K, V, T <: HList, KIn <: HList, VIn <: HList, M <: MethodType, O] (implicit kWit: Witness.Aux[K], vWit: Witness.Aux[V], kShow: WitnessToString[K], vShow: WitnessToString[V], compiler: RequestDataBuilder[T, KIn, VIn, M, O]) = new RequestDataBuilder[FixedHeader[K, V] :: T, KIn, VIn, M, O] { type Out = compiler.Out def apply(inputs: VIn, uri: Builder[String, List[String]], queries: Map[String, List[String]], headers: Map[String, String]): Out = { val key = kShow.show(kWit.value) val value = vShow.show(vWit.value) compiler(inputs, uri, queries, Map((key, value)) ++ headers) } } implicit def clientHeaderCompiler[K, V, T <: HList, KIn <: HList, VIn <: HList, M <: MethodType, O] (implicit kWit: Witness.Aux[K], vWit: Witness.Aux[V], kShow: WitnessToString[K], vShow: WitnessToString[V], compiler: RequestDataBuilder[T, KIn, VIn, M, O]) = new RequestDataBuilder[ClientHeader[K, V] :: T, KIn, VIn, M, O] { type Out = compiler.Out def apply(inputs: VIn, uri: Builder[String, List[String]], queries: Map[String, List[String]], headers: Map[String, String]): Out = { val key = kShow.show(kWit.value) val value = vShow.show(vWit.value) compiler(inputs, uri, queries, Map((key, value)) ++ headers) } } implicit def clientHeaderInputCompiler[K, V, T <: HList, KIn <: HList, VIn <: HList, M <: MethodType, O] (implicit kWit: Witness.Aux[K], kShow: WitnessToString[K], compiler: RequestDataBuilder[T, KIn, VIn, M, O]) = new RequestDataBuilder[ClientHeaderInput :: T, K :: KIn, V :: VIn, M, O] { type Out = compiler.Out def apply(inputs: V :: VIn, uri: Builder[String, List[String]], queries: Map[String, List[String]], headers: Map[String, String]): Out = { val key = kShow.show(kWit.value) val value = inputs.head.toString compiler(inputs.tail, uri, queries, Map((key, value)) ++ headers) } } implicit def clientHeaderCollInputCompiler[V, T <: HList, KIn <: HList, VIn <: HList, M <: MethodType, O] (implicit compiler: RequestDataBuilder[T, KIn, VIn, M, O]) = new RequestDataBuilder[ClientHeaderCollInput :: T, KIn, Map[String, V] :: VIn, M, O] { type Out = compiler.Out def apply(inputs: Map[String, V] :: VIn, uri: Builder[String, List[String]], queries: Map[String, List[String]], headers: Map[String, String]): Out = { val coll = inputs.head.mapValues(_.toString) compiler(inputs.tail, uri, queries, coll ++ headers) } } type Data = List[String] :: Map[String, List[String]] :: Map[String, String] :: HNil type DataWithBody[Bd] = List[String] :: Map[String, List[String]] :: Map[String, String] :: Bd :: HNil private def accept[MT <: MediaType](headers: Map[String, String], media: MT): Map[String, String] = Map(("Accept", media.value)) ++ headers implicit def getCompiler[MT <: MediaType, A](implicit media: Witness.Aux[MT]) = new RequestDataBuilder[HNil, HNil, HNil, GetCall, FieldType[MT, A]] { type Out = Data def apply(inputs: HNil, uri: Builder[String, List[String]], queries: Map[String, List[String]], headers: Map[String, String]): Out = { val out = uri.result() :: queries :: accept(headers, media.value) :: HNil out } } implicit def putCompiler[MT <: MediaType, A](implicit media: Witness.Aux[MT]) = new RequestDataBuilder[HNil, HNil, HNil, PutCall, FieldType[MT, A]] { type Out = Data def apply(inputs: HNil, uri: Builder[String, List[String]], queries: Map[String, List[String]], headers: Map[String, String]): Out = { val out = uri.result() :: queries :: accept(headers, media.value) :: HNil out } } implicit def putWithBodyCompiler[BMT <: MediaType, Bd, MT <: MediaType, A](implicit media: Witness.Aux[MT]) = new RequestDataBuilder[HNil, FieldType[BMT, BodyField.T] :: HNil, Bd :: HNil, PutWithBodyCall, FieldType[MT, A]] { type Out = DataWithBody[Bd] def apply(inputs: Bd :: HNil, uri: Builder[String, List[String]], queries: Map[String, List[String]], headers: Map[String, String]): Out = { val out = uri.result() :: queries :: accept(headers, media.value) :: inputs.head :: HNil out } } implicit def postCompiler[MT <: MediaType, A](implicit media: Witness.Aux[MT]) = new RequestDataBuilder[HNil, HNil, HNil, PostCall, FieldType[MT, A]] { type Out = Data def apply(inputs: HNil, uri: Builder[String, List[String]], queries: Map[String, List[String]], headers: Map[String, String]): Out = { val out = uri.result() :: queries :: accept(headers, media.value) :: HNil out } } implicit def postWithBodyCompiler[BMT <: MediaType, Bd, MT <: MediaType, A](implicit media: Witness.Aux[MT]) = new RequestDataBuilder[HNil, FieldType[BMT, BodyField.T] :: HNil, Bd :: HNil, PostWithBodyCall, FieldType[MT, A]] { type Out = DataWithBody[Bd] def apply(inputs: Bd :: HNil, uri: Builder[String, List[String]], queries: Map[String, List[String]], headers: Map[String, String]): Out = { val out = uri.result() :: queries :: accept(headers, media.value) :: inputs.head :: HNil out } } implicit def deleteCompiler[MT <: MediaType, A](implicit media: Witness.Aux[MT]) = new RequestDataBuilder[HNil, HNil, HNil, DeleteCall, FieldType[MT, A]] { type Out = Data def apply(inputs: HNil, uri: Builder[String, List[String]], queries: Map[String, List[String]], headers: Map[String, String]): Out = { val out = uri.result() :: queries :: accept(headers, media.value) :: HNil out } } } trait RequestDataBuilderMediumPrio extends RequestDataBuilderLowPrio { implicit def queryOptInputCompiler[T <: HList, K, V, KIn <: HList, VIn <: HList, M <: MethodType, O] (implicit wit: Witness.Aux[K], show: WitnessToString[K], compiler: RequestDataBuilder[T, KIn, VIn, M, O]) = new RequestDataBuilder[QueryInput :: T, K :: KIn, Option[V] :: VIn, M, O] { type Out = compiler.Out def apply(inputs: Option[V] :: VIn, uri: Builder[String, List[String]], queries: Map[String, List[String]], headers: Map[String, String]): Out = { val queryName = show.show(wit.value) val queryValue = inputs.head val updatedQueries = queryValue.fold(queries)(q => Map(queryName -> List(q.toString())) ++ queries) compiler(inputs.tail, uri, updatedQueries, headers) } } implicit def queryListInputCompiler[T <: HList, K, V, KIn <: HList, VIn <: HList, M <: MethodType, O] (implicit wit: Witness.Aux[K], show: WitnessToString[K], compiler: RequestDataBuilder[T, KIn, VIn, M, O]) = new RequestDataBuilder[QueryInput :: T, K :: KIn, List[V] :: VIn, M, O] { type Out = compiler.Out def apply(inputs: List[V] :: VIn, uri: Builder[String, List[String]], queries: Map[String, List[String]], headers: Map[String, String]): Out = { val queryName = show.show(wit.value) val queryValue = inputs.head if (queryValue.isEmpty) compiler(inputs.tail, uri, queries, headers) else compiler(inputs.tail, uri, Map((queryName, queryValue.map(_.toString()))) ++ queries, headers) } } implicit def headersOptInputCompiler[T <: HList, K, V, KIn <: HList, VIn <: HList, M <: MethodType, O] (implicit wit: Witness.Aux[K], show: WitnessToString[K], compiler: RequestDataBuilder[T, KIn, VIn, M, O]) = new RequestDataBuilder[HeaderInput :: T, K :: KIn, Option[V] :: VIn, M, O] { type Out = compiler.Out def apply(inputs: Option[V] :: VIn, uri: Builder[String, List[String]], queries: Map[String, List[String]], headers: Map[String, String]): Out = { val headerName = show.show(wit.value) val headerValue = inputs.head val updatedHeaders = headerValue.fold(headers)(h => Map(headerName -> h.toString()) ++ headers) compiler(inputs.tail, uri, queries, updatedHeaders) } } } @implicitNotFound("""Woops, you shouldn't be here. We cannot find the RequestDataBuilderList. This seems to be a bug. list: ${H}""") trait RequestDataBuilderList[H <: HList] { type Out <: HList def builders: Out } object RequestDataBuilderList extends RequestDataBuilderListLowPrio { type Aux[H <: HList, Out0 <: HList] = RequestDataBuilderList[H] { type Out = Out0 } } trait RequestDataBuilderListLowPrio { implicit def lastCompilerList[El <: HList, KIn <: HList, VIn <: HList, M <: MethodType, O, D <: HList](implicit builder: RequestDataBuilder.Aux[El, KIn, VIn, M, O, D]) = new RequestDataBuilderList[(El, KIn, VIn, M, O) :: HNil] { type Out = RequestDataBuilder.Aux[El, KIn, VIn, M, O, D] :: HNil val builders = builder :: HNil } implicit def builderList[El <: HList, KIn <: HList, VIn <: HList, M <: MethodType, O, D <: HList, T <: HList](implicit builder: RequestDataBuilder.Aux[El, KIn, VIn, M, O, D], next: RequestDataBuilderList[T]) = new RequestDataBuilderList[(El, KIn, VIn, M, O) :: T] { type Out = RequestDataBuilder.Aux[El, KIn, VIn, M, O, D] :: next.Out val builders = builder :: next.builders } } ================================================ FILE: client/src/main/scala/typedapi/client/package.scala ================================================ package typedapi import typedapi.shared._ import shapeless._ import shapeless.labelled.FieldType import shapeless.ops.hlist.Tupler import shapeless.ops.function.FnFromProduct package object client extends TypeLevelFoldLeftLowPrio with TypeLevelFoldLeftListLowPrio with WitnessToStringLowPrio with ApiTransformer { def deriveUriString(cm: ClientManager[_], uri: List[String]): String = cm.base + "/" + uri.mkString("/") def derive[H <: HList, FH <: HList, El <: HList, KIn <: HList, VIn <: HList, M <: MethodType, MT <: MediaType, Out, D <: HList] (apiList: ApiTypeCarrier[H]) (implicit filter: FilterServerElements.Aux[H, FH], folder: Lazy[TypeLevelFoldLeft.Aux[FH, Unit, (El, KIn, VIn, M, FieldType[MT, Out])]], builder: RequestDataBuilder.Aux[El, KIn, VIn, M, FieldType[MT, Out], D], inToFn: FnFromProduct[VIn => ExecutableDerivation[El, KIn, VIn, M, MT, Out, D]]): inToFn.Out = inToFn.apply(input => new ExecutableDerivation[El, KIn, VIn, M, MT, Out, D](builder, input)) def deriveAll[H <: HList, FH <: HList, In <: HList, Fold <: HList, B <: HList, Ex <: HList] (apiLists: CompositionCons[H]) (implicit filter: FilterServerElementsList.Aux[H, FH], folders: TypeLevelFoldLeftList.Aux[FH, Fold], builderList: RequestDataBuilderList.Aux[Fold, B], executables: ExecutablesFromHList.Aux[B, Ex], tupler: Tupler[Ex]): tupler.Out = executables(builderList.builders).tupled } ================================================ FILE: client/src/main/scala/typedapi/client/test/RequestInput.scala ================================================ package typedapi.client.test final case class ReqInput(method: String, uri: List[String], queries: Map[String, List[String]], headers: Map[String, String]) final case class ReqInputWithBody[Bd](method: String, uri: List[String], queries: Map[String, List[String]], headers: Map[String, String], body: Bd) ================================================ FILE: client/src/main/scala/typedapi/client/test/package.scala ================================================ package typedapi.client import scala.language.higherKinds package object test { type TestClientM = ClientManager[Unit] val clientManager: TestClientM = ClientManager((), "", 0) def testRawGet[F[_]](pure: ReqInput => F[ReqInput]) = new RawGetRequest[Unit, F] { type Resp = ReqInput def apply(uri: List[String], queries: Map[String, List[String]], headers: Map[String, String], cm: TestClientM): F[Resp] = pure(ReqInput("GET", uri, queries, headers)) } def testGet[F[_], A](f: ReqInput => F[A]) = new GetRequest[Unit, F, A] { def apply(uri: List[String], queries: Map[String, List[String]], headers: Map[String, String], cm: TestClientM): F[A] = f(ReqInput("GET", uri, queries, headers)) } def testRawPut[F[_]](pure: ReqInput => F[ReqInput]) = new RawPutRequest[Unit, F] { type Resp = ReqInput def apply(uri: List[String], queries: Map[String, List[String]], headers: Map[String, String], cm: TestClientM): F[Resp] = pure(ReqInput("PUT", uri, queries, headers)) } def testPut[F[_], A](f: ReqInput => F[A]) = new PutRequest[Unit, F, A] { def apply(uri: List[String], queries: Map[String, List[String]], headers: Map[String, String], cm: TestClientM): F[A] = f(ReqInput("PUT", uri, queries, headers)) } def testRawPutWithBody[F[_], Bd](pure: ReqInputWithBody[Bd] => F[ReqInputWithBody[Bd]]) = new RawPutWithBodyRequest[Unit, F, Bd] { type Resp = ReqInputWithBody[Bd] def apply(uri: List[String], queries: Map[String, List[String]], headers: Map[String, String], body: Bd, cm: TestClientM): F[Resp] = pure(ReqInputWithBody("PUT", uri, queries, headers, body)) } def testPutWithBody[F[_], Bd, A](f: ReqInputWithBody[Bd] => F[A]) = new PutWithBodyRequest[Unit, F, Bd, A] { def apply(uri: List[String], queries: Map[String, List[String]], headers: Map[String, String], body: Bd, cm: TestClientM): F[A] = f(ReqInputWithBody("PUT", uri, queries, headers, body)) } def testRawPost[F[_]](pure: ReqInput => F[ReqInput]) = new RawPostRequest[Unit, F] { type Resp = ReqInput def apply(uri: List[String], queries: Map[String, List[String]], headers: Map[String, String], cm: TestClientM): F[Resp] = pure(ReqInput("POST", uri, queries, headers)) } def testPost[F[_], A](f: ReqInput => F[A]) = new PostRequest[Unit, F, A] { def apply(uri: List[String], queries: Map[String, List[String]], headers: Map[String, String], cm: TestClientM): F[A] = f(ReqInput("POST", uri, queries, headers)) } def testRawPostWithBody[F[_], Bd](pure: ReqInputWithBody[Bd] => F[ReqInputWithBody[Bd]]) = new RawPostWithBodyRequest[Unit, F, Bd] { type Resp = ReqInputWithBody[Bd] def apply(uri: List[String], queries: Map[String, List[String]], headers: Map[String, String], body: Bd, cm: TestClientM): F[Resp] = pure(ReqInputWithBody("POST", uri, queries, headers, body)) } def testPostWithBody[F[_], Bd, A](f: ReqInputWithBody[Bd] => F[A]) = new PostWithBodyRequest[Unit, F, Bd, A] { def apply(uri: List[String], queries: Map[String, List[String]], headers: Map[String, String], body: Bd, cm: TestClientM): F[A] = f(ReqInputWithBody("POST", uri, queries, headers, body)) } def testRawDelete[F[_]](pure: ReqInput => F[ReqInput]) = new RawDeleteRequest[Unit, F] { type Resp = ReqInput def apply(uri: List[String], queries: Map[String, List[String]], headers: Map[String, String], cm: TestClientM): F[Resp] = pure(ReqInput("DELETE", uri, queries, headers)) } def testDelete[F[_], A](f: ReqInput => F[A]) = new DeleteRequest[Unit, F, A] { def apply(uri: List[String], queries: Map[String, List[String]], headers: Map[String, String], cm: TestClientM): F[A] = f(ReqInput("DELETE", uri, queries, headers)) } } ================================================ FILE: client/src/test/scala/typedapi/client/ClientManagerSpec.scala ================================================ package typedapi.client import org.specs2.mutable.Specification final class ClientManagerSpec extends Specification { "optional port definition" >> { ClientManager((), "my-host", 80).base === "my-host:80" ClientManager((), "my-host").base === "my-host" } } ================================================ FILE: client/src/test/scala/typedapi/client/RequestDataBuilderSpec.scala ================================================ package typedapi.client import typedapi.dsl._ import typedapi.client.test._ import shapeless.Id import org.specs2.mutable.Specification final class RequestDataBuilderSpec extends Specification { case class Foo() type Result = (String, List[String], Map[String, String], Map[String, String], Option[Foo]) implicit val get = testGet[Id, ReqInput](identity) implicit val put = testPut[Id, ReqInput](identity) implicit def putB[Bd] = testPutWithBody[Id, Bd, ReqInputWithBody[Bd]](identity) implicit val post = testPost[Id, ReqInput](identity) implicit def postB[Bd] = testPostWithBody[Id, Bd, ReqInputWithBody[Bd]](identity) implicit val delete = testDelete[Id, ReqInput](identity) "executes compiled api" >> { val cm = clientManager "single api" >> { "method" >> { val api0 = derive(:= :> Get[Json, ReqInput]) api0().run[Id](cm) === ReqInput("GET", Nil, Map(), Map(("Accept", "application/json"))) val api1 = derive(:= :> Put[Json, ReqInput]) api1().run[Id](cm) === ReqInput("PUT", Nil, Map(), Map(("Accept", "application/json"))) val api2 = derive(:= :> Post[Json, ReqInput]) api2().run[Id](cm) === ReqInput("POST", Nil, Map(), Map(("Accept", "application/json"))) val api3 = derive(:= :> Delete[Json, ReqInput]) api3().run[Id](cm) === ReqInput("DELETE", Nil, Map(), Map(("Accept", "application/json"))) } "segment" >> { val api0 = derive(:= :> Segment[Int]('i0) :> Get[Json, ReqInput]) api0(0).run[Id](cm) === ReqInput("GET", "0" :: Nil, Map(), Map(("Accept", "application/json"))) val api1 = derive(:= :> Segment[Int]('i0) :> Segment[Int]('i1) :> Get[Json, ReqInput]) api1(0, 1).run[Id](cm) === ReqInput("GET", "0" :: "1" :: Nil, Map(), Map(("Accept", "application/json"))) } "query" >> { val api0 = derive(:= :> Query[Int]('i0) :> Get[Json, ReqInput]) api0(0).run[Id](cm) === ReqInput("GET", Nil, Map("i0" -> List("0")), Map(("Accept", "application/json"))) val api1 = derive(:= :> Query[Int]('i0) :> Query[Int]('i1) :> Get[Json, ReqInput]) api1(0, 1).run[Id](cm) === ReqInput("GET", Nil, Map("i0" -> List("0"), "i1" -> List("1")), Map(("Accept", "application/json"))) val api2 = derive(:= :> Query[List[Int]]('i0) :> Get[Json, ReqInput]) api2(List(0, 1)).run[Id](cm) === ReqInput("GET", Nil, Map("i0" -> List("0", "1")), Map(("Accept", "application/json"))) api2(Nil).run[Id](cm) === ReqInput("GET", Nil, Map.empty, Map(("Accept", "application/json"))) val api3 = derive(:= :> Query[Option[Int]]('i0) :> Get[Json, ReqInput]) api3(Some(0)).run[Id](cm) === ReqInput("GET", Nil, Map("i0" -> List("0")), Map(("Accept", "application/json"))) api3(None).run[Id](cm) === ReqInput("GET", Nil, Map.empty, Map(("Accept", "application/json"))) } "header" >> { val api0 = derive(:= :> Header[Int]('i0) :> Get[Json, ReqInput]) api0(0).run[Id](cm) === ReqInput("GET", Nil, Map(), Map("Accept" -> "application/json", "i0" -> "0")) val api1 = derive(:= :> Header[Int]('i0) :> Header[Int]('i1) :> Get[Json, ReqInput]) api1(0, 1).run[Id](cm) === ReqInput("GET", Nil, Map(), Map("Accept" -> "application/json", "i0" -> "0", "i1" -> "1")) val api2 = derive(:= :> Header[Option[Int]]('i0) :> Get[Json, ReqInput]) api2(Some(0)).run[Id](cm) === ReqInput("GET", Nil, Map(), Map("Accept" -> "application/json", "i0" -> "0")) api2(None).run[Id](cm) === ReqInput("GET", Nil, Map(), Map("Accept" -> "application/json")) val api3 = derive(:= :> Header('i0, 'i1) :> Get[Json, ReqInput]) api3().run[Id](cm) === ReqInput("GET", Nil, Map(), Map("Accept" -> "application/json", "i0" -> "i1")) val api4 = derive(:= :> Client.Header[Int]('i0) :> Get[Json, ReqInput]) api4(0).run[Id](cm) === ReqInput("GET", Nil, Map(), Map("Accept" -> "application/json", "i0" -> "0")) val api5 = derive(:= :> Client.Header('i0, 'i1) :> Get[Json, ReqInput]) api5().run[Id](cm) === ReqInput("GET", Nil, Map(), Map("Accept" -> "application/json", "i0" -> "i1")) val api6 = derive(:= :> Client.Coll[Int] :> Get[Json, ReqInput]) api6(Map("hello" -> 5)).run[Id](cm) === ReqInput("GET", Nil, Map(), Map("Accept" -> "application/json", "hello" -> "5")) } "ignore server elements" >> { val api0 = derive(:= :> Server.Match[String]("Hello-") :> Server.Send("a", "b") :> Client.Header[Int]('i0) :> Get[Json, ReqInput]) api0(0).run[Id](cm) === ReqInput("GET", Nil, Map(), Map("Accept" -> "application/json", "i0" -> "0")) } "request body" >> { val api0 = derive(:= :> ReqBody[Json, Int] :> Put[Json, ReqInputWithBody[Int]]) api0(0).run[Id](cm) === ReqInputWithBody("PUT", Nil, Map(), Map(("Accept", "application/json")), 0) } "path" >> { val api0 = derive(:= :> "hello" :> "world" :> Get[Json, ReqInput]) api0().run[Id](cm) === ReqInput("GET", "hello" :: "world" :: Nil, Map(), Map(("Accept", "application/json"))) } } "raw" >> { implicit def rawPutB[Bd] = testRawPutWithBody[Id, Bd](identity) val api0 = derive(:= :> ReqBody[Json, Int] :> Put[Json, ReqInputWithBody[Int]]) api0(0).run[Id].raw(cm) === ReqInputWithBody("PUT", Nil, Map(), Map(("Accept", "application/json")), 0) } "composition" >> { val api = (:= :> "find" :> Server.Match[String]("Hello-") :> Server.Send("a", "b") :> Get[Json, ReqInput]) :|: (:= :> "fetch" :> Segment[String]('type) :> Get[Json, ReqInput]) :|: (:= :> "store" :> ReqBody[Json, Int] :> Post[Json, ReqInputWithBody[Int]]) val (find, fetch, store) = deriveAll(api) find().run[Id](cm) === ReqInput("GET", "find" :: Nil, Map(), Map(("Accept", "application/json"))) fetch("all").run[Id](cm) === ReqInput("GET", "fetch" :: "all" :: Nil, Map(), Map(("Accept", "application/json"))) store(0).run[Id](cm) === ReqInputWithBody("POST", "store" :: Nil, Map(), Map(("Accept", "application/json")), 0) } } } ================================================ FILE: docs/ApiDefinition.md ================================================ ## How to define an API The central idea behind Typedapi is to make client and server implementation as boilerplate-free, typesafe and simple as possible. - On the client-side you only define what you expect from an API provided by a server. In other words, you define a contract between the client and the server. - The server-side then has to comply with that contract by implementing proper endpoint functions. But how do you create this API definitions/contracts? This document will show you two ways provided by Typedapi: - use the DSL (`import typedapi.dsl._`) - or function-call-like definition (`import typedapi._`) ### Base case Every API has to fullfil the base case, meaning it has to have a root path and a method description: ```Scala // dsl := :> Get[MediaTypes.`application/json`, A] // or := :> Get[MT.`application/json`, A] // or in case of JSON := :> Get[Json, A] // function api(Get[Json, A]) // or api(method = Get[Json, A], path = Root) ``` This translates to `GET /` returning some `Json A`. ### Methods So far Typedapi supports the following methods: ```Scala // dsl := :> Get[Json, A] := :> Put[Json, A] := :> Post[Json, A] := :> Delete[Json, A] // function api(Get[Json, A]) api(Put[Json, A]) api(Post[Json, A]) api(Delete[Json, A]) ``` ### Request Body You may noticed that `Put` and `Post` don't have a field to describe a request body. To add that you have to explicitly define it with an element in your Api: ```Scala // PUT {body: User} / // dsl := :> ReqBody[Json, B] :> Put[Json, A] // function apiWithBody(Put[Json, A], ReqBody[Json, B]) // POST {body: User} / // dsl := :> ReqBody[Json, B] :> Post[Json, A] // function apiWithBody(Post[Json, A], ReqBody[Json, B]) ``` By the way, you can only add `Put` and `Post` as the next element of `ReqBody`. Everything else will not compile. Thus, you end up with a valid API description and not something like `:= :> ReqBody[Json, B] :> Get[Json, A]` or `api(Get[Json, A], ReqBody[Json, B])`. ### One word to encodings You can find a list of provided encodings [here](https://github.com/pheymann/typedapi/blob/master/shared/src/main/scala/typedapi/shared/MediaTypes.scala). If you need something else implement `trait MediaType`. ### Path ```Scala // GET /hello/world // dsl := :> "hello" :> "world" :> Get[Json, A] // function api(Get[Json, A], Root / "hello" / "world") ``` All path elements are translated to singleton types and therefore encoded in the type of the API. ### Segment Have a dynamic path element: ```Scala // GET /user/{name: String} // dsl := :> "user" :> Segment[String]("name") :> Get[Json, A] // function api(Get[Json, A], Root / "user" / Segment[String]("name")) ``` Every segment gets a name which is again encoded as singleton type in the API type. ### Query Parameter ```Scala // GET /query?{id: Int} // dsl := :> "query" :> Query[Int]("id") :> Get[Json, A] // function api(Get[Json, A], Root / "query", Queries.add[Int]("id")) ``` Every query gets a name which is again encoded as singleton type in the API type. #### Optional Query ```Scala // GET /query/opt?{id: Option[Int]} // dsl := :> "query" :> "opt" :> Query[Option[Int]]("id") :> Get[Json, A] // function api(Get[Json, A], Root / "query" / "opt", Queries.add[Option[Int]]("id")) ``` #### Query with a List of elements ```Scala // GET /query/list?{id: List[Int]} // dsl := :> "query" :> "list" :> Query[List[Int]]("id") :> Get[Json, A] // function api(Get[Json, A], Root / "query" / "list", Queries.add[List[Int]]("id")) ``` ### Header ```Scala // GET /header {headers: id: Int} // dsl := :> "header" :> Header[Int]("id") :> Get[Json, A] // function api(Get[Json, A], Root / "header", headers = Headers.add[Int]("id")) ``` This header is an expected input parameter. Every header gets a name which is again encoded as singleton type in the API type. #### Optional Header ```Scala // GET /header/opt {headers: id: Option[Int]} // dsl := :> "header" :> "opt" :> Header[Option[Int]]("id") :> Get[Json, A] // function api(Get[Json, A], Root / "header" / "opt", headers = Headers.add[Option[Int]]("id")) ``` #### Fixed Headers aka static headers If you have a set of headers which are statically known and have to be provided by all sides you can add them as follows: ```Scala // GET /header/fixed {headers: consumer=me} // dsl := :> "header" :> "fixed" :> Header("consumer", "me") :> Get[Json, A] // function api(Get[Json, A], Root / "header" / "fixed", headers = Headers.add("consumer", "me")) ``` #### Client-Side: Headers You have to send headers from the client-side but not server side? Here you go: ```Scala // GET /header/client {header: consumer: String} // dsl := :> "header" :> "client" :> Client.Header[String]("consumer") :> Get[Json, A] // function api(Get[Json, A], Root / "header" / "client", headers = Headers.client[String]("consumer")) ``` #### Client-Side: fixed/static Headers You have to send static headers from the client-side but not server side? Here you go: ```Scala // GET /header/client/fixed {header: consumer=me} // dsl := :> "header" :> "client" :> "fixed" :> Client.Header("consumer", "me") :> Get[Json, A] // function api(Get[Json, A], Root / "header" / "client" / "fixed", headers = Headers.client("consumer", "me")) ``` #### Client-Side: Header collections You can send header collections (`Map[String, V]`) as a single argument: ```Scala // GET /header/client/coll {headers: a:b:...} //dsl := :> "header" :> "client" :> "coll" :> Client.Coll[Int] :> Get[Json, A] //function api(Get[Json, A], Root / "header" / "client" / "coll", headers = Headers.clientColl[Int]) ``` #### Server-Side: send Headers You have to send headers from the server-side, e.g. for CORS? Here you go: ```Scala // GET /header/server/send => {header: consumer=me} // dsl := :> "header" :> "server" :> "send" :> Server.Send("consumer", "me") :> Get[Json, A] // function api(Get[Json, A], Root / "header" / "server" / "send", headers = Headers.serverSend("consumer", "me")) ``` #### Server-Side: extract matching Headers keys You want to extract all headers which contain a certain `String`? Here you go: ```Scala // GET /header/server/match {header: test1=me, test2=you} // dsl := :> "header" :> "server" :> "match" :> Server.Match[String]("test") :> Get[Json, A] // function api(Get[Json, A], Root / "header" / "server" / "match", headers = Headers.serverMatch[String]("test")) ``` This will give you a `Set[V]` with `V = String` in this example. ### Multiple definitions in a single API You can put multiple definitions into a single API element: ```Scala val Api = (:= :> "hello" :> Get[Json, A]) :|: (:= :> "world" :> Query[Int]('foo) :> Delete[B]) ``` ================================================ FILE: docs/ClientCreation.md ================================================ ## Create a client from your API After we [defined](https://github.com/pheymann/typedapi/blob/master/docs/ApiDefinition.md) our API we have to derive a function/set of functions we can use to make our calls. ```Scala val Api = (api(Get[Json, User], Root / "user" / Segment[String]("name"))) :|: (apiWithBody(Put[Json, User], ReqBody[Json, User], Root / "user")) ``` ### First thing first, derive your functions Lets derive our functions: ```Scala import typedapi.client._ final case class User(name: String) // implicit encoders and decoders val (get, create) = deriveAll(Api) ``` ### Http4s If you want to use [http4s](https://github.com/http4s/http4s) as your client backend you have to add the following code: ```Scala import typedapi.client.http4s._ import org.http4s.client.blaze.Http1Client val client = Http1Client[IO]().unsafeRunSync val cm = ClientManager(client, "http://my-host", myPort) ``` ### Akka-Http If you want to use [akka-http](https://github.com/akka/akka-http) as your client backend you have to add the following code: ```Scala import typedapi.client.akkahttp._ import akka.actor.ActorSystem import akka.stream.ActorMaterializer import akka.http.scaladsl.Http implicit val timeout = 5.second implicit val system = ActorSystem("akka-http-client") implicit val mat = ActorMaterializer() import system.dispatcher val cm = ClientManager(Http(), "http://my-host", myPort) ``` ### Scalaj-Http If you want to use [scalaj-http](https://github.com/scalaj/scalaj-http) as your client backend you have to add the following code: ```Scala import typedapi.client.scalajhttp._ import scalaj.http._ val cm = ClientManager(Http, "http://my-host", myPort) ``` Be aware that `typedapi.util` provides an `Encoder[F[_], A]` and `Decoder[F[_], A]` trait to marshall and unmarhsall bodies. You have to provide implementations for your types. ```Scala implicit val decoder = Decoder[Future, User] { json => // unmarshall the json using some known lib like circe } implicit val encoder = Encoder[Future, User] { user => // marshall the user using some known lib like circe } ``` ### Ammonite There is special support for [Ammonite](http://ammonite.io/#Ammonite-REPL) and [ScalaScripts](http://ammonite.io/#ScalaScripts). It lets you tinker with the raw response and reduces the amount of imports you have to do: ```Scala import $ivy.`com.github.pheymann::typedapi-ammonite-client:` import typedapi._ import client._ import amm._ val cm = clientManager("http://localhost", 9000) final case class User(name: String, age: Int) val Api = api(Get[Json, User], Root / "user" / "url") val get = derive(Api) // gives you the raw scalaj-http response val response = get().run[Id].raw(cm) response.body response.headers ... ``` No `Decoder` needed if you use `raw(cm)`. Under the covers it uses scalaj-http as a client library. It can be, that Ammonite isn't able to load `com.dwijnand:sbt-compat:1.0.0`. If that is the case execute the following command: ```Scala interp.repositories() ++= Seq(coursier.ivy.IvyRepository.fromPattern( "https://dl.bintray.com/dwijnand/sbt-plugins/" +: coursier.ivy.Pattern.default )) ``` ### ScalaJS If you want to compile to [ScalaJS](https://www.scala-js.org/) you have to use the [Ajax](https://github.com/scala-js/scala-js-dom/blob/master/src/main/scala/org/scalajs/dom/ext/Extensions.scala#L253) with: ```Scala import typedapi.client.js._ import org.scalajs.dom.ext.Ajax val cm = ClientManager(Ajax, "http://my-host", myPort) ``` Be aware that `typedapi.util` provides an `Encoder[F[_], A]` and `Decoder[F[_], A]` trait to marshall and unmarhsall bodies. You have to provide implementations for your types. ```Scala implicit val decoder = Decoder[Future, User] { json => // unmarshall the json using some known lib like circe } implicit val encoder = Encoder[Future, User] { user => // marshall the user using some known lib like circe } ``` ### Usage Now we can to use our client functions: ```Scala for { _ <- create(User("Joe", 42)).run[IO](cm) user <- get("Joe").run[IO](cm) } yield user //F[User] ``` **Make sure** you have the proper encoders and decoders in place. ================================================ FILE: docs/ExtendIt.md ================================================ ## Extend Typedapi to fit your needs You ended up in this file if: - the default implements for a HTTP framework doesn't fit your needs - if the framework you want to use is not supported - you need more specilized [RequestDataBuilder](https://github.com/pheymann/typedapi/blob/master/client/src/main/scala/typedapi/client/RequestDataBuilder.scala), [RouteExtractors](https://github.com/pheymann/typedapi/blob/master/server/src/main/scala/typedapi/server/RouteExtractor.scala), [MediaTypes](https://github.com/pheymann/typedapi/blob/master/shared/src/main/scala/typedapi/shared/ApiElement.scala#L58), or the like ### General remark I kept most of the type-classes open. That means you can override them as you like. If a certain class like *RequestDataBuilder* doesn't fullfil your needs just add another instance. Take a look at available implementations to get an idea how it works or ask a question in Gitter. ### Write your own Client backend To write your own client backend you have to implement the [ApiRequest](https://github.com/pheymann/typedapi/blob/master/client/src/main/scala/typedapi/client/ApiRequest.scala) type-classes: - `GetRequest` - `PutRequest` and `PutWithBodyRequest` - `PostRequest` and `PostWithBodyRequest` - `DeleteRequest` Take a look into [http4s-client](https://github.com/pheymann/typedapi/blob/master/http4s-client/src/main/scala/typedapi/client/http4s/package.scala) to get an idea how to do it. You can implement all type-classes or just a subset to override implementations provided by TypedApi. ### Write your own Server backend To write your own server backend you have to implement the [EndpointExecutor](https://github.com/pheymann/typedapi/blob/master/server/src/main/scala/typedapi/server/EndpointExecutor.scala) and [MountEndpoints](https://github.com/pheymann/typedapi/blob/master/server/src/main/scala/typedapi/server/ServerManager.scala) type-classes Take a look into [http4s-server](https://github.com/pheymann/typedapi/blob/master/http4s-server/src/main/scala/typedapi/server/http4s/package.scala) to get an idea how to do it. You can implement all type-classes or just a subset to override implementations provided by TypedApi. ================================================ FILE: docs/ServerCreation.md ================================================ ## Create a server from your API After we [defined](https://github.com/pheymann/typedapi/blob/master/docs/ApiDefinition.md) our API we have to derive the endpoint/set of endpoints we can mount and serve to the world. ```Scala val Api = (api(Get[Json, User], Root / "user" / Segment[String]("name"))) :|: (apiWithBody(Put[Json, User], ReqBody[Json, User], Root / "user")) ``` ### First things first, derive the endpoints ```Scala import typedapi.server._ final case class User(name: String) // implicit encoders and decoders val endpoints = deriveAll[IO](Api).from( name => // retrieve and return user user => // store user ) ``` ### Set Status Codes #### Success ```Scala deriveAll[IO](Api).from( name => val user: User = ??? success(user) // creates a 200 ... } ``` or ```Scala deriveAll[IO](Api).from( name => val user: User = ??? successWith(StatusCodes.Ok)(user) // set code ... } ``` #### Error ```Scala deriveAll[IO](Api).from( name => val user: Option[User] = ??? user.fold(errorWith(StatusCodes.NotFound, s"no user $id")(user => success(user)) ... } ``` ### Http4s If you want to use [http4s](https://github.com/http4s/http4s) as your server backend you have to add the following code: ```Scala import typedapi.server.http4s._ import org.http4s.server.blaze.BlazeBuilder val sm = ServerManager(BlazeBuilder[IO], "http://my-host", myPort) ``` ### Akka-Http If you want to use [akka-http](https://github.com/akka/akka-http) as your server backend you have to add the following code: ```Scala implicit val timeout = 5.second implicit val system = ActorSystem("akka-http-server") implicit val mat = ActorMaterializer() import system.dispatcher val sm = ServerManager(Http(), "http://my-host", myPort) ``` ### Start server Now we can mount `endpoints` and serve to to the world: ```Scala val server = mount(sm, endpoints) server.unsafeRunSync() ``` **Make sure** you have the proper encoders and decoders in place. ================================================ FILE: docs/example/ammonite_client_example.sc ================================================ import $ivy.`com.github.pheymann::typedapi-ammonite-client:0.2.0-M1` import typedapi._ import client._ import amm._ val cm = clientManager("http://localhost", 9000) final case class User(name: String, age: Int) val Api = api(Get[Json, User], Root, headers = Headers.serverSend("Access-Control-Allow-Origin", "*")) val get = derive(Api) val response = get().run[Id].raw(cm) ================================================ FILE: docs/example/build.sbt ================================================ val typedapiVersion = "0.2.0" val http4sVersion = "0.18.0" val commonSettings = Seq( scalaVersion := "2.12.4" ) lazy val root = project .in(file(".")) .aggregate(`shared-jvm`, `shared-js`, `client-jvm`, `client-js`, server) lazy val shared = crossProject.crossType(CrossType.Pure) .in(file("shared")) .settings(commonSettings: _*) .settings( libraryDependencies ++= Seq( "com.github.pheymann" %%% "typedapi-shared" % typedapiVersion, "io.circe" %%% "circe-core" % "0.9.1", "io.circe" %%% "circe-parser" % "0.9.1", "io.circe" %%% "circe-generic" % "0.9.1" ) ) lazy val `shared-js` = shared.js lazy val `shared-jvm` = shared.jvm lazy val server = project .in(file("server")) .settings(commonSettings: _*) .settings( libraryDependencies ++= Seq( "com.github.pheymann" %% "typedapi-http4s-server" % typedapiVersion, "org.http4s" %% "http4s-circe" % http4sVersion, "org.http4s" %% "http4s-blaze-server" % http4sVersion, "org.http4s" %% "http4s-dsl" % http4sVersion ) ) .dependsOn(`shared-jvm`) lazy val `client-jvm` = project .in(file("client-jvm")) .settings(commonSettings: _*) .settings( libraryDependencies ++= Seq( "com.github.pheymann" %% "typedapi-http4s-client" % typedapiVersion, "org.http4s" %% "http4s-circe" % http4sVersion, "org.http4s" %% "http4s-blaze-client" % http4sVersion, "org.http4s" %% "http4s-dsl" % http4sVersion ) ) .dependsOn(`shared-jvm`) lazy val `client-js` = project .in(file("client-js")) .enablePlugins(ScalaJSPlugin) .settings(commonSettings: _*) .settings( libraryDependencies ++= Seq( "com.github.pheymann" %%% "typedapi-js-client" % typedapiVersion, ), scalaJSUseMainModuleInitializer := true ) .dependsOn(`shared-js`) ================================================ FILE: docs/example/client-js/index.html ================================================ ================================================ FILE: docs/example/client-js/src/main/scala/Client.scala ================================================ import typedapi.client._ import typedapi.client.js._ import org.scalajs.dom.ext.Ajax import io.circe._ import io.circe.generic.auto._ import io.circe.parser._ import io.circe.syntax._ import scala.concurrent.Future import scala.concurrent.ExecutionContext.Implicits.global import scala.concurrent.duration._ object Client { type Id[A] = A final case class DecodeException(msg: String) extends Exception implicit val decoder = typedapi.util.Decoder[Future, User](json => decode[User](json).fold( error => Future.successful(Left(DecodeException(error.toString()))), user => Future.successful(Right(user)) )) implicit val encoder = typedapi.util.Encoder[Future, User](user => Future.successful(user.asJson.noSpaces)) val (get, put, post, delete, path, putBody, segment, search, header, fixed, client, coll, matches) = deriveAll(FromDsl.MyApi) def main(args: Array[String]): Unit = { val cm = ClientManager(Ajax, "http://localhost", 9000) (for { u0 <- putBody(User("joe", 27)).run[Future](cm) u1 <- search("joe").run[Future](cm) } yield (u0, u1)).foreach { case (u0, u1) => println(u0) println(u1) } } } ================================================ FILE: docs/example/client-jvm/src/main/scala/Client.scala ================================================ import typedapi.client._ import typedapi.client.http4s._ import cats.effect.IO import org.http4s._ import org.http4s.circe._ object Client { implicit val decoder = jsonOf[IO, User] implicit val encoder = jsonEncoderOf[IO, User] val (get, put, post, delete, path, putBody, segment, search, header, fixed, client, coll, matches) = deriveAll(FromDsl.MyApi) def main(args: Array[String]): Unit = { import User._ import cats.effect.IO import org.http4s.client.blaze.Http1Client val cm = ClientManager(Http1Client[IO]().unsafeRunSync, "http://localhost", 9000) (for { u0 <- putBody(User("joe", 27)).run[IO](cm) u1 <- search("joe").run[IO](cm) } yield { println(u0) println(u1) () }).unsafeRunSync() } } ================================================ FILE: docs/example/project/build.properties ================================================ sbt.version=1.0.4 ================================================ FILE: docs/example/project/plugins.sbt ================================================ addSbtPlugin("org.scala-js" % "sbt-scalajs" % "0.6.22") ================================================ FILE: docs/example/server/src/main/scala/Server.scala ================================================ import typedapi.server._ import typedapi.server.http4s._ import cats.effect.IO import org.http4s._ import org.http4s.circe._ object Server { implicit val decoder = jsonOf[IO, User] implicit val encoder = jsonEncoderOf[IO, User] val get: () => IO[Result[User]] = () => IO.pure(success(User("Joe", 42))) val put: () => IO[Result[User]] = get val post: () => IO[Result[User]] = get val delete: () => IO[Result[User]] = get val path: () => IO[Result[User]] = get val putBody: User => IO[Result[User]] = user => IO.pure(success(user)) val segment: String => IO[Result[User]] = name => IO.pure(success(User(name, 42))) val search: String => IO[Result[User]] = segment val header: String => IO[Result[User]] = consumer => IO.pure(success(User("found: " + consumer, 42))) val fixed: () => IO[Result[User]] = get val client: () => IO[Result[User]] = get val coll: () => IO[Result[User]] = get val matching: Map[String, String] => IO[Result[User]] = matches => IO.pure(success(User(matches.mkString(","), 42))) val endpoints = deriveAll[IO](FromDefinition.MyApi).from( get, put, post, delete, path, putBody, segment, search, header, fixed, client, coll, matching ) def main(args: Array[String]): Unit = { import org.http4s.server.blaze.BlazeBuilder val sm = ServerManager(BlazeBuilder[IO], "localhost", 9000) mount(sm, endpoints).unsafeRunSync() scala.io.StdIn.readLine("Press 'Enter' to stop ...") } } ================================================ FILE: docs/example/shared/src/main/scala/Apis.scala ================================================ object FromDsl { import typedapi.dsl._ /* NOTE: we have to add the 'Access-Control-Allow-Origin' header to the server-side to allow the * browser (ScalaJS) to access the server (CORS) */ val MyApi = // basic GET request (:= :> Server.Send("Access-Control-Allow-Origin", "*") :> Get[Json, User]) :|: // basic PUT request (:= :> Server.Send("Access-Control-Allow-Origin", "*") :> Put[Json, User]) :|: // basic POST request (:= :> Server.Send("Access-Control-Allow-Origin", "*") :> Post[Json, User]) :|: // basic DELETE request (:= :> Server.Send("Access-Control-Allow-Origin", "*") :> Delete[Json, User]) :|: // define a path (:= :> "my" :> "path" :> Server.Send("Access-Control-Allow-Origin", "*") :> Get[Json, User]) :|: // add a request body (:= :> "with" :> "body" :> Server.Send("Access-Control-Allow-Origin", "*") :> ReqBody[Json, User] :> Put[Json, User]) :|: // add segments (:= :> "name" :> Segment[String]("name") :> Server.Send("Access-Control-Allow-Origin", "*") :> Get[Json, User]) :|: // add query (:= :> "search" :> "user" :> Query[String]("name") :> Server.Send("Access-Control-Allow-Origin", "*") :> Get[Json, User]) :|: // add header (:= :> "header" :> Header[String]("consumer") :> Server.Send("Access-Control-Allow-Origin", "*") :> Get[Json, User]) :|: (:= :> "header" :> "fixed" :> Header("consumer", "me") :> Server.Send("Access-Control-Allow-Origin", "*") :> Get[Json, User]) :|: (:= :> "header" :> "client" :> Client.Header("client", "me") :> Server.Send("Access-Control-Allow-Origin", "*") :> Get[Json, User]) :|: (:= :> "header" :> "client" :> "coll" :> Client.Coll[Int] :> Server.Send("Access-Control-Allow-Origin", "*") :> Get[Json, User]) :|: (:= :> "header" :> "server" :> Server.Match[String]("Control-") :> Server.Send("Access-Control-Allow-Origin", "*") :> Get[Json, User]) } object FromDefinition { import typedapi._ val MyApi = // basic GET request api(Get[Json, User], headers = Headers.serverSend("Access-Control-Allow-Origin", "*")) :|: // basic PUT request api(Put[Json, User], headers = Headers.serverSend("Access-Control-Allow-Origin", "*")) :|: // basic POST request api(Post[Json, User], headers = Headers.serverSend("Access-Control-Allow-Origin", "*")) :|: // basic Delete request api(Delete[Json, User], headers = Headers.serverSend("Access-Control-Allow-Origin", "*")) :|: // define a path api(Get[Json, User], Root / "my" / "path", headers = Headers.serverSend("Access-Control-Allow-Origin", "*")) :|: // add a request body apiWithBody(Put[Json, User], ReqBody[Json, User], Root / "with" / "body", headers = Headers.serverSend("Access-Control-Allow-Origin", "*")) :|: // add segments api(Get[Json, User], Root / "name" / Segment[String]("name"), headers = Headers.serverSend("Access-Control-Allow-Origin", "*")) :|: // add query api(Get[Json, User], Root / "search" / "user", Queries.add[String]("name"), Headers.serverSend("Access-Control-Allow-Origin", "*")) :|: // add header api(Get[Json, User], Root / "header", headers = Headers.add[String]("consumer").serverSend("Access-Control-Allow-Origin", "*")) :|: api(Get[Json, User], Root / "header" / "fixed", headers = Headers.add("consumer", "me").serverSend("Access-Control-Allow-Origin", "*")) :|: api(Get[Json, User], Root / "header" / "client", headers = Headers.client("client", "me").serverSend("Access-Control-Allow-Origin", "*")) :|: api(Get[Json, User], Root / "header" / "client" / "coll", headers = Headers.clientColl[Int].serverSend("Access-Control-Allow-Origin", "*")) :|: api(Get[Json, User], Root / "header" / "server", headers = Headers.serverMatch[String]("Control-").serverSend("Access-Control-Allow-Origin", "*")) } ================================================ FILE: docs/example/shared/src/main/scala/User.scala ================================================ import io.circe.syntax._ import io.circe.generic.JsonCodec final case class User(name: String, age: Int) object User { implicit val enc = io.circe.generic.semiauto.deriveEncoder[User] implicit val dec = io.circe.generic.semiauto.deriveDecoder[User] } ================================================ FILE: http-support-tests/src/test/scala/http/support/tests/User.scala ================================================ package http.support.tests import cats.effect.IO import io.circe.generic.semiauto._ import org.http4s.circe._ import org.http4s.dsl.io._ final case class User(name: String, age: Int) object UserCoding { implicit val enc = deriveEncoder[User] implicit val dec = deriveDecoder[User] implicit val decoderIO = jsonOf[IO, User] implicit val encoderIO = jsonEncoderOf[IO, User] } ================================================ FILE: http-support-tests/src/test/scala/http/support/tests/client/AkkaHttpClientSupportSpec.scala ================================================ package http.support.tests.client import http.support.tests.{User, UserCoding, Api} import typedapi.client._ import typedapi.client.akkahttp._ import akka.actor.ActorSystem import akka.stream.ActorMaterializer import akka.http.scaladsl.Http import de.heikoseeberger.akkahttpcirce.FailFastCirceSupport import org.specs2.mutable.Specification import org.specs2.concurrent.ExecutionEnv import scala.concurrent.duration._ import scala.concurrent.{Future, Await} final class AkkaHttpClientSupportSpec(implicit ee: ExecutionEnv) extends Specification { import UserCoding._ import FailFastCirceSupport._ sequential implicit val timeout = 5.second implicit val system = ActorSystem("akka-http-client-spec", defaultExecutionContext = Some(ee.ec)) implicit val mat = ActorMaterializer() import system.dispatcher val cm = ClientManager(Http(), "http://localhost", 9001) val server = TestServer.start() "akka http client support" >> { val (p, s, q, header, fixed, clInH, clFixH, clColl, serMatchH, serSendH, m0, m1, m2, m3, m4, m5, _, _, _) = deriveAll(Api) "paths and segments" >> { p().run[Future](cm) must beEqualTo(User("foo", 27)).awaitFor(timeout) s("jim").run[Future](cm) must beEqualTo(User("jim", 27)).awaitFor(timeout) } "queries" >> { q(42).run[Future](cm) must beEqualTo(User("foo", 42)).awaitFor(timeout) } "headers" >> { header(42).run[Future](cm) must beEqualTo(User("foo", 42)).awaitFor(timeout) fixed().run[Future](cm) must beEqualTo(User("joe", 27)).awaitFor(timeout) clInH("jim").run[Future](cm) must beEqualTo(User("jim", 27)).awaitFor(timeout) clFixH().run[Future](cm) must beEqualTo(User("joe", 27)).awaitFor(timeout) clColl(Map("coll" -> "joe", "collect" -> "jim")).run[Future](cm) must beEqualTo(User("coll: joe,collect: jim", 27)).awaitFor(timeout) serMatchH().run[Future](cm) must beEqualTo(User("joe", 27)).awaitFor(timeout) serSendH().run[Future](cm) must beEqualTo(User("joe", 27)).awaitFor(timeout) } "methods" >> { m0().run[Future](cm) must beEqualTo(User("foo", 27)).awaitFor(timeout) m1().run[Future](cm) must beEqualTo(User("foo", 27)).awaitFor(timeout) m2(User("jim", 42)).run[Future](cm) must beEqualTo(User("jim", 42)).awaitFor(timeout) m3().run[Future](cm) must beEqualTo(User("foo", 27)).awaitFor(timeout) m4(User("jim", 42)).run[Future](cm) must beEqualTo(User("jim", 42)).awaitFor(timeout) m5(List("because")).run[Future](cm) must beEqualTo(User("foo", 27)).awaitFor(timeout) } step { server.shutdown.unsafeRunSync() Await.ready(system.terminate, timeout) } } } ================================================ FILE: http-support-tests/src/test/scala/http/support/tests/client/Http4sClientSupportSpec.scala ================================================ package http.support.tests.client import http.support.tests.{UserCoding, User, Api} import typedapi.client._ import typedapi.client.http4s._ import cats.effect.IO import org.http4s.client.blaze.Http1Client import org.specs2.mutable.Specification final class Http4sClientSupportSpec extends Specification { import UserCoding._ sequential val cm = ClientManager(Http1Client[IO]().unsafeRunSync, "http://localhost", 9001) val server = TestServer.start() "http4s client support" >> { val (p, s, q, header, fixed, clInH, clFixH, clColl, serMatchH, serSendH, m0, m1, m2, m3, m4, m5, _, _, _) = deriveAll(Api) "paths and segments" >> { p().run[IO](cm).unsafeRunSync() === User("foo", 27) s("jim").run[IO](cm).unsafeRunSync() === User("jim", 27) } "queries" >> { q(42).run[IO](cm).unsafeRunSync() === User("foo", 42) } "headers" >> { header(42).run[IO](cm).unsafeRunSync() === User("foo", 42) fixed().run[IO](cm).unsafeRunSync() === User("joe", 27) clInH("jim").run[IO](cm).unsafeRunSync === User("jim", 27) clFixH().run[IO](cm).unsafeRunSync() === User("joe", 27) clColl(Map("coll" -> "joe", "collect" -> "jim")).run[IO](cm).unsafeRunSync === User("coll: joe,collect: jim", 27) serMatchH().run[IO](cm).unsafeRunSync() === User("joe", 27) serSendH().run[IO](cm).unsafeRunSync() === User("joe", 27) } "methods" >> { m0().run[IO](cm).unsafeRunSync() === User("foo", 27) m1().run[IO](cm).unsafeRunSync() === User("foo", 27) m2(User("jim", 42)).run[IO](cm).unsafeRunSync() === User("jim", 42) m3().run[IO](cm).unsafeRunSync() === User("foo", 27) m4(User("jim", 42)).run[IO](cm).unsafeRunSync() === User("jim", 42) m5(List("because")).run[IO](cm).unsafeRunSync() === User("foo", 27) } step { server.shutdown.unsafeRunSync() } } } ================================================ FILE: http-support-tests/src/test/scala/http/support/tests/client/ScalajHttpClientSupportSpec.scala ================================================ package http.support.tests.client import http.support.tests.{UserCoding, User, Api} import typedapi.client._ import typedapi.client.scalajhttp._ import scalaj.http.Http import io.circe.parser._ import io.circe.syntax._ import org.specs2.mutable.Specification final class ScalajHttpClientSupportSpec extends Specification { import UserCoding._ sequential case class DecodeException(msg: String) extends Exception implicit val decoder = typedapi.util.Decoder[Id, User](json => decode[User](json).fold( error => Left(DecodeException(error.toString())), user => Right(user) )) implicit val encoder = typedapi.util.Encoder[Id, User](user => user.asJson.noSpaces) val cm = ClientManager(Http, "http://localhost", 9001) val server = TestServer.start() "http4s client support" >> { val (p, s, q, header, fixed, clInH, clFixH, clColl, serMatchH, serSendH, m0, m1, m2, m3, m4, m5, _, _, _) = deriveAll(Api) "paths and segments" >> { p().run[Blocking](cm) === Right(User("foo", 27)) s("jim").run[Blocking](cm) === Right(User("jim", 27)) } "queries" >> { q(42).run[Blocking](cm) === Right(User("foo", 42)) } "headers" >> { header(42).run[Blocking](cm) === Right(User("foo", 42)) fixed().run[Blocking](cm) === Right(User("joe", 27)) clInH("jim").run[Blocking](cm) === Right(User("jim", 27)) clFixH().run[Blocking](cm) === Right(User("joe", 27)) clColl(Map("coll" -> "joe", "collect" -> "jim")).run[Blocking](cm) === Right(User("collect: jim,coll: joe", 27)) serMatchH().run[Blocking](cm) === Right(User("joe", 27)) serSendH().run[Blocking](cm) === Right(User("joe", 27)) } "methods" >> { m0().run[Blocking](cm) === Right(User("foo", 27)) m1().run[Blocking](cm) === Right(User("foo", 27)) m2(User("jim", 42)).run[Blocking](cm) === Right(User("jim", 42)) m3().run[Blocking](cm) === Right(User("foo", 27)) m4(User("jim", 42)).run[Blocking](cm) === Right(User("jim", 42)) m5(List("because")).run[Blocking](cm) === Right(User("foo", 27)) } "raw" >> { m0().run[Id].raw(cm).body === """{"name":"foo","age":27}""" } step { server.shutdown.unsafeRunSync() } } } ================================================ FILE: http-support-tests/src/test/scala/http/support/tests/client/TestServer.scala ================================================ package http.support.tests.client import http.support.tests.{User, UserCoding} import cats.effect.IO import io.circe.syntax._ import org.http4s._ import org.http4s.circe._ import org.http4s.dsl.io._ import org.http4s.server.Server import org.http4s.server.blaze._ object Age extends QueryParamDecoderMatcher[Int]("age") object Reasons { def unapplySeq(params: Map[String, Seq[String]]) = params.get("reasons") def unapply(params: Map[String, Seq[String]]) = unapplySeq(params) } object TestServer { import UserCoding._ val service = HttpService[IO] { case GET -> Root / "path" => Ok(User("foo", 27)) case GET -> Root / "segment" / name => Ok(User(name, 27)) case GET -> Root / "query" :? Age(age) => Ok(User("foo", age)) case req @ GET -> Root / "header" => val headers = req.headers.toList val age = headers.find(_.name.value == "age").get.value.toInt Ok(User("foo", age)) case req @ GET -> Root / "header" / "fixed" => val headers = req.headers.toList val value = headers.find(_.name.value == "Hello").get.value if (value != "*") throw new IllegalArgumentException("unexpected header value " + value) Ok(User("joe", 27)) case req @ GET -> Root / "header" / "client" => val headers = req.headers.toList val value = headers.find(_.name.value == "Hello").get.value if (value != "*") throw new IllegalArgumentException("unexpected header value " + value) Ok(User("joe", 27)) case req @ GET -> Root / "header" / "client" / "coll" => val headers = req.headers.toList val values = headers.filter(_.name.value.contains("coll")) if (values.isEmpty) throw new IllegalArgumentException("no header collection ") Ok(User(values.mkString(","), 27)) case req @ GET -> Root / "header" / "input" / "client" => val headers = req.headers.toList val value = headers.find(_.name.value == "Hello").get.value Ok(User(value, 27)) case req @ GET -> Root / "header" / "server" / "send" => Ok(User("joe", 27)).map(resp => resp.copy(headers = resp.headers put Header("Hello", "*"))) case req @ GET -> Root / "header" / "server" / "match" => Ok(User("joe", 27)) case GET -> Root => Ok(User("foo", 27)) case PUT -> Root => Ok(User("foo", 27)) case req @ PUT -> Root / "body" => Ok(User("foo", 27)) for { user <- req.as[User] resp <- Ok(user.asJson) } yield resp case POST -> Root => Ok(User("foo", 27)) case req @ POST -> Root / "body" => Ok(User("foo", 27)) for { user <- req.as[User] resp <- Ok(user.asJson) } yield resp case DELETE -> Root :? Reasons(reasons) => println(reasons) Ok(User("foo", 27)) } def start(): Server[IO] = { val builder = BlazeBuilder[IO] .bindHttp(9001, "localhost") .mountService(service, "/") .start builder.unsafeRunSync() } } ================================================ FILE: http-support-tests/src/test/scala/http/support/tests/package.scala ================================================ package http.support import typedapi.dsl._ package object tests { val Api = (:= :> "path" :> Get[Json, User]) :|: (:= :> "segment" :> Segment[String]('name) :> Get[Json, User]) :|: (:= :> "query" :> Query[Int]('age) :> Get[Json, User]) :|: (:= :> "header" :> Header[Int]('age) :> Get[Json, User]) :|: (:= :> "header" :> "fixed" :> Header("Hello", "*") :> Get[Json, User]) :|: (:= :> "header" :> "input" :> "client" :> Client.Header[String]("Hello") :> Get[Json, User]) :|: (:= :> "header" :> "client" :> Client.Header("Hello", "*") :> Get[Json, User]) :|: (:= :> "header" :> "client" :> "coll" :> Client.Coll[String] :> Get[Json, User]) :|: (:= :> "header" :> "server" :> "match" :> Server.Match[String]("test") :> Get[Json, User]) :|: (:= :> "header" :> "server" :> "send" :> Server.Send("Hello", "*") :> Get[Json, User]) :|: (:= :> Get[Json, User]) :|: (:= :> Put[Json, User]) :|: (:= :> "body" :> ReqBody[Json, User] :> Put[Json, User]) :|: (:= :> Post[Json, User]) :|: (:= :> "body" :> ReqBody[Json, User] :> Post[Json, User]) :|: (:= :> Query[List[String]]('reasons) :> Delete[Json, User]) :|: (:= :> "status" :> "200" :> Get[Plain, String]) :|: (:= :> "status" :> "400" :> Get[Plain, String]) :|: (:= :> "status" :> "500" :> Get[Plain, String]) } ================================================ FILE: http-support-tests/src/test/scala/http/support/tests/server/AkkaHttpServerSupportSpec.scala ================================================ package http.support.tests.server import http.support.tests.{Api, UserCoding} import typedapi.server._ import typedapi.server.akkahttp._ import akka.actor.ActorSystem import akka.stream.ActorMaterializer import akka.http.scaladsl.Http import de.heikoseeberger.akkahttpcirce.FailFastCirceSupport import cats.implicits._ import org.specs2.concurrent.ExecutionEnv import scala.concurrent.{Future, Await} import scala.concurrent.duration._ final class AkkaHttpServerSupportSpec(implicit ee: ExecutionEnv) extends ServerSupportSpec[Future]()(catsStdInstancesForFuture(ee.ec)) { import UserCoding._ import FailFastCirceSupport._ implicit val timeout = 5.second implicit val system = ActorSystem("akka-http-server-spec", defaultExecutionContext = Some(ee.ec)) implicit val mat = ActorMaterializer() import system.dispatcher val endpoints = deriveAll[Future](Api).from( path, segment, query, header, fixed, input, clientHdr, coll, matching, send, get, put, putB, post, postB, delete, code200, code400, code500 ) val sm = ServerManager(Http(), "localhost", 9000) val server = mount(sm, endpoints) "akka http implements TypedApi's server interface" >> { tests(9000) step { Await.ready(server.map(_.unbind()), timeout) Await.ready(system.terminate, timeout) } } } ================================================ FILE: http-support-tests/src/test/scala/http/support/tests/server/Http4sServerSupportSpec.scala ================================================ package http.support.tests.server import http.support.tests.{UserCoding, Api} import typedapi.server._ import typedapi.server.http4s._ import cats.effect.IO import org.http4s.server.blaze.BlazeBuilder final class Http4sServerSupportSpec extends ServerSupportSpec[IO] { import UserCoding._ val endpoints = deriveAll[IO](Api).from( path, segment, query, header, fixed, input, clientHdr, coll, matching, send, get, put, putB, post, postB, delete, code200, code400, code500 ) val sm = ServerManager(BlazeBuilder[IO], "localhost", 9000) val server = mount(sm, endpoints).unsafeRunSync() "http4s implements TypedApi's server interface" >> { tests(9000) step { server.shutdown.unsafeRunSync() } } } ================================================ FILE: http-support-tests/src/test/scala/http/support/tests/server/ServerSupportSpec.scala ================================================ package http.support.tests.server import typedapi.server._ import http.support.tests.{User, UserCoding} import org.http4s._ import org.http4s.dsl.io._ import org.http4s.client.blaze._ import org.http4s.client.dsl.io._ import cats.Applicative import cats.effect.IO import org.specs2.mutable.Specification import scala.language.higherKinds abstract class ServerSupportSpec[F[_]: Applicative] extends Specification { import StatusCodes._ sequential val client = Http1Client[IO]().unsafeRunSync def tests(port: Int) = { import UserCoding._ "paths and segments" >> { client.expect[User](s"http://localhost:$port/path").unsafeRunSync() === User("path", 27) client.expect[User](s"http://localhost:$port/segment/jim").unsafeRunSync() === User("jim", 27) } "queries" >> { client.expect[User](s"http://localhost:$port/query?age=42").unsafeRunSync() === User("query", 42) } "headers" >> { client.expect[User](Request[IO]( method = GET, uri = Uri.fromString(s"http://localhost:$port/header").right.get, headers = Headers(Header("age", "42")) )).unsafeRunSync() === User("header", 42) client.expect[User](Request[IO]( method = GET, uri = Uri.fromString(s"http://localhost:$port/header/fixed").right.get, headers = Headers(Header("Hello", "*")) )).unsafeRunSync() === User("fixed", 27) client.expect[User](Request[IO]( method = GET, uri = Uri.fromString(s"http://localhost:$port/header/client").right.get )).unsafeRunSync() === User("client header", 27) client.expect[User](Request[IO]( method = GET, uri = Uri.fromString(s"http://localhost:$port/header/input/client").right.get )).unsafeRunSync() === User("input", 27) client.fetch[Option[Header]]( Request[IO]( method = GET, uri = Uri.fromString(s"http://localhost:$port/header/server/send").right.get ) )( resp => IO { resp.headers.toList.find(_.name.toString == "Hello") } ).unsafeRunSync() === Some(Header("Hello", "*")) client.expect[User](Request[IO]( method = GET, uri = Uri.fromString(s"http://localhost:$port/header/server/match").right.get, headers = Headers(Header("test", "foo"), Header("testy", "bar"), Header("meh", "NONO")) )).unsafeRunSync() === User("test -> foo,testy -> bar", 27) client.fetch[Option[Header]]( Request[IO]( method = OPTIONS, uri = Uri.fromString(s"http://localhost:$port/header/fixed").right.get, headers = Headers(Header("Hello", "*")) ) )( resp => IO { resp.headers.toList.find(_.name.toString == "Access-Control-Allow-Methods") } ).unsafeRunSync() === Some(Header("Access-Control-Allow-Methods", "GET")) } "methods" >> { client.expect[User](s"http://localhost:$port/").unsafeRunSync() === User("get", 27) client.expect[User](PUT(Uri.fromString(s"http://localhost:$port/").right.get)).unsafeRunSync() === User("put", 27) client.expect[User](PUT(Uri.fromString(s"http://localhost:$port/body").right.get, User("joe", 27))).unsafeRunSync() === User("joe", 27) client.expect[User](POST(Uri.fromString(s"http://localhost:$port/").right.get)).unsafeRunSync() === User("post", 27) client.expect[User](POST(Uri.fromString(s"http://localhost:$port/body").right.get, User("joe", 27))).unsafeRunSync() === User("joe", 27) client.expect[User](DELETE(Uri.fromString(s"http://localhost:$port/?reasons=because").right.get)).unsafeRunSync() === User("because", 27) } "status codes" >> { client.fetch[Int](GET(Uri.fromString(s"http://localhost:$port/status/200").right.get))(resp => IO.pure(resp.status.code)).unsafeRunSync === 200 client.fetch[Int](GET(Uri.fromString(s"http://localhost:$port/status/400").right.get))(resp => IO.pure(resp.status.code)).unsafeRunSync === 400 client.fetch[Int](GET(Uri.fromString(s"http://localhost:$port/status/500").right.get))(resp => IO.pure(resp.status.code)).unsafeRunSync === 500 } } val path: () => F[Result[User]] = () => Applicative[F].pure(successWith(Ok)(User("path", 27))) val segment: String => F[Result[User]] = name => Applicative[F].pure(successWith(Ok)(User(name, 27))) val query: Int => F[Result[User]] = age => Applicative[F].pure(successWith(Ok)(User("query", age))) val header: Int => F[Result[User]] = age => Applicative[F].pure(successWith(Ok)(User("header", age))) val fixed: () => F[Result[User]] = () => Applicative[F].pure(successWith(Ok)(User("fixed", 27))) val input: () => F[Result[User]] = () => Applicative[F].pure(successWith(Ok)(User("input", 27))) val clientHdr: () => F[Result[User]] = () => Applicative[F].pure(successWith(Ok)(User("client header", 27))) val coll: () => F[Result[User]] = () => Applicative[F].pure(successWith(Ok)(User("coll", 27))) val matching: Map[String, String] => F[Result[User]] = matches => Applicative[F].pure(successWith(Ok)(User(matches.mkString(","), 27))) val send: () => F[Result[User]] = () => Applicative[F].pure(successWith(Ok)(User("send", 27))) val get: () => F[Result[User]] = () => Applicative[F].pure(successWith(Ok)(User("get", 27))) val put: () => F[Result[User]] = () => Applicative[F].pure(successWith(Ok)(User("put", 27))) val putB: User => F[Result[User]] = user => Applicative[F].pure(successWith(Ok)(user)) val post: () => F[Result[User]] = () => Applicative[F].pure(successWith(Ok)(User("post", 27))) val postB: User => F[Result[User]] = user => Applicative[F].pure(successWith(Ok)(user)) val delete: List[String] => F[Result[User]] = reasons => { Applicative[F].pure(successWith(Ok)(User(reasons.mkString(","), 27))) } val code200: () => F[Result[String]] = () => Applicative[F].pure(successWith(Ok)("")) val code400: () => F[Result[String]] = () => Applicative[F].pure(errorWith(BadRequest, "meh")) val code500: () => F[Result[String]] = () => Applicative[F].pure(errorWith(InternalServerError, "boom")) } ================================================ FILE: http4s-client/src/main/scala/typedapi/client/http4s/package.scala ================================================ package typedapi.client import cats.{Monad, MonadError, Applicative} import org.http4s._ import org.http4s.client._ import org.http4s.Status.Successful import scala.language.higherKinds package object http4s { private implicit class Http4sRequestOps[F[_]](req: Request[F]) { def withQuery(queries: Map[String, List[String]]): Request[F] = { if (queries.nonEmpty) { val q = org.http4s.Query.fromMap(queries) val uri = Uri(req.uri.scheme, req.uri.authority, req.uri.path, q, req.uri.fragment) req.withUri(uri) } else req } def withHeaders(headers: Map[String, String]): Request[F] = { if (headers.nonEmpty) { val h: List[Header] = headers.map { case (k, v) => org.http4s.Header(k, v) }(collection.breakOut) req.withHeaders(Headers(h)) } else req } def run(cm: ClientManager[Client[F]])(implicit F: Applicative[F]): F[Response[F]] = cm.client.fetch(req)(resp => F.pure(resp)) } private implicit class Http4sResponseOps[F[_]](resp: Response[F]) { def decode[A](implicit d: EntityDecoder[F, A], F: MonadError[F, Throwable]): F[A] = resp match { case Successful(_resp) => d.decode(_resp, strict = false).fold(throw _, identity) case failedResponse => F.raiseError(UnexpectedStatus(failedResponse.status)) } } implicit def rawGetRequest[F[_]](implicit F: Applicative[F]) = new RawGetRequest[Client[F], F] { type Resp = Response[F] def apply(uri: List[String], queries: Map[String, List[String]], headers: Map[String, String], cm: ClientManager[Client[F]]): F[Resp] = { val request = Request[F](Method.GET, Uri.unsafeFromString(deriveUriString(cm, uri))) .withQuery(queries) .withHeaders(headers) request.run(cm) } } implicit def getRequest[F[_], A](implicit decoder: EntityDecoder[F, A], F: MonadError[F, Throwable]) = new GetRequest[Client[F], F, A] { private val raw = rawGetRequest[F] def apply(uri: List[String], queries: Map[String, List[String]], headers: Map[String, String], cm: ClientManager[Client[F]]): F[A] = F.flatMap(raw(uri, queries, headers, cm))(_.decode[A]) } implicit def rawPutRequest[F[_]](implicit F: Applicative[F]) = new RawPutRequest[Client[F], F] { type Resp = Response[F] def apply(uri: List[String], queries: Map[String, List[String]], headers: Map[String, String], cm: ClientManager[Client[F]]): F[Resp] = { val request = Request[F](Method.PUT, Uri.unsafeFromString(deriveUriString(cm, uri))) .withQuery(queries) .withHeaders(headers) request.run(cm) } } implicit def putRequest[F[_], A](implicit decoder: EntityDecoder[F, A], F: MonadError[F, Throwable]) = new PutRequest[Client[F], F, A] { private val raw = rawPutRequest[F] def apply(uri: List[String], queries: Map[String, List[String]], headers: Map[String, String], cm: ClientManager[Client[F]]): F[A] = F.flatMap(raw(uri, queries, headers, cm))(_.decode[A]) } implicit def rawPutBodyRequest[F[_], Bd](implicit encoder: EntityEncoder[F, Bd], F: Monad[F]) = new RawPutWithBodyRequest[Client[F], F, Bd] { type Resp = Response[F] def apply(uri: List[String], queries: Map[String, List[String]], headers: Map[String, String], body: Bd, cm: ClientManager[Client[F]]): F[Resp] = { val requestF = Request[F](Method.PUT, Uri.unsafeFromString(deriveUriString(cm, uri))) .withQuery(queries) .withHeaders(headers) .withBody(body) F.flatMap(requestF)(_.run(cm)) } } implicit def putBodyRequest[F[_], Bd, A](implicit encoder: EntityEncoder[F, Bd], decoder: EntityDecoder[F, A], F: MonadError[F, Throwable]) = new PutWithBodyRequest[Client[F], F, Bd, A] { private val raw = rawPutBodyRequest[F, Bd] def apply(uri: List[String], queries: Map[String, List[String]], headers: Map[String, String], body: Bd, cm: ClientManager[Client[F]]): F[A] = F.flatMap(raw(uri, queries, headers, body, cm))(_.decode[A]) } implicit def rawPostRequest[F[_]](implicit F: Applicative[F]) = new RawPostRequest[Client[F], F] { type Resp = Response[F] def apply(uri: List[String], queries: Map[String, List[String]], headers: Map[String, String], cm: ClientManager[Client[F]]): F[Resp] = { val request = Request[F](Method.POST, Uri.unsafeFromString(deriveUriString(cm, uri))) .withQuery(queries) .withHeaders(headers) request.run(cm) } } implicit def postRequest[F[_], A](implicit decoder: EntityDecoder[F, A], F: MonadError[F, Throwable]) = new PostRequest[Client[F], F, A] { private val raw = rawPostRequest[F] def apply(uri: List[String], queries: Map[String, List[String]], headers: Map[String, String], cm: ClientManager[Client[F]]): F[A] = F.flatMap(raw(uri, queries, headers, cm))(_.decode[A]) } implicit def rawPostBodyRequest[F[_], Bd](implicit encoder: EntityEncoder[F, Bd], F: Monad[F]) = new RawPostWithBodyRequest[Client[F], F, Bd] { type Resp = Response[F] def apply(uri: List[String], queries: Map[String, List[String]], headers: Map[String, String], body: Bd, cm: ClientManager[Client[F]]): F[Resp] = { val requestF = Request[F](Method.POST, Uri.unsafeFromString(deriveUriString(cm, uri))) .withQuery(queries) .withHeaders(headers) .withBody(body) F.flatMap(requestF)(_.run(cm)) } } implicit def postBodyRequest[F[_], Bd, A](implicit encoder: EntityEncoder[F, Bd], decoder: EntityDecoder[F, A], F: MonadError[F, Throwable]) = new PostWithBodyRequest[Client[F], F, Bd, A] { private val raw = rawPostBodyRequest[F, Bd] def apply(uri: List[String], queries: Map[String, List[String]], headers: Map[String, String], body: Bd, cm: ClientManager[Client[F]]): F[A] = F.flatMap(raw(uri, queries, headers, body, cm))(_.decode[A]) } implicit def rawDeleteRequest[F[_]](implicit F: Applicative[F]) = new RawDeleteRequest[Client[F], F] { type Resp = Response[F] def apply(uri: List[String], queries: Map[String, List[String]], headers: Map[String, String], cm: ClientManager[Client[F]]): F[Resp] = { val request = Request[F](Method.DELETE, Uri.unsafeFromString(deriveUriString(cm, uri))) .withQuery(queries) .withHeaders(headers) request.run(cm) } } implicit def deleteRequest[F[_], A](implicit decoder: EntityDecoder[F, A], F: MonadError[F, Throwable]) = new DeleteRequest[Client[F], F, A] { private val raw = rawDeleteRequest[F] def apply(uri: List[String], queries: Map[String, List[String]], headers: Map[String, String], cm: ClientManager[Client[F]]): F[A] = F.flatMap(raw(uri, queries, headers, cm))(_.decode[A]) } } ================================================ FILE: http4s-server/src/main/scala/typedapi/server/htt4ps/package.scala ================================================ package typedapi.server import typedapi.shared.MethodType import cats.Monad import cats.implicits._ import cats.effect.IO import org.http4s._ import org.http4s.dsl._ import org.http4s.dsl.impl.EntityResponseGenerator import org.http4s.server.Server import org.http4s.server.blaze.BlazeBuilder import shapeless._ import shapeless.ops.hlist.Prepend import scala.language.higherKinds package object http4s { private def getHeaders(raw: Map[String, String]): List[Header.Raw] = raw.map { case (key, value) => Header(key, value) }(collection.breakOut) implicit def noReqBodyExecutor[El <: HList, KIn <: HList, VIn <: HList, M <: MethodType, F[_]: Monad, FOut](implicit encoder: EntityEncoder[F, FOut]) = new NoReqBodyExecutor[El, KIn, VIn, M, F, FOut] { type R = Request[F] type Out = F[Response[F]] private val dsl = Http4sDsl[F] import dsl._ private def respGenerator(code: Status) = new EntityResponseGenerator[F] { val status = code } def apply(req: R, eReq: EndpointRequest, endpoint: Endpoint[El, KIn, VIn, M, VIn, F, FOut]): Either[ExtractionError, Out] = { extract(eReq, endpoint).map { extracted => Monad[F].flatMap(execute(extracted, endpoint)) { case Right((code, response)) => respGenerator(Status(code.statusCode))(response, getHeaders(endpoint.headers): _*) case Left(HttpError(code, msg)) => respGenerator(Status(code.statusCode))(msg, getHeaders(endpoint.headers): _*) } } } } implicit def withReqBodyExecutor[El <: HList, KIn <: HList, VIn <: HList, Bd, M <: MethodType, ROut <: HList, POut <: HList, F[_]: Monad, FOut] (implicit encoder: EntityEncoder[F, FOut], decoder: EntityDecoder[F, Bd], _prepend: Prepend.Aux[ROut, Bd :: HNil, POut], _eqProof: POut =:= VIn) = new ReqBodyExecutor[El, KIn, VIn, Bd, M, ROut, POut, F, FOut] { type R = Request[F] type Out = F[Response[F]] implicit val prepend = _prepend implicit val eqProof = _eqProof private val dsl = Http4sDsl[F] import dsl._ private def respGenerator(code: Status) = new EntityResponseGenerator[F] { val status = code } def apply(req: R, eReq: EndpointRequest, endpoint: Endpoint[El, KIn, VIn, M, (BodyType[Bd], ROut), F, FOut]): Either[ExtractionError, Out] = { extract(eReq, endpoint).map { case (_, extracted) => for { body <- req.as[Bd] response <- execute(extracted, body, endpoint).flatMap { case Right((code, response)) => respGenerator(Status(code.statusCode))(response, getHeaders(endpoint.headers): _*) case Left(HttpError(code, msg)) => respGenerator(Status(code.statusCode))(msg, getHeaders(endpoint.headers): _*) } } yield response } } } implicit val mountEndpoints = new MountEndpoints[BlazeBuilder[IO], Request[IO], IO[Response[IO]]] { import io._ type Out = IO[Server[IO]] def apply(server: ServerManager[BlazeBuilder[IO]], endpoints: List[Serve[Request[IO], IO[Response[IO]]]]): Out = { val service = HttpService[IO] { case request => def execute(eps: List[Serve[Request[IO], IO[Response[IO]]]], eReq: EndpointRequest): IO[Response[IO]] = eps match { case collection.immutable.::(endpoint, tail) => endpoint(request, eReq) match { case Right(response) => response case Left(RouteNotFound) => execute(tail, eReq) case Left(BadRouteRequest(msg)) => io.BadRequest(msg) } case Nil => io.NotFound("uri = " + request.uri) } val eReq = EndpointRequest( request.method.name, { val path = request.uri.path.split("/") if (path.isEmpty) List.empty else path.tail.toList }, request.uri.multiParams.map { case (key, value) => key -> value.toList }, request.headers.toList.map(header => header.name.toString.toLowerCase -> header.value)(collection.breakOut) ) if (request.method.name == "OPTIONS") { IO(Response(headers = Headers(getHeaders(optionsHeaders(endpoints, eReq))))) } else execute(endpoints, eReq) } server.server.bindHttp(server.port, server.host).mountService(service, "/").start } } } ================================================ FILE: js-client/src/main/scala/typedapi/client/js/package.scala ================================================ package typedapi.client import typedapi.util._ import org.scalajs.dom.XMLHttpRequest import org.scalajs.dom.ext.Ajax import scala.concurrent.{Future, ExecutionContext} package object js { private def renderQueries(queries: Map[String, List[String]]): String = if (queries.nonEmpty) queries .map { case (key, values) => s"$key=${values.mkString(",")}" } .mkString("?", "&", "") else "" private def flatten[A](decoded: Future[Either[Exception, A]])(implicit ec: ExecutionContext): Future[A] = decoded.flatMap { case Right(a) => Future.successful(a) case Left(error) => Future.failed(error) } implicit def rawGetRequest(implicit ec: ExecutionContext) = new RawGetRequest[Ajax.type, Future] { type Resp = XMLHttpRequest def apply(uri: List[String], queries: Map[String, List[String]], headers: Map[String, String], cm: ClientManager[Ajax.type]): Future[Resp] = cm.client .get( url = deriveUriString(cm, uri) + renderQueries(queries), headers = headers ) } implicit def getRequest[A](implicit decoder: Decoder[Future, A], ec: ExecutionContext) = new GetRequest[Ajax.type, Future, A] { private val raw = rawGetRequest def apply(uri: List[String], queries: Map[String, List[String]], headers: Map[String, String], cm: ClientManager[Ajax.type]): Future[A] = raw(uri, queries, headers, cm).flatMap(response => flatten(decoder(response.responseText))) } implicit def rawPutRequest(implicit ec: ExecutionContext) = new RawPutRequest[Ajax.type, Future] { type Resp = XMLHttpRequest def apply(uri: List[String], queries: Map[String, List[String]], headers: Map[String, String], cm: ClientManager[Ajax.type]): Future[Resp] = cm.client .put( url = deriveUriString(cm, uri) + renderQueries(queries), headers = headers ) } implicit def putRequest[A](implicit decoder: Decoder[Future, A], ec: ExecutionContext) = new PutRequest[Ajax.type, Future, A] { private val raw = rawPutRequest def apply(uri: List[String], queries: Map[String, List[String]], headers: Map[String, String], cm: ClientManager[Ajax.type]): Future[A] = raw(uri, queries, headers, cm).flatMap(response => flatten(decoder(response.responseText))) } implicit def rawPutBodyRequest[Bd](implicit encoder: Encoder[Future, Bd], ec: ExecutionContext) = new RawPutWithBodyRequest[Ajax.type, Future, Bd] { type Resp = XMLHttpRequest def apply(uri: List[String], queries: Map[String, List[String]], headers: Map[String, String], body: Bd, cm: ClientManager[Ajax.type]): Future[Resp] = encoder(body).flatMap { encoded => cm.client .put( url = deriveUriString(cm, uri) + renderQueries(queries), headers = headers, data = encoded ) } } implicit def putBodyRequest[Bd, A](implicit encoder: Encoder[Future, Bd], decoder: Decoder[Future, A], ec: ExecutionContext) = new PutWithBodyRequest[Ajax.type, Future, Bd, A] { private val raw = rawPutBodyRequest[Bd] def apply(uri: List[String], queries: Map[String, List[String]], headers: Map[String, String], body: Bd, cm: ClientManager[Ajax.type]): Future[A] = raw(uri, queries, headers, body, cm).flatMap(response => flatten(decoder(response.responseText))) } implicit def rawPostRequest(implicit ec: ExecutionContext) = new RawPostRequest[Ajax.type, Future] { type Resp = XMLHttpRequest def apply(uri: List[String], queries: Map[String, List[String]], headers: Map[String, String], cm: ClientManager[Ajax.type]): Future[Resp] = cm.client .post( url = deriveUriString(cm, uri) + renderQueries(queries), headers = headers ) } implicit def postRequest[A](implicit decoder: Decoder[Future, A], ec: ExecutionContext) = new PostRequest[Ajax.type, Future, A] { private val raw = rawPostRequest def apply(uri: List[String], queries: Map[String, List[String]], headers: Map[String, String], cm: ClientManager[Ajax.type]): Future[A] = raw(uri, queries, headers, cm).flatMap(response => flatten(decoder(response.responseText))) } implicit def rawPostBodyRequest[Bd](implicit encoder: Encoder[Future, Bd], ec: ExecutionContext) = new RawPostWithBodyRequest[Ajax.type, Future, Bd] { type Resp = XMLHttpRequest def apply(uri: List[String], queries: Map[String, List[String]], headers: Map[String, String], body: Bd, cm: ClientManager[Ajax.type]): Future[Resp] = encoder(body).flatMap { encoded => cm.client .post( url = deriveUriString(cm, uri) + renderQueries(queries), headers = headers, data = encoded ) } } implicit def postBodyRequest[Bd, A](implicit encoder: Encoder[Future, Bd], decoder: Decoder[Future, A], ec: ExecutionContext) = new PostWithBodyRequest[Ajax.type, Future, Bd, A] { private val raw = rawPostBodyRequest[Bd] def apply(uri: List[String], queries: Map[String, List[String]], headers: Map[String, String], body: Bd, cm: ClientManager[Ajax.type]): Future[A] = raw(uri, queries, headers, body, cm).flatMap(response => flatten(decoder(response.responseText))) } implicit def rawDeleteRequest(implicit ec: ExecutionContext) = new RawDeleteRequest[Ajax.type, Future] { type Resp = XMLHttpRequest def apply(uri: List[String], queries: Map[String, List[String]], headers: Map[String, String], cm: ClientManager[Ajax.type]): Future[Resp] = cm.client .delete( url = deriveUriString(cm, uri) + renderQueries(queries), headers = headers ) } implicit def deleteRequest[A](implicit decoder: Decoder[Future, A], ec: ExecutionContext) = new DeleteRequest[Ajax.type, Future, A] { private val raw = rawDeleteRequest def apply(uri: List[String], queries: Map[String, List[String]], headers: Map[String, String], cm: ClientManager[Ajax.type]): Future[A] = raw(uri, queries, headers, cm).flatMap(response => flatten(decoder(response.responseText))) } } ================================================ FILE: project/build.properties ================================================ sbt.version=1.0.4 ================================================ FILE: project/build.scala ================================================ import sbt._ object Dependencies { private val specs2V = "3.9.4" val shared = Seq( "com.chuusai" %% "shapeless" % "2.3.3" % Compile, "org.specs2" %% "specs2-core" % specs2V % Test ) val client = Seq( "org.specs2" %% "specs2-core" % specs2V % Test ) val server = Seq( "org.specs2" %% "specs2-core" % specs2V % Test ) private val http4sV = "0.18.0" val http4sClient = Seq( "org.http4s" %% "http4s-blaze-client" % http4sV % Provided, ) val http4sServer = Seq( "org.http4s" %% "http4s-blaze-server" % http4sV % Provided, "org.http4s" %% "http4s-dsl" % http4sV % Provided ) private val akkaHttpV = "10.0.13" val akkaHttpClient = Seq( "com.typesafe.akka" %% "akka-http" % akkaHttpV % Provided, "com.typesafe.akka" %% "akka-http-core" % akkaHttpV % Provided ) val akkaHttpServer = Seq( "com.typesafe.akka" %% "akka-http" % akkaHttpV % Provided, "com.typesafe.akka" %% "akka-http-core" % akkaHttpV % Provided ) private val scalajHttpV = "2.4.1" val scalajHttpClient = Seq( "org.scalaj" %% "scalaj-http" % scalajHttpV % Provided ) private val circeV = "0.9.1" val httpSupportTests = Seq( "org.specs2" %% "specs2-core" % specs2V % Test, "org.http4s" %% "http4s-blaze-client" % http4sV % Test, "org.http4s" %% "http4s-blaze-server" % http4sV % Test, "org.http4s" %% "http4s-dsl" % http4sV % Test, "org.http4s" %% "http4s-circe" % http4sV % Test, "com.typesafe.akka" %% "akka-http" % akkaHttpV % Test, "org.scalaj" %% "scalaj-http" % scalajHttpV % Test, "io.circe" %% "circe-core" % circeV % Test, "io.circe" %% "circe-parser" % circeV % Test, "io.circe" %% "circe-generic" % circeV % Test, "de.heikoseeberger" %% "akka-http-circe" % "1.21.0" % Test, "org.specs2" %% "specs2-core" % specs2V % Test ) val ammoniteSupport = Seq( "org.scalaj" %% "scalaj-http" % scalajHttpV % Compile ) } ================================================ FILE: project/plugins.sbt ================================================ addSbtPlugin("org.xerial.sbt" % "sbt-sonatype" % "2.3") addSbtPlugin("com.jsuereth" % "sbt-pgp" % "1.1.0") addSbtPlugin("org.scala-js" % "sbt-scalajs" % "0.6.22") ================================================ FILE: scalaj-http-client/src/main/scala/typedapi/client/scalajhttp/package.scala ================================================ package typedapi.client import typedapi.util._ import scalaj.http._ package object scalajhttp { type Id[A] = A type Blocking[A] = Either[Exception, A] private def reduceQueries(queries: Map[String, List[String]]): Map[String, String] = queries.map { case (key, values) => key -> values.mkString(",") }(collection.breakOut) implicit def rawGetRequest = new RawGetRequest[Http.type, Id] { type Resp = HttpResponse[String] def apply(uri: List[String], queries: Map[String, List[String]], headers: Map[String, String], cm: ClientManager[Http.type]): Id[Resp] = { val req = cm.client(deriveUriString(cm, uri)).params(reduceQueries(queries)).headers(headers).method("GET") req.asString } } implicit def getRequest[A](implicit decoder: Decoder[Id, A]) = new GetRequest[Http.type, Blocking, A] { private val raw = rawGetRequest def apply(uri: List[String], queries: Map[String, List[String]], headers: Map[String, String], cm: ClientManager[Http.type]): Blocking[A] = decoder(raw(uri, queries, headers, cm).body) } implicit def rawPutRequest = new RawPutRequest[Http.type, Id] { type Resp = HttpResponse[String] def apply(uri: List[String], queries: Map[String, List[String]], headers: Map[String, String], cm: ClientManager[Http.type]): Id[Resp] = { val req = cm.client(deriveUriString(cm, uri)).params(reduceQueries(queries)).headers(headers).method("PUT") req.asString } } implicit def putRequest[A](implicit decoder: Decoder[Id, A]) = new PutRequest[Http.type, Blocking, A] { private val raw = rawPutRequest def apply(uri: List[String], queries: Map[String, List[String]], headers: Map[String, String], cm: ClientManager[Http.type]): Blocking[A] = decoder(raw(uri, queries, headers, cm).body) } implicit def rawPutBodyRequest[Bd](implicit encoder: Encoder[Id, Bd]) = new RawPutWithBodyRequest[Http.type, Id, Bd] { type Resp = HttpResponse[String] def apply(uri: List[String], queries: Map[String, List[String]], headers: Map[String, String], body: Bd, cm: ClientManager[Http.type]): Id[Resp] = { val req = cm.client(deriveUriString(cm, uri)).params(reduceQueries(queries)).headers(headers).put(encoder(body)) req.asString } } implicit def putBodyRequest[Bd, A](implicit encoder: Encoder[Id, Bd], decoder: Decoder[Id, A]) = new PutWithBodyRequest[Http.type, Blocking, Bd, A] { private val raw = rawPutBodyRequest[Bd] def apply(uri: List[String], queries: Map[String, List[String]], headers: Map[String, String], body: Bd, cm: ClientManager[Http.type]): Blocking[A] = decoder(raw(uri, queries, headers, body, cm).body) } implicit def rawPostRequest = new RawPostRequest[Http.type, Id] { type Resp = HttpResponse[String] def apply(uri: List[String], queries: Map[String, List[String]], headers: Map[String, String], cm: ClientManager[Http.type]): Id[Resp] = { val req = cm.client(deriveUriString(cm, uri)).params(reduceQueries(queries)).headers(headers).method("POST") req.asString } } implicit def postRequest[A](implicit decoder: Decoder[Id, A]) = new PostRequest[Http.type, Blocking, A] { private val raw = rawPostRequest def apply(uri: List[String], queries: Map[String, List[String]], headers: Map[String, String], cm: ClientManager[Http.type]): Blocking[A] = decoder(raw(uri, queries, headers, cm).body) } implicit def rawPostBodyRequest[Bd](implicit encoder: Encoder[Id, Bd]) = new RawPostWithBodyRequest[Http.type, Id, Bd] { type Resp = HttpResponse[String] def apply(uri: List[String], queries: Map[String, List[String]], headers: Map[String, String], body: Bd, cm: ClientManager[Http.type]): Id[Resp] = { val req = cm.client(deriveUriString(cm, uri)).params(reduceQueries(queries)).headers(headers).postData(encoder(body)) req.asString } } implicit def postBodyRequest[Bd, A](implicit encoder: Encoder[Id, Bd], decoder: Decoder[Id, A]) = new PostWithBodyRequest[Http.type, Blocking, Bd, A] { private val raw = rawPostBodyRequest[Bd] def apply(uri: List[String], queries: Map[String, List[String]], headers: Map[String, String], body: Bd, cm: ClientManager[Http.type]): Blocking[A] = decoder(raw(uri, queries, headers, body, cm).body) } implicit def rawDeleteRequest = new RawDeleteRequest[Http.type, Id] { type Resp = HttpResponse[String] def apply(uri: List[String], queries: Map[String, List[String]], headers: Map[String, String], cm: ClientManager[Http.type]): Id[Resp] = { val req = cm.client(deriveUriString(cm, uri)).params(reduceQueries(queries)).headers(headers).method("DELETE") req.asString } } implicit def deleteRequest[A](implicit decoder: Decoder[Id, A]) = new DeleteRequest[Http.type, Blocking, A] { private val raw = rawDeleteRequest def apply(uri: List[String], queries: Map[String, List[String]], headers: Map[String, String], cm: ClientManager[Http.type]): Blocking[A] = decoder(raw(uri, queries, headers, cm).body) } } ================================================ FILE: server/src/main/scala/typedapi/server/Endpoint.scala ================================================ package typedapi.server import typedapi.shared._ import shapeless._ import shapeless.labelled.FieldType import shapeless.ops.function._ import scala.language.higherKinds /** Represents a server endpoint and is basically a function which gets the expected input `VIn` and returns the expected output. */ abstract class Endpoint[El <: HList, KIn <: HList, VIn <: HList, M <: MethodType, ROut, F[_], Out] (val method: String, val extractor: RouteExtractor.Aux[El, KIn, VIn, M, HNil, ROut], val headers: Map[String, String]) { def apply(in: VIn): F[Result[Out]] } /** Request representation which every server implementation has to provide. */ final case class EndpointRequest(method: String, uri: List[String], queries: Map[String, List[String]], headers: Map[String, String]) final class ExecutableDerivation[F[_]] { final class Derivation[El <: HList, KIn <: HList, VIn <: HList, M <: MethodType, ROut, Fn, Out] (extractor: RouteExtractor.Aux[El, KIn, VIn, M, HNil, ROut], method: String, headers: Map[String, String], fnToVIn: FnToProduct.Aux[Fn, VIn => F[Result[Out]]]) { /** Restricts type of parameter `fn` to a function defined by the given API: * * {{{ * val Api = := :> Segment[String]('name) :> Get[User] * * derive[IO](Api).from(name: String => IO.pure(User(name))) * }}} */ def from(fn: Fn): Endpoint[El, KIn, VIn, M, ROut, F, Out] = new Endpoint[El, KIn, VIn, M, ROut, F, Out](method, extractor, headers) { private val fin = fnToVIn(fn) def apply(in: VIn): F[Result[Out]] = fin(in) } } def apply[H <: HList, FH <: HList, El <: HList, KIn <: HList, VIn <: HList, ROut, Fn, M <: MethodType, MT <: MediaType, Out] (apiList: ApiTypeCarrier[H]) (implicit filter: FilterClientElements.Aux[H, FH], folder: Lazy[TypeLevelFoldLeft.Aux[FH, Unit, (El, KIn, VIn, M, FieldType[MT, Out])]], extractor: RouteExtractor.Aux[El, KIn, VIn, M, HNil, ROut], methodShow: MethodToString[M], serverHeaders: ServerHeaderExtractor[El], inToFn: Lazy[FnFromProduct.Aux[VIn => F[Result[Out]], Fn]], fnToVIn: Lazy[FnToProduct.Aux[Fn, VIn => F[Result[Out]]]]): Derivation[El, KIn, VIn, M, ROut, Fn, Out] = new Derivation[El, KIn, VIn, M, ROut, Fn, Out](extractor, methodShow.show, serverHeaders(Map.empty), fnToVIn.value) } ================================================ FILE: server/src/main/scala/typedapi/server/EndpointComposition.scala ================================================ package typedapi.server import typedapi.shared._ import shapeless._ import shapeless.labelled.FieldType import shapeless.ops.function._ import scala.language.higherKinds import scala.annotation.implicitNotFound /** Fuses [[RouteExtractor]] and the endpoint function into an [[Endpoint]]. */ trait EndpointConstructor[F[_], Fn, El <: HList, KIn <: HList, VIn <: HList, M <: MethodType, ROut, Out] { def apply(fn: Fn): Endpoint[El, KIn, VIn, M, ROut, F, Out] } /** Compiles RouteExtractor and FunApply for every API endpoint and generates expected list of endpoint functions. */ @implicitNotFound("""Could not precompile your API. This can happen when you try to extract an value from the route which is not supported (ValueExtractor in RouteExtractor.scala) transformed: ${H}""") sealed trait PrecompileEndpoint[F[_], H <: HList] { // list of expected endpoint functions type Fns <: HList // list of endpoint constructors type Consts <: HList def constructors: Consts } object PrecompileEndpoint extends PrecompileEndpointLowPrio { type Aux[F[_], H <: HList, Fns0 <: HList, Consts0 <: HList] = PrecompileEndpoint[F, H] { type Fns = Fns0 type Consts = Consts0 } } trait PrecompileEndpointLowPrio { implicit def hnilPrecompiledCase[F[_]] = new PrecompileEndpoint[F, HNil] { type Fns = HNil type Consts = HNil val constructors = HNil } implicit def constructorsCase[F[_], Fn, El <: HList, KIn <: HList, VIn <: HList, MT <: MediaType, Out, M <: MethodType, ROut, T <: HList] (implicit extractor: RouteExtractor.Aux[El, KIn, VIn, M, HNil, ROut], methodShow: MethodToString[M], serverHeaders: ServerHeaderExtractor[El], vinToFn: FnFromProduct.Aux[VIn => F[Result[Out]], Fn], fnToVIn: Lazy[FnToProduct.Aux[Fn, VIn => F[Result[Out]]]], next: PrecompileEndpoint[F, T]) = new PrecompileEndpoint[F, (El, KIn, VIn, M, FieldType[MT, Out]) :: T] { type Fns = Fn :: next.Fns type Consts = EndpointConstructor[F, Fn, El, KIn, VIn, M, ROut, Out] :: next.Consts val constructor = new EndpointConstructor[F, Fn, El, KIn, VIn, M, ROut, Out] { def apply(fn: Fn): Endpoint[El, KIn, VIn, M, ROut, F, Out] = new Endpoint[El, KIn, VIn, M, ROut, F, Out](methodShow.show, extractor, serverHeaders(Map.empty)) { private val fin = fnToVIn.value(fn) def apply(in: VIn): F[Result[Out]] = fin(in) } } val constructors = constructor :: next.constructors } } @implicitNotFound("""Whoops, you should not be here. This seems to be a bug. constructors: ${Consts} functions: ${Fns}""") sealed trait MergeToEndpoint[F[_], Consts <: HList, Fns <: HList] { type Out <: HList def apply(constructors: Consts, fns: Fns): Out } object MergeToEndpoint extends MergeToEndpointLowPrio { type Aux[F[_], Consts <: HList, Fns <: HList, Out0 <: HList] = MergeToEndpoint[F, Consts, Fns] { type Out = Out0 } } trait MergeToEndpointLowPrio { implicit def hnilMergeCase[F[_]] = new MergeToEndpoint[F, HNil, HNil] { type Out = HNil def apply(constructors: HNil, fns: HNil): Out = HNil } implicit def mergeCase[F[_], El <: HList, KIn <: HList, VIn <: HList, Out0, M <: MethodType, ROut, Consts <: HList, Fn, Fns <: HList] (implicit next: MergeToEndpoint[F, Consts, Fns]) = new MergeToEndpoint[F, EndpointConstructor[F, Fn, El, KIn, VIn, M, ROut, Out0] :: Consts, Fn :: Fns] { type Out = Endpoint[El, KIn, VIn, M, ROut, F, Out0] :: next.Out def apply(constructors: EndpointConstructor[F, Fn, El, KIn, VIn, M, ROut, Out0] :: Consts, fns: Fn :: Fns): Out = { val endpoint = constructors.head(fns.head) endpoint :: next(constructors.tail, fns.tail) } } } final class ExecutableCompositionDerivation[F[_]] { final class Derivation[H <: HList, Fns <: HList, Consts <: HList, Out <: HList, Drv](pre: PrecompileEndpoint.Aux[F, H, Fns, Consts], merge: MergeToEndpoint.Aux[F, Consts, Fns, Out], derived: FnFromProduct.Aux[Fns => Out, Drv]) { /** Restricts type of input parameter to a composition of functions defined by the precompile-stage. * * {{{ * val Api = * (:= :> Segment[String]('name) :> Get[User]) :|: * (:= :> "foo" :> Segment[String]('name) :> Get[User]) * * val f0: String => IO[User] = name => IO.pure(User(name)) * val f1: String => IO[User] = name => IO.pure(User(name)) * deriveAll[IO](Api).from(f0 _, f1 _) * }}} */ val from: Drv = derived.apply(fns => merge(pre.constructors, fns)) } def apply[H <: HList, FH <: HList, Fold <: HList, Fns <: HList, FnsTup, Consts <: HList, Out <: HList, Drv](apiLists: CompositionCons[H]) (implicit filter: FilterClientElementsList.Aux[H, FH], folder: TypeLevelFoldLeftList.Aux[FH, Fold], pre: PrecompileEndpoint.Aux[F, Fold, Fns, Consts], merge: MergeToEndpoint.Aux[F, Consts, Fns, Out], derived: FnFromProduct.Aux[Fns => Out, Drv]): Derivation[Fold, Fns, Consts, Out, Drv] = new Derivation[Fold, Fns, Consts, Out, Drv](pre, merge, derived) } ================================================ FILE: server/src/main/scala/typedapi/server/EndpointExecutor.scala ================================================ package typedapi.server import typedapi.shared.MethodType import shapeless._ import shapeless.ops.hlist.Prepend import scala.language.higherKinds import scala.annotation.implicitNotFound @implicitNotFound("""Cannot find EndpointExecutor. Do you miss some implicit values e.g. encoder/decoder? elements: ${El} input keys: ${KIn} input values: ${VIn}""") sealed trait EndpointExecutor[El <: HList, KIn <: HList, VIn <: HList, M <: MethodType, ROut, F[_], FOut] { type R type Out def extract(eReq: EndpointRequest, endpoint: Endpoint[El, KIn, VIn, M, ROut, F, FOut]): Either[ExtractionError, ROut] = endpoint.extractor(eReq, HNil) def apply(req: R, eReq: EndpointRequest, endpoint: Endpoint[El, KIn, VIn, M, ROut, F, FOut]): Either[ExtractionError, Out] } object EndpointExecutor { type Aux[R0, El <: HList, KIn <: HList, VIn <: HList, M <: MethodType, ROut, F[_], FOut, Out0] = EndpointExecutor[El, KIn, VIn, M, ROut, F, FOut] { type R = R0 type Out = Out0 } } trait NoReqBodyExecutor[El <: HList, KIn <: HList, VIn <: HList, M <: MethodType, F[_], FOut] extends EndpointExecutor[El, KIn, VIn, M, VIn, F, FOut] { protected def execute(input: VIn, endpoint: Endpoint[El, KIn, VIn, M, VIn, F, FOut]): F[Result[FOut]] = endpoint.apply(input) } trait ReqBodyExecutor[El <: HList, KIn <: HList, VIn <: HList, Bd, M <: MethodType, ROut <: HList, POut <: HList, F[_], FOut] extends EndpointExecutor[El, KIn, VIn, M, (BodyType[Bd], ROut), F, FOut] { implicit def prepend: Prepend.Aux[ROut, Bd :: HNil, POut] implicit def eqProof: POut =:= VIn protected def execute(input: ROut, body: Bd, endpoint: Endpoint[El, KIn, VIn, M, (BodyType[Bd], ROut), F, FOut]): F[Result[FOut]] = endpoint.apply(input :+ body) } ================================================ FILE: server/src/main/scala/typedapi/server/FilterClientElements.scala ================================================ package typedapi.server import typedapi.shared.{ClientHeaderElement, ClientHeaderParam, ClientHeaderCollParam} import shapeless._ sealed trait FilterClientElements[H <: HList] { type Out <: HList } sealed trait FilterClientElementsLowPrio { implicit val filterClientResult = new FilterClientElements[HNil] { type Out = HNil } implicit def filterClientKeep[El, T <: HList](implicit next: FilterClientElements[T]) = new FilterClientElements[El :: T] { type Out = El :: next.Out } } object FilterClientElements extends FilterClientElementsLowPrio { type Aux[H <: HList, Out0 <: HList] = FilterClientElements[H] { type Out = Out0 } implicit def filterClientEl[K, V, T <: HList](implicit next: FilterClientElements[T]) = new FilterClientElements[ClientHeaderElement[K, V] :: T] { type Out = next.Out } implicit def filterClientParam[K, V, T <: HList](implicit next: FilterClientElements[T]) = new FilterClientElements[ClientHeaderParam[K, V] :: T] { type Out = next.Out } implicit def filterClientCollParam[V, T <: HList](implicit next: FilterClientElements[T]) = new FilterClientElements[ClientHeaderCollParam[V] :: T] { type Out = next.Out } } sealed trait FilterClientElementsList[H <: HList] { type Out <: HList } object FilterClientElementsList { type Aux[H <: HList, Out0 <: HList] = FilterClientElementsList[H] { type Out = Out0 } implicit val filterClientListResult = new FilterClientElementsList[HNil] { type Out = HNil } implicit def filterClientListStep[Api <: HList, T <: HList](implicit filtered: FilterClientElements[Api], next: FilterClientElementsList[T]) = new FilterClientElementsList[Api :: T] { type Out = filtered.Out :: next.Out } } ================================================ FILE: server/src/main/scala/typedapi/server/RouteExtractor.scala ================================================ package typedapi.server import typedapi.shared._ import shapeless.{HList, HNil, Witness} import shapeless.labelled.FieldType import shapeless.ops.hlist.Reverse import scala.util.Try import scala.annotation.implicitNotFound sealed trait ExtractionError case object RouteNotFound extends ExtractionError final case class BadRouteRequest(msg: String) extends ExtractionError /** Builds a function which extracts inputs from a given requests based on the API. * - if a request path does not fit the API definition `RouteNotFound` is returned * - if a query, header, body, etc is missing `BadRouteRequest` is returned */ @implicitNotFound("""Cannot find RouteExtractor. Maybe a ValueExtractor could not be found. elements: ${El} input keys: ${KIn} inout values: ${VIn} method: ${M}""") trait RouteExtractor[El <: HList, KIn <: HList, VIn <: HList, M <: MethodType, EIn <: HList] { type Out def apply(request: EndpointRequest, inAgg: EIn): Either[ExtractionError, Out] } object RouteExtractor extends RouteExtractorMediumPrio { type Aux[El <: HList, KIn <: HList, VIn <: HList, M <: MethodType, EIn <: HList, Out0] = RouteExtractor[El, KIn, VIn, M, EIn] { type Out = Out0 } type Extract[Out] = Either[ExtractionError, Out] def NotFoundE[Out]: Extract[Out] = Left(RouteNotFound) def BadRequestE[Out](msg: String): Extract[Out] = Left(BadRouteRequest(msg)) } trait RouteExtractorLowPrio { import RouteExtractor._ implicit def pathExtractor[S, El <: HList, KIn <: HList, VIn <: HList, M <: MethodType, EIn <: HList] (implicit wit: Witness.Aux[S], show: WitnessToString[S], next: RouteExtractor[El, KIn, VIn, M, EIn]) = new RouteExtractor[shapeless.::[S, El], KIn, VIn, M, EIn] { type Out = next.Out def apply(request: EndpointRequest, inAgg: EIn): Extract[Out] = request.uri match { case p :: tail => if (p == show.show(wit.value)) next(request.copy(uri = tail), inAgg) else NotFoundE case Nil => NotFoundE } } } trait RouteExtractorMediumPrio extends RouteExtractorLowPrio { import RouteExtractor._ private def checkEmptyPath[Out](request: EndpointRequest)(f: EndpointRequest => Extract[Out]): Extract[Out] = if (request.uri.isEmpty) f(request) else NotFoundE implicit def segmentExtractor[El <: HList, K, V, KIn <: HList, VIn <: HList, M <: MethodType, EIn <: HList](implicit value: ValueExtractor[V], next: RouteExtractor[El, KIn, VIn, M, shapeless.::[V, EIn]]) = new RouteExtractor[shapeless.::[SegmentInput, El], shapeless.::[K, KIn], shapeless.::[V, VIn], M, EIn] { type Out = next.Out def apply(request: EndpointRequest, inAgg: EIn): Extract[Out] = request.uri match { case p :: tail => value(p).fold(NotFoundE[Out])(v => next(request.copy(uri = tail), v :: inAgg)) case Nil => NotFoundE } } implicit def queryExtractor[El <: HList, K, V, KIn <: HList, VIn <: HList, M <: MethodType, EIn <: HList] (implicit wit: Witness.Aux[K], show: WitnessToString[K], value: ValueExtractor[V], next: RouteExtractor[El, KIn, VIn, M, shapeless.::[V, EIn]]) = new RouteExtractor[shapeless.::[QueryInput, El], shapeless.::[K, KIn], shapeless.::[V, VIn], M, EIn] { type Out = next.Out def apply(request: EndpointRequest, inAgg: EIn): Extract[Out] = checkEmptyPath(request) { req => val key = show.show(wit.value) req.queries.get(key).fold(BadRequestE[Out](s"missing query '$key'")) { raw => raw.headOption.flatMap(value.apply).fold(BadRequestE[Out](s"query '$key' has not type ${value.typeDesc}")) { v => next(request, v :: inAgg) } } } } implicit def queryOptListExtractor[El <: HList, K, V, KIn <: HList, VIn <: HList, M <: MethodType, EIn <: HList] (implicit wit: Witness.Aux[K], show: WitnessToString[K], value: ValueExtractor[V], next: RouteExtractor[El, KIn, VIn, M, shapeless.::[Option[V], EIn]]) = new RouteExtractor[shapeless.::[QueryInput, El], shapeless.::[K, KIn], shapeless.::[Option[V], VIn], M, EIn] { type Out = next.Out def apply(request: EndpointRequest, inAgg: EIn): Extract[Out] = checkEmptyPath(request) { req => val key = show.show(wit.value) req.queries.get(key).fold(next(request, None :: inAgg)) { raw => raw.headOption.flatMap(value.apply).fold(BadRequestE[Out](s"query '$key' has not type ${value.typeDesc}")) { v => next(request, Some(v) :: inAgg) } } } } implicit def queryListExtractor[El <: HList, K, V, KIn <: HList, VIn <: HList, M <: MethodType, EIn <: HList] (implicit wit: Witness.Aux[K], show: WitnessToString[K], value: ValueExtractor[V], next: RouteExtractor[El, KIn, VIn, M, shapeless.::[List[V], EIn]]) = new RouteExtractor[shapeless.::[QueryInput, El], shapeless.::[K, KIn], shapeless.::[List[V], VIn], M, EIn] { type Out = next.Out def apply(request: EndpointRequest, inAgg: EIn): Extract[Out] = checkEmptyPath(request) { req => val key = show.show(wit.value) req.queries.get(key).fold(next(request, Nil :: inAgg)) { raw => val vs = raw.flatMap(value.apply) if (vs.length < raw.length) BadRequestE(s"some values of query '$key' are no ${value.typeDesc}") else next(request, vs :: inAgg) } } } implicit def headerExtractor[El <: HList, K, V, KIn <: HList, VIn <: HList, M <: MethodType, EIn <: HList] (implicit wit: Witness.Aux[K], show: WitnessToString[K], value: ValueExtractor[V], next: RouteExtractor[El, KIn, VIn, M, shapeless.::[V, EIn]]) = new RouteExtractor[shapeless.::[HeaderInput, El], shapeless.::[K, KIn], shapeless.::[V, VIn], M, EIn] { type Out = next.Out def apply(request: EndpointRequest, inAgg: EIn): Extract[Out] = checkEmptyPath(request) { req => val key = show.show(wit.value).toLowerCase req.headers.get(key).fold(BadRequestE[Out](s"missing header '$key'")) { raw => value(raw).fold(BadRequestE[Out](s"header '$key' has not type ${value.typeDesc}")) { v => next(request, v :: inAgg) } } } } implicit def headerOptExtractor[El <: HList, K, V, KIn <: HList, VIn <: HList, M <: MethodType, EIn <: HList] (implicit wit: Witness.Aux[K], show: WitnessToString[K], value: ValueExtractor[V], next: RouteExtractor[El, KIn, VIn, M, shapeless.::[Option[V], EIn]]) = new RouteExtractor[shapeless.::[HeaderInput, El], shapeless.::[K, KIn], shapeless.::[Option[V], VIn], M, EIn] { type Out = next.Out def apply(request: EndpointRequest, inAgg: EIn): Extract[Out] = checkEmptyPath(request) { req => val key = show.show(wit.value) req.headers.get(key.toLowerCase).fold(next(request, None :: inAgg)) { raw => value(raw).fold(BadRequestE[Out](s"header '$key' has not type ${value.typeDesc}")) { v => next(request, Some(v) :: inAgg) } } } } implicit def fixedHeaderExtractor[K, V, El <: HList, KIn <: HList, VIn <: HList, M <: MethodType, EIn <: HList] (implicit kWit: Witness.Aux[K], kShow: WitnessToString[K], vWit: Witness.Aux[V], vShow: WitnessToString[V], next: RouteExtractor[El, KIn, VIn, M, EIn]) = new RouteExtractor[shapeless.::[FixedHeader[K, V], El], KIn, VIn, M, EIn] { type Out = next.Out def apply(request: EndpointRequest, inAgg: EIn): Extract[Out] = checkEmptyPath(request) { req => val key = kShow.show(kWit.value) val value = vShow.show(vWit.value) req.headers.get(key.toLowerCase).fold(BadRequestE[Out](s"missing header '$key'")) { raw => if (raw != value) BadRequestE[Out](s"header '$key' has unexpected value '${raw}' - expected '${value}'") else next(request, inAgg) } } } implicit def ignoreServerHeaderSendExtractor[K, V, El <: HList, KIn <: HList, VIn <: HList, M <: MethodType, EIn <: HList] (implicit next: RouteExtractor[El, KIn, VIn, M, EIn]) = new RouteExtractor[shapeless.::[ServerHeaderSend[K, V], El], KIn, VIn, M, EIn] { type Out = next.Out def apply(request: EndpointRequest, inAgg: EIn): Extract[Out] = checkEmptyPath(request) { req => next(request, inAgg) } } implicit def serverHeaderMatchExtractor[El <: HList, K, V, KIn <: HList, VIn <: HList, M <: MethodType, EIn <: HList] (implicit wit: Witness.Aux[K], show: WitnessToString[K], value: ValueExtractor[V], next: RouteExtractor[El, KIn, VIn, M, shapeless.::[Map[String, V], EIn]]) = new RouteExtractor[shapeless.::[ServerHeaderMatchInput, El], shapeless.::[K, KIn], shapeless.::[Map[String, V], VIn], M, EIn] { type Out = next.Out def apply(request: EndpointRequest, inAgg: EIn): Extract[Out] = checkEmptyPath(request) { req => val key = show.show(wit.value).toLowerCase val matchesE = req.headers.foldLeft[Either[ExtractionError, Map[String, V]]](Right(Map.empty)) { case (Right(agg), (k, raw)) => if (k.contains(key)) { value(raw).fold(BadRequestE[Map[String, V]](s"header '$k' has not type ${value.typeDesc}")) { v => Right(agg + (k -> v)) } } else Right(agg) case (error, _) => error } matchesE.fold( error => Left(error), matches => next(request, matches :: inAgg) ) } } implicit def getExtractor[EIn <: HList, REIn <: HList](implicit rev: Reverse.Aux[EIn, REIn]) = new RouteExtractor[HNil, HNil, HNil, GetCall, EIn] { type Out = REIn def apply(request: EndpointRequest, inAgg: EIn): Extract[Out] = checkEmptyPath(request) { req => if (req.method == "GET" || req.method == "OPTIONS") Right(inAgg.reverse) else NotFoundE } } implicit def putExtractor[EIn <: HList, REIn <: HList](implicit rev: Reverse.Aux[EIn, REIn]) = new RouteExtractor[HNil, HNil, HNil, PutCall, EIn] { type Out = REIn def apply(request: EndpointRequest, inAgg: EIn): Extract[Out] = checkEmptyPath(request) { req => if (req.method == "PUT" || req.method == "OPTIONS") Right(inAgg.reverse) else NotFoundE } } implicit def putWithBodyExtractor[BMT <: MediaType, Bd, EIn <: HList] = new RouteExtractor[HNil, shapeless.::[FieldType[BMT, BodyField.T], HNil], shapeless.::[Bd, HNil], PutWithBodyCall, EIn] { type Out = (BodyType[Bd], EIn) def apply(request: EndpointRequest, inAgg: EIn): Extract[Out] = checkEmptyPath(request) { req => if (req.method == "PUT" || req.method == "OPTIONS") Right((BodyType[Bd], inAgg)) else NotFoundE } } implicit def postExtractor[EIn <: HList] = new RouteExtractor[HNil, HNil, HNil, PostCall, EIn] { type Out = EIn def apply(request: EndpointRequest, inAgg: EIn): Extract[Out] = checkEmptyPath(request) { req => if (req.method == "POST" || req.method == "OPTIONS") Right(inAgg) else NotFoundE } } implicit def postWithBodyExtractor[BMT <: MediaType, Bd, EIn <: HList, REIn <: HList] = new RouteExtractor[HNil, shapeless.::[FieldType[BMT, BodyField.T], HNil], shapeless.::[Bd, HNil], PostWithBodyCall, EIn] { type Out = (BodyType[Bd], EIn) def apply(request: EndpointRequest, inAgg: EIn): Extract[Out] = checkEmptyPath(request) { req => if (req.method == "POST" || req.method == "OPTIONS") Right((BodyType[Bd], inAgg)) else NotFoundE } } implicit def deleteExtractor[EIn <: HList] = new RouteExtractor[HNil, HNil, HNil, DeleteCall, EIn] { type Out = EIn def apply(request: EndpointRequest, inAgg: EIn): Extract[Out] = checkEmptyPath(request) { req => if (req.method == "DELETE" || req.method == "OPTIONS") Right(inAgg) else NotFoundE } } } trait ValueExtractor[A] extends (String => Option[A]) { def typeDesc: String } object ValueExtractor extends ValueExtractorInstances trait ValueExtractorInstances { def extract[A](f: String => A, _typeDesc: String) = new ValueExtractor[A] { val typeDesc = _typeDesc def apply(raw: String): Option[A] = Try(f(raw)).toOption } implicit val booleanExtractor = extract[Boolean](_.toBoolean, "Boolean") implicit val byteExtractor = extract[Byte](_.toByte, "Byte") implicit val shortExtractor = extract[Short](_.toShort, "Short") implicit val intExtractor = extract[Int](_.toInt, "Int") implicit val longExtractor = extract[Long](_.toLong, "Long") implicit val floatExtractor = extract[Float](_.toFloat, "Float") implicit val doubleExtractor = extract[Double](_.toDouble, "Double") implicit val stringExtractor = extract[String](identity, "String") } final case class BodyType[Bd]() ================================================ FILE: server/src/main/scala/typedapi/server/Serve.scala ================================================ package typedapi.server /** Reduces an Endpoint and its EndpointExecutor to a simple Request => Response function. */ trait Serve[Req, Resp] { def options(eReq: EndpointRequest): Option[(String, Map[String, String])] def apply(req: Req, eReq: EndpointRequest): Either[ExtractionError, Resp] } ================================================ FILE: server/src/main/scala/typedapi/server/ServeToList.scala ================================================ package typedapi.server import shapeless._ import scala.annotation.implicitNotFound @implicitNotFound("""Cannot find ServeToList instance to map Serve HList to List. Maybe you have different Request, Response types defined?. serves: ${H} request: ${Req} response: ${Resp}""") sealed trait ServeToList[H <: HList, Req, Resp] { def apply(h: H): List[Serve[Req, Resp]] } object ServeToList extends ServeToListLowPrio trait ServeToListLowPrio { implicit def hnilToList[Req, Resp] = new ServeToList[HNil, Req, Resp] { def apply(h: HNil): List[Serve[Req, Resp]] = Nil } implicit def serveToList[Req, Resp, T <: HList](implicit next: ServeToList[T, Req, Resp]) = new ServeToList[Serve[Req, Resp] :: T, Req, Resp] { def apply(h: Serve[Req, Resp] :: T): List[Serve[Req, Resp]] = h.head :: next(h.tail) } } ================================================ FILE: server/src/main/scala/typedapi/server/ServerHeaderExtractor.scala ================================================ package typedapi.server import typedapi.shared._ import shapeless._ import scala.annotation.implicitNotFound @implicitNotFound("""Whoops, you should not be here. This seems to be a bug. elements: ${El}""") sealed trait ServerHeaderExtractor[El <: HList] { def apply(agg: Map[String, String]): Map[String, String] } sealed trait ServerHeaderExtractorLowPrio { implicit val serverHeaderReturnCase = new ServerHeaderExtractor[HNil] { def apply(agg: Map[String, String]): Map[String, String] = agg } implicit def serverHeaderIgnoreCase[H, T <: HList](implicit next: ServerHeaderExtractor[T]) = new ServerHeaderExtractor[H :: T] { def apply(agg: Map[String, String]): Map[String, String] = next(agg) } } object ServerHeaderExtractor extends ServerHeaderExtractorLowPrio { implicit def serverHeaderExtractCase[K, V, T <: HList] (implicit kWit: Witness.Aux[K], kShow: WitnessToString[K], vWit: Witness.Aux[V], vShow: WitnessToString[V], next: ServerHeaderExtractor[T]) = new ServerHeaderExtractor[ServerHeaderSend[K, V] :: T] { def apply(agg: Map[String, String]): Map[String, String] = { val key = kShow.show(kWit.value) val value = vShow.show(vWit.value) next(agg + (key -> value)) } } } ================================================ FILE: server/src/main/scala/typedapi/server/ServerManager.scala ================================================ package typedapi.server import scala.annotation.tailrec final case class ServerManager[S](server: S, host: String, port: Int) object ServerManager { def mount[S, Req, Resp, Out](server: ServerManager[S], endpoints: List[Serve[Req, Resp]])(implicit mounting: MountEndpoints.Aux[S, Req, Resp, Out]): Out = mounting(server, endpoints) } trait MountEndpoints[S, Req, Resp] { type Out final def optionsHeaders(eps: List[Serve[Req, Resp]], eReq: EndpointRequest): Map[String, String] = { @tailrec def collect(serve: List[Serve[Req, Resp]], methods: List[String], headers: Map[String, String]): (List[String], Map[String, String]) = serve match { case collection.immutable.::(endpoint, tail) => endpoint.options(eReq) match { case Some((method, hds)) => collect(tail, method :: methods, hds ++ headers) case _ => collect(tail, methods, headers) } case Nil => (methods, headers) } val (methods, headers) = collect(eps, Nil, Map.empty) headers + (("Access-Control-Allow-Methods", methods.mkString(","))) } def apply(server: ServerManager[S], endpoints: List[Serve[Req, Resp]]): Out } object MountEndpoints { type Aux[S, Req, Resp, Out0] = MountEndpoints[S, Req, Resp] { type Out = Out0 } } ================================================ FILE: server/src/main/scala/typedapi/server/StatusCodes.scala ================================================ package typedapi.server final case class SuccessCode(statusCode: Int) extends AnyVal final case class ErrorCode(statusCode: Int) extends AnyVal final case class HttpError(code: ErrorCode, message: String) object StatusCodes { // success codes final val Continue = SuccessCode(100) final val SwitchingProtocols = SuccessCode(101) final val Processing = SuccessCode(102) final val Ok = SuccessCode(200) final val Created = SuccessCode(201) final val Accepted = SuccessCode(202) final val NonAuthoritativeInformation = SuccessCode(203) final val NoContent = SuccessCode(204) final val ResetContent = SuccessCode(205) final val PartialContent = SuccessCode(206) final val MultiStatus = SuccessCode(207) final val AlreadyReported = SuccessCode(208) final val IMUsed = SuccessCode(226) final val MultipleChoices = SuccessCode(300) final val MovedPermanently = SuccessCode(301) final val Found = SuccessCode(302) final val SeeOther = SuccessCode(303) final val NotModified = SuccessCode(304) final val UseProxy = SuccessCode(305) final val TemporaryRedirect = SuccessCode(307) final val PermanentRedirect = SuccessCode(308) // error codes final val BadRequest = ErrorCode(400) final val Unauthorized = ErrorCode(401) final val PaymentRequired = ErrorCode(402) final val Forbidden = ErrorCode(403) final val NotFound = ErrorCode(404) final val MethodNotAllowed = ErrorCode(405) final val NotAcceptable = ErrorCode(406) final val ProxyAuthenticationRequired = ErrorCode(407) final val RequestTimeout = ErrorCode(408) final val Conflict = ErrorCode(409) final val Gone = ErrorCode(410) final val LengthRequired = ErrorCode(411) final val PreconditionFailed = ErrorCode(412) final val PayloadTooLarge = ErrorCode(413) final val RequestURITooLong = ErrorCode(414) final val UnsupportedMediaType = ErrorCode(415) final val RequestedRangeNotSatisfiable = ErrorCode(416) final val ExpectationFailed = ErrorCode(417) final val ImAteapot = ErrorCode(418) final val MisdirectedRequest = ErrorCode(421) final val UnprocessableEntity = ErrorCode(422) final val Locked = ErrorCode(423) final val FailedDependency = ErrorCode(424) final val UpgradeRequired = ErrorCode(426) final val PreconditionRequired = ErrorCode(428) final val TooManyRequests = ErrorCode(429) final val RequestHeaderFieldsTooLarge = ErrorCode(431) final val ConnectionClosedWithoutResult = ErrorCode(444) final val UnavailableForLegalReasons = ErrorCode(451) final val ClientClosedRequest = ErrorCode(499) final val InternalServerError = ErrorCode(500) final val NotImplemented = ErrorCode(501) final val BadGateway = ErrorCode(502) final val ServiceUnavailable = ErrorCode(503) final val GatewayTimeout = ErrorCode(504) final val HTTPVersionNotSupported = ErrorCode(505) final val VariantAlsoNegotiates = ErrorCode(506) final val InsufficientStorage = ErrorCode(507) final val LoopDetected = ErrorCode(508) final val NotExtended = ErrorCode(510) final val NetworkAuthenticationRequired = ErrorCode(511) final val NetworkConnectTimeoutError = ErrorCode(599) } ================================================ FILE: server/src/main/scala/typedapi/server/package.scala ================================================ package typedapi import typedapi.shared._ import shapeless._ import shapeless.ops.hlist.Mapper import scala.language.higherKinds package object server extends TypeLevelFoldLeftLowPrio with TypeLevelFoldLeftListLowPrio with WitnessToStringLowPrio with ApiTransformer { val SC = StatusCodes type Result[A] = Either[HttpError, (SuccessCode, A)] def successWith[A](code: SuccessCode)(a: A): Result[A] = Right(code -> a) def success[A](a: A): Result[A] = successWith(StatusCodes.Ok)(a) def errorWith[A](code: ErrorCode, message: String): Result[A] = Left(HttpError(code, message)) def derive[F[_]]: ExecutableDerivation[F] = new ExecutableDerivation[F] def mount[S, El <: HList, KIn <: HList, VIn <: HList, M <: MethodType, ROut, F[_], FOut, Req, Resp, Out] (server: ServerManager[S], endpoint: Endpoint[El, KIn, VIn, M, ROut, F, FOut]) (implicit executor: EndpointExecutor.Aux[Req, El, KIn, VIn, M, ROut, F, FOut, Resp], mounting: MountEndpoints.Aux[S, Req, Resp, Out]): Out = mounting(server, List(new Serve[executor.R, executor.Out] { def options(eReq: EndpointRequest): Option[(String, Map[String, String])] = { endpoint.extractor(eReq, HNil) match { case Right(_) => Some((endpoint.method, endpoint.headers)) case _ => None } } def apply(req: executor.R, eReq: EndpointRequest): Either[ExtractionError, executor.Out] = executor(req, eReq, endpoint) })) def deriveAll[F[_]]: ExecutableCompositionDerivation[F] = new ExecutableCompositionDerivation[F] object endpointToServe extends Poly1 { implicit def default[El <: HList, KIn <: HList, VIn <: HList, M <: MethodType, ROut, F[_], FOut](implicit executor: EndpointExecutor[El, KIn, VIn, M, ROut, F, FOut]) = at[Endpoint[El, KIn, VIn, M, ROut, F, FOut]] { endpoint => new Serve[executor.R, executor.Out] { def options(eReq: EndpointRequest): Option[(String, Map[String, String])] = { endpoint.extractor(eReq, HNil) match { case Right(_) => Some((endpoint.method, endpoint.headers)) case _ => None } } def apply(req: executor.R, eReq: EndpointRequest): Either[ExtractionError, executor.Out] = executor(req, eReq, endpoint) } } } def mount[S, End <: HList, Serv <: HList, Req, Resp, Out](server: ServerManager[S], end: End)(implicit mapper: Mapper.Aux[endpointToServe.type, End, Serv], toList: ServeToList[Serv, Req, Resp], mounting: MountEndpoints.Aux[S, Req, Resp, Out]): Out = mounting(server, toList(end.map(endpointToServe))) } ================================================ FILE: server/src/test/scala/typedapi/server/ApiToEndpointLinkSpec.scala ================================================ package typedapi.server import typedapi.dsl._ import shapeless._ import org.specs2.mutable.Specification final class ApiToEndpointLinkSpec extends Specification { import StatusCodes._ case class Foo(name: String) "link api definitions to endpoint functions" >> { val Api = := :> "find" :> typedapi.dsl.Segment[String]('name) :> Query[Int]('limit) :> Client.Header('hello, 'world) :> Server.Send('foo, 'bar) :> Server.Match[String]("hi") :> Get[Json, List[Foo]] val endpoint0 = derive[Option](Api).from((name, limit, hi) => Some(successWith(Ok)(List(Foo(name)).take(limit)))) endpoint0("john" :: 10 :: Map("hi" -> "whats", "hi-ho" -> "up") :: HNil) === Some(Right(Ok -> List(Foo("john")))) endpoint0.headers == Map("foo" -> "bar") endpoint0.method == "GET" } } ================================================ FILE: server/src/test/scala/typedapi/server/RouteExtractorSpec.scala ================================================ package typedapi.server import typedapi.dsl._ import typedapi.shared._ import shapeless.{HList, HNil, Lazy} import org.specs2.mutable.Specification final class RouteExtractorSpec extends Specification { case class Foo(name: String) def extract[H <: HList, El <: HList, KIn <: HList, VIn <: HList, M <: MethodType, ROut, Out] (api: ApiTypeCarrier[H]) (implicit folder: Lazy[TypeLevelFoldLeft.Aux[H, Unit, (El, KIn, VIn, M, Out)]], extractor: RouteExtractor.Aux[El, KIn, VIn, M, HNil, ROut]): RouteExtractor.Aux[El, KIn, VIn, M, HNil, ROut] = extractor "determine routes defined by requests and extract included data (segments, queries, headers)" >> { "no data" >> { val ext = extract(:= :> "hello" :> "world" :> Get[Json, Foo]) ext(EndpointRequest("GET", List("hello", "world"), Map.empty, Map.empty), HNil) === Right(HNil) ext(EndpointRequest("GET", List("hello", "wrong"), Map.empty, Map.empty), HNil) === RouteExtractor.NotFoundE ext(EndpointRequest("GET", List("hello"), Map.empty, Map.empty), HNil) === RouteExtractor.NotFoundE ext(EndpointRequest("GET", Nil, Map.empty, Map.empty), HNil) === RouteExtractor.NotFoundE } "segments" >> { val ext = extract(:= :> "foo" :> Segment[Int]('age) :> Get[Json, Foo]) ext(EndpointRequest("GET", List("foo", "0"), Map.empty, Map.empty), HNil) === Right(0 :: HNil) ext(EndpointRequest("GET", List("foo", "wrong"), Map.empty, Map.empty), HNil) === RouteExtractor.NotFoundE ext(EndpointRequest("GET", List("foo"), Map.empty, Map.empty), HNil) === RouteExtractor.NotFoundE ext(EndpointRequest("GET", Nil, Map.empty, Map.empty), HNil) === RouteExtractor.NotFoundE } "queries" >> { val ext0 = extract(:= :> "foo" :> Query[Int]('age) :> Get[Json, Foo]) ext0(EndpointRequest("GET", List("foo"), Map("age" -> List("0")), Map.empty), HNil) === Right(0 :: HNil) ext0(EndpointRequest("GET", List("foo"), Map("age" -> List("wrong")), Map.empty), HNil) === RouteExtractor.BadRequestE("query 'age' has not type Int") ext0(EndpointRequest("GET", List("foo"), Map("wrong" -> List("0")), Map.empty), HNil) === RouteExtractor.BadRequestE("missing query 'age'") ext0(EndpointRequest("GET", List("foo"), Map.empty, Map.empty), HNil) === RouteExtractor.BadRequestE("missing query 'age'") ext0(EndpointRequest("GET", List("foo", "bar"), Map.empty, Map.empty), HNil) === RouteExtractor.NotFoundE val ext1 = extract(:= :> "foo" :> Query[Option[Int]]('age) :> Get[Json, Foo]) ext1(EndpointRequest("GET", List("foo"), Map("age" -> List("0")), Map.empty), HNil) === Right(Some(0) :: HNil) ext1(EndpointRequest("GET", List("foo"), Map("wrong" -> List("0")), Map.empty), HNil) === Right(None :: HNil) val ext2 = extract(:= :> "foo" :> Query[List[Int]]('age) :> Get[Json, Foo]) ext2(EndpointRequest("GET", List("foo"), Map("age" -> List("0", "1")), Map.empty), HNil) === Right(List(0, 1) :: HNil) ext2(EndpointRequest("GET", List("foo"), Map.empty, Map.empty), HNil) === Right(Nil :: HNil) } "headers" >> { val ext0 = extract(:= :> "foo" :> Header[Int]('age) :> Get[Json, Foo]) ext0(EndpointRequest("GET", List("foo"), Map.empty, Map("age" -> "0")), HNil) === Right(0 :: HNil) ext0(EndpointRequest("GET", List("foo"), Map.empty, Map("age" -> "wrong")), HNil) === RouteExtractor.BadRequestE("header 'age' has not type Int") ext0(EndpointRequest("GET", List("foo"), Map.empty, Map("wrong" -> "0")), HNil) === RouteExtractor.BadRequestE("missing header 'age'") ext0(EndpointRequest("GET", List("foo"), Map.empty, Map.empty), HNil) === RouteExtractor.BadRequestE("missing header 'age'") ext0(EndpointRequest("GET", List("foo", "bar"), Map.empty, Map.empty), HNil) === RouteExtractor.NotFoundE val ext2 = extract(:= :> "foo" :> Header[Option[Int]]('age) :> Get[Json, Foo]) ext2(EndpointRequest("GET", List("foo"), Map.empty, Map("age" -> "0")), HNil) === Right(Some(0) :: HNil) ext2(EndpointRequest("GET", List("foo"), Map.empty, Map.empty), HNil) === Right(None :: HNil) val ext3 = extract(:= :> "foo" :> Header("Accept", "*") :> Get[Json, Foo]) ext3(EndpointRequest("GET", List("foo"), Map.empty, Map("accept" -> "*")), HNil) === Right(HNil) ext3(EndpointRequest("GET", List("foo"), Map.empty, Map("wrong" -> "*")), HNil) === RouteExtractor.BadRequestE("missing header 'Accept'") ext3(EndpointRequest("GET", List("foo"), Map.empty, Map("accept" -> "wrong")), HNil) === RouteExtractor.BadRequestE("header 'Accept' has unexpected value 'wrong' - expected '*'") val ext4 = extract(:= :> "foo" :> Server.Send("Accept", "*") :> Get[Json, Foo]) ext4(EndpointRequest("GET", List("foo"), Map.empty, Map.empty), HNil) === Right(HNil) val ext6 = extract(:= :> "foo" :> Server.Match[Int]("age") :> Get[Json, Foo]) ext6(EndpointRequest("GET", List("foo"), Map.empty, Map("age" -> "0")), HNil) === Right(Map("age" ->0) :: HNil) ext6(EndpointRequest("GET", List("foo"), Map.empty, Map("age-and-what-not" -> "0")), HNil) === Right(Map("age-and-what-not" -> 0) :: HNil) ext6(EndpointRequest("GET", List("foo"), Map.empty, Map("nope" -> "0")), HNil) === Right(Map.empty :: HNil) ext6(EndpointRequest("GET", List("foo"), Map.empty, Map("age-" -> "hello")), HNil) === RouteExtractor.BadRequestE("header 'age-' has not type Int") } "body type" >> { val ext0 = extract(:= :> ReqBody[Json, Foo] :> Put[Json, Foo]) ext0(EndpointRequest("PUT", Nil, Map.empty, Map.empty), HNil) === Right((BodyType[Foo], HNil)) ext0(EndpointRequest("PUT", List("foo", "bar"), Map.empty, Map.empty), HNil) === RouteExtractor.NotFoundE ext0(EndpointRequest("OPTIONS", List("foo", "bar"), Map.empty, Map.empty), HNil) === RouteExtractor.NotFoundE val ext1 = extract(:= :> ReqBody[Json, Foo] :> Post[Json, Foo]) ext1(EndpointRequest("POST", Nil, Map.empty, Map.empty), HNil) === Right((BodyType[Foo], HNil)) ext1(EndpointRequest("OPTIONS", Nil, Map.empty, Map.empty), HNil) === Right((BodyType[Foo], HNil)) } "methods" >> { val ext0 = extract(:= :> Get[Json, Foo]) ext0(EndpointRequest("GET", Nil, Map.empty, Map.empty), HNil) === Right(HNil) ext0(EndpointRequest("WRONG", Nil, Map.empty, Map.empty), HNil) === RouteExtractor.NotFoundE ext0(EndpointRequest("GET", List("foo"), Map.empty, Map.empty), HNil) === RouteExtractor.NotFoundE ext0(EndpointRequest("OPTIONS", Nil, Map.empty, Map.empty), HNil) === Right(HNil) val ext1 = extract(:= :> Put[Json, Foo]) ext1(EndpointRequest("PUT", Nil, Map.empty, Map.empty), HNil) === Right(HNil) ext1(EndpointRequest("OPTIONS", Nil, Map.empty, Map.empty), HNil) === Right(HNil) val ext2 = extract(:= :> Post[Json, Foo]) ext2(EndpointRequest("POST", Nil, Map.empty, Map.empty), HNil) === Right(HNil) ext2(EndpointRequest("OPTIONS", Nil, Map.empty, Map.empty), HNil) === Right(HNil) val ext3 = extract(:= :> Delete[Json, Foo]) ext3(EndpointRequest("DELETE", Nil, Map.empty, Map.empty), HNil) === Right(HNil) ext3(EndpointRequest("OPTIONS", Nil, Map.empty, Map.empty), HNil) === Right(HNil) } "combinations" >> { val ext0 = extract(:= :> "foo" :> Query[Int]('age) :> Header[String]('id) :> Get[Json, Foo]) ext0(EndpointRequest("GET", List("foo"), Map("age" -> List("0")), Map("id" -> "john")), HNil) === Right(0 :: "john" :: HNil) val ext1 = extract(:= :> Get[Json, Foo]) ext1(EndpointRequest("GET", Nil, Map.empty, Map.empty), HNil) === Right(HNil) } } } ================================================ FILE: server/src/test/scala/typedapi/server/ServeAndMountSpec.scala ================================================ package typedapi.server import typedapi.dsl._ import typedapi.shared.MethodType import shapeless.{HList, HNil, ::} import shapeless.ops.hlist.{Prepend, Mapper} import org.specs2.mutable.Specification import scala.language.higherKinds final class ServeAndMountSpec extends Specification { import StatusCodes._ case class Foo(name: String) sealed trait Req case class TestRequest(uri: List[String], queries: Map[String, List[String]], headers: Map[String, String]) extends Req case class TestRequestWithBody[Bd](uri: List[String], queries: Map[String, List[String]], headers: Map[String, String], body: Bd) extends Req case class TestResponse(raw: Any) implicit def execNoBodyId[El <: HList, KIn <: HList, VIn <: HList, M <: MethodType, FOut] = new NoReqBodyExecutor[El, KIn, VIn, M, Option, FOut] { type R = Req type Out = TestResponse def apply(req: Req, eReq: EndpointRequest, endpoint: Endpoint[El, KIn, VIn, M, VIn, Option, FOut]): Either[ExtractionError, Out] = extract(eReq, endpoint).right.map { extracted => TestResponse(execute(extracted, endpoint)) } } implicit def execWithBody[El <: HList, KIn <: HList, VIn <: HList, Bd, M <: MethodType, ROut <: HList, POut <: HList, FOut](implicit _prepend: Prepend.Aux[ROut, Bd :: HNil, POut], _eqProof: POut =:= VIn) = new ReqBodyExecutor[El, KIn, VIn, Bd, M, ROut, POut, Option, FOut] { type R = Req type Out = TestResponse implicit val prepend = _prepend implicit def eqProof = _eqProof def apply(req: Req, eReq: EndpointRequest, endpoint: Endpoint[El, KIn, VIn, M, (BodyType[Bd], ROut), Option, FOut]): Either[ExtractionError, Out] = extract(eReq, endpoint).right.map { case (_, extracted) => TestResponse(execute(extracted, req.asInstanceOf[TestRequestWithBody[Bd]].body, endpoint)) } } def toList[El <: HList, KIn <: HList, VIn <: HList, M <: MethodType, ROut, F[_], FOut](endpoint: Endpoint[El, KIn, VIn, M, ROut, F, FOut]) (implicit executor: EndpointExecutor[El, KIn, VIn, M, ROut, F, FOut]): List[Serve[executor.R, executor.Out]] = List(new Serve[executor.R, executor.Out] { def options(eReq: EndpointRequest): Option[(String, Map[String, String])] = { endpoint.extractor(eReq, HNil) match { case Right(_) => Some((endpoint.method, endpoint.headers)) case _ => None } } def apply(req: executor.R, eReq: EndpointRequest): Either[ExtractionError, executor.Out] = executor(req, eReq, endpoint) }) def toList[End <: HList, Serv <: HList](end: End)(implicit mapper: Mapper.Aux[endpointToServe.type, End, Serv], s: ServeToList[Serv, Req, TestResponse]): List[Serve[Req, TestResponse]] = s(end.map(endpointToServe)) "serve endpoints as simple Request -> Response functions and mount them into a server" >> { "serve single endpoint and no body" >> { val Api = := :> "find" :> "user" :> Segment[String]('name) :> Query[Int]('sortByAge) :> Get[Json, List[Foo]] val endpoint = derive[Option](Api).from((name, sortByAge) => Some(successWith(Ok)(List(Foo(name))))) val served = toList(endpoint) val req = TestRequest(List("find", "user", "joe"), Map("sortByAge" -> List("1")), Map.empty) val eReq = EndpointRequest("GET", req.uri, req.queries, req.headers) served.head(req, eReq) === Right(TestResponse(Some(Right(Ok -> List(Foo("joe")))))) } "check if route exists and return method" >> { val Api = := :> "find" :> "user" :> Segment[String]('name) :> Query[Int]('sortByAge) :> Server.Send("Hello", "*") :> Get[Json, List[Foo]] val endpoint = derive[Option](Api).from((name, sortByAge) => Some(successWith(Ok)(List(Foo(name))))) val served = toList(endpoint) val req0 = TestRequest(List("find", "user", "joe"), Map("sortByAge" -> List("1")), Map.empty) val eReq0 = EndpointRequest("GET", req0.uri, req0.queries, req0.headers) served.head.options(eReq0) === Some(("GET", Map(("Hello", "*")))) val eReq1 = EndpointRequest("POST", req0.uri, req0.queries, req0.headers) served.head.options(eReq1) === None } "serve single endpoint and with body" >> { val Api = := :> "find" :> "user" :> Segment[String]('name) :> ReqBody[Json, Foo] :> Post[Json, List[Foo]] val endpoint = derive[Option](Api).from((name, body) => Some(successWith(Ok)(List(Foo(name), body)))) val served = toList(endpoint) val req = TestRequestWithBody(List("find", "user", "joe"), Map.empty, Map.empty, Foo("jim")) val eReq = EndpointRequest("POST", req.uri, req.queries, req.headers) served.head(req, eReq) === Right(TestResponse(Some(Right(Ok -> List(Foo("joe"), Foo("jim")))))) } "serve multiple endpoints" >> { val Api = (:= :> "find" :> "user" :> Segment[String]('name) :> Query[Int]('sortByAge) :> Get[Json, List[Foo]]) :|: (:= :> "create" :> "user" :> ReqBody[Json, Foo] :> Post[Json, Foo]) def find(name: String, age: Int): Option[Result[List[Foo]]] = Some(successWith(Ok)(List(Foo(name)))) def create(foo: Foo): Option[Result[Foo]] = Some(successWith(Ok)(foo)) val endpoints = deriveAll[Option](Api).from(find _, create _) val served = toList(endpoints) val req = TestRequest(List("find", "user", "joe"), Map("sortByAge" -> List("1")), Map.empty) val eReq = EndpointRequest("GET", req.uri, req.queries, req.headers) served.head(req, eReq) === Right(TestResponse(Some(Right(Ok -> List(Foo("joe")))))) } } } ================================================ FILE: shared/src/main/scala/typedapi/dsl/ApiDsl.scala ================================================ package typedapi.dsl import typedapi.shared._ import shapeless._ sealed trait ApiList[H <: HList] /** Basic operations. */ sealed trait MethodOps[H <: HList] { def :>[MT <: MediaType, A](body: TypeCarrier[ReqBodyElement[MT, A]]): WithBodyCons[MT, A, H] = WithBodyCons() def :>[M <: MethodElement](method: TypeCarrier[M]): ApiTypeCarrier[M :: H] = ApiTypeCarrier() } sealed trait PathOps[H <: HList] { def :>[S](path: Witness.Lt[S]): PathCons[PathElement[S] :: H] = PathCons() def :>[K, V](segment: TypeCarrier[SegmentParam[K, V]]): SegmentCons[SegmentParam[K, V] :: H] = SegmentCons() } sealed trait HeaderOps[H <: HList] { def :>[K, V](header: TypeCarrier[HeaderParam[K, V]]): InputHeaderCons[HeaderParam[K, V] :: H] = InputHeaderCons() def :>[K, V](fixed: TypeCarrier[FixedHeaderElement[K, V]]): FixedHeaderCons[FixedHeaderElement[K, V] :: H] = FixedHeaderCons() def :>[K, V](client: TypeCarrier[ClientHeaderElement[K, V]]): ClientHeaderElCons[ClientHeaderElement[K, V] :: H] = ClientHeaderElCons() def :>[K, V](client: TypeCarrier[ClientHeaderParam[K, V]]): ClientHeaderParamCons[ClientHeaderParam[K, V] :: H] = ClientHeaderParamCons() def :>[V](client: TypeCarrier[ClientHeaderCollParam[V]]): ClientHeaderCollParamCons[ClientHeaderCollParam[V] :: H] = ClientHeaderCollParamCons() def :>[K, V](server: TypeCarrier[ServerHeaderMatchParam[K, V]]): ServerHeaderMatchParamCons[ServerHeaderMatchParam[K, V] :: H] = ServerHeaderMatchParamCons() def :>[K, V](server: TypeCarrier[ServerHeaderSendElement[K, V]]): ServerHeaderSendElCons[ServerHeaderSendElement[K, V] :: H] = ServerHeaderSendElCons() } /** Initial element with empty api description. */ case object EmptyCons extends PathOps[HNil] with HeaderOps[HNil] with MethodOps[HNil] with ApiList[HNil] { def :>[K, V](query: TypeCarrier[QueryParam[K, V]]): QueryCons[QueryParam[K, V] :: HNil] = QueryCons() } /** Last set element is a path. */ final case class PathCons[H <: HList]() extends PathOps[H] with HeaderOps[H] with MethodOps[H] with ApiList[H] { def :>[K, V](query: TypeCarrier[QueryParam[K, V]]): QueryCons[QueryParam[K, V] :: H] = QueryCons() } /** Last set element is a segment. */ final case class SegmentCons[H <: HList]() extends PathOps[H] with HeaderOps[H] with MethodOps[H] with ApiList[H] { def :>[K, V](query: TypeCarrier[QueryParam[K, V]]): QueryCons[QueryParam[K, V] :: H] = QueryCons() } /** Last set element is a query parameter. */ final case class QueryCons[H <: HList]() extends HeaderOps[H] with MethodOps[H] with ApiList[H] { def :>[K, V](query: TypeCarrier[QueryParam[K, V]]): QueryCons[QueryParam[K, V] :: H] = QueryCons() } /** Last set element is a header. */ sealed trait HeaderCons[H <: HList] extends HeaderOps[H] with MethodOps[H] with ApiList[H] final case class InputHeaderCons[H <: HList]() extends HeaderCons[H] final case class FixedHeaderCons[H <: HList]() extends HeaderCons[H] final case class ClientHeaderElCons[H <: HList]() extends HeaderCons[H] final case class ClientHeaderParamCons[H <: HList]() extends HeaderCons[H] final case class ClientHeaderCollParamCons[H <: HList]() extends HeaderCons[H] final case class ServerHeaderMatchParamCons[H <: HList]() extends HeaderCons[H] final case class ServerHeaderSendElCons[H <: HList]() extends HeaderCons[H] /** Last set element is a request body. */ final case class WithBodyCons[BMT <: MediaType, Bd, H <: HList]() extends ApiList[H] { def :>[M <: MethodElement](method: TypeCarrier[M])(implicit out: MethodToReqBody[M, BMT, Bd]): ApiTypeCarrier[out.Out :: H] = ApiTypeCarrier() } ================================================ FILE: shared/src/main/scala/typedapi/dsl/package.scala ================================================ package typedapi import typedapi.shared._ package object dsl extends MethodToReqBodyLowPrio with MethodToStringLowPrio { val MediaTypes = typedapi.shared.MediaTypes val MT = typedapi.shared.MediaTypes def := = EmptyCons def Segment[V] = new PairTypeFromWitnessKey[SegmentParam, V] def Query[V] = new PairTypeFromWitnessKey[QueryParam, V] def Header[V] = new PairTypeFromWitnessKey[HeaderParam, V] def Header = new PairTypeFromWitnesses[FixedHeaderElement] object Client { def Header = new PairTypeFromWitnesses[ClientHeaderElement] def Header[V] = new PairTypeFromWitnessKey[ClientHeaderParam, V] def Coll[V] = TypeCarrier[ClientHeaderCollParam[V]]() } object Server { def Send = new PairTypeFromWitnesses[ServerHeaderSendElement] def Match[V] = new PairTypeFromWitnessKey[ServerHeaderMatchParam, V] } type Json = MT.`Application/json` type Plain = MT.`Text/plain` def ReqBody[MT <: MediaType, A] = TypeCarrier[ReqBodyElement[MT, A]]() def Get[MT <: MediaType, A] = TypeCarrier[GetElement[MT, A]]() def Put[MT <: MediaType, A] = TypeCarrier[PutElement[MT, A]]() def Post[MT <: MediaType, A] = TypeCarrier[PostElement[MT, A]]() def Delete[MT <: MediaType, A] = TypeCarrier[DeleteElement[MT, A]]() } ================================================ FILE: shared/src/main/scala/typedapi/package.scala ================================================ import typedapi.shared._ import shapeless._ import shapeless.ops.hlist.Prepend package object typedapi extends MethodToReqBodyLowPrio with MethodToStringLowPrio { val MediaTypes = typedapi.shared.MediaTypes val MT = typedapi.shared.MediaTypes val Root = PathListBuilder[HNil]() def Segment[V] = new PairTypeFromWitnessKey[SegmentParam, V] val Queries = QueryListBuilder[HNil]() val NoQueries = Queries val Headers = HeaderListBuilder[HNil]() val NoHeaders = Headers def ReqBody[MT <: MediaType, A] = TypeCarrier[ReqBodyElement[MT, A]]() def Get[MT <: MediaType, A] = TypeCarrier[GetElement[MT, A]]() def Put[MT <: MediaType, A] = TypeCarrier[PutElement[MT, A]]() def Post[MT <: MediaType, A] = TypeCarrier[PostElement[MT, A]]() def Delete[MT <: MediaType, A] = TypeCarrier[DeleteElement[MT, A]]() type Json = MT.`Application/json` type Plain = MT.`Text/plain` def api[M <: MethodElement, P <: HList, Q <: HList, H <: HList, Prep <: HList, Api <: HList] (method: TypeCarrier[M], path: PathListBuilder[P] = Root, queries: QueryListBuilder[Q] = NoQueries, headers: HeaderListBuilder[H] = NoHeaders) (implicit prepQP: Prepend.Aux[Q, P, Prep], prepH: Prepend.Aux[H, Prep, Api]): ApiTypeCarrier[M :: Api] = ApiTypeCarrier() def apiWithBody[M <: MethodElement, P <: HList, Q <: HList, H <: HList, Prep <: HList, Api <: HList, BMT <: MediaType, Bd] (method: TypeCarrier[M], body: TypeCarrier[ReqBodyElement[BMT, Bd]], path: PathListBuilder[P] = Root, queries: QueryListBuilder[Q] = NoQueries, headers: HeaderListBuilder[H] = NoHeaders) (implicit prepQP: Prepend.Aux[Q, P, Prep], prepH: Prepend.Aux[H, Prep, Api], m: MethodToReqBody[M, BMT, Bd]): ApiTypeCarrier[m.Out :: Api] = ApiTypeCarrier() } ================================================ FILE: shared/src/main/scala/typedapi/shared/ApiElement.scala ================================================ package typedapi.shared import scala.annotation.implicitNotFound sealed trait ApiElement /** Type-container providing the singleton-type of an static path element */ sealed trait PathElement[P] /** Type-container providing the name (singleton) and value type for a path parameter. */ sealed trait SegmentParam[K, V] extends ApiElement /** Type-container providing the name (singleton) and value type for a query parameter. */ sealed trait QueryParam[K, V] extends ApiElement /** Type-container providing the name (singleton) and value type for a header parameter. */ sealed trait HeaderParam[K, V] extends ApiElement /** Type-container providing the name (singleton) and value type for a static header element. */ sealed trait FixedHeaderElement[K, V] extends ApiElement /** Type-container providing the name (singleton) and value type for a static header element only used for the client. */ sealed trait ClientHeaderElement[K, V] extends ApiElement /** Type-container providing the name (singleton) and value type for a header parameter only used for the client. */ sealed trait ClientHeaderParam[K, V] extends ApiElement /** Type-container providing a collection of headers (Map[String, V]) only used for the client. */ sealed trait ClientHeaderCollParam[V] extends ApiElement /** Type-container providing the name (singleton) and value type for a static header element sent by server. */ sealed trait ServerHeaderSendElement[K, V] extends ApiElement /** Type-container providing the name (singleton) and value type describing a sub-string headers have to match only used for the server. */ sealed trait ServerHeaderMatchParam[K, V] extends ApiElement /** Type-container providing the media-type and value type for a request body. */ sealed trait ReqBodyElement[MT <: MediaType, A] extends ApiElement trait MethodElement extends ApiElement /** Type-container representing a GET operation with a media-type and value type for the result. */ sealed trait GetElement[MT <: MediaType, A] extends MethodElement /** Type-container representing a PUT operation with a media-type and value type for the result. */ sealed trait PutElement[MT <: MediaType, A] extends MethodElement /** Type-container representing a PUT operation with a media-type and value type for the result and a body. */ sealed trait PutWithBodyElement[BMT <: MediaType, Bd, MT <: MediaType, A] extends MethodElement /** Type-container representing a POST operation with a media-type and value type for the result. */ sealed trait PostElement[MT <: MediaType, A] extends MethodElement /** Type-container representing a POST operation with a media-type and value type for the result and a body. */ sealed trait PostWithBodyElement[BMT <: MediaType, Bd, MT <: MediaType, A] extends MethodElement /** Type-container representing a DELETE operation with a media-type and value type for the result. */ sealed trait DeleteElement[MT <: MediaType, A] extends MethodElement @implicitNotFound("""You try to add a request body to a method which doesn't expect one. method: ${M} """) trait MethodToReqBody[M <: MethodElement, MT <: MediaType, Bd] { type Out <: MethodElement } trait MethodToReqBodyLowPrio { implicit def reqBodyForPut[MT <: MediaType, A, BMT <: MediaType, Bd] = new MethodToReqBody[PutElement[MT, A], BMT, Bd] { type Out = PutWithBodyElement[BMT, Bd, MT, A] } implicit def reqBodyForPost[MT <: MediaType, A, BMT <: MediaType, Bd] = new MethodToReqBody[PostElement[MT, A], BMT, Bd] { type Out = PostWithBodyElement[BMT, Bd, MT, A] } } ================================================ FILE: shared/src/main/scala/typedapi/shared/ApiList.scala ================================================ package typedapi.shared import shapeless._ /** Typecarrier to construct a complete path description from [[PathElement]]s and [[SegmentParam]]s. */ final case class PathListBuilder[P <: HList]() { def /[S](path: Witness.Lt[S]): PathListBuilder[PathElement[S] :: P] = PathListBuilder() def /[K, V](segment: TypeCarrier[SegmentParam[K, V]]): PathListBuilder[SegmentParam[K, V] :: P] = PathListBuilder() } /** Typecarrier to construct a set of queries from [[QueryParam]]s. */ final case class QueryListBuilder[Q <: HList]() { final class WitnessDerivation[V] { def apply[K](wit: Witness.Lt[K]): QueryListBuilder[QueryParam[K, V] :: Q] = QueryListBuilder() } def add[V]: WitnessDerivation[V] = new WitnessDerivation[V] } /** Typecarrier to construct a set of headers from [[HeaderParam]]s, [[FixedHeaderElement]]s, [[ClientHeaderElement]]s, [[ServerHeaderSendElement]]s and [ServerHeaderMatchParam]]s. */ final case class HeaderListBuilder[H <: HList]() { final class WitnessDerivation[V] { def apply[K](wit: Witness.Lt[K]): HeaderListBuilder[HeaderParam[K, V] :: H] = HeaderListBuilder() } def add[V]: WitnessDerivation[V] = new WitnessDerivation[V] def add[K, V](kWit: Witness.Lt[K], vWit: Witness.Lt[V]): HeaderListBuilder[FixedHeaderElement[K, V] :: H] = HeaderListBuilder() final class ClientWitnessDerivation[V] { def apply[K](wit: Witness.Lt[K]): HeaderListBuilder[ClientHeaderParam[K, V] :: H] = HeaderListBuilder() } def client[V]: ClientWitnessDerivation[V] = new ClientWitnessDerivation[V] def client[K, V](kWit: Witness.Lt[K], vWit: Witness.Lt[V]): HeaderListBuilder[ClientHeaderElement[K, V] :: H] = HeaderListBuilder() def clientColl[V]: HeaderListBuilder[ClientHeaderCollParam[V] :: H] = HeaderListBuilder() final class ServerMatchWitnessDerivation[V] { def apply[K](wit: Witness.Lt[K]): HeaderListBuilder[ServerHeaderMatchParam[K, V] :: H] = HeaderListBuilder() } def serverMatch[V]: ServerMatchWitnessDerivation[V] = new ServerMatchWitnessDerivation[V] def serverSend[K, V](kWit: Witness.Lt[K], vWit: Witness.Lt[V]): HeaderListBuilder[ServerHeaderSendElement[K, V] :: H] = HeaderListBuilder() } ================================================ FILE: shared/src/main/scala/typedapi/shared/ApiTransformer.scala ================================================ package typedapi.shared import shapeless._ import shapeless.labelled.FieldType import scala.annotation.implicitNotFound trait ApiOp sealed trait SegmentInput extends ApiOp sealed trait QueryInput extends ApiOp sealed trait HeaderInput extends ApiOp sealed trait FixedHeader[K, V] extends ApiOp sealed trait ClientHeader[K, V] extends ApiOp sealed trait ClientHeaderInput extends ApiOp sealed trait ClientHeaderCollInput extends ApiOp sealed trait ServerHeaderSend[K, V] extends ApiOp sealed trait ServerHeaderMatchInput extends ApiOp trait MethodType extends ApiOp sealed trait GetCall extends MethodType sealed trait PutCall extends MethodType sealed trait PutWithBodyCall extends MethodType sealed trait PostCall extends MethodType sealed trait PostWithBodyCall extends MethodType sealed trait DeleteCall extends MethodType /** Transforms a [[MethodType]] to a `String`. */ @implicitNotFound("Missing String transformation for this method = ${M}.") trait MethodToString[M <: MethodType] { def show: String } trait MethodToStringLowPrio { implicit val getToStr = new MethodToString[GetCall] { val show = "GET" } implicit val putToStr = new MethodToString[PutCall] { val show = "PUT" } implicit val putBodyToStr = new MethodToString[PutWithBodyCall] { val show = "PUT" } implicit val postToStr = new MethodToString[PostCall] { val show = "POST" } implicit val postBodyToStr = new MethodToString[PostWithBodyCall] { val show = "POST" } implicit val deleteToStr = new MethodToString[DeleteCall] { val show = "DELETE" } } /** Tranforms API type shape into five distinct types: * - El: elements of the API (path elements, segment/query/header input placeholder, etc.) * - KIn: expected input key types (from parameters) * - VIn: expected input value types (from parameters) * - M: method type * - Out: output type * * ``` * val api: TypeCarrier[Get[Json, Foo] :: Segment["name".type, String] :: "find".type :: HNil] * val trans: ("name".type :: SegmentInput :: HNil, "name".type :: HNil, String :: HNil], Field[Json, GetCall], Foo) * ``` */ trait ApiTransformer { import TypeLevelFoldFunction.at implicit def pathElementTransformer[S, El <: HList, KIn <: HList, VIn <: HList, M <: MethodType, Out] = at[PathElement[S], (El, KIn, VIn, M, Out), (S :: El, KIn, VIn, M, Out)] implicit def segmentParamTransformer[S, A, El <: HList, KIn <: HList, VIn <: HList, M <: MethodType, Out] = at[SegmentParam[S, A], (El, KIn, VIn, M, Out), (SegmentInput :: El, S :: KIn, A :: VIn, M, Out)] implicit def queryParamTransformer[S, A, El <: HList, KIn <: HList, VIn <: HList, M <: MethodType, Out] = at[QueryParam[S, A], (El, KIn, VIn, M, Out), (QueryInput :: El, S :: KIn, A :: VIn, M, Out)] implicit def queryListParamTransformer[S, A, El <: HList, KIn <: HList, VIn <: HList, M <: MethodType, Out] = at[QueryParam[S, List[A]], (El, KIn, VIn, M, Out), (QueryInput :: El, S :: KIn, List[A] :: VIn, M, Out)] implicit def headerParamTransformer[S, A, El <: HList, KIn <: HList, VIn <: HList, M <: MethodType, Out] = at[HeaderParam[S, A], (El, KIn, VIn, M, Out), (HeaderInput :: El, S :: KIn, A :: VIn, M, Out)] implicit def fixedHeaderElementTransformer[K, V, El <: HList, KIn <: HList, VIn <: HList, M <: MethodType, Out] = at[FixedHeaderElement[K, V], (El, KIn, VIn, M, Out), (FixedHeader[K, V] :: El, KIn, VIn, M, Out)] implicit def clientHeaderElementTransformer[K, V, El <: HList, KIn <: HList, VIn <: HList, M <: MethodType, Out] = at[ClientHeaderElement[K, V], (El, KIn, VIn, M, Out), (ClientHeader[K, V] :: El, KIn, VIn, M, Out)] implicit def clientHeaderParamTransformer[K, V, El <: HList, KIn <: HList, VIn <: HList, M <: MethodType, Out] = at[ClientHeaderParam[K, V], (El, KIn, VIn, M, Out), (ClientHeaderInput :: El, K :: KIn, V :: VIn, M, Out)] implicit def clientHeaderCollParamTransformer[V, El <: HList, KIn <: HList, VIn <: HList, M <: MethodType, Out] = at[ClientHeaderCollParam[V], (El, KIn, VIn, M, Out), (ClientHeaderCollInput :: El, KIn, Map[String, V] :: VIn, M, Out)] implicit def serverHeaderSendElementTransformer[K, V, El <: HList, KIn <: HList, VIn <: HList, M <: MethodType, Out] = at[ServerHeaderSendElement[K, V], (El, KIn, VIn, M, Out), (ServerHeaderSend[K, V] :: El, KIn, VIn, M, Out)] implicit def serverHeaderMatchParamTransformer[K, V, El <: HList, KIn <: HList, VIn <: HList, M <: MethodType, Out] = at[ServerHeaderMatchParam[K, V], (El, KIn, VIn, M, Out), (ServerHeaderMatchInput :: El, K :: KIn, Map[String, V] :: VIn, M, Out)] implicit def getTransformer[MT <: MediaType, A] = at[GetElement[MT, A], Unit, (HNil, HNil, HNil, GetCall, FieldType[MT, A])] implicit def putTransformer[MT <: MediaType, A] = at[PutElement[MT, A], Unit, (HNil, HNil, HNil, PutCall, FieldType[MT, A])] implicit def putWithBodyTransformer[BMT <: MediaType, Bd, MT <: MediaType, A] = at[PutWithBodyElement[BMT, Bd, MT, A], Unit, (HNil, FieldType[BMT, BodyField.T] :: HNil, Bd :: HNil, PutWithBodyCall, FieldType[MT, A])] implicit def postTransformer[MT <: MediaType, A] = at[PostElement[MT, A], Unit, (HNil, HNil, HNil, PostCall, FieldType[MT, A])] implicit def postWithBodyTransformer[BMT <: MediaType, Bd, MT <: MediaType, A] = at[PostWithBodyElement[BMT, Bd, MT, A], Unit, (HNil, FieldType[BMT, BodyField.T] :: HNil, Bd :: HNil, PostWithBodyCall, FieldType[MT, A])] implicit def deleteTransformer[MT <: MediaType, A] = at[DeleteElement[MT, A], Unit, (HNil, HNil, HNil, DeleteCall, FieldType[MT, A])] } ================================================ FILE: shared/src/main/scala/typedapi/shared/MediaTypes.scala ================================================ package typedapi.shared trait MediaType { self => def value: String } object MediaTypes { case object NoMediaType extends MediaType { val value = "NO MEDIA TYPE" } case object `application/atom+xml` extends MediaType { val value = "application/atom+xml" } type `Application/atom+xml` = `application/atom+xml`.type case object `application/base64` extends MediaType { val value = "application/base64" } type `Application/base64` = `application/base64`.type case object `application/excel` extends MediaType { val value = "application/excel" } type `Application/excel` = `application/excel`.type case object `application/font-woff` extends MediaType { val value = "application/font-woff" } type `Application/font-woff` = `application/font-woff`.type case object `application/gnutar` extends MediaType { val value = "application/gnutar" } type `Application/gnutar` = `application/gnutar`.type case object `application/java-archive` extends MediaType { val value = "application/java-archive" } type `Application/java-archive` = `application/java-archive`.type case object `application/javascript` extends MediaType { val value = "application/javascript" } type `Application/javascript` = `application/javascript`.type case object `application/json` extends MediaType { val value = "application/json" } type `Application/json` = `application/json`.type case object `application/json-patch+json` extends MediaType { val value = "application/json-path+json" } type `Application/json-patch+json` = `application/json-patch+json`.type case object `application/grpc+proto` extends MediaType { val value = "application/grpc+proto" type `Application/grpc+proto` = `application/grpc+proto`.type } case object `application/lha` extends MediaType { val value = "application/lha" } type `Application/lha` = `application/lha`.type case object `application/lzx` extends MediaType { val value = "application/lzx" } type `Application/lzx` = `application/lzx`.type case object `application/mspowerpoint` extends MediaType { val value = "application/mspowerpoint" } type `Application/mspowerpoint` = `application/mspowerpoint`.type case object `application/msword` extends MediaType { val value = "application/msword" } type `Application/msword` = `application/msword`.type case object `application/octet-stream` extends MediaType { val value = "application/octet-stream" } type `Application/octet-stream` = `application/octet-stream`.type case object `application/pdf` extends MediaType { val value = "application/pdf" } type `Application/pdf` = `application/pdf`.type case object `application/postscript` extends MediaType { val value = "application/postscript" } type `Application/postscript` = `application/postscript`.type case object `application/rss+xml` extends MediaType { val value = "application/rss+xml" } type `Application/rss+xml` = `application/rss+xml`.type case object `application/soap+xml` extends MediaType { val value = "application/soap+xml" } type `Application/soap+xml` = `application/soap+xml`.type case object `application/vnd.api+json` extends MediaType { val value = "application/vnd.api+json" } type `Application/vnd.api+json` = `application/vnd.api+json`.type case object `application/vnd.google-earth.kml+xml` extends MediaType { val value = "application/vnd.google-earth.kml+xml" } type `Application/vnd.google-earth.kml+xml` = `application/vnd.google-earth.kml+xml`.type case object `application/vnd.google-earth.kmz` extends MediaType { val value = "application/vnd.google-earth.kmz" } type `Application/vnd.google-earth.kmz` = `application/vnd.google-earth.kmz`.type case object `application/vnd.ms-fontobject` extends MediaType { val value = "application/vnd.ms-fontobject" } type `Application/vnd.ms-fontobject` = `application/vnd.ms-fontobject`.type case object `application/vnd.oasis.opendocument.chart` extends MediaType { val value = "application/vnd.oasis.opendocument.chart" } type `Application/vnd.oasis.opendocument.chart` = `application/vnd.oasis.opendocument.chart`.type case object `application/vnd.oasis.opendocument.database` extends MediaType { val value = "application/vnd.oasis.opendocument.database" } type `Application/vnd.oasis.opendocument.database` = `application/vnd.oasis.opendocument.database`.type case object `application/vnd.oasis.opendocument.formula` extends MediaType { val value = "application/vnd.oasis.opendocument.formula" } type `Application/vnd.oasis.opendocument.formula` = `application/vnd.oasis.opendocument.formula`.type case object `application/vnd.oasis.opendocument.graphics` extends MediaType { val value = "application/vnd.oasis.opendocument.graphics" } type `Application/vnd.oasis.opendocument.graphics` = `application/vnd.oasis.opendocument.graphics`.type case object `application/vnd.oasis.opendocument.image` extends MediaType { val value = "application/vnd.oasis.opendocument.image" } type `Application/vnd.oasis.opendocument.image` = `application/vnd.oasis.opendocument.image`.type case object `application/vnd.oasis.opendocument.presentation` extends MediaType { val value = "application/vnd.oasis.opendocument.presentation" } type `Application/vnd.oasis.opendocument.presentation` = `application/vnd.oasis.opendocument.presentation`.type case object `application/vnd.oasis.opendocument.spreadsheet` extends MediaType { val value = "application/vnd.oasis.opendocument.spreadsheet" } type `Application/vnd.oasis.opendocument.spreadsheet` = `application/vnd.oasis.opendocument.spreadsheet`.type case object `application/vnd.oasis.opendocument.text` extends MediaType { val value = "application/vnd.oasis.opendocument.text" } type `Application/vnd.oasis.opendocument.text` = `application/vnd.oasis.opendocument.text`.type case object `application/vnd.oasis.opendocument.text-master` extends MediaType { val value = "application/vnd.oasis.opendocument.text-master" } type `Application/vnd.oasis.opendocument.text-master` = `application/vnd.oasis.opendocument.text-master`.type case object `application/vnd.oasis.opendocument.text-web` extends MediaType { val value = "application/vnd.oasis.opendocument.text-web" } type `Application/vnd.oasis.opendocument.text-web` = `application/vnd.oasis.opendocument.text-web`.type case object `application/vnd.openxmlformats-officedocument.presentationml.presentation` extends MediaType { val value = "application/vnd.openxmlformats-officedocument.presentationml.presentation" } type `Application/vnd.openxmlformats-officedocument.presentationml.presentation` = `application/vnd.openxmlformats-officedocument.presentationml.presentation`.type case object `application/vnd.openxmlformats-officedocument.presentationml.slide` extends MediaType { val value = "application/vnd.openxmlformats-officedocument.presentationml.slide" } type `Application/vnd.openxmlformats-officedocument.presentationml.slide` = `application/vnd.openxmlformats-officedocument.presentationml.slide`.type case object `application/vnd.openxmlformats-officedocument.presentationml.slideshow` extends MediaType { val value = "application/vnd.openxmlformats-officedocument.presentationml.slideshow" } type `Application/vnd.openxmlformats-officedocument.presentationml.slideshow` = `application/vnd.openxmlformats-officedocument.presentationml.slideshow`.type case object `application/vnd.openxmlformats-officedocument.presentationml.template` extends MediaType { val value = "application/vnd.openxmlformats-officedocument.presentationml.template" } type `Application/vnd.openxmlformats-officedocument.presentationml.template` = `application/vnd.openxmlformats-officedocument.presentationml.template`.type case object `application/vnd.openxmlformats-officedocument.spreadsheetml.sheet` extends MediaType { val value = "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet" } type `Application/vnd.openxmlformats-officedocument.spreadsheetml.sheet` = `application/vnd.openxmlformats-officedocument.spreadsheetml.sheet`.type case object `application/vnd.openxmlformats-officedocument.spreadsheetml.template` extends MediaType { val value = "application/vnd.openxmlformats-officedocument.spreadsheetml.template" } type `Application/vnd.openxmlformats-officedocument.spreadsheetml.template` = `application/vnd.openxmlformats-officedocument.spreadsheetml.template`.type case object `application/vnd.openxmlformats-officedocument.wordprocessingml.document` extends MediaType { val value = "application/vnd.openxmlformats-officedocument.wordprocessingml.document" } type `Application/vnd.openxmlformats-officedocument.wordprocessingml.document` = `application/vnd.openxmlformats-officedocument.wordprocessingml.document`.type case object `application/vnd.openxmlformats-officedocument.wordprocessingml.template` extends MediaType { val value = "application/vnd.openxmlformats-officedocument.wordprocessingml.template" } type `Application/vnd.openxmlformats-officedocument.wordprocessingml.template` = `application/vnd.openxmlformats-officedocument.wordprocessingml.template`.type case object `application/x-7z-compressed` extends MediaType { val value = "application/x-7z-compressed" } type `Application/x-7z-compressed` = `application/x-7z-compressed`.type case object `application/x-ace-compressed` extends MediaType { val value = "application/x-ace-compressed" } type `Application/x-ace-compressed` = `application/x-ace-compressed`.type case object `application/x-apple-diskimage` extends MediaType { val value = "application/x-apple-diskimage" } type `Application/x-apple-diskimage` = `application/x-apple-diskimage`.type case object `application/x-arc-compressed` extends MediaType { val value = "application/x-arc-compressed" } type `Application/x-arc-compressed` = `application/x-arc-compressed`.type case object `application/x-bzip` extends MediaType { val value = "application/x-bzip" } type `Application/x-bzip` = `application/x-bzip`.type case object `application/x-bzip2` extends MediaType { val value = "application/x-bzip2" } type `Application/x-bzip2` = `application/x-bzip2`.type case object `application/x-chrome-extension` extends MediaType { val value = "application/x-chrome-extension" } type `Application/x-chrome-extension` = `application/x-chrome-extension`.type case object `application/x-compress` extends MediaType { val value = "application/x-compress" } type `Application/x-compress` = `application/x-compress`.type case object `application/x-compressed` extends MediaType { val value = "application/x-compressed" } type `Application/x-compressed` = `application/x-compressed`.type case object `application/x-debian-package` extends MediaType { val value = "application/x-debian-package" } type `Application/x-debian-package` = `application/x-debian-package`.type case object `application/x-dvi` extends MediaType { val value = "application/x-dvi" } type `Application/x-dvi` = `application/x-dvi`.type case object `application/x-font-truetype` extends MediaType { val value = "application/x-font-truetype" } type `Application/x-font-truetype` = `application/x-font-truetype`.type case object `application/x-font-opentype` extends MediaType { val value = "application/x-font-opentype" } type `Application/x-font-opentype` = `application/x-font-opentype`.type case object `application/x-gtar` extends MediaType { val value = "application/x-gtar" } type `Application/x-gtar` = `application/x-gtar`.type case object `application/x-gzip` extends MediaType { val value = "application/x-gzip" } type `Application/x-gzip` = `application/x-gzip`.type case object `application/x-latex` extends MediaType { val value = "application/x-latex" } type `Application/x-latex` = `application/x-latex`.type case object `application/x-rar-compressed` extends MediaType { val value = "application/x-rar-compressed" } type `Application/x-rar-compressed` = `application/x-rar-compressed`.type case object `application/x-redhat-package-manager` extends MediaType { val value = "application/x-redhat-package-manager" } type `Application/x-redhat-package-manager` = `application/x-redhat-package-manager`.type case object `application/x-shockwave-flash` extends MediaType { val value = "application/x-shockwave-flash" } type `Application/x-shockwave-flash` = `application/x-shockwave-flash`.type case object `application/x-tar` extends MediaType { val value = "application/x-tar" } type `Application/x-tar` = `application/x-tar`.type case object `application/x-tex` extends MediaType { val value = "application/x-tex" } type `Application/x-tex` = `application/x-tex`.type case object `application/x-texinfo` extends MediaType { val value = "application/x-texinfo" } type `Application/x-texinfo` = `application/x-texinfo`.type case object `application/x-vrml` extends MediaType { val value = "application/x-vrml" } type `Application/x-vrml` = `application/x-vrml`.type case object `application/x-www-form-urlencoded` extends MediaType { val value = "application/x-www-form-urlencoded" } type `Application/x-www-form-urlencoded` = `application/x-www-form-urlencoded`.type case object `application/x-x509-ca-cert` extends MediaType { val value = "application/x-x509-ca-cert" } type `Application/x-x509-ca-cert` = `application/x-x509-ca-cert`.type case object `application/x-xpinstall` extends MediaType { val value = "application/x-xpinstall" } type `Application/x-xpinstall` = `application/x-xpinstall`.type case object `application/xhtml+xml` extends MediaType { val value = "application/xhtml+xml" } type `Application/xhtml+xml` = `application/xhtml+xml`.type case object `application/xml-dtd` extends MediaType { val value = "application/xml-dtd" } type `Application/xml-dtd` = `application/xml-dtd`.type case object `application/xml` extends MediaType { val value = "application/xml" } type `Application/xml` = `application/xml`.type case object `application/zip` extends MediaType { val value = "application/zip" } type `Application/zip` = `application/zip`.type case object `audio/aiff` extends MediaType { val value = "audio/aiff" } type `Audio/aiff` = `audio/aiff`.type case object `audio/basic` extends MediaType { val value = "audio/basic" } type `Audio/basic` = `audio/basic`.type case object `audio/midi` extends MediaType { val value = "audio/midi" } type `Audio/midi` = `audio/midi`.type case object `audio/mod` extends MediaType { val value = "audio/mod" } type `Audio/mod` = `audio/mod`.type case object `audio/mpeg` extends MediaType { val value = "audio/mpeg" } type `Audio/mpeg` = `audio/mpeg`.type case object `audio/ogg` extends MediaType { val value = "audio/ogg" } type `Audio/ogg` = `audio/ogg`.type case object `audio/voc` extends MediaType { val value = "audio/voc" } type `Audio/voc` = `audio/voc`.type case object `audio/vorbis` extends MediaType { val value = "audio/vorbis" } type `Audio/vorbis` = `audio/vorbis`.type case object `audio/voxware` extends MediaType { val value = "audio/voxware" } type `Audio/voxware` = `audio/voxware`.type case object `audio/wav` extends MediaType { val value = "audio/wav" } type `Audio/wav` = `audio/wav`.type case object `audio/x-realaudio` extends MediaType { val value = "audio/x-reala" } type `Audio/x-realaudio` = `audio/x-realaudio`.type case object `audio/x-psid` extends MediaType { val value = "audio/x-psid" } type `Audio/x-psid` = `audio/x-psid`.type case object `audio/xm` extends MediaType { val value = "audio/xm" } type `Audio/xm` = `audio/xm`.type case object `audio/webm` extends MediaType { val value = "audio/webm" } type `Audio/webm` = `audio/webm`.type case object `image/gif` extends MediaType { val value = "image/gif" } type `Image/gif` = `image/gif`.type case object `image/jpeg` extends MediaType { val value = "image/jpeg" } type `Image/jpeg` = `image/jpeg`.type case object `image/pict` extends MediaType { val value = "image/pict" } type `Image/pict` = `image/pict`.type case object `image/png` extends MediaType { val value = "image/png" } type `Image/png` = `image/png`.type case object `image/svg+xml` extends MediaType { val value = "image/svg+xml" } type `Image/svg+xml` = `image/svg+xml`.type case object `image/svgz` extends MediaType { val value = "image/svgz" } type `Image/svgz` = `image/svgz`.type case object `image/tiff` extends MediaType { val value = "image/tiff" } type `Image/tiff` = `image/tiff`.type case object `image/x-icon` extends MediaType { val value = "image/x-icon" } type `Image/x-icon` = `image/x-icon`.type case object `image/x-ms-bmp` extends MediaType { val value = "image/x-ms-bmp" } type `Image/x-ms-bmp` = `image/x-ms-bmp`.type case object `image/x-pcx` extends MediaType { val value = "image/x-pcx" } type `Image/x-pcx` = `image/x-pcx`.type case object `image/x-pict` extends MediaType { val value = "image/x-pict" } type `Image/x-pict` = `image/x-pict`.type case object `image/x-quicktime` extends MediaType { val value = "image/x-quicktime" } type `Image/x-quicktime` = `image/x-quicktime`.type case object `image/x-rgb` extends MediaType { val value = "image/x-rgb" } type `Image/x-rgb` = `image/x-rgb`.type case object `image/x-xbitmap` extends MediaType { val value = "image/x-xbitmap" } type `Image/x-xbitmap` = `image/x-xbitmap`.type case object `image/x-xpixmap` extends MediaType { val value = "image/x-xpixmap" } type `Image/x-xpixmap` = `image/x-xpixmap`.type case object `image/webp` extends MediaType { val value = "image/webp" } type `Image/webp` = `image/webp`.type case object `message/http` extends MediaType { val value = "message/http" } type `Message/http` = `message/http`.type case object `message/delivery-status` extends MediaType { val value = "message/delivery-status" } type `Message/delivery-status` = `message/delivery-status`.type case object `message/rfc822` extends MediaType { val value = "message/rfc822" } type `Message/rfc822` = `message/rfc822`.type case object `text/asp` extends MediaType { val value = "text/asp" } type `Text/asp` = `text/asp`.type case object `text/cache-manifest` extends MediaType { val value = "text/cache-manifest" } type `Text/cache-manifest` = `text/cache-manifest`.type case object `text/calendar` extends MediaType { val value = "text/calendar" } type `Text/calendar` = `text/calendar`.type case object `text/css` extends MediaType { val value = "text/css" } type `Text/css` = `text/css`.type case object `text/csv` extends MediaType { val value = "text/css" } type `Text/csv` = `text/csv`.type case object `text/event-stream` extends MediaType { val value = "text/event-stream" } type `Text/event-stream` = `text/event-stream`.type case object `text/html` extends MediaType { val value = "text/html" } type `Text/html` = `text/html`.type case object `text/markdown` extends MediaType { val value = "text/markdown" } type `Text/markdown` = `text/markdown`.type case object `text/mcf` extends MediaType { val value = "text/mcf" } type `Text/mcf` = `text/mcf`.type case object `text/plain` extends MediaType { val value = "text/plain" } type `Text/plain` = `text/plain`.type case object `text/richtext` extends MediaType { val value = "text/richtext" } type `Text/richtext` = `text/richtext`.type case object `text/tab-separated-values` extends MediaType { val value = "text/tab-separated-values" } type `Text/tab-separated-values` = `text/tab-separated-values`.type case object `text/uri-list` extends MediaType { val value = "text/uri-list" } type `Text/uri-list` = `text/uri-list`.type case object `text/vnd.wap.wml` extends MediaType { val value = "text/vnd.wap.wml" } type `Text/vnd.wap.wml` = `text/vnd.wap.wml`.type case object `text/vnd.wap.wmlscript` extends MediaType { val value = "text/vnd.wap.wmlscript" } type `Text/vnd.wap.wmlscript` = `text/vnd.wap.wmlscript`.type case object `text/x-asm` extends MediaType { val value = "text/x-asm" } type `Text/x-asm` = `text/x-asm`.type case object `text/x-c` extends MediaType { val value = "text/x-c" } type `Text/x-c` = `text/x-c`.type case object `text/x-component` extends MediaType { val value = "text/x-component" } type `Text/x-component` = `text/x-component`.type case object `text/x-h` extends MediaType { val value = "text/x-h" } type `Text/x-h` = `text/x-h`.type case object `text/x-java-source` extends MediaType { val value = "text/x-java-source" } type `Text/x-java-source` = `text/x-java-source`.type case object `text/x-pascal` extends MediaType { val value = "text/x-pascal" } type `Text/x-pascal` = `text/x-pascal`.type case object `text/x-script` extends MediaType { val value = "text/x-script" } type `Text/x-script` = `text/x-script`.type case object `text/x-scriptcsh` extends MediaType { val value = "text/x-scriptcsh" } type `Text/x-scriptcsh` = `text/x-scriptcsh`.type case object `text/x-scriptelisp` extends MediaType { val value = "text/x-scriptelisp" } type `Text/x-scriptelisp` = `text/x-scriptelisp`.type case object `text/x-scriptksh` extends MediaType { val value = "text/x-scriptksh" } type `Text/x-scriptksh` = `text/x-scriptksh`.type case object `text/x-scriptlisp` extends MediaType { val value = "text/x-scriptlisp" } type `Text/x-scriptlisp` = `text/x-scriptlisp`.type case object `text/x-scriptperl` extends MediaType { val value = "text/x-scriptperl" } type `Text/x-scriptperl` = `text/x-scriptperl`.type case object `text/x-scriptperl-module` extends MediaType { val value = "text/x-scriptperl-module" } type `Text/x-scriptperl-module` = `text/x-scriptperl-module`.type case object `text/x-scriptphyton` extends MediaType { val value = "text/x-scriptphyton" } type `Text/x-scriptphyton` = `text/x-scriptphyton`.type case object `text/x-scriptrexx` extends MediaType { val value = "text/x-scriptrexx" } type `Text/x-scriptrexx` = `text/x-scriptrexx`.type case object `text/x-scriptscheme` extends MediaType { val value = "text/x-scriptscheme" } type `Text/x-scriptscheme` = `text/x-scriptscheme`.type case object `text/x-scriptsh` extends MediaType { val value = "text/x-scriptsh" } type `Text/x-scriptsh` = `text/x-scriptsh`.type case object `text/x-scripttcl` extends MediaType { val value = "text/x-scripttcl" } type `Text/x-scripttcl` = `text/x-scripttcl`.type case object `text/x-scripttcsh` extends MediaType { val value = "text/x-scripttcsh" } type `Text/x-scripttcsh` = `text/x-scripttcsh`.type case object `text/x-scriptzsh` extends MediaType { val value = "text/x-scriptzsh" } type `Text/x-scriptzsh` = `text/x-scriptzsh`.type case object `text/x-server-parsed-html` extends MediaType { val value = "text/x-server-parsed-html" } type `Text/x-server-parsed-html` = `text/x-server-parsed-html`.type case object `text/x-setext` extends MediaType { val value = "text/x-setext" } type `Text/x-setext` = `text/x-setext`.type case object `text/x-sgml` extends MediaType { val value = "text/x-sgml" } type `Text/x-sgml` = `text/x-sgml`.type case object `text/x-speech` extends MediaType { val value = "text/x-speech" } type `Text/x-speech` = `text/x-speech`.type case object `text/x-uuencode` extends MediaType { val value = "text/x-uuencode" } type `Text/x-uuencode` = `text/x-uuencode`.type case object `text/x-vcalendar` extends MediaType { val value = "text/x-vcalendar" } type `Text/x-vcalendar` = `text/x-vcalendar`.type case object `text/x-vcard` extends MediaType { val value = "text/x-vcard" } type `Text/x-vcard` = `text/x-vcard`.type case object `text/xml` extends MediaType { val value = "text/xml" } type `Text/xml` = `text/xml`.type } ================================================ FILE: shared/src/main/scala/typedapi/shared/TypeCarrier.scala ================================================ package typedapi.shared import shapeless._ import scala.language.higherKinds /** As the name says this case class is only there it pass types around on the value level. */ final case class TypeCarrier[A]() /** Derive a [[TypeCarrier]] from a type parameter and a singleton type. */ final class PairTypeFromWitnessKey[F[_, _], V] { def apply[K](wit: Witness.Lt[K]): TypeCarrier[F[K, V]] = TypeCarrier() } /** Derive a [[TypeCarrier]] from two singleton types. */ final class PairTypeFromWitnesses[F[_, _]] { def apply[K, V](kWit: Witness.Lt[K], vWit: Witness.Lt[V]): TypeCarrier[F[K, V]] = TypeCarrier() } /** Specific [[TypeCarrier]] for complete API types. */ final case class ApiTypeCarrier[H <: HList]() { def :|:[H1 <: HList](next: ApiTypeCarrier[H1]): CompositionCons[H1 :: H :: HNil] = CompositionCons() } /** Specific [[TypeCarrier]] for multiple API types. */ final case class CompositionCons[H <: HList]() { def :|:[H1 <: HList](next: ApiTypeCarrier[H1]): CompositionCons[H1 :: H] = CompositionCons() } ================================================ FILE: shared/src/main/scala/typedapi/shared/TypeLevelFoldLeft.scala ================================================ package typedapi.shared import shapeless._ import scala.annotation.implicitNotFound // INTERNAL API /** Reimplements shapeles Case2 but on the type level (no real HList instance). */ @implicitNotFound("""Woops, you shouldn't be here. We cannot find TypeLevelFoldFunction instance. input: ${In} aggregation: ${Agg}""") sealed trait TypeLevelFoldFunction[In, Agg] { type Out } object TypeLevelFoldFunction { type Aux[In, Agg, Out0] = TypeLevelFoldFunction[In, Agg] { type Out = Out0 } def at[In, Agg, Out0]: Aux[In, Agg, Out0] = new TypeLevelFoldFunction[In, Agg] { type Out = Out0 } } /** Reimplements shapeless LeftFolder but on the type level (no real HList instance) */ @implicitNotFound("""Woops, you shouldn't be here. We cannot find TypeLevelFold instance. list: ${H} aggregation: ${Agg}""") sealed trait TypeLevelFoldLeft[H <: HList, Agg] extends Serializable { type Out } object TypeLevelFoldLeft { type Aux[H <: HList, Agg, Out0] = TypeLevelFoldLeft[H, Agg] { type Out = Out0 } } trait TypeLevelFoldLeftLowPrio { implicit def hnilCase[Agg]: TypeLevelFoldLeft.Aux[HNil, Agg, Agg] = new TypeLevelFoldLeft[HNil, Agg] { type Out = Agg } implicit def foldCase[H, T <: HList, Agg, FtOut, FOut](implicit f: TypeLevelFoldFunction.Aux[H, Agg, FtOut], next: Lazy[TypeLevelFoldLeft.Aux[T, FtOut, FOut]]): TypeLevelFoldLeft.Aux[H :: T, Agg, FOut] = new TypeLevelFoldLeft[H :: T, Agg] { type Out = next.value.Out } } /** Helper to work on a composition of HLists we want to fold over. */ @implicitNotFound("""Woops, you shouldn't be here. We cannot find TypeLevelFoldList instance. apis: ${H}""") trait TypeLevelFoldLeftList[H <: HList] { type Out <: HList } object TypeLevelFoldLeftList { type Aux[H <: HList, Out0 <: HList] = TypeLevelFoldLeftList[H] { type Out = Out0 } } trait TypeLevelFoldLeftListLowPrio { implicit def lastFoldLeftList[H <: HList, Agg](implicit folder0: TypeLevelFoldLeft[H, Agg]) = new TypeLevelFoldLeftList[H :: HNil] { type Out = folder0.Out :: HNil } implicit def folderLeftList[H <: HList, Agg, T <: HList](implicit folder0: TypeLevelFoldLeft[H, Agg], list: TypeLevelFoldLeftList[T]) = new TypeLevelFoldLeftList[H :: T] { type Out = folder0.Out :: list.Out } } ================================================ FILE: shared/src/main/scala/typedapi/shared/WitnessToString.scala ================================================ package typedapi.shared import scala.annotation.implicitNotFound @implicitNotFound("Couldn't find transformation for witness ${K} to String.") sealed trait WitnessToString[K] { def show(key: K): String } trait WitnessToStringLowPrio { implicit def symbolKey[K <: Symbol] = new WitnessToString[K] { def show(key: K): String = key.name } implicit def stringKey[K <: String] = new WitnessToString[K] { def show(key: K): String = key } } ================================================ FILE: shared/src/main/scala/typedapi/shared/package.scala ================================================ package typedapi import shapeless.Witness package object shared { final val BodyField = Witness('body) final val RawHeadersField = Witness('rawHeaders) } ================================================ FILE: shared/src/main/scala/typedapi/util/Decoder.scala ================================================ package typedapi.util import scala.language.higherKinds trait Decoder[F[_], A] extends (String => F[Either[Exception, A]]) object Decoder { def apply[F[_], A](decoder: String => F[Either[Exception, A]]): Decoder[F, A] = new Decoder[F, A] { def apply(raw: String): F[Either[Exception, A]] = decoder(raw) } } ================================================ FILE: shared/src/main/scala/typedapi/util/Encoder.scala ================================================ package typedapi.util import scala.language.higherKinds trait Encoder[F[_], A] extends (A => F[String]) object Encoder { def apply[F[_], A](encoder: A => F[String]): Encoder[F, A] = new Encoder[F, A] { def apply(a: A): F[String] = encoder(a) } } ================================================ FILE: shared/src/test/scala/typedapi/ApiDefinitionSpec.scala ================================================ package typedapi import typedapi.shared._ import shapeless._ import SpecUtil._ // compilation-only test object ApiDefinitionSpec { import MediaTypes._ case class Foo() val testW = Witness("test") val test2W = Witness("test2") val fooW = Witness('foo) val barW = Witness('bah) type Base = PathElement[testW.T] :: HNil // path lists testCompile(Root)[HNil] testCompile(Root / "test")[PathElement[testW.T] :: HNil] testCompile(Root / "test" / "test2")[PathElement[test2W.T] :: PathElement[testW.T] :: HNil] testCompile(Root / "test" / Segment[Int]('foo))[SegmentParam[fooW.T, Int] :: PathElement[testW.T] :: HNil] testCompile(Root / Segment[Int]('foo) / "test")[PathElement[testW.T] :: SegmentParam[fooW.T, Int] :: HNil] // query lists testCompile(Queries)[HNil] testCompile(Queries add[Int](fooW))[QueryParam[fooW.T, Int] :: HNil] testCompile(Queries add[Int](fooW) add[Int](barW))[QueryParam[barW.T, Int] :: QueryParam[fooW.T, Int] :: HNil] // header lists testCompile(Headers)[HNil] testCompile(Headers add[Int](fooW))[HeaderParam[fooW.T, Int] :: HNil] testCompile(Headers add[Int](fooW) add[Int](barW))[HeaderParam[barW.T, Int] :: HeaderParam[fooW.T, Int] :: HNil] testCompile(Headers add(fooW, testW))[FixedHeaderElement[fooW.T, testW.T] :: HNil] testCompile(Headers client(fooW, testW))[ClientHeaderElement[fooW.T, testW.T] :: HNil] testCompile(Headers client[String](fooW))[ClientHeaderParam[fooW.T, String] :: HNil] testCompile(Headers.clientColl[String])[ClientHeaderCollParam[String] :: HNil] testCompile(Headers serverSend(fooW, testW))[ServerHeaderSendElement[fooW.T, testW.T] :: HNil] testCompile(Headers serverMatch[String](fooW))[ServerHeaderMatchParam[fooW.T, String] :: HNil] testCompile(Headers serverMatch[String](fooW))[ServerHeaderMatchParam[fooW.T, String] :: HNil] // methods testCompile(api(Get[Json, Foo]))[GetElement[`Application/json`, Foo] :: HNil] test.illTyped("apiWothBody(Get[Foo], ReqBody[Foo])") testCompile(api(Put[Json, Foo]))[PutElement[`Application/json`, Foo] :: HNil] testCompile(apiWithBody(Put[Json, Foo], ReqBody[Plain, Foo]))[PutWithBodyElement[`Text/plain`, Foo, `Application/json`, Foo] :: HNil] testCompile(api(Post[Json, Foo]))[PostElement[`Application/json`, Foo] :: HNil] testCompile(apiWithBody(Post[Json, Foo], ReqBody[Plain, Foo]))[PostWithBodyElement[`Text/plain`, Foo, `Application/json`, Foo] :: HNil] testCompile(api(Delete[Json, Foo]))[DeleteElement[`Application/json`, Foo] :: HNil] test.illTyped("apiWothBody(Delete[Json, Foo], ReqBody[Plain, Foo])") // whole api testCompile( api(Get[Json, Foo], Root / "test" / Segment[Int]('foo), Queries add[String]('foo), Headers add[Double]('foo)) )[GetElement[`Application/json`, Foo] :: HeaderParam[fooW.T, Double] :: QueryParam[fooW.T, String] :: SegmentParam[fooW.T, Int] :: PathElement[testW.T] :: HNil] } ================================================ FILE: shared/src/test/scala/typedapi/SpecUtil.scala ================================================ package typedapi import shapeless.HList import scala.language.higherKinds object SpecUtil { class TestHelper[Act <: HList] { def apply[Exp <: HList](implicit ev: Act =:= Exp) = Unit } def testCompile[F[_ <: HList], Act <: HList](cons: F[Act]) = new TestHelper[Act] } ================================================ FILE: shared/src/test/scala/typedapi/dsl/ApiDslSpec.scala ================================================ package typedapi.dsl import typedapi.SpecUtil._ import typedapi.shared._ import shapeless._ // compilation-only test object ApiDslSpec { import MediaTypes._ case class Foo() val testW = Witness("test") val fooW = Witness('foo) val base = := :> "test" type Base = PathElement[testW.T] :: HNil val a = Query[Int].apply(fooW) // empty path testCompile(:= :> Segment[Int](fooW))[SegmentParam[fooW.T, Int] :: HNil] testCompile(:= :> Query[Int](fooW))[QueryParam[fooW.T, Int] :: HNil] testCompile(:= :> Header[Int](fooW))[HeaderParam[fooW.T, Int] :: HNil] testCompile(:= :> Get[Json, Foo])[GetElement[`Application/json`, Foo] :: HNil] // path: add every element testCompile(base :> Segment[Int](fooW))[SegmentParam[fooW.T, Int] :: Base] testCompile(base :> Query[Int](fooW))[QueryParam[fooW.T, Int] :: Base] testCompile(base :> Header[Int](fooW))[HeaderParam[fooW.T, Int] :: Base] testCompile(base :> Get[Json, Foo])[GetElement[`Application/json`, Foo] :: Base] // segment: add every element val _baseSeg = base :> Segment[Int](fooW) type _BaseSeg = SegmentParam[fooW.T, Int] :: Base testCompile(_baseSeg :> Segment[Int](fooW))[SegmentParam[fooW.T, Int] :: _BaseSeg] testCompile(_baseSeg :> Query[Int](fooW))[QueryParam[fooW.T, Int] :: _BaseSeg] testCompile(_baseSeg :> Header[Int](fooW))[HeaderParam[fooW.T, Int] :: _BaseSeg] testCompile(_baseSeg :> Get[Json, Foo])[GetElement[`Application/json`, Foo] :: _BaseSeg] // query: add queries, headers, body and final val _baseQ = base :> Query[Int](fooW) type _BaseQ = QueryParam[fooW.T, Int] :: Base test.illTyped("_baseQ :> \"fail\"") test.illTyped("_baseQ :> Segment[Int](fooW)") testCompile(_baseQ :> Query[Int](fooW))[QueryParam[fooW.T, Int] :: _BaseQ] testCompile(_baseQ :> Header[Int](fooW))[HeaderParam[fooW.T, Int] :: _BaseQ] testCompile(_baseQ :> Get[Json, Foo])[GetElement[`Application/json`, Foo] :: _BaseQ] // header: add header, final val _baseH = base :> Header[Int](fooW) type _BaseH = HeaderParam[fooW.T, Int] :: Base test.illTyped("_baseH :> \"fail\"") test.illTyped("_baseH :> Segment[Int](fooW)") test.illTyped("_baseH :> Query[Int](fooW)") testCompile(_baseH :> Header[Int](fooW))[HeaderParam[fooW.T, Int] :: _BaseH] testCompile(_baseH :> Header(fooW, testW) :> Header[Int](fooW))[HeaderParam[fooW.T, Int] :: FixedHeaderElement[fooW.T, testW.T] :: _BaseH] testCompile(_baseH :> Client.Header[String](fooW))[ClientHeaderParam[fooW.T, String] :: _BaseH] testCompile(_baseH :> Client.Header(fooW, testW))[ClientHeaderElement[fooW.T, testW.T] :: _BaseH] testCompile(_baseH :> Server.Send(fooW, testW))[ServerHeaderSendElement[fooW.T, testW.T] :: _BaseH] testCompile(_baseH :> Server.Match[String](fooW))[ServerHeaderMatchParam[fooW.T, String] :: _BaseH] testCompile(_baseH :> Get[Json, Foo])[GetElement[`Application/json`, Foo] :: _BaseH] // request body: add put or post val _baseRB = base :> ReqBody[Plain, Foo] type _BaseRB = Base test.illTyped("_baseRB :> Segment[Int](fooW)") test.illTyped("_baseRB :> Query[Int](fooW)") test.illTyped("_baseRB :> Header[Int](fooW)") test.illTyped("_baseRB :> Get[Json, Foo]") testCompile(_baseRB :> Put[Json, Foo])[PutWithBodyElement[`Text/plain`, Foo, `Application/json`, Foo] :: _BaseRB] testCompile(_baseRB :> Post[Json, Foo])[PostWithBodyElement[`Text/plain`, Foo, `Application/json`, Foo] :: _BaseRB] // method: nothing at all val _baseF = base :> Get[Json, Foo] test.illTyped("_baseF :> Segment[Int](fooW)") test.illTyped("_baseF :> Query[Int](fooW)") test.illTyped("_baseF :> Header[Int](fooW)") test.illTyped("_baseF :> Get[Json, Foo]") } ================================================ FILE: shared/src/test/scala/typedapi/shared/ApiTransformerSpec.scala ================================================ package typedapi.shared import typedapi.Json import shapeless._ import shapeless.labelled.FieldType // compilation-only test final class ApiTransformerSpec extends TypeLevelFoldLeftLowPrio with ApiTransformer { case class Foo() def testCompile[H <: HList, Out](implicit folder: TypeLevelFoldLeft.Aux[H, Unit, Out]): TypeLevelFoldLeft.Aux[H, Unit, Out] = folder val pathW = Witness("test") val fooW = Witness('foo) val barW = Witness('bar) testCompile[GetElement[Json, Foo] :: HNil, (HNil, HNil, HNil, GetCall, FieldType[Json, Foo])] testCompile[PutElement[Json, Foo] :: HNil, (HNil, HNil, HNil, PutCall, FieldType[Json, Foo])] testCompile[PutWithBodyElement[Json, Foo, Json, Foo] :: HNil, (HNil, FieldType[Json, BodyField.T] :: HNil, Foo :: HNil, PutWithBodyCall, FieldType[Json, Foo])] testCompile[PostElement[Json, Foo] :: HNil, (HNil, HNil, HNil, PostCall, FieldType[Json, Foo])] testCompile[PostWithBodyElement[Json, Foo, Json, Foo] :: HNil, (HNil, FieldType[Json, BodyField.T] :: HNil, Foo :: HNil, PostWithBodyCall, FieldType[Json, Foo])] testCompile[DeleteElement[Json, Foo] :: HNil, (HNil, HNil, HNil, DeleteCall, FieldType[Json, Foo])] testCompile[GetElement[Json, Foo] :: PathElement[pathW.T] :: HNil, (pathW.T :: HNil, HNil, HNil, GetCall, FieldType[Json, Foo])] testCompile[GetElement[Json, Foo] :: SegmentParam[fooW.T, String] :: HNil, (SegmentInput :: HNil, fooW.T :: HNil, String :: HNil, GetCall, FieldType[Json, Foo])] testCompile[GetElement[Json, Foo] :: QueryParam[fooW.T, String] :: HNil, (QueryInput :: HNil, fooW.T :: HNil, String :: HNil, GetCall, FieldType[Json, Foo])] testCompile[GetElement[Json, Foo] :: QueryParam[fooW.T, List[String]] :: HNil, (QueryInput :: HNil, fooW.T :: HNil, List[String] :: HNil, GetCall, FieldType[Json, Foo])] testCompile[GetElement[Json, Foo] :: HeaderParam[fooW.T, String] :: HNil, (HeaderInput :: HNil, fooW.T :: HNil, String :: HNil, GetCall, FieldType[Json, Foo])] testCompile[GetElement[Json, Foo] :: FixedHeaderElement[fooW.T, barW.T] :: HNil, (FixedHeader[fooW.T, barW.T] :: HNil, HNil, HNil, GetCall, FieldType[Json, Foo])] testCompile[GetElement[Json, Foo] :: ClientHeaderParam[fooW.T, String] :: HNil, (ClientHeaderInput :: HNil, fooW.T :: HNil, String :: HNil, GetCall, FieldType[Json, Foo])] testCompile[GetElement[Json, Foo] :: ClientHeaderElement[fooW.T, barW.T] :: HNil, (ClientHeader[fooW.T, barW.T] :: HNil, HNil, HNil, GetCall, FieldType[Json, Foo])] testCompile[GetElement[Json, Foo] :: ClientHeaderCollParam[Int] :: HNil, (ClientHeaderCollInput :: HNil, HNil, Map[String, Int] :: HNil, GetCall, FieldType[Json, Foo])] testCompile[GetElement[Json, Foo] :: ServerHeaderMatchParam[fooW.T, String] :: HNil, (ServerHeaderMatchInput :: HNil, fooW.T :: HNil, Map[String, String] :: HNil, GetCall, FieldType[Json, Foo])] testCompile[GetElement[Json, Foo] :: ServerHeaderSendElement[fooW.T, barW.T] :: HNil, (ServerHeaderSend[fooW.T, barW.T] :: HNil, HNil, HNil, GetCall, FieldType[Json, Foo])] testCompile[ GetElement[Json, Foo] :: HeaderParam[fooW.T, Boolean] :: QueryParam[fooW.T, Int] :: SegmentParam[fooW.T, String] :: PathElement[pathW.T] :: HNil, (pathW.T :: SegmentInput :: QueryInput :: HeaderInput :: HNil, fooW.T :: fooW.T :: fooW.T :: HNil, String :: Int :: Boolean :: HNil, GetCall, FieldType[Json, Foo]) ] }