├── .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 /.gitignore: -------------------------------------------------------------------------------- 1 | # ensime 2 | *.*~ 3 | .ensime 4 | .ensime_cache 5 | project/ensime-plugin.sbt 6 | 7 | # sbt 8 | target/ 9 | project/target/ 10 | project/project/ -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: scala 2 | 3 | scala: 4 | - 2.11.11 5 | - 2.12.3 6 | 7 | jdk: 8 | - oraclejdk8 9 | 10 | script: 11 | - sbt ++$TRAVIS_SCALA_VERSION clean test 12 | - sbt "project sharedJS" "fastOptJS" 13 | - sbt "project js-client" "fastOptJS" 14 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ### 0.2.0 2 | - `StatusCodes` and `MediaTypes` are in distinct `object`s, thus, have to be imported explicitly 3 | - improved Ammonite integration 4 | - internal: separated decoded and raw requests with `RawApiRequest` and `ApiRequest` 5 | - fixed `implicitNotFound` message for `ApiRequest` 6 | - 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) 7 | - server endpoints have to define a response code: 8 | ```Scala 9 | derive[IO](Api) { input => 10 | success(/*...*/) 11 | ^ 12 | } 13 | ``` 14 | 15 | 16 | ### 0.1.0 17 | - internal cleanups and refactorings 18 | - extended example project and added ScalaJS client 19 | - centralized http-support specs 20 | - added akka-http support on server and client-side 21 | - added scalaj-http support on the client-side 22 | - added ScalaJS compilation support for shared and client code 23 | - implemented basic ScalaJS client 24 | - added body encoding types and made them mandatory (several hundred Mediatypes supported) 25 | ```Scala 26 | := :> ReqBody[Json, User] :> Get[Json, User] 27 | _______________^__________________^ 28 | ``` 29 | 30 | - `RawHeaders` was removed 31 | - fixed headers were added; a fixed header is a statically known key-value pair, therefore, no input is required 32 | ```Scala 33 | // dsl 34 | := :> Header("Access-Control-Allow-Origin", "*") :> Get[Json, User] 35 | 36 | // function 37 | api(Get[Json, User], headers = Headers add("Access-Control-Allow-Origin", "*")) 38 | ``` 39 | 40 | - changes to the server API: 41 | - `NoReqBodyExecutor` and `ReqBodyExecutor` now expect a `MethodType`: 42 | ```Scala 43 | new NoReqBodyExecutor[El, KIn, VIn, M, F, FOut] { 44 | ____________________________________^ 45 | 46 | new ReqBodyExecutor[El, KIn, VIn, Bd, M, ROut, POut, F, FOut] { 47 | ______________________________________^ 48 | ``` 49 | 50 | - fixed header only sent by the server 51 | ```Scala 52 | := :> Server.Send("Access-Control-Allow-Origin", "*") :> Get[Json, User] 53 | 54 | api(Get[Json, User], Headers.serverSend("Access-Control-Allow-Origin", "*")) 55 | ``` 56 | - extract headers which have keys that match a `String` 57 | ```Scala 58 | := :> Server.Match[String]("Control") :> Get[Json, User] 59 | 60 | api(Get[Json, User], Headers.serverMatch[String]("Control")) 61 | ``` 62 | - changes to the client API: 63 | - new encoding types add `Content-Type` and `Accept` headers 64 | - fixed header only sent by the client 65 | ```Scala 66 | := :> Client.Header("Access-Control-Allow-Origin", "*") :> Get[Json, User] 67 | 68 | api(Get[Json, User], Headers.client("Access-Control-Allow-Origin", "*")) 69 | ``` 70 | - send dynamic header ignore it on the server-side 71 | ```Scala 72 | := :> Client.Header[String]("Foo") :> Get[Json, User] 73 | 74 | api(Get[Json, User], Headers.client[String]("Foo")) 75 | ``` 76 | 77 | ### 0.1.0-RC5 / Almost there 78 | - changes to the client API: 79 | ```Scala 80 | val ApiList = 81 | (:= :> Get[Foo]) :|: 82 | (:= :> RequestBody[Foo] :> Put[Foo]) 83 | 84 | // `:|:` removed for API compositions 85 | val (get, put) = deriveAll(ApiList) 86 | ``` 87 | 88 | - changes to the server API: 89 | ```Scala 90 | // same for endpoint compositions 91 | val e = deriveAll[IO](ApiList).from(get, put) 92 | ``` 93 | 94 | ### 0.1.0-RC4 / Towards a stable API 95 | - changes to the client API: 96 | ```Scala 97 | val Api = := :> Get[Foo] 98 | val ApiList = 99 | (:= :> Get[Foo]) :|: 100 | (:= :> RequestBody[Foo] :> Put[Foo]) 101 | 102 | // not `compile`, but 103 | val foo = derive(Api) 104 | 105 | val (foo2 :|: bar :|: =:) = deriveAll(ApiList) 106 | 107 | ... 108 | // explicitly pass ClientManager 109 | foo().run[IO](cm) 110 | _______________^ 111 | ``` 112 | 113 | - changes to the server API 114 | ```Scala 115 | // not `link.to`, but 116 | val endpoint = derive[IO](Api).from(...) 117 | 118 | val endpoints = deriveAll[IO](ApiList).from(...) 119 | ``` 120 | 121 | - major changes were applied to the internal code to reach a stable state (see [this PR](https://github.com/pheymann/typedapi/pull/13)) 122 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 Paul Heymann 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![Build Status](https://travis-ci.org/pheymann/typedapi.svg?branch=master)](https://travis-ci.org/pheymann/typedapi) 2 | [![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) 3 | [![Gitter](https://badges.gitter.im/Join%20Chat.svg)](https://gitter.im/pheymann/Lobby) 4 | [![Scala.js](https://www.scala-js.org/assets/badges/scalajs-0.6.17.svg)](https://www.scala-js.org/) 5 | 6 | *experimental project*: see issues [#39](https://github.com/pheymann/typedapi/issues/39) and [#41](https://github.com/pheymann/typedapi/issues/41) 7 | 8 | # Typedapi 9 | Define type safe APIs and let the Scala compiler do the rest: 10 | 11 | ### Api definition 12 | ```Scala 13 | import typedapi._ 14 | 15 | val MyApi = 16 | // GET {body: User} /fetch/user?{name: String} 17 | api(method = Get[MT.`application/json`, User], 18 | path = Root / "fetch" / "user", 19 | queries = Queries add Query[String]('name)) :|: 20 | // POST {body: User} /create/user 21 | apiWithBody(method = Post[MT.`application/json`, User], 22 | body = ReqBody[Json, User], 23 | path = Root / "create" / "user") 24 | ``` 25 | 26 | And for the Servant lovers: 27 | 28 | ```Scala 29 | import typedapi.dsl._ 30 | 31 | val MyApi = 32 | // GET {body: User} /fetch/user?{name: String} 33 | (:= :> "fetch" :> "user" :> Query[String]('name) :> Get[MT.`application/json`, User]) :|: 34 | // POST {body: User} /create/user 35 | (:= :> "create" :> "user" :> ReqBody[Json, User] :> Post[MT.`application/json`, User]) 36 | ``` 37 | 38 | ### Client side 39 | ```Scala 40 | import typedapi.client._ 41 | 42 | val (fetch, create) = deriveAll(MyApi) 43 | 44 | import typedapi.client.http4s._; import cats.effect.IO; import org.http4s.client.blaze.Http1Client 45 | 46 | val cm = ClientManager(Http1Client[IO]().unsafeRunSync, "http://my-host", 8080) 47 | 48 | fetch("joe").run[IO](cm): IO[User] 49 | ``` 50 | 51 | ### Server side 52 | ```Scala 53 | import typedapi.server._ 54 | 55 | val fetch: String => IO[Result[User]] = name => findUserIO(name).map(success) 56 | val create: User => IO[Result[User]] = user => createUserIO(user).map(success) 57 | 58 | val endpoints = deriveAll[IO](MyApi).from(fetch, create) 59 | 60 | import typedapi.server.http4s._; import cats.effect.IO; import org.http4s.server.blaze.BlazeBuilder 61 | 62 | val sm = ServerManager(BlazeBuilder[IO], "http://my-host", 8080) 63 | val server = mount(sm, endpoints) 64 | 65 | server.unsafeRunSync() 66 | ``` 67 | 68 | This is all you have to do to define an API with multiple endpoints and to create a working client and server for them. 69 | 70 | You can find the above code as a complete project [here](https://github.com/pheymann/typedapi/tree/master/docs/example). 71 | 72 | ## Motivation 73 | This library is the result of the following questions: 74 | 75 | > 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? 76 | 77 | 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: 78 | 79 | - [http4s](https://github.com/http4s/http4s) 80 | - [akka-http](https://github.com/akka/akka-http) 81 | - [scalaj-http](https://github.com/scalaj/scalaj-http) on the client-side 82 | - ScalaJS on the client-side 83 | 84 | 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). 85 | 86 | ## Get this library 87 | It is available for Scala 2.11, 2.12 and ScalaJS and can be downloaded as Maven artifact: 88 | 89 | ``` 90 | // dsl 91 | "com.github.pheymann" %% "typedapi-client" % 92 | "com.github.pheymann" %% "typedapi-server" % 93 | 94 | // http4s support 95 | "com.github.pheymann" %% "typedapi-http4s-client" % 96 | "com.github.pheymann" %% "typedapi-http4s-server" % 97 | 98 | // akka-http support 99 | "com.github.pheymann" %% "typedapi-akka-http-client" % 100 | "com.github.pheymann" %% "typedapi-akka-http-server" % 101 | 102 | // Scalaj-Http client support 103 | "com.github.pheymann" %% "typedapi-scalaj-http-client" % 104 | 105 | // ScalaJS client support 106 | "com.github.pheymann" %% "typedapi-js-client" % 107 | ``` 108 | 109 | You can also build it on your machine: 110 | 111 | ``` 112 | git clone https://github.com/pheymann/typedapi.git 113 | cd typedapi 114 | sbt "+ publishLocal" 115 | ``` 116 | 117 | ## Ammonite 118 | Typedapi also offers an improved experience for [Ammonite](http://ammonite.io/#Ammonite-REPL) and [ScalaScripts](http://ammonite.io/#ScalaScripts): 119 | 120 | ```Scala 121 | import $ivy.`com.github.pheymann::typedapi-ammonite-client:` 122 | 123 | import typedapi._ 124 | import client._ 125 | import amm._ 126 | 127 | val Readme = api(Get[MT.`text/html`, String], Root / "pheymann" / "typedapi" / "master" / "README.md") 128 | val readme = derive(Readme) 129 | 130 | // gives you the raw scalaj-http response 131 | val cm = clientManager("https://raw.githubusercontent.com") 132 | val response = get().run[Id].raw(cm) 133 | 134 | response.body 135 | response.headers 136 | ... 137 | ``` 138 | 139 | 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. 140 | 141 | ## Documentation 142 | The documentation is located in [docs](https://github.com/pheymann/typedapi/blob/master/docs) and covers the following topics so far: 143 | - [How to define an API](https://github.com/pheymann/typedapi/blob/master/docs/ApiDefinition.md) 144 | - [How to create a client](https://github.com/pheymann/typedapi/blob/master/docs/ClientCreation.md) 145 | - [How to create a server](https://github.com/pheymann/typedapi/blob/master/docs/ServerCreation.md) 146 | - [Extend the library](https://github.com/pheymann/typedapi/blob/master/docs/ExtendIt.md) 147 | - Typelevel Summit 2018 Berlin Talk [Typedapi: Define your API on the type-level](https://github.com/pheymann/typelevel-summit-2018) 148 | - and a [post](https://typelevel.org/blog/2018/06/15/typedapi.html) on the Typelevel Blog describing the basic concept behind this library. 149 | 150 | ## Dependencies 151 | - [shapeless 2.3.3](https://github.com/milessabin/shapeless/) 152 | 153 | ## Contribution 154 | 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. 155 | -------------------------------------------------------------------------------- /akka-http-server/src/main/scala/typedapi/server/akkahttp/package.scala: -------------------------------------------------------------------------------- 1 | package typedapi.server 2 | 3 | import typedapi.shared.MethodType 4 | import shapeless._ 5 | import shapeless.ops.hlist.Prepend 6 | import akka.http.scaladsl.{HttpExt, Http} 7 | import akka.http.scaladsl.model._ 8 | import akka.http.scaladsl.unmarshalling.{Unmarshal, FromEntityUnmarshaller} 9 | import akka.http.scaladsl.marshalling.{Marshal, ToEntityMarshaller} 10 | import akka.stream.Materializer 11 | import akka.stream.scaladsl.Sink 12 | 13 | import scala.collection.mutable.Builder 14 | import scala.concurrent.{Future, ExecutionContext} 15 | import scala.annotation.tailrec 16 | 17 | package object akkahttp { 18 | 19 | case class AkkaHttpHeaderParseException(msg: String) extends Exception(msg) 20 | 21 | private def getHeaders(raw: Map[String, String]): List[HttpHeader] = 22 | raw.map { case (key, value) => HttpHeader.parse(key, value) match { 23 | case HttpHeader.ParsingResult.Ok(header, _) => header 24 | case HttpHeader.ParsingResult.Error(cause) => throw AkkaHttpHeaderParseException(cause.formatPretty) 25 | }}(collection.breakOut) 26 | 27 | implicit def noReqBodyExecutor[El <: HList, KIn <: HList, VIn <: HList, M <: MethodType, FOut](implicit encoder: ToEntityMarshaller[FOut], ec: ExecutionContext) = 28 | new NoReqBodyExecutor[El, KIn, VIn, M, Future, FOut] { 29 | type R = HttpRequest 30 | type Out = Future[HttpResponse] 31 | 32 | def apply(req: R, eReq: EndpointRequest, endpoint: Endpoint[El, KIn, VIn, M, VIn, Future, FOut]): Either[ExtractionError, Out] = { 33 | extract(eReq, endpoint).right.map { extracted => 34 | execute(extracted, endpoint).flatMap { 35 | case Right((code, response)) => 36 | Marshal(response).to[ResponseEntity].map { marshalledBody => 37 | HttpResponse(status = StatusCode.int2StatusCode(code.statusCode), entity = marshalledBody, headers = getHeaders(endpoint.headers)) 38 | } 39 | 40 | case Left(HttpError(code, msg)) => 41 | Future.successful(HttpResponse(status = StatusCode.int2StatusCode(code.statusCode), entity = msg, headers = getHeaders(endpoint.headers))) 42 | } 43 | } 44 | } 45 | } 46 | 47 | implicit def withReqBodyExecutor[El <: HList, KIn <: HList, VIn <: HList, Bd, M <: MethodType, ROut <: HList, POut <: HList, FOut] 48 | (implicit encoder: ToEntityMarshaller[FOut], 49 | decoder: FromEntityUnmarshaller[Bd], 50 | _prepend: Prepend.Aux[ROut, Bd :: HNil, POut], 51 | _eqProof: POut =:= VIn, 52 | mat: Materializer, 53 | ec: ExecutionContext) = new ReqBodyExecutor[El, KIn, VIn, Bd, M, ROut, POut, Future, FOut] { 54 | type R = HttpRequest 55 | type Out = Future[HttpResponse] 56 | 57 | implicit val prepend = _prepend 58 | implicit val eqProof = _eqProof 59 | 60 | def apply(req: R, eReq: EndpointRequest, endpoint: Endpoint[El, KIn, VIn, M, (BodyType[Bd], ROut), Future, FOut]): Either[ExtractionError, Out] = { 61 | extract(eReq, endpoint).right.map { case (_, extracted) => 62 | for { 63 | body <- Unmarshal(req.entity).to[Bd] 64 | response <- execute(extracted, body, endpoint).flatMap { 65 | case Right((code, response)) => 66 | Marshal(response).to[ResponseEntity].map { marshalledBody => 67 | HttpResponse(status = StatusCode.int2StatusCode(code.statusCode), entity = marshalledBody, headers = getHeaders(endpoint.headers)) 68 | } 69 | 70 | case Left(HttpError(code, msg)) => 71 | Future.successful(HttpResponse(status = StatusCode.int2StatusCode(code.statusCode), entity = msg, headers = getHeaders(endpoint.headers))) 72 | } 73 | } yield response 74 | } 75 | } 76 | } 77 | 78 | implicit def mountEndpoints(implicit mat: Materializer) = new MountEndpoints[HttpExt, HttpRequest, Future[HttpResponse]] { 79 | type Out = Future[Http.ServerBinding] 80 | 81 | def apply(server: ServerManager[HttpExt], endpoints: List[Serve[HttpRequest, Future[HttpResponse]]]): Out = { 82 | val service: HttpRequest => Future[HttpResponse] = request => { 83 | def execute(eps: List[Serve[HttpRequest, Future[HttpResponse]]], eReq: EndpointRequest): Future[HttpResponse] = eps match { 84 | case collection.immutable.::(endpoint, tail) => endpoint(request, eReq) match { 85 | case Right(response) => response 86 | case Left(RouteNotFound) => execute(tail, eReq) 87 | case Left(BadRouteRequest(msg)) => Future.successful(HttpResponse(400, entity = msg)) 88 | } 89 | 90 | case Nil => Future.successful(HttpResponse(404, entity = "uri = " + request.uri)) 91 | } 92 | 93 | @tailrec 94 | def toListPath(path: Uri.Path, agg: Builder[String, List[String]]): List[String] = path match { 95 | case Uri.Path.Slash(tail) => toListPath(tail, agg) 96 | case Uri.Path.Segment(p, tail) => toListPath(tail, agg += p) 97 | case Uri.Path.Empty => agg.result() 98 | } 99 | 100 | val eReq = EndpointRequest( 101 | request.method.name, 102 | toListPath(request.uri.path, List.newBuilder), 103 | request.uri.query().toMultiMap, 104 | request.headers.map(header => header.lowercaseName -> header.value)(collection.breakOut) 105 | ) 106 | 107 | if (request.method.name == "OPTIONS") { 108 | Future.successful(HttpResponse(headers = getHeaders(optionsHeaders(endpoints, eReq)))) 109 | } 110 | else 111 | execute(endpoints, eReq) 112 | } 113 | 114 | server.server.bind(server.host, server.port).to(Sink.foreach { connection => 115 | connection.handleWithAsyncHandler(service) 116 | }).run() 117 | } 118 | } 119 | } 120 | -------------------------------------------------------------------------------- /ammonite-client-support/src/main/scala/typedapi/client/package.scala: -------------------------------------------------------------------------------- 1 | package typedapi.client 2 | 3 | import typedapi.util._ 4 | import scalaj.http._ 5 | 6 | package object amm { 7 | 8 | type Id[A] = scalajhttp.Id[A] 9 | type Blocking[A] = scalajhttp.Blocking[A] 10 | 11 | def clientManager(host: String, port: Int): ClientManager[Http.type] = ClientManager(Http, host, port) 12 | def clientManager(host: String): ClientManager[Http.type] = ClientManager(Http, host) 13 | 14 | implicit def rawGetRequestAmm = scalajhttp.rawGetRequest 15 | implicit def getRequestAmm[A](implicit decoder: Decoder[Id, A]) = scalajhttp.getRequest[A] 16 | 17 | implicit def rawPutRequestAmm = scalajhttp.rawPutRequest 18 | implicit def putRequestAmm[A](implicit decoder: Decoder[Id, A]) = scalajhttp.putRequest[A] 19 | 20 | implicit def rawPutBodyRequestAmm[Bd](implicit encoder: Encoder[Id, Bd]) = scalajhttp.rawPutBodyRequest[Bd] 21 | implicit def putBodyRequestAmm[Bd, A](implicit encoder: Encoder[Id, Bd], decoder: Decoder[Id, A]) = scalajhttp.putBodyRequest[Bd, A] 22 | 23 | implicit def rawPostRequestAmm = scalajhttp.rawPostRequest 24 | implicit def postRequest[A](implicit decoder: Decoder[Id, A]) = scalajhttp.postRequest[A] 25 | 26 | implicit def rawPostBodyRequestAm[Bd](implicit encoder: Encoder[Id, Bd]) = scalajhttp.rawPostBodyRequest[Bd] 27 | implicit def postBodyRequestAmm[Bd, A](implicit encoder: Encoder[Id, Bd], decoder: Decoder[Id, A]) = scalajhttp.postBodyRequest[Bd, A] 28 | 29 | implicit def rawDeleteRequestAmm = scalajhttp.rawDeleteRequest 30 | implicit def deleteRequestAmm[A](implicit decoder: Decoder[Id, A]) = scalajhttp.deleteRequest[A] 31 | } 32 | -------------------------------------------------------------------------------- /build.sbt: -------------------------------------------------------------------------------- 1 | import sbt.Keys._ 2 | 3 | val `compiler-2.12` = Seq( 4 | "-deprecation", 5 | "-encoding", "utf-8", 6 | "-explaintypes", 7 | "-feature", 8 | "-unchecked", 9 | "-Xfatal-warnings", 10 | "-Xfuture", 11 | "-Xlint:inaccessible", 12 | "-Xlint:infer-any", 13 | "-Xlint:missing-interpolator", 14 | "-Xlint:option-implicit", 15 | "-Xlint:type-parameter-shadow", 16 | "-Xlint:unsound-match", 17 | "-Ywarn-dead-code", 18 | "-Ywarn-inaccessible", 19 | "-Ywarn-infer-any", 20 | "-Ywarn-numeric-widen", 21 | // "-Ywarn-unused:implicits", -> get errors for implicit evidence 22 | "-Ywarn-unused:imports", 23 | // "-Ywarn-unused:locals", 24 | "-Ywarn-unused:privates" 25 | ) 26 | 27 | val `compiler-2.11` = Seq( 28 | "-deprecation", 29 | "-encoding", "utf-8", 30 | "-explaintypes", 31 | "-feature", 32 | "-unchecked", 33 | "-Xfatal-warnings", 34 | "-Xfuture", 35 | "-Xlint:inaccessible", 36 | "-Xlint:infer-any", 37 | "-Xlint:missing-interpolator", 38 | "-Xlint:option-implicit", 39 | "-Xlint:type-parameter-shadow", 40 | "-Xlint:unsound-match", 41 | "-Ywarn-dead-code", 42 | "-Ywarn-numeric-widen", 43 | "-Ywarn-unused", 44 | "-Ywarn-inaccessible", 45 | "-Ywarn-infer-any" 46 | ) 47 | 48 | lazy val commonSettings = Seq( 49 | organization := "com.github.pheymann", 50 | version := "0.2.0", 51 | crossScalaVersions := Seq("2.11.11", "2.12.4"), 52 | scalaVersion := "2.12.4", 53 | scalacOptions ++= { CrossVersion.partialVersion(scalaVersion.value) match { 54 | case Some((2, 12)) => `compiler-2.12` 55 | case Some((2, 11)) => `compiler-2.11` 56 | case _ => Seq.empty[String] 57 | }}, 58 | publishTo := sonatypePublishTo.value 59 | ) 60 | 61 | lazy val mavenSettings = Seq( 62 | sonatypeProfileName := "pheymann", 63 | publishMavenStyle := true, 64 | pomExtra in Global := { 65 | https://github.com/pheymann/typedapi 66 | 67 | 68 | MIT 69 | https://github.com/pheymann/typedapi/blob/master/LICENSE 70 | 71 | 72 | 73 | scm:git:github.com/pheymann/typedapi 74 | scm:git:git@github.com:pheymann/typedapi 75 | github.com/pheymann/typedapi 76 | 77 | 78 | 79 | pheymann 80 | Paul Heymann 81 | https://github.com/pheymann 82 | 83 | 84 | } 85 | ) 86 | 87 | lazy val typedapi = project 88 | .in(file(".")) 89 | .settings(commonSettings: _*) 90 | .aggregate( 91 | `shared-js`, 92 | `shared-jvm`, 93 | `client-js`, 94 | `client-jvm`, 95 | server, 96 | `http4s-client`, 97 | `http4s-server`, 98 | `akka-http-client`, 99 | `akka-http-server`, 100 | `js-client`, 101 | `scalaj-http-client`, 102 | `http-support-tests`, 103 | `ammonite-client-support` 104 | ) 105 | 106 | lazy val shared = crossProject.crossType(CrossType.Pure) 107 | .in(file("shared")) 108 | .settings( 109 | commonSettings, 110 | mavenSettings, 111 | name := "typedapi-shared", 112 | libraryDependencies ++= Dependencies.shared 113 | ) 114 | 115 | lazy val `shared-js` = shared.js 116 | lazy val `shared-jvm` = shared.jvm 117 | 118 | lazy val client = crossProject.crossType(CrossType.Pure) 119 | .in(file("client")) 120 | .settings( 121 | commonSettings, 122 | mavenSettings, 123 | name := "typedapi-client", 124 | libraryDependencies ++= Dependencies.client 125 | ) 126 | .dependsOn(shared) 127 | 128 | lazy val `client-js` = client.js 129 | lazy val `client-jvm` = client.jvm 130 | 131 | lazy val server = project 132 | .in(file("server")) 133 | .settings( 134 | commonSettings, 135 | mavenSettings, 136 | name := "typedapi-server", 137 | libraryDependencies ++= Dependencies.server 138 | ) 139 | .dependsOn(`shared-jvm`) 140 | 141 | 142 | lazy val `http4s-client` = project 143 | .in(file("http4s-client")) 144 | .settings( 145 | commonSettings, 146 | mavenSettings, 147 | name := "typedapi-http4s-client", 148 | libraryDependencies ++= Dependencies.http4sClient, 149 | ) 150 | .dependsOn(`client-jvm`) 151 | 152 | lazy val `http4s-server` = project 153 | .in(file("http4s-server")) 154 | .settings( 155 | commonSettings, 156 | mavenSettings, 157 | name := "typedapi-http4s-server", 158 | libraryDependencies ++= Dependencies.http4sServer, 159 | ) 160 | .dependsOn(server) 161 | 162 | lazy val `akka-http-client` = project 163 | .in(file("akka-http-client")) 164 | .settings( 165 | commonSettings, 166 | mavenSettings, 167 | name := "typedapi-akka-http-client", 168 | libraryDependencies ++= Dependencies.akkaHttpClient 169 | ) 170 | .dependsOn(`client-jvm`) 171 | 172 | lazy val `akka-http-server` = project 173 | .in(file("akka-http-server")) 174 | .settings( 175 | commonSettings, 176 | mavenSettings, 177 | name := "typedapi-akka-http-server", 178 | libraryDependencies ++= Dependencies.akkaHttpServer 179 | ) 180 | .dependsOn(server) 181 | 182 | lazy val `js-client` = project 183 | .in(file("js-client")) 184 | .enablePlugins(ScalaJSPlugin) 185 | .settings( 186 | commonSettings, 187 | mavenSettings, 188 | name := "typedapi-js-client", 189 | libraryDependencies ++= Seq( 190 | "org.scala-js" %%% "scalajs-dom" % "0.9.6" % Compile 191 | ) 192 | ) 193 | .dependsOn(`client-js`) 194 | 195 | lazy val `scalaj-http-client` = project 196 | .in(file("scalaj-http-client")) 197 | .settings( 198 | commonSettings, 199 | mavenSettings, 200 | name := "typedapi-scalaj-http-client", 201 | libraryDependencies ++= Dependencies.scalajHttpClient 202 | ) 203 | .dependsOn(`client-jvm`) 204 | 205 | lazy val `http-support-tests` = project 206 | .in(file("http-support-tests")) 207 | .settings( 208 | commonSettings, 209 | parallelExecution in Test := false, 210 | libraryDependencies ++= Dependencies.httpSupportTests 211 | ) 212 | .dependsOn(`http4s-client`, `http4s-server`, `akka-http-client`, `akka-http-server`, `scalaj-http-client`) 213 | 214 | lazy val `ammonite-client-support` = project 215 | .in(file("ammonite-client-support")) 216 | .settings( 217 | commonSettings, 218 | mavenSettings, 219 | name := "typedapi-ammonite-client", 220 | libraryDependencies ++= Dependencies.ammoniteSupport 221 | ) 222 | .dependsOn(`scalaj-http-client`) 223 | -------------------------------------------------------------------------------- /client/src/main/scala/typedapi/client/ApiRequest.scala: -------------------------------------------------------------------------------- 1 | package typedapi.client 2 | 3 | import RequestDataBuilder.{Data, DataWithBody} 4 | import typedapi.shared._ 5 | import shapeless._ 6 | 7 | import scala.language.higherKinds 8 | import scala.annotation.implicitNotFound 9 | 10 | /** Basic api request element. Provides a function to create an effect representing the actual request. */ 11 | @implicitNotFound("""Cannot find RawApiRequest instance for ${M}. 12 | 13 | context: ${F}""") 14 | trait RawApiRequest[M <: MethodType, D <: HList, C, F[_]] { 15 | 16 | type Resp 17 | 18 | def apply(data: D, cm: ClientManager[C]): F[Resp] 19 | } 20 | 21 | @implicitNotFound("""Cannot find ApiRequest instance for ${M}. Do you miss some implicit value e.g. encoders/decoders? 22 | 23 | ouput: ${Out} 24 | context: ${F}""") 25 | trait ApiRequest[M <: MethodType, D <: HList, C, F[_], Out] { 26 | 27 | def apply(data: D, cm: ClientManager[C]): F[Out] 28 | } 29 | 30 | trait RawApiWithoutBodyRequest[M <: MethodType, C, F[_]] extends RawApiRequest[M, Data, C, F] { 31 | 32 | def apply(data: Data, cm: ClientManager[C]): F[Resp] = { 33 | val (uri :: queries :: headers :: HNil): Data = data 34 | 35 | apply(uri, queries, headers, cm) 36 | } 37 | 38 | def apply(uri: List[String], queries: Map[String, List[String]], headers: Map[String, String], cm: ClientManager[C]): F[Resp] 39 | } 40 | 41 | trait ApiWithoutBodyRequest[M <: MethodType, C, F[_], Out] extends ApiRequest[M, Data, C, F, Out] { 42 | 43 | def apply(data: Data, cm: ClientManager[C]): F[Out] = { 44 | val (uri :: queries :: headers :: HNil): Data = data 45 | 46 | apply(uri, queries, headers, cm) 47 | } 48 | 49 | def apply(uri: List[String], queries: Map[String, List[String]], headers: Map[String, String], cm: ClientManager[C]): F[Out] 50 | } 51 | 52 | trait RawApiWithBodyRequest[M <: MethodType, C, F[_], Bd] extends RawApiRequest[M, DataWithBody[Bd], C, F] { 53 | 54 | def apply(data: DataWithBody[Bd], cm: ClientManager[C]): F[Resp] = { 55 | val (uri :: queries :: headers :: body :: HNil): DataWithBody[Bd] = data 56 | 57 | apply(uri, queries, headers, body, cm) 58 | } 59 | 60 | def apply(uri: List[String], queries: Map[String, List[String]], headers: Map[String, String], body: Bd, cm: ClientManager[C]): F[Resp] 61 | } 62 | 63 | trait ApiWithBodyRequest[M <: MethodType, C, F[_], Bd, Out] extends ApiRequest[M, DataWithBody[Bd], C, F, Out] { 64 | 65 | def apply(data: DataWithBody[Bd], cm: ClientManager[C]): F[Out] = { 66 | val (uri :: queries :: headers :: body :: HNil): DataWithBody[Bd] = data 67 | 68 | apply(uri, queries, headers, body, cm) 69 | } 70 | 71 | def apply(uri: List[String], queries: Map[String, List[String]], headers: Map[String, String], body: Bd, cm: ClientManager[C]): F[Out] 72 | } 73 | 74 | trait RawGetRequest[C, F[_]] extends RawApiWithoutBodyRequest[GetCall, C, F] 75 | trait GetRequest[C, F[_], A] extends ApiWithoutBodyRequest[GetCall, C, F, A] 76 | trait RawPutRequest[C, F[_]] extends RawApiWithoutBodyRequest[PutCall, C, F] 77 | trait PutRequest[C, F[_], A] extends ApiWithoutBodyRequest[PutCall, C, F, A] 78 | trait RawPutWithBodyRequest[C, F[_], Bd] extends RawApiWithBodyRequest[PutWithBodyCall, C, F, Bd] 79 | trait PutWithBodyRequest[C, F[_], Bd, A] extends ApiWithBodyRequest[PutWithBodyCall, C, F, Bd, A] 80 | trait RawPostRequest[C, F[_]] extends RawApiWithoutBodyRequest[PostCall, C, F] 81 | trait PostRequest[C, F[_], A] extends ApiWithoutBodyRequest[PostCall, C, F, A] 82 | trait RawPostWithBodyRequest[C, F[_], Bd] extends RawApiWithBodyRequest[PostWithBodyCall, C, F, Bd] 83 | trait PostWithBodyRequest[C, F[_], Bd, A] extends ApiWithBodyRequest[PostWithBodyCall, C, F, Bd, A] 84 | trait RawDeleteRequest[C, F[_]] extends RawApiWithoutBodyRequest[DeleteCall, C, F] 85 | trait DeleteRequest[C, F[_], A] extends ApiWithoutBodyRequest[DeleteCall, C, F, A] 86 | -------------------------------------------------------------------------------- /client/src/main/scala/typedapi/client/ClientManager.scala: -------------------------------------------------------------------------------- 1 | package typedapi.client 2 | 3 | /** Provides a supported client instance and some basic configuration. */ 4 | final case class ClientManager[C](client: C, host: String, portO: Option[Int]) { 5 | 6 | val base = portO match { 7 | case Some(p) => s"$host:$p" 8 | case None => host 9 | } 10 | } 11 | 12 | object ClientManager { 13 | 14 | def apply[C](client: C, host: String, port: Int): ClientManager[C] = ClientManager(client, host, Some(port)) 15 | def apply[C](client: C, host: String): ClientManager[C] = ClientManager(client, host, None) 16 | } 17 | -------------------------------------------------------------------------------- /client/src/main/scala/typedapi/client/ExecutableDerivation.scala: -------------------------------------------------------------------------------- 1 | package typedapi.client 2 | 3 | import typedapi.shared.{MethodType, MediaType} 4 | import shapeless._ 5 | import shapeless.labelled.FieldType 6 | 7 | import scala.language.higherKinds 8 | 9 | /** Helper class to match the [[RequestDataBuilder]] with an [[ApiRequest]] instance. */ 10 | final class ExecutableDerivation[El <: HList, KIn <: HList, VIn <: HList, M <: MethodType, MT <: MediaType, O, D <: HList] 11 | (builder: RequestDataBuilder.Aux[El, KIn, VIn, M, FieldType[MT, O], D], input: VIn) { 12 | 13 | final class Derivation[F[_]] { 14 | 15 | def apply[C](cm: ClientManager[C])(implicit req: ApiRequest[M, D, C, F, O]): F[O] = { 16 | val data = builder(input, List.newBuilder, Map.empty, Map.empty) 17 | 18 | req(data, cm) 19 | } 20 | 21 | def raw[C](cm: ClientManager[C])(implicit req: RawApiRequest[M, D, C, F]): F[req.Resp] = { 22 | val data = builder(input, List.newBuilder, Map.empty, Map.empty) 23 | 24 | req(data, cm) 25 | } 26 | } 27 | 28 | def run[F[_]]: Derivation[F] = new Derivation[F] 29 | } 30 | -------------------------------------------------------------------------------- /client/src/main/scala/typedapi/client/ExecutablesFromHList.scala: -------------------------------------------------------------------------------- 1 | package typedapi.client 2 | 3 | import typedapi.shared.{MethodType, MediaType} 4 | import shapeless._ 5 | import shapeless.labelled.FieldType 6 | import shapeless.ops.function.FnFromProduct 7 | 8 | /** Derives executables from list of RequestBuilders. */ 9 | sealed trait ExecutablesFromHList[H <: HList] { 10 | 11 | type Out <: HList 12 | 13 | def apply(h: H): Out 14 | } 15 | 16 | object ExecutablesFromHList extends ExecutablesFromHListLowPrio { 17 | 18 | type Aux[H <: HList, Out0 <: HList] = ExecutablesFromHList[H] { type Out = Out0 } 19 | } 20 | 21 | trait ExecutablesFromHListLowPrio { 22 | 23 | implicit val noExecutable = new ExecutablesFromHList[HNil] { 24 | type Out = HNil 25 | 26 | def apply(h: HNil): Out = HNil 27 | } 28 | 29 | implicit def deriveExcutable[El <: HList, KIn <: HList, VIn <: HList, M <: MethodType, MT <: MediaType, O, D <: HList, T <: HList] 30 | (implicit next: ExecutablesFromHList[T], 31 | vinToFn: FnFromProduct[VIn => ExecutableDerivation[El, KIn, VIn, M, MT, O, D]]) = 32 | new ExecutablesFromHList[RequestDataBuilder.Aux[El, KIn, VIn, M, FieldType[MT, O], D] :: T] { 33 | type Out = vinToFn.Out :: next.Out 34 | 35 | def apply(comps: RequestDataBuilder.Aux[El, KIn, VIn, M, FieldType[MT, O], D] :: T): Out = { 36 | val fn = vinToFn.apply(input => new ExecutableDerivation[El, KIn, VIn, M, MT, O, D](comps.head, input)) 37 | 38 | fn :: next(comps.tail) 39 | } 40 | } 41 | } 42 | 43 | -------------------------------------------------------------------------------- /client/src/main/scala/typedapi/client/FilterServerElements.scala: -------------------------------------------------------------------------------- 1 | package typedapi.client 2 | 3 | import typedapi.shared._ 4 | import shapeless._ 5 | 6 | //TODO replace with Typelevelfoldleft 7 | sealed trait FilterServerElements[H <: HList] { 8 | 9 | type Out <: HList 10 | } 11 | 12 | sealed trait FilterServerElementsLowPrio { 13 | 14 | implicit val filterServerResult = new FilterServerElements[HNil] { 15 | type Out = HNil 16 | } 17 | 18 | implicit def filterServerKeep[El, T <: HList](implicit next: FilterServerElements[T]) = new FilterServerElements[El :: T] { 19 | type Out = El :: next.Out 20 | } 21 | } 22 | 23 | object FilterServerElements extends FilterServerElementsLowPrio { 24 | 25 | type Aux[H <: HList, Out0 <: HList] = FilterServerElements[H] { type Out = Out0 } 26 | 27 | implicit def filterServerHeaderMatch[K, V, T <: HList](implicit next: FilterServerElements[T]) = new FilterServerElements[ServerHeaderMatchParam[K, V] :: T] { 28 | type Out = next.Out 29 | } 30 | 31 | implicit def filterServerHeaderSend[K, V, T <: HList](implicit next: FilterServerElements[T]) = new FilterServerElements[ServerHeaderSendElement[K, V] :: T] { 32 | type Out = next.Out 33 | } 34 | } 35 | 36 | sealed trait FilterServerElementsList[H <: HList] { 37 | 38 | type Out <: HList 39 | } 40 | 41 | object FilterServerElementsList { 42 | 43 | type Aux[H <: HList, Out0 <: HList] = FilterServerElementsList[H] { type Out = Out0 } 44 | 45 | implicit val filterServerListResult = new FilterServerElementsList[HNil] { 46 | type Out = HNil 47 | } 48 | 49 | implicit def filterServerListStep[Api <: HList, T <: HList](implicit filtered: FilterServerElements[Api], next: FilterServerElementsList[T]) = 50 | new FilterServerElementsList[Api :: T] { 51 | type Out = filtered.Out :: next.Out 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /client/src/main/scala/typedapi/client/RequestDataBuilder.scala: -------------------------------------------------------------------------------- 1 | package typedapi.client 2 | 3 | import typedapi.shared._ 4 | import shapeless._ 5 | import shapeless.labelled.FieldType 6 | 7 | import scala.collection.mutable.Builder 8 | import scala.annotation.implicitNotFound 9 | 10 | /** 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. */ 11 | @implicitNotFound("""Woops, you shouldn't be here. We cannot find the RequestDataBuilder. This seems to be a bug. 12 | 13 | elements: ${El} 14 | input keys: ${KIn} 15 | input values: ${VIn} 16 | method: ${M} 17 | expected result: ${O}""") 18 | trait RequestDataBuilder[El <: HList, KIn <: HList, VIn <: HList, M <: MethodType, O] { 19 | 20 | type Out <: HList 21 | 22 | def apply(inputs: VIn, 23 | uri: Builder[String, List[String]], 24 | queries: Map[String, List[String]], 25 | headers: Map[String, String]): Out 26 | } 27 | 28 | object RequestDataBuilder extends RequestDataBuilderMediumPrio { 29 | 30 | type Aux[El <: HList, KIn <: HList, VIn <: HList, M <: MethodType, O, Out0 <: HList] = RequestDataBuilder[El, KIn, VIn, M, O] { type Out = Out0 } 31 | } 32 | 33 | trait RequestDataBuilderLowPrio { 34 | 35 | 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]) = 36 | new RequestDataBuilder[S :: T, KIn, VIn, M, O] { 37 | type Out = compiler.Out 38 | 39 | def apply(inputs: VIn, uri: Builder[String, List[String]], queries: Map[String, List[String]], headers: Map[String, String]): Out = { 40 | compiler(inputs, uri += wit.value.toString(), queries, headers) 41 | } 42 | } 43 | 44 | implicit def segmentInputCompiler[T <: HList, K, V, KIn <: HList, VIn <: HList, M <: MethodType, O](implicit compiler: RequestDataBuilder[T, KIn, VIn, M, O]) = 45 | new RequestDataBuilder[SegmentInput :: T, K :: KIn, V :: VIn, M, O] { 46 | type Out = compiler.Out 47 | 48 | def apply(inputs: V :: VIn, uri: Builder[String, List[String]], queries: Map[String, List[String]], headers: Map[String, String]): Out = { 49 | val segValue = inputs.head 50 | 51 | compiler(inputs.tail, uri += segValue.toString(), queries, headers) 52 | } 53 | } 54 | 55 | implicit def queryInputCompiler[T <: HList, K, V, KIn <: HList, VIn <: HList, M <: MethodType, O] 56 | (implicit wit: Witness.Aux[K], show: WitnessToString[K], compiler: RequestDataBuilder[T, KIn, VIn, M, O]) = 57 | new RequestDataBuilder[QueryInput :: T, K :: KIn, V :: VIn, M, O] { 58 | type Out = compiler.Out 59 | 60 | def apply(inputs: V :: VIn, uri: Builder[String, List[String]], queries: Map[String, List[String]], headers: Map[String, String]): Out = { 61 | val queryName = show.show(wit.value) 62 | val queryValue = inputs.head 63 | 64 | compiler(inputs.tail, uri, Map((queryName, List(queryValue.toString()))) ++ queries, headers) 65 | } 66 | } 67 | 68 | implicit def headerInputCompiler[T <: HList, K, V, KIn <: HList, VIn <: HList, M <: MethodType, O] 69 | (implicit wit: Witness.Aux[K], show: WitnessToString[K], compiler: RequestDataBuilder[T, KIn, VIn, M, O]) = 70 | new RequestDataBuilder[HeaderInput :: T, K :: KIn, V :: VIn, M, O] { 71 | type Out = compiler.Out 72 | 73 | def apply(inputs: V :: VIn, uri: Builder[String, List[String]], queries: Map[String, List[String]], headers: Map[String, String]): Out = { 74 | val headerName = show.show(wit.value) 75 | val headerValue = inputs.head 76 | 77 | compiler(inputs.tail, uri, queries, Map((headerName, headerValue.toString())) ++ headers) 78 | } 79 | } 80 | 81 | implicit def fixedHeaderCompiler[K, V, T <: HList, KIn <: HList, VIn <: HList, M <: MethodType, O] 82 | (implicit kWit: Witness.Aux[K], vWit: Witness.Aux[V], kShow: WitnessToString[K], vShow: WitnessToString[V], compiler: RequestDataBuilder[T, KIn, VIn, M, O]) = 83 | new RequestDataBuilder[FixedHeader[K, V] :: T, KIn, VIn, M, O] { 84 | type Out = compiler.Out 85 | 86 | def apply(inputs: VIn, uri: Builder[String, List[String]], queries: Map[String, List[String]], headers: Map[String, String]): Out = { 87 | val key = kShow.show(kWit.value) 88 | val value = vShow.show(vWit.value) 89 | 90 | compiler(inputs, uri, queries, Map((key, value)) ++ headers) 91 | } 92 | } 93 | 94 | implicit def clientHeaderCompiler[K, V, T <: HList, KIn <: HList, VIn <: HList, M <: MethodType, O] 95 | (implicit kWit: Witness.Aux[K], vWit: Witness.Aux[V], kShow: WitnessToString[K], vShow: WitnessToString[V], compiler: RequestDataBuilder[T, KIn, VIn, M, O]) = 96 | new RequestDataBuilder[ClientHeader[K, V] :: T, KIn, VIn, M, O] { 97 | type Out = compiler.Out 98 | 99 | def apply(inputs: VIn, uri: Builder[String, List[String]], queries: Map[String, List[String]], headers: Map[String, String]): Out = { 100 | val key = kShow.show(kWit.value) 101 | val value = vShow.show(vWit.value) 102 | 103 | compiler(inputs, uri, queries, Map((key, value)) ++ headers) 104 | } 105 | } 106 | 107 | implicit def clientHeaderInputCompiler[K, V, T <: HList, KIn <: HList, VIn <: HList, M <: MethodType, O] 108 | (implicit kWit: Witness.Aux[K], kShow: WitnessToString[K], compiler: RequestDataBuilder[T, KIn, VIn, M, O]) = 109 | new RequestDataBuilder[ClientHeaderInput :: T, K :: KIn, V :: VIn, M, O] { 110 | type Out = compiler.Out 111 | 112 | def apply(inputs: V :: VIn, uri: Builder[String, List[String]], queries: Map[String, List[String]], headers: Map[String, String]): Out = { 113 | val key = kShow.show(kWit.value) 114 | val value = inputs.head.toString 115 | 116 | compiler(inputs.tail, uri, queries, Map((key, value)) ++ headers) 117 | } 118 | } 119 | 120 | implicit def clientHeaderCollInputCompiler[V, T <: HList, KIn <: HList, VIn <: HList, M <: MethodType, O] 121 | (implicit compiler: RequestDataBuilder[T, KIn, VIn, M, O]) = 122 | new RequestDataBuilder[ClientHeaderCollInput :: T, KIn, Map[String, V] :: VIn, M, O] { 123 | type Out = compiler.Out 124 | 125 | def apply(inputs: Map[String, V] :: VIn, uri: Builder[String, List[String]], queries: Map[String, List[String]], headers: Map[String, String]): Out = { 126 | val coll = inputs.head.mapValues(_.toString) 127 | 128 | compiler(inputs.tail, uri, queries, coll ++ headers) 129 | } 130 | } 131 | 132 | type Data = List[String] :: Map[String, List[String]] :: Map[String, String] :: HNil 133 | type DataWithBody[Bd] = List[String] :: Map[String, List[String]] :: Map[String, String] :: Bd :: HNil 134 | 135 | private def accept[MT <: MediaType](headers: Map[String, String], media: MT): Map[String, String] = 136 | Map(("Accept", media.value)) ++ headers 137 | 138 | implicit def getCompiler[MT <: MediaType, A](implicit media: Witness.Aux[MT]) = new RequestDataBuilder[HNil, HNil, HNil, GetCall, FieldType[MT, A]] { 139 | type Out = Data 140 | 141 | def apply(inputs: HNil, uri: Builder[String, List[String]], queries: Map[String, List[String]], headers: Map[String, String]): Out = { 142 | val out = uri.result() :: queries :: accept(headers, media.value) :: HNil 143 | 144 | out 145 | } 146 | } 147 | 148 | implicit def putCompiler[MT <: MediaType, A](implicit media: Witness.Aux[MT]) = new RequestDataBuilder[HNil, HNil, HNil, PutCall, FieldType[MT, A]] { 149 | type Out = Data 150 | 151 | def apply(inputs: HNil, uri: Builder[String, List[String]], queries: Map[String, List[String]], headers: Map[String, String]): Out = { 152 | val out = uri.result() :: queries :: accept(headers, media.value) :: HNil 153 | 154 | out 155 | } 156 | } 157 | 158 | implicit def putWithBodyCompiler[BMT <: MediaType, Bd, MT <: MediaType, A](implicit media: Witness.Aux[MT]) = 159 | new RequestDataBuilder[HNil, FieldType[BMT, BodyField.T] :: HNil, Bd :: HNil, PutWithBodyCall, FieldType[MT, A]] { 160 | type Out = DataWithBody[Bd] 161 | 162 | def apply(inputs: Bd :: HNil, uri: Builder[String, List[String]], queries: Map[String, List[String]], headers: Map[String, String]): Out = { 163 | val out = uri.result() :: queries :: accept(headers, media.value) :: inputs.head :: HNil 164 | 165 | out 166 | } 167 | } 168 | 169 | implicit def postCompiler[MT <: MediaType, A](implicit media: Witness.Aux[MT]) = new RequestDataBuilder[HNil, HNil, HNil, PostCall, FieldType[MT, A]] { 170 | type Out = Data 171 | 172 | def apply(inputs: HNil, uri: Builder[String, List[String]], queries: Map[String, List[String]], headers: Map[String, String]): Out = { 173 | val out = uri.result() :: queries :: accept(headers, media.value) :: HNil 174 | 175 | out 176 | } 177 | } 178 | 179 | implicit def postWithBodyCompiler[BMT <: MediaType, Bd, MT <: MediaType, A](implicit media: Witness.Aux[MT]) = 180 | new RequestDataBuilder[HNil, FieldType[BMT, BodyField.T] :: HNil, Bd :: HNil, PostWithBodyCall, FieldType[MT, A]] { 181 | type Out = DataWithBody[Bd] 182 | 183 | def apply(inputs: Bd :: HNil, uri: Builder[String, List[String]], queries: Map[String, List[String]], headers: Map[String, String]): Out = { 184 | val out = uri.result() :: queries :: accept(headers, media.value) :: inputs.head :: HNil 185 | 186 | out 187 | } 188 | } 189 | 190 | implicit def deleteCompiler[MT <: MediaType, A](implicit media: Witness.Aux[MT]) = new RequestDataBuilder[HNil, HNil, HNil, DeleteCall, FieldType[MT, A]] { 191 | type Out = Data 192 | 193 | def apply(inputs: HNil, uri: Builder[String, List[String]], queries: Map[String, List[String]], headers: Map[String, String]): Out = { 194 | val out = uri.result() :: queries :: accept(headers, media.value) :: HNil 195 | 196 | out 197 | } 198 | } 199 | } 200 | 201 | trait RequestDataBuilderMediumPrio extends RequestDataBuilderLowPrio { 202 | 203 | implicit def queryOptInputCompiler[T <: HList, K, V, KIn <: HList, VIn <: HList, M <: MethodType, O] 204 | (implicit wit: Witness.Aux[K], show: WitnessToString[K], compiler: RequestDataBuilder[T, KIn, VIn, M, O]) = 205 | new RequestDataBuilder[QueryInput :: T, K :: KIn, Option[V] :: VIn, M, O] { 206 | type Out = compiler.Out 207 | 208 | def apply(inputs: Option[V] :: VIn, uri: Builder[String, List[String]], queries: Map[String, List[String]], headers: Map[String, String]): Out = { 209 | val queryName = show.show(wit.value) 210 | val queryValue = inputs.head 211 | val updatedQueries = queryValue.fold(queries)(q => Map(queryName -> List(q.toString())) ++ queries) 212 | 213 | compiler(inputs.tail, uri, updatedQueries, headers) 214 | } 215 | } 216 | 217 | implicit def queryListInputCompiler[T <: HList, K, V, KIn <: HList, VIn <: HList, M <: MethodType, O] 218 | (implicit wit: Witness.Aux[K], show: WitnessToString[K], compiler: RequestDataBuilder[T, KIn, VIn, M, O]) = 219 | new RequestDataBuilder[QueryInput :: T, K :: KIn, List[V] :: VIn, M, O] { 220 | type Out = compiler.Out 221 | 222 | def apply(inputs: List[V] :: VIn, uri: Builder[String, List[String]], queries: Map[String, List[String]], headers: Map[String, String]): Out = { 223 | val queryName = show.show(wit.value) 224 | val queryValue = inputs.head 225 | 226 | if (queryValue.isEmpty) 227 | compiler(inputs.tail, uri, queries, headers) 228 | else 229 | compiler(inputs.tail, uri, Map((queryName, queryValue.map(_.toString()))) ++ queries, headers) 230 | } 231 | } 232 | 233 | implicit def headersOptInputCompiler[T <: HList, K, V, KIn <: HList, VIn <: HList, M <: MethodType, O] 234 | (implicit wit: Witness.Aux[K], show: WitnessToString[K], compiler: RequestDataBuilder[T, KIn, VIn, M, O]) = 235 | new RequestDataBuilder[HeaderInput :: T, K :: KIn, Option[V] :: VIn, M, O] { 236 | type Out = compiler.Out 237 | 238 | def apply(inputs: Option[V] :: VIn, uri: Builder[String, List[String]], queries: Map[String, List[String]], headers: Map[String, String]): Out = { 239 | val headerName = show.show(wit.value) 240 | val headerValue = inputs.head 241 | val updatedHeaders = headerValue.fold(headers)(h => Map(headerName -> h.toString()) ++ headers) 242 | 243 | compiler(inputs.tail, uri, queries, updatedHeaders) 244 | } 245 | } 246 | } 247 | 248 | @implicitNotFound("""Woops, you shouldn't be here. We cannot find the RequestDataBuilderList. This seems to be a bug. 249 | 250 | list: ${H}""") 251 | trait RequestDataBuilderList[H <: HList] { 252 | 253 | type Out <: HList 254 | 255 | def builders: Out 256 | } 257 | 258 | object RequestDataBuilderList extends RequestDataBuilderListLowPrio { 259 | 260 | type Aux[H <: HList, Out0 <: HList] = RequestDataBuilderList[H] { type Out = Out0 } 261 | } 262 | 263 | trait RequestDataBuilderListLowPrio { 264 | 265 | implicit def lastCompilerList[El <: HList, KIn <: HList, VIn <: HList, M <: MethodType, O, D <: HList](implicit builder: RequestDataBuilder.Aux[El, KIn, VIn, M, O, D]) = 266 | new RequestDataBuilderList[(El, KIn, VIn, M, O) :: HNil] { 267 | type Out = RequestDataBuilder.Aux[El, KIn, VIn, M, O, D] :: HNil 268 | 269 | val builders = builder :: HNil 270 | } 271 | 272 | 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]) = 273 | new RequestDataBuilderList[(El, KIn, VIn, M, O) :: T] { 274 | type Out = RequestDataBuilder.Aux[El, KIn, VIn, M, O, D] :: next.Out 275 | 276 | val builders = builder :: next.builders 277 | } 278 | } 279 | -------------------------------------------------------------------------------- /client/src/main/scala/typedapi/client/package.scala: -------------------------------------------------------------------------------- 1 | package typedapi 2 | 3 | import typedapi.shared._ 4 | import shapeless._ 5 | import shapeless.labelled.FieldType 6 | import shapeless.ops.hlist.Tupler 7 | import shapeless.ops.function.FnFromProduct 8 | 9 | package object client extends TypeLevelFoldLeftLowPrio 10 | with TypeLevelFoldLeftListLowPrio 11 | with WitnessToStringLowPrio 12 | with ApiTransformer { 13 | 14 | def deriveUriString(cm: ClientManager[_], uri: List[String]): String = cm.base + "/" + uri.mkString("/") 15 | 16 | def derive[H <: HList, FH <: HList, El <: HList, KIn <: HList, VIn <: HList, M <: MethodType, MT <: MediaType, Out, D <: HList] 17 | (apiList: ApiTypeCarrier[H]) 18 | (implicit filter: FilterServerElements.Aux[H, FH], 19 | folder: Lazy[TypeLevelFoldLeft.Aux[FH, Unit, (El, KIn, VIn, M, FieldType[MT, Out])]], 20 | builder: RequestDataBuilder.Aux[El, KIn, VIn, M, FieldType[MT, Out], D], 21 | inToFn: FnFromProduct[VIn => ExecutableDerivation[El, KIn, VIn, M, MT, Out, D]]): inToFn.Out = 22 | inToFn.apply(input => new ExecutableDerivation[El, KIn, VIn, M, MT, Out, D](builder, input)) 23 | 24 | def deriveAll[H <: HList, FH <: HList, In <: HList, Fold <: HList, B <: HList, Ex <: HList] 25 | (apiLists: CompositionCons[H]) 26 | (implicit filter: FilterServerElementsList.Aux[H, FH], 27 | folders: TypeLevelFoldLeftList.Aux[FH, Fold], 28 | builderList: RequestDataBuilderList.Aux[Fold, B], 29 | executables: ExecutablesFromHList.Aux[B, Ex], 30 | tupler: Tupler[Ex]): tupler.Out = 31 | executables(builderList.builders).tupled 32 | } 33 | -------------------------------------------------------------------------------- /client/src/main/scala/typedapi/client/test/RequestInput.scala: -------------------------------------------------------------------------------- 1 | package typedapi.client.test 2 | 3 | final case class ReqInput(method: String, 4 | uri: List[String], 5 | queries: Map[String, List[String]], 6 | headers: Map[String, String]) 7 | 8 | final case class ReqInputWithBody[Bd](method: String, 9 | uri: List[String], 10 | queries: Map[String, List[String]], 11 | headers: Map[String, String], 12 | body: Bd) 13 | -------------------------------------------------------------------------------- /client/src/main/scala/typedapi/client/test/package.scala: -------------------------------------------------------------------------------- 1 | package typedapi.client 2 | 3 | import scala.language.higherKinds 4 | 5 | package object test { 6 | 7 | type TestClientM = ClientManager[Unit] 8 | 9 | val clientManager: TestClientM = ClientManager((), "", 0) 10 | 11 | def testRawGet[F[_]](pure: ReqInput => F[ReqInput]) = new RawGetRequest[Unit, F] { 12 | type Resp = ReqInput 13 | 14 | def apply(uri: List[String], queries: Map[String, List[String]], headers: Map[String, String], cm: TestClientM): F[Resp] = 15 | pure(ReqInput("GET", uri, queries, headers)) 16 | } 17 | def testGet[F[_], A](f: ReqInput => F[A]) = new GetRequest[Unit, F, A] { 18 | def apply(uri: List[String], queries: Map[String, List[String]], headers: Map[String, String], cm: TestClientM): F[A] = 19 | f(ReqInput("GET", uri, queries, headers)) 20 | } 21 | 22 | def testRawPut[F[_]](pure: ReqInput => F[ReqInput]) = new RawPutRequest[Unit, F] { 23 | type Resp = ReqInput 24 | 25 | def apply(uri: List[String], queries: Map[String, List[String]], headers: Map[String, String], cm: TestClientM): F[Resp] = 26 | pure(ReqInput("PUT", uri, queries, headers)) 27 | } 28 | def testPut[F[_], A](f: ReqInput => F[A]) = new PutRequest[Unit, F, A] { 29 | def apply(uri: List[String], queries: Map[String, List[String]], headers: Map[String, String], cm: TestClientM): F[A] = 30 | f(ReqInput("PUT", uri, queries, headers)) 31 | } 32 | 33 | def testRawPutWithBody[F[_], Bd](pure: ReqInputWithBody[Bd] => F[ReqInputWithBody[Bd]]) = 34 | new RawPutWithBodyRequest[Unit, F, Bd] { 35 | type Resp = ReqInputWithBody[Bd] 36 | 37 | def apply(uri: List[String], queries: Map[String, List[String]], headers: Map[String, String], body: Bd, cm: TestClientM): F[Resp] = 38 | pure(ReqInputWithBody("PUT", uri, queries, headers, body)) 39 | } 40 | def testPutWithBody[F[_], Bd, A](f: ReqInputWithBody[Bd] => F[A]) = 41 | new PutWithBodyRequest[Unit, F, Bd, A] { 42 | def apply(uri: List[String], queries: Map[String, List[String]], headers: Map[String, String], body: Bd, cm: TestClientM): F[A] = 43 | f(ReqInputWithBody("PUT", uri, queries, headers, body)) 44 | } 45 | 46 | def testRawPost[F[_]](pure: ReqInput => F[ReqInput]) = new RawPostRequest[Unit, F] { 47 | type Resp = ReqInput 48 | 49 | def apply(uri: List[String], queries: Map[String, List[String]], headers: Map[String, String], cm: TestClientM): F[Resp] = 50 | pure(ReqInput("POST", uri, queries, headers)) 51 | } 52 | def testPost[F[_], A](f: ReqInput => F[A]) = new PostRequest[Unit, F, A] { 53 | def apply(uri: List[String], queries: Map[String, List[String]], headers: Map[String, String], cm: TestClientM): F[A] = 54 | f(ReqInput("POST", uri, queries, headers)) 55 | } 56 | 57 | def testRawPostWithBody[F[_], Bd](pure: ReqInputWithBody[Bd] => F[ReqInputWithBody[Bd]]) = 58 | new RawPostWithBodyRequest[Unit, F, Bd] { 59 | type Resp = ReqInputWithBody[Bd] 60 | 61 | def apply(uri: List[String], queries: Map[String, List[String]], headers: Map[String, String], body: Bd, cm: TestClientM): F[Resp] = 62 | pure(ReqInputWithBody("POST", uri, queries, headers, body)) 63 | } 64 | def testPostWithBody[F[_], Bd, A](f: ReqInputWithBody[Bd] => F[A]) = 65 | new PostWithBodyRequest[Unit, F, Bd, A] { 66 | def apply(uri: List[String], queries: Map[String, List[String]], headers: Map[String, String], body: Bd, cm: TestClientM): F[A] = 67 | f(ReqInputWithBody("POST", uri, queries, headers, body)) 68 | } 69 | 70 | def testRawDelete[F[_]](pure: ReqInput => F[ReqInput]) = new RawDeleteRequest[Unit, F] { 71 | type Resp = ReqInput 72 | 73 | def apply(uri: List[String], queries: Map[String, List[String]], headers: Map[String, String], cm: TestClientM): F[Resp] = 74 | pure(ReqInput("DELETE", uri, queries, headers)) 75 | } 76 | def testDelete[F[_], A](f: ReqInput => F[A]) = new DeleteRequest[Unit, F, A] { 77 | def apply(uri: List[String], queries: Map[String, List[String]], headers: Map[String, String], cm: TestClientM): F[A] = 78 | f(ReqInput("DELETE", uri, queries, headers)) 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /client/src/test/scala/typedapi/client/ClientManagerSpec.scala: -------------------------------------------------------------------------------- 1 | package typedapi.client 2 | 3 | import org.specs2.mutable.Specification 4 | 5 | final class ClientManagerSpec extends Specification { 6 | 7 | "optional port definition" >> { 8 | ClientManager((), "my-host", 80).base === "my-host:80" 9 | ClientManager((), "my-host").base === "my-host" 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /client/src/test/scala/typedapi/client/RequestDataBuilderSpec.scala: -------------------------------------------------------------------------------- 1 | package typedapi.client 2 | 3 | import typedapi.dsl._ 4 | import typedapi.client.test._ 5 | 6 | import shapeless.Id 7 | import org.specs2.mutable.Specification 8 | 9 | final class RequestDataBuilderSpec extends Specification { 10 | 11 | case class Foo() 12 | 13 | type Result = (String, List[String], Map[String, String], Map[String, String], Option[Foo]) 14 | 15 | implicit val get = testGet[Id, ReqInput](identity) 16 | implicit val put = testPut[Id, ReqInput](identity) 17 | implicit def putB[Bd] = testPutWithBody[Id, Bd, ReqInputWithBody[Bd]](identity) 18 | implicit val post = testPost[Id, ReqInput](identity) 19 | implicit def postB[Bd] = testPostWithBody[Id, Bd, ReqInputWithBody[Bd]](identity) 20 | implicit val delete = testDelete[Id, ReqInput](identity) 21 | 22 | "executes compiled api" >> { 23 | val cm = clientManager 24 | 25 | "single api" >> { 26 | "method" >> { 27 | val api0 = derive(:= :> Get[Json, ReqInput]) 28 | api0().run[Id](cm) === ReqInput("GET", Nil, Map(), Map(("Accept", "application/json"))) 29 | val api1 = derive(:= :> Put[Json, ReqInput]) 30 | api1().run[Id](cm) === ReqInput("PUT", Nil, Map(), Map(("Accept", "application/json"))) 31 | val api2 = derive(:= :> Post[Json, ReqInput]) 32 | api2().run[Id](cm) === ReqInput("POST", Nil, Map(), Map(("Accept", "application/json"))) 33 | val api3 = derive(:= :> Delete[Json, ReqInput]) 34 | api3().run[Id](cm) === ReqInput("DELETE", Nil, Map(), Map(("Accept", "application/json"))) 35 | } 36 | 37 | "segment" >> { 38 | val api0 = derive(:= :> Segment[Int]('i0) :> Get[Json, ReqInput]) 39 | api0(0).run[Id](cm) === ReqInput("GET", "0" :: Nil, Map(), Map(("Accept", "application/json"))) 40 | val api1 = derive(:= :> Segment[Int]('i0) :> Segment[Int]('i1) :> Get[Json, ReqInput]) 41 | api1(0, 1).run[Id](cm) === ReqInput("GET", "0" :: "1" :: Nil, Map(), Map(("Accept", "application/json"))) 42 | } 43 | 44 | "query" >> { 45 | val api0 = derive(:= :> Query[Int]('i0) :> Get[Json, ReqInput]) 46 | api0(0).run[Id](cm) === ReqInput("GET", Nil, Map("i0" -> List("0")), Map(("Accept", "application/json"))) 47 | val api1 = derive(:= :> Query[Int]('i0) :> Query[Int]('i1) :> Get[Json, ReqInput]) 48 | api1(0, 1).run[Id](cm) === ReqInput("GET", Nil, Map("i0" -> List("0"), "i1" -> List("1")), Map(("Accept", "application/json"))) 49 | val api2 = derive(:= :> Query[List[Int]]('i0) :> Get[Json, ReqInput]) 50 | api2(List(0, 1)).run[Id](cm) === ReqInput("GET", Nil, Map("i0" -> List("0", "1")), Map(("Accept", "application/json"))) 51 | api2(Nil).run[Id](cm) === ReqInput("GET", Nil, Map.empty, Map(("Accept", "application/json"))) 52 | val api3 = derive(:= :> Query[Option[Int]]('i0) :> Get[Json, ReqInput]) 53 | api3(Some(0)).run[Id](cm) === ReqInput("GET", Nil, Map("i0" -> List("0")), Map(("Accept", "application/json"))) 54 | api3(None).run[Id](cm) === ReqInput("GET", Nil, Map.empty, Map(("Accept", "application/json"))) 55 | } 56 | 57 | "header" >> { 58 | val api0 = derive(:= :> Header[Int]('i0) :> Get[Json, ReqInput]) 59 | api0(0).run[Id](cm) === ReqInput("GET", Nil, Map(), Map("Accept" -> "application/json", "i0" -> "0")) 60 | val api1 = derive(:= :> Header[Int]('i0) :> Header[Int]('i1) :> Get[Json, ReqInput]) 61 | api1(0, 1).run[Id](cm) === ReqInput("GET", Nil, Map(), Map("Accept" -> "application/json", "i0" -> "0", "i1" -> "1")) 62 | val api2 = derive(:= :> Header[Option[Int]]('i0) :> Get[Json, ReqInput]) 63 | api2(Some(0)).run[Id](cm) === ReqInput("GET", Nil, Map(), Map("Accept" -> "application/json", "i0" -> "0")) 64 | api2(None).run[Id](cm) === ReqInput("GET", Nil, Map(), Map("Accept" -> "application/json")) 65 | val api3 = derive(:= :> Header('i0, 'i1) :> Get[Json, ReqInput]) 66 | api3().run[Id](cm) === ReqInput("GET", Nil, Map(), Map("Accept" -> "application/json", "i0" -> "i1")) 67 | val api4 = derive(:= :> Client.Header[Int]('i0) :> Get[Json, ReqInput]) 68 | api4(0).run[Id](cm) === ReqInput("GET", Nil, Map(), Map("Accept" -> "application/json", "i0" -> "0")) 69 | val api5 = derive(:= :> Client.Header('i0, 'i1) :> Get[Json, ReqInput]) 70 | api5().run[Id](cm) === ReqInput("GET", Nil, Map(), Map("Accept" -> "application/json", "i0" -> "i1")) 71 | val api6 = derive(:= :> Client.Coll[Int] :> Get[Json, ReqInput]) 72 | api6(Map("hello" -> 5)).run[Id](cm) === ReqInput("GET", Nil, Map(), Map("Accept" -> "application/json", "hello" -> "5")) 73 | } 74 | 75 | "ignore server elements" >> { 76 | val api0 = derive(:= :> Server.Match[String]("Hello-") :> Server.Send("a", "b") :> Client.Header[Int]('i0) :> Get[Json, ReqInput]) 77 | api0(0).run[Id](cm) === ReqInput("GET", Nil, Map(), Map("Accept" -> "application/json", "i0" -> "0")) 78 | } 79 | 80 | "request body" >> { 81 | val api0 = derive(:= :> ReqBody[Json, Int] :> Put[Json, ReqInputWithBody[Int]]) 82 | api0(0).run[Id](cm) === ReqInputWithBody("PUT", Nil, Map(), Map(("Accept", "application/json")), 0) 83 | } 84 | 85 | "path" >> { 86 | val api0 = derive(:= :> "hello" :> "world" :> Get[Json, ReqInput]) 87 | api0().run[Id](cm) === ReqInput("GET", "hello" :: "world" :: Nil, Map(), Map(("Accept", "application/json"))) 88 | } 89 | } 90 | 91 | "raw" >> { 92 | implicit def rawPutB[Bd] = testRawPutWithBody[Id, Bd](identity) 93 | 94 | val api0 = derive(:= :> ReqBody[Json, Int] :> Put[Json, ReqInputWithBody[Int]]) 95 | api0(0).run[Id].raw(cm) === ReqInputWithBody("PUT", Nil, Map(), Map(("Accept", "application/json")), 0) 96 | } 97 | 98 | "composition" >> { 99 | val api = 100 | (:= :> "find" :> Server.Match[String]("Hello-") :> Server.Send("a", "b") :> Get[Json, ReqInput]) :|: 101 | (:= :> "fetch" :> Segment[String]('type) :> Get[Json, ReqInput]) :|: 102 | (:= :> "store" :> ReqBody[Json, Int] :> Post[Json, ReqInputWithBody[Int]]) 103 | 104 | val (find, fetch, store) = deriveAll(api) 105 | 106 | find().run[Id](cm) === ReqInput("GET", "find" :: Nil, Map(), Map(("Accept", "application/json"))) 107 | fetch("all").run[Id](cm) === ReqInput("GET", "fetch" :: "all" :: Nil, Map(), Map(("Accept", "application/json"))) 108 | store(0).run[Id](cm) === ReqInputWithBody("POST", "store" :: Nil, Map(), Map(("Accept", "application/json")), 0) 109 | } 110 | } 111 | } 112 | -------------------------------------------------------------------------------- /docs/ApiDefinition.md: -------------------------------------------------------------------------------- 1 | ## How to define an API 2 | The central idea behind Typedapi is to make client and server implementation as boilerplate-free, typesafe and simple as possible. 3 | 4 | - 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. 5 | - The server-side then has to comply with that contract by implementing proper endpoint functions. 6 | 7 | But how do you create this API definitions/contracts? This document will show you two ways provided by Typedapi: 8 | - use the DSL (`import typedapi.dsl._`) 9 | - or function-call-like definition (`import typedapi._`) 10 | 11 | ### Base case 12 | Every API has to fullfil the base case, meaning it has to have a root path and a method description: 13 | 14 | ```Scala 15 | // dsl 16 | := :> Get[MediaTypes.`application/json`, A] 17 | 18 | // or 19 | := :> Get[MT.`application/json`, A] 20 | 21 | // or in case of JSON 22 | := :> Get[Json, A] 23 | 24 | // function 25 | api(Get[Json, A]) 26 | // or 27 | api(method = Get[Json, A], path = Root) 28 | ``` 29 | 30 | This translates to `GET /` returning some `Json A`. 31 | 32 | ### Methods 33 | So far Typedapi supports the following methods: 34 | 35 | ```Scala 36 | // dsl 37 | := :> Get[Json, A] 38 | := :> Put[Json, A] 39 | := :> Post[Json, A] 40 | := :> Delete[Json, A] 41 | 42 | // function 43 | api(Get[Json, A]) 44 | api(Put[Json, A]) 45 | api(Post[Json, A]) 46 | api(Delete[Json, A]) 47 | ``` 48 | 49 | ### Request Body 50 | 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: 51 | 52 | ```Scala 53 | // PUT {body: User} / 54 | // dsl 55 | := :> ReqBody[Json, B] :> Put[Json, A] 56 | 57 | // function 58 | apiWithBody(Put[Json, A], ReqBody[Json, B]) 59 | 60 | // POST {body: User} / 61 | // dsl 62 | := :> ReqBody[Json, B] :> Post[Json, A] 63 | 64 | // function 65 | apiWithBody(Post[Json, A], ReqBody[Json, B]) 66 | ``` 67 | 68 | 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])`. 69 | 70 | ### One word to encodings 71 | 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`. 72 | 73 | ### Path 74 | ```Scala 75 | // GET /hello/world 76 | // dsl 77 | := :> "hello" :> "world" :> Get[Json, A] 78 | 79 | // function 80 | api(Get[Json, A], Root / "hello" / "world") 81 | ``` 82 | 83 | All path elements are translated to singleton types and therefore encoded in the type of the API. 84 | 85 | ### Segment 86 | Have a dynamic path element: 87 | 88 | ```Scala 89 | // GET /user/{name: String} 90 | // dsl 91 | := :> "user" :> Segment[String]("name") :> Get[Json, A] 92 | 93 | // function 94 | api(Get[Json, A], Root / "user" / Segment[String]("name")) 95 | ``` 96 | 97 | Every segment gets a name which is again encoded as singleton type in the API type. 98 | 99 | ### Query Parameter 100 | ```Scala 101 | // GET /query?{id: Int} 102 | // dsl 103 | := :> "query" :> Query[Int]("id") :> Get[Json, A] 104 | 105 | // function 106 | api(Get[Json, A], Root / "query", Queries.add[Int]("id")) 107 | ``` 108 | 109 | Every query gets a name which is again encoded as singleton type in the API type. 110 | 111 | #### Optional Query 112 | ```Scala 113 | // GET /query/opt?{id: Option[Int]} 114 | // dsl 115 | := :> "query" :> "opt" :> Query[Option[Int]]("id") :> Get[Json, A] 116 | 117 | // function 118 | api(Get[Json, A], Root / "query" / "opt", Queries.add[Option[Int]]("id")) 119 | ``` 120 | 121 | #### Query with a List of elements 122 | ```Scala 123 | // GET /query/list?{id: List[Int]} 124 | // dsl 125 | := :> "query" :> "list" :> Query[List[Int]]("id") :> Get[Json, A] 126 | 127 | // function 128 | api(Get[Json, A], Root / "query" / "list", Queries.add[List[Int]]("id")) 129 | ``` 130 | 131 | ### Header 132 | ```Scala 133 | // GET /header {headers: id: Int} 134 | // dsl 135 | := :> "header" :> Header[Int]("id") :> Get[Json, A] 136 | 137 | // function 138 | api(Get[Json, A], Root / "header", headers = Headers.add[Int]("id")) 139 | ``` 140 | 141 | This header is an expected input parameter. 142 | 143 | Every header gets a name which is again encoded as singleton type in the API type. 144 | 145 | #### Optional Header 146 | ```Scala 147 | // GET /header/opt {headers: id: Option[Int]} 148 | // dsl 149 | := :> "header" :> "opt" :> Header[Option[Int]]("id") :> Get[Json, A] 150 | 151 | // function 152 | api(Get[Json, A], Root / "header" / "opt", headers = Headers.add[Option[Int]]("id")) 153 | ``` 154 | 155 | #### Fixed Headers aka static headers 156 | 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: 157 | 158 | ```Scala 159 | // GET /header/fixed {headers: consumer=me} 160 | // dsl 161 | := :> "header" :> "fixed" :> Header("consumer", "me") :> Get[Json, A] 162 | 163 | // function 164 | api(Get[Json, A], Root / "header" / "fixed", headers = Headers.add("consumer", "me")) 165 | ``` 166 | 167 | #### Client-Side: Headers 168 | You have to send headers from the client-side but not server side? Here you go: 169 | 170 | ```Scala 171 | // GET /header/client {header: consumer: String} 172 | // dsl 173 | := :> "header" :> "client" :> Client.Header[String]("consumer") :> Get[Json, A] 174 | 175 | // function 176 | api(Get[Json, A], Root / "header" / "client", headers = Headers.client[String]("consumer")) 177 | ``` 178 | 179 | #### Client-Side: fixed/static Headers 180 | You have to send static headers from the client-side but not server side? Here you go: 181 | 182 | ```Scala 183 | // GET /header/client/fixed {header: consumer=me} 184 | // dsl 185 | := :> "header" :> "client" :> "fixed" :> Client.Header("consumer", "me") :> Get[Json, A] 186 | 187 | // function 188 | api(Get[Json, A], Root / "header" / "client" / "fixed", headers = Headers.client("consumer", "me")) 189 | ``` 190 | 191 | #### Client-Side: Header collections 192 | You can send header collections (`Map[String, V]`) as a single argument: 193 | 194 | ```Scala 195 | // GET /header/client/coll {headers: a:b:...} 196 | //dsl 197 | := :> "header" :> "client" :> "coll" :> Client.Coll[Int] :> Get[Json, A] 198 | 199 | //function 200 | api(Get[Json, A], Root / "header" / "client" / "coll", headers = Headers.clientColl[Int]) 201 | ``` 202 | 203 | #### Server-Side: send Headers 204 | You have to send headers from the server-side, e.g. for CORS? Here you go: 205 | 206 | ```Scala 207 | // GET /header/server/send => {header: consumer=me} 208 | // dsl 209 | := :> "header" :> "server" :> "send" :> Server.Send("consumer", "me") :> Get[Json, A] 210 | 211 | // function 212 | api(Get[Json, A], Root / "header" / "server" / "send", headers = Headers.serverSend("consumer", "me")) 213 | ``` 214 | 215 | #### Server-Side: extract matching Headers keys 216 | You want to extract all headers which contain a certain `String`? Here you go: 217 | 218 | ```Scala 219 | // GET /header/server/match {header: test1=me, test2=you} 220 | // dsl 221 | := :> "header" :> "server" :> "match" :> Server.Match[String]("test") :> Get[Json, A] 222 | 223 | // function 224 | api(Get[Json, A], Root / "header" / "server" / "match", headers = Headers.serverMatch[String]("test")) 225 | ``` 226 | 227 | This will give you a `Set[V]` with `V = String` in this example. 228 | 229 | ### Multiple definitions in a single API 230 | You can put multiple definitions into a single API element: 231 | 232 | ```Scala 233 | val Api = 234 | (:= :> "hello" :> Get[Json, A]) :|: 235 | (:= :> "world" :> Query[Int]('foo) :> Delete[B]) 236 | ``` 237 | -------------------------------------------------------------------------------- /docs/ClientCreation.md: -------------------------------------------------------------------------------- 1 | ## Create a client from your API 2 | 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. 3 | 4 | ```Scala 5 | val Api = 6 | (api(Get[Json, User], Root / "user" / Segment[String]("name"))) :|: 7 | (apiWithBody(Put[Json, User], ReqBody[Json, User], Root / "user")) 8 | ``` 9 | 10 | ### First thing first, derive your functions 11 | Lets derive our functions: 12 | 13 | ```Scala 14 | import typedapi.client._ 15 | 16 | final case class User(name: String) 17 | 18 | // implicit encoders and decoders 19 | 20 | val (get, create) = deriveAll(Api) 21 | ``` 22 | 23 | ### Http4s 24 | If you want to use [http4s](https://github.com/http4s/http4s) as your client backend you have to add the following code: 25 | 26 | ```Scala 27 | import typedapi.client.http4s._ 28 | import org.http4s.client.blaze.Http1Client 29 | 30 | val client = Http1Client[IO]().unsafeRunSync 31 | val cm = ClientManager(client, "http://my-host", myPort) 32 | ``` 33 | 34 | ### Akka-Http 35 | If you want to use [akka-http](https://github.com/akka/akka-http) as your client backend you have to add the following code: 36 | 37 | ```Scala 38 | import typedapi.client.akkahttp._ 39 | import akka.actor.ActorSystem 40 | import akka.stream.ActorMaterializer 41 | import akka.http.scaladsl.Http 42 | 43 | implicit val timeout = 5.second 44 | implicit val system = ActorSystem("akka-http-client") 45 | implicit val mat = ActorMaterializer() 46 | 47 | import system.dispatcher 48 | 49 | val cm = ClientManager(Http(), "http://my-host", myPort) 50 | ``` 51 | 52 | ### Scalaj-Http 53 | If you want to use [scalaj-http](https://github.com/scalaj/scalaj-http) as your client backend you have to add the following code: 54 | 55 | ```Scala 56 | import typedapi.client.scalajhttp._ 57 | import scalaj.http._ 58 | 59 | val cm = ClientManager(Http, "http://my-host", myPort) 60 | ``` 61 | 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. 62 | 63 | ```Scala 64 | implicit val decoder = Decoder[Future, User] { json => 65 | // unmarshall the json using some known lib like circe 66 | } 67 | 68 | implicit val encoder = Encoder[Future, User] { user => 69 | // marshall the user using some known lib like circe 70 | } 71 | ``` 72 | 73 | ### Ammonite 74 | 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: 75 | 76 | ```Scala 77 | import $ivy.`com.github.pheymann::typedapi-ammonite-client:` 78 | 79 | import typedapi._ 80 | import client._ 81 | import amm._ 82 | 83 | val cm = clientManager("http://localhost", 9000) 84 | 85 | final case class User(name: String, age: Int) 86 | 87 | val Api = api(Get[Json, User], Root / "user" / "url") 88 | 89 | val get = derive(Api) 90 | 91 | // gives you the raw scalaj-http response 92 | val response = get().run[Id].raw(cm) 93 | 94 | response.body 95 | response.headers 96 | ... 97 | ``` 98 | 99 | No `Decoder` needed if you use `raw(cm)`. Under the covers it uses scalaj-http as a client library. 100 | 101 | 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: 102 | 103 | ```Scala 104 | interp.repositories() ++= Seq(coursier.ivy.IvyRepository.fromPattern( 105 | "https://dl.bintray.com/dwijnand/sbt-plugins/" +: 106 | coursier.ivy.Pattern.default 107 | )) 108 | ``` 109 | 110 | ### ScalaJS 111 | 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: 112 | 113 | ```Scala 114 | import typedapi.client.js._ 115 | import org.scalajs.dom.ext.Ajax 116 | 117 | val cm = ClientManager(Ajax, "http://my-host", myPort) 118 | ``` 119 | 120 | 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. 121 | 122 | ```Scala 123 | implicit val decoder = Decoder[Future, User] { json => 124 | // unmarshall the json using some known lib like circe 125 | } 126 | 127 | implicit val encoder = Encoder[Future, User] { user => 128 | // marshall the user using some known lib like circe 129 | } 130 | ``` 131 | 132 | ### Usage 133 | Now we can to use our client functions: 134 | 135 | ```Scala 136 | for { 137 | _ <- create(User("Joe", 42)).run[IO](cm) 138 | user <- get("Joe").run[IO](cm) 139 | } yield user 140 | 141 | //F[User] 142 | ``` 143 | 144 | **Make sure** you have the proper encoders and decoders in place. 145 | -------------------------------------------------------------------------------- /docs/ExtendIt.md: -------------------------------------------------------------------------------- 1 | ## Extend Typedapi to fit your needs 2 | You ended up in this file if: 3 | - the default implements for a HTTP framework doesn't fit your needs 4 | - if the framework you want to use is not supported 5 | - 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 6 | 7 | ### General remark 8 | 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. 9 | 10 | ### Write your own Client backend 11 | 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: 12 | - `GetRequest` 13 | - `PutRequest` and `PutWithBodyRequest` 14 | - `PostRequest` and `PostWithBodyRequest` 15 | - `DeleteRequest` 16 | 17 | 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. 18 | 19 | You can implement all type-classes or just a subset to override implementations provided by TypedApi. 20 | 21 | ### Write your own Server backend 22 | 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 23 | 24 | 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. 25 | 26 | You can implement all type-classes or just a subset to override implementations provided by TypedApi. 27 | -------------------------------------------------------------------------------- /docs/ServerCreation.md: -------------------------------------------------------------------------------- 1 | ## Create a server from your API 2 | 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. 3 | 4 | ```Scala 5 | val Api = 6 | (api(Get[Json, User], Root / "user" / Segment[String]("name"))) :|: 7 | (apiWithBody(Put[Json, User], ReqBody[Json, User], Root / "user")) 8 | ``` 9 | 10 | ### First things first, derive the endpoints 11 | ```Scala 12 | import typedapi.server._ 13 | 14 | final case class User(name: String) 15 | 16 | // implicit encoders and decoders 17 | 18 | val endpoints = deriveAll[IO](Api).from( 19 | name => // retrieve and return user 20 | user => // store user 21 | ) 22 | ``` 23 | 24 | ### Set Status Codes 25 | #### Success 26 | ```Scala 27 | deriveAll[IO](Api).from( 28 | name => 29 | val user: User = ??? 30 | 31 | success(user) // creates a 200 32 | ... 33 | } 34 | ``` 35 | 36 | or 37 | 38 | ```Scala 39 | deriveAll[IO](Api).from( 40 | name => 41 | val user: User = ??? 42 | 43 | successWith(StatusCodes.Ok)(user) // set code 44 | ... 45 | } 46 | ``` 47 | 48 | #### Error 49 | ```Scala 50 | deriveAll[IO](Api).from( 51 | name => 52 | val user: Option[User] = ??? 53 | 54 | user.fold(errorWith(StatusCodes.NotFound, s"no user $id")(user => success(user)) 55 | ... 56 | } 57 | ``` 58 | 59 | ### Http4s 60 | If you want to use [http4s](https://github.com/http4s/http4s) as your server backend you have to add the following code: 61 | 62 | ```Scala 63 | import typedapi.server.http4s._ 64 | import org.http4s.server.blaze.BlazeBuilder 65 | 66 | val sm = ServerManager(BlazeBuilder[IO], "http://my-host", myPort) 67 | ``` 68 | 69 | ### Akka-Http 70 | If you want to use [akka-http](https://github.com/akka/akka-http) as your server backend you have to add the following code: 71 | 72 | ```Scala 73 | implicit val timeout = 5.second 74 | implicit val system = ActorSystem("akka-http-server") 75 | implicit val mat = ActorMaterializer() 76 | 77 | import system.dispatcher 78 | 79 | val sm = ServerManager(Http(), "http://my-host", myPort) 80 | ``` 81 | 82 | ### Start server 83 | Now we can mount `endpoints` and serve to to the world: 84 | 85 | ```Scala 86 | val server = mount(sm, endpoints) 87 | 88 | server.unsafeRunSync() 89 | ``` 90 | 91 | **Make sure** you have the proper encoders and decoders in place. 92 | -------------------------------------------------------------------------------- /docs/example/ammonite_client_example.sc: -------------------------------------------------------------------------------- 1 | import $ivy.`com.github.pheymann::typedapi-ammonite-client:0.2.0-M1` 2 | 3 | import typedapi._ 4 | import client._ 5 | import amm._ 6 | 7 | val cm = clientManager("http://localhost", 9000) 8 | 9 | final case class User(name: String, age: Int) 10 | 11 | val Api = api(Get[Json, User], Root, headers = Headers.serverSend("Access-Control-Allow-Origin", "*")) 12 | 13 | val get = derive(Api) 14 | 15 | val response = get().run[Id].raw(cm) 16 | -------------------------------------------------------------------------------- /docs/example/build.sbt: -------------------------------------------------------------------------------- 1 | 2 | val typedapiVersion = "0.2.0" 3 | val http4sVersion = "0.18.0" 4 | 5 | val commonSettings = Seq( 6 | scalaVersion := "2.12.4" 7 | ) 8 | 9 | lazy val root = project 10 | .in(file(".")) 11 | .aggregate(`shared-jvm`, `shared-js`, `client-jvm`, `client-js`, server) 12 | 13 | lazy val shared = crossProject.crossType(CrossType.Pure) 14 | .in(file("shared")) 15 | .settings(commonSettings: _*) 16 | .settings( 17 | libraryDependencies ++= Seq( 18 | "com.github.pheymann" %%% "typedapi-shared" % typedapiVersion, 19 | 20 | "io.circe" %%% "circe-core" % "0.9.1", 21 | "io.circe" %%% "circe-parser" % "0.9.1", 22 | "io.circe" %%% "circe-generic" % "0.9.1" 23 | ) 24 | ) 25 | 26 | lazy val `shared-js` = shared.js 27 | lazy val `shared-jvm` = shared.jvm 28 | 29 | lazy val server = project 30 | .in(file("server")) 31 | .settings(commonSettings: _*) 32 | .settings( 33 | libraryDependencies ++= Seq( 34 | "com.github.pheymann" %% "typedapi-http4s-server" % typedapiVersion, 35 | 36 | "org.http4s" %% "http4s-circe" % http4sVersion, 37 | "org.http4s" %% "http4s-blaze-server" % http4sVersion, 38 | "org.http4s" %% "http4s-dsl" % http4sVersion 39 | ) 40 | ) 41 | .dependsOn(`shared-jvm`) 42 | 43 | lazy val `client-jvm` = project 44 | .in(file("client-jvm")) 45 | .settings(commonSettings: _*) 46 | .settings( 47 | libraryDependencies ++= Seq( 48 | "com.github.pheymann" %% "typedapi-http4s-client" % typedapiVersion, 49 | 50 | "org.http4s" %% "http4s-circe" % http4sVersion, 51 | "org.http4s" %% "http4s-blaze-client" % http4sVersion, 52 | "org.http4s" %% "http4s-dsl" % http4sVersion 53 | ) 54 | ) 55 | .dependsOn(`shared-jvm`) 56 | 57 | lazy val `client-js` = project 58 | .in(file("client-js")) 59 | .enablePlugins(ScalaJSPlugin) 60 | .settings(commonSettings: _*) 61 | .settings( 62 | libraryDependencies ++= Seq( 63 | "com.github.pheymann" %%% "typedapi-js-client" % typedapiVersion, 64 | ), 65 | scalaJSUseMainModuleInitializer := true 66 | ) 67 | .dependsOn(`shared-js`) 68 | -------------------------------------------------------------------------------- /docs/example/client-js/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /docs/example/client-js/src/main/scala/Client.scala: -------------------------------------------------------------------------------- 1 | 2 | import typedapi.client._ 3 | import typedapi.client.js._ 4 | import org.scalajs.dom.ext.Ajax 5 | import io.circe._ 6 | import io.circe.generic.auto._ 7 | import io.circe.parser._ 8 | import io.circe.syntax._ 9 | 10 | import scala.concurrent.Future 11 | import scala.concurrent.ExecutionContext.Implicits.global 12 | import scala.concurrent.duration._ 13 | 14 | object Client { 15 | 16 | type Id[A] = A 17 | 18 | final case class DecodeException(msg: String) extends Exception 19 | 20 | implicit val decoder = typedapi.util.Decoder[Future, User](json => decode[User](json).fold( 21 | error => Future.successful(Left(DecodeException(error.toString()))), 22 | user => Future.successful(Right(user)) 23 | )) 24 | implicit val encoder = typedapi.util.Encoder[Future, User](user => Future.successful(user.asJson.noSpaces)) 25 | 26 | val (get, put, post, delete, path, putBody, segment, search, header, fixed, client, coll, matches) = deriveAll(FromDsl.MyApi) 27 | 28 | def main(args: Array[String]): Unit = { 29 | val cm = ClientManager(Ajax, "http://localhost", 9000) 30 | 31 | (for { 32 | u0 <- putBody(User("joe", 27)).run[Future](cm) 33 | u1 <- search("joe").run[Future](cm) 34 | } yield (u0, u1)).foreach { case (u0, u1) => 35 | println(u0) 36 | println(u1) 37 | } 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /docs/example/client-jvm/src/main/scala/Client.scala: -------------------------------------------------------------------------------- 1 | 2 | import typedapi.client._ 3 | import typedapi.client.http4s._ 4 | import cats.effect.IO 5 | import org.http4s._ 6 | import org.http4s.circe._ 7 | 8 | object Client { 9 | 10 | implicit val decoder = jsonOf[IO, User] 11 | implicit val encoder = jsonEncoderOf[IO, User] 12 | 13 | val (get, put, post, delete, path, putBody, segment, search, header, fixed, client, coll, matches) = deriveAll(FromDsl.MyApi) 14 | 15 | def main(args: Array[String]): Unit = { 16 | import User._ 17 | import cats.effect.IO 18 | import org.http4s.client.blaze.Http1Client 19 | 20 | val cm = ClientManager(Http1Client[IO]().unsafeRunSync, "http://localhost", 9000) 21 | 22 | (for { 23 | u0 <- putBody(User("joe", 27)).run[IO](cm) 24 | u1 <- search("joe").run[IO](cm) 25 | } yield { 26 | println(u0) 27 | println(u1) 28 | () 29 | }).unsafeRunSync() 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /docs/example/project/build.properties: -------------------------------------------------------------------------------- 1 | sbt.version=1.0.4 -------------------------------------------------------------------------------- /docs/example/project/plugins.sbt: -------------------------------------------------------------------------------- 1 | addSbtPlugin("org.scala-js" % "sbt-scalajs" % "0.6.22") 2 | -------------------------------------------------------------------------------- /docs/example/server/src/main/scala/Server.scala: -------------------------------------------------------------------------------- 1 | 2 | import typedapi.server._ 3 | import typedapi.server.http4s._ 4 | import cats.effect.IO 5 | import org.http4s._ 6 | import org.http4s.circe._ 7 | 8 | object Server { 9 | 10 | implicit val decoder = jsonOf[IO, User] 11 | implicit val encoder = jsonEncoderOf[IO, User] 12 | 13 | val get: () => IO[Result[User]] = () => IO.pure(success(User("Joe", 42))) 14 | val put: () => IO[Result[User]] = get 15 | val post: () => IO[Result[User]] = get 16 | val delete: () => IO[Result[User]] = get 17 | 18 | val path: () => IO[Result[User]] = get 19 | 20 | val putBody: User => IO[Result[User]] = user => IO.pure(success(user)) 21 | val segment: String => IO[Result[User]] = name => IO.pure(success(User(name, 42))) 22 | val search: String => IO[Result[User]] = segment 23 | 24 | val header: String => IO[Result[User]] = consumer => IO.pure(success(User("found: " + consumer, 42))) 25 | val fixed: () => IO[Result[User]] = get 26 | val client: () => IO[Result[User]] = get 27 | val coll: () => IO[Result[User]] = get 28 | val matching: Map[String, String] => IO[Result[User]] = matches => IO.pure(success(User(matches.mkString(","), 42))) 29 | 30 | val endpoints = deriveAll[IO](FromDefinition.MyApi).from( 31 | get, 32 | put, 33 | post, 34 | delete, 35 | path, 36 | putBody, 37 | segment, 38 | search, 39 | header, 40 | fixed, 41 | client, 42 | coll, 43 | matching 44 | ) 45 | 46 | def main(args: Array[String]): Unit = { 47 | import org.http4s.server.blaze.BlazeBuilder 48 | 49 | val sm = ServerManager(BlazeBuilder[IO], "localhost", 9000) 50 | 51 | mount(sm, endpoints).unsafeRunSync() 52 | 53 | scala.io.StdIn.readLine("Press 'Enter' to stop ...") 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /docs/example/shared/src/main/scala/Apis.scala: -------------------------------------------------------------------------------- 1 | 2 | object FromDsl { 3 | 4 | import typedapi.dsl._ 5 | 6 | /* NOTE: we have to add the 'Access-Control-Allow-Origin' header to the server-side to allow the 7 | * browser (ScalaJS) to access the server (CORS) 8 | */ 9 | 10 | val MyApi = 11 | // basic GET request 12 | (:= :> Server.Send("Access-Control-Allow-Origin", "*") :> Get[Json, User]) :|: 13 | // basic PUT request 14 | (:= :> Server.Send("Access-Control-Allow-Origin", "*") :> Put[Json, User]) :|: 15 | // basic POST request 16 | (:= :> Server.Send("Access-Control-Allow-Origin", "*") :> Post[Json, User]) :|: 17 | // basic DELETE request 18 | (:= :> Server.Send("Access-Control-Allow-Origin", "*") :> Delete[Json, User]) :|: 19 | // define a path 20 | (:= :> "my" :> "path" :> Server.Send("Access-Control-Allow-Origin", "*") :> Get[Json, User]) :|: 21 | // add a request body 22 | (:= :> "with" :> "body" :> Server.Send("Access-Control-Allow-Origin", "*") :> ReqBody[Json, User] :> Put[Json, User]) :|: 23 | // add segments 24 | (:= :> "name" :> Segment[String]("name") :> Server.Send("Access-Control-Allow-Origin", "*") :> Get[Json, User]) :|: 25 | // add query 26 | (:= :> "search" :> "user" :> Query[String]("name") :> Server.Send("Access-Control-Allow-Origin", "*") :> Get[Json, User]) :|: 27 | // add header 28 | (:= :> "header" :> Header[String]("consumer") :> Server.Send("Access-Control-Allow-Origin", "*") :> Get[Json, User]) :|: 29 | (:= :> "header" :> "fixed" :> Header("consumer", "me") :> Server.Send("Access-Control-Allow-Origin", "*") :> Get[Json, User]) :|: 30 | (:= :> "header" :> "client" :> Client.Header("client", "me") :> Server.Send("Access-Control-Allow-Origin", "*") :> Get[Json, User]) :|: 31 | (:= :> "header" :> "client" :> "coll" :> Client.Coll[Int] :> Server.Send("Access-Control-Allow-Origin", "*") :> Get[Json, User]) :|: 32 | (:= :> "header" :> "server" :> Server.Match[String]("Control-") :> Server.Send("Access-Control-Allow-Origin", "*") :> Get[Json, User]) 33 | } 34 | 35 | object FromDefinition { 36 | 37 | import typedapi._ 38 | 39 | val MyApi = 40 | // basic GET request 41 | api(Get[Json, User], headers = Headers.serverSend("Access-Control-Allow-Origin", "*")) :|: 42 | // basic PUT request 43 | api(Put[Json, User], headers = Headers.serverSend("Access-Control-Allow-Origin", "*")) :|: 44 | // basic POST request 45 | api(Post[Json, User], headers = Headers.serverSend("Access-Control-Allow-Origin", "*")) :|: 46 | // basic Delete request 47 | api(Delete[Json, User], headers = Headers.serverSend("Access-Control-Allow-Origin", "*")) :|: 48 | // define a path 49 | api(Get[Json, User], Root / "my" / "path", headers = Headers.serverSend("Access-Control-Allow-Origin", "*")) :|: 50 | // add a request body 51 | apiWithBody(Put[Json, User], ReqBody[Json, User], Root / "with" / "body", headers = Headers.serverSend("Access-Control-Allow-Origin", "*")) :|: 52 | // add segments 53 | api(Get[Json, User], Root / "name" / Segment[String]("name"), headers = Headers.serverSend("Access-Control-Allow-Origin", "*")) :|: 54 | // add query 55 | api(Get[Json, User], Root / "search" / "user", Queries.add[String]("name"), Headers.serverSend("Access-Control-Allow-Origin", "*")) :|: 56 | // add header 57 | api(Get[Json, User], Root / "header", headers = Headers.add[String]("consumer").serverSend("Access-Control-Allow-Origin", "*")) :|: 58 | api(Get[Json, User], Root / "header" / "fixed", headers = Headers.add("consumer", "me").serverSend("Access-Control-Allow-Origin", "*")) :|: 59 | api(Get[Json, User], Root / "header" / "client", headers = Headers.client("client", "me").serverSend("Access-Control-Allow-Origin", "*")) :|: 60 | api(Get[Json, User], Root / "header" / "client" / "coll", headers = Headers.clientColl[Int].serverSend("Access-Control-Allow-Origin", "*")) :|: 61 | api(Get[Json, User], Root / "header" / "server", headers = Headers.serverMatch[String]("Control-").serverSend("Access-Control-Allow-Origin", "*")) 62 | } 63 | -------------------------------------------------------------------------------- /docs/example/shared/src/main/scala/User.scala: -------------------------------------------------------------------------------- 1 | 2 | import io.circe.syntax._ 3 | import io.circe.generic.JsonCodec 4 | 5 | final case class User(name: String, age: Int) 6 | 7 | object User { 8 | 9 | implicit val enc = io.circe.generic.semiauto.deriveEncoder[User] 10 | implicit val dec = io.circe.generic.semiauto.deriveDecoder[User] 11 | } 12 | -------------------------------------------------------------------------------- /http-support-tests/src/test/scala/http/support/tests/User.scala: -------------------------------------------------------------------------------- 1 | package http.support.tests 2 | 3 | import cats.effect.IO 4 | import io.circe.generic.semiauto._ 5 | import org.http4s.circe._ 6 | import org.http4s.dsl.io._ 7 | 8 | final case class User(name: String, age: Int) 9 | 10 | object UserCoding { 11 | 12 | implicit val enc = deriveEncoder[User] 13 | implicit val dec = deriveDecoder[User] 14 | 15 | implicit val decoderIO = jsonOf[IO, User] 16 | implicit val encoderIO = jsonEncoderOf[IO, User] 17 | } 18 | -------------------------------------------------------------------------------- /http-support-tests/src/test/scala/http/support/tests/client/AkkaHttpClientSupportSpec.scala: -------------------------------------------------------------------------------- 1 | package http.support.tests.client 2 | 3 | import http.support.tests.{User, UserCoding, Api} 4 | import typedapi.client._ 5 | import typedapi.client.akkahttp._ 6 | import akka.actor.ActorSystem 7 | import akka.stream.ActorMaterializer 8 | import akka.http.scaladsl.Http 9 | import de.heikoseeberger.akkahttpcirce.FailFastCirceSupport 10 | import org.specs2.mutable.Specification 11 | import org.specs2.concurrent.ExecutionEnv 12 | 13 | import scala.concurrent.duration._ 14 | import scala.concurrent.{Future, Await} 15 | 16 | final class AkkaHttpClientSupportSpec(implicit ee: ExecutionEnv) extends Specification { 17 | 18 | import UserCoding._ 19 | import FailFastCirceSupport._ 20 | 21 | sequential 22 | 23 | implicit val timeout = 5.second 24 | implicit val system = ActorSystem("akka-http-client-spec", defaultExecutionContext = Some(ee.ec)) 25 | implicit val mat = ActorMaterializer() 26 | 27 | import system.dispatcher 28 | 29 | val cm = ClientManager(Http(), "http://localhost", 9001) 30 | val server = TestServer.start() 31 | 32 | "akka http client support" >> { 33 | val (p, s, q, header, fixed, clInH, clFixH, clColl, serMatchH, serSendH, m0, m1, m2, m3, m4, m5, _, _, _) = deriveAll(Api) 34 | 35 | "paths and segments" >> { 36 | p().run[Future](cm) must beEqualTo(User("foo", 27)).awaitFor(timeout) 37 | s("jim").run[Future](cm) must beEqualTo(User("jim", 27)).awaitFor(timeout) 38 | } 39 | 40 | "queries" >> { 41 | q(42).run[Future](cm) must beEqualTo(User("foo", 42)).awaitFor(timeout) 42 | } 43 | 44 | "headers" >> { 45 | header(42).run[Future](cm) must beEqualTo(User("foo", 42)).awaitFor(timeout) 46 | fixed().run[Future](cm) must beEqualTo(User("joe", 27)).awaitFor(timeout) 47 | clInH("jim").run[Future](cm) must beEqualTo(User("jim", 27)).awaitFor(timeout) 48 | clFixH().run[Future](cm) must beEqualTo(User("joe", 27)).awaitFor(timeout) 49 | clColl(Map("coll" -> "joe", "collect" -> "jim")).run[Future](cm) must beEqualTo(User("coll: joe,collect: jim", 27)).awaitFor(timeout) 50 | serMatchH().run[Future](cm) must beEqualTo(User("joe", 27)).awaitFor(timeout) 51 | serSendH().run[Future](cm) must beEqualTo(User("joe", 27)).awaitFor(timeout) 52 | } 53 | 54 | "methods" >> { 55 | m0().run[Future](cm) must beEqualTo(User("foo", 27)).awaitFor(timeout) 56 | m1().run[Future](cm) must beEqualTo(User("foo", 27)).awaitFor(timeout) 57 | m2(User("jim", 42)).run[Future](cm) must beEqualTo(User("jim", 42)).awaitFor(timeout) 58 | m3().run[Future](cm) must beEqualTo(User("foo", 27)).awaitFor(timeout) 59 | m4(User("jim", 42)).run[Future](cm) must beEqualTo(User("jim", 42)).awaitFor(timeout) 60 | m5(List("because")).run[Future](cm) must beEqualTo(User("foo", 27)).awaitFor(timeout) 61 | } 62 | 63 | step { 64 | server.shutdown.unsafeRunSync() 65 | Await.ready(system.terminate, timeout) 66 | } 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /http-support-tests/src/test/scala/http/support/tests/client/Http4sClientSupportSpec.scala: -------------------------------------------------------------------------------- 1 | package http.support.tests.client 2 | 3 | import http.support.tests.{UserCoding, User, Api} 4 | import typedapi.client._ 5 | import typedapi.client.http4s._ 6 | import cats.effect.IO 7 | import org.http4s.client.blaze.Http1Client 8 | import org.specs2.mutable.Specification 9 | 10 | final class Http4sClientSupportSpec extends Specification { 11 | 12 | import UserCoding._ 13 | 14 | sequential 15 | 16 | val cm = ClientManager(Http1Client[IO]().unsafeRunSync, "http://localhost", 9001) 17 | 18 | val server = TestServer.start() 19 | 20 | "http4s client support" >> { 21 | val (p, s, q, header, fixed, clInH, clFixH, clColl, serMatchH, serSendH, m0, m1, m2, m3, m4, m5, _, _, _) = deriveAll(Api) 22 | 23 | "paths and segments" >> { 24 | p().run[IO](cm).unsafeRunSync() === User("foo", 27) 25 | s("jim").run[IO](cm).unsafeRunSync() === User("jim", 27) 26 | } 27 | 28 | "queries" >> { 29 | q(42).run[IO](cm).unsafeRunSync() === User("foo", 42) 30 | } 31 | 32 | "headers" >> { 33 | header(42).run[IO](cm).unsafeRunSync() === User("foo", 42) 34 | fixed().run[IO](cm).unsafeRunSync() === User("joe", 27) 35 | clInH("jim").run[IO](cm).unsafeRunSync === User("jim", 27) 36 | clFixH().run[IO](cm).unsafeRunSync() === User("joe", 27) 37 | clColl(Map("coll" -> "joe", "collect" -> "jim")).run[IO](cm).unsafeRunSync === User("coll: joe,collect: jim", 27) 38 | serMatchH().run[IO](cm).unsafeRunSync() === User("joe", 27) 39 | serSendH().run[IO](cm).unsafeRunSync() === User("joe", 27) 40 | } 41 | 42 | "methods" >> { 43 | m0().run[IO](cm).unsafeRunSync() === User("foo", 27) 44 | m1().run[IO](cm).unsafeRunSync() === User("foo", 27) 45 | m2(User("jim", 42)).run[IO](cm).unsafeRunSync() === User("jim", 42) 46 | m3().run[IO](cm).unsafeRunSync() === User("foo", 27) 47 | m4(User("jim", 42)).run[IO](cm).unsafeRunSync() === User("jim", 42) 48 | m5(List("because")).run[IO](cm).unsafeRunSync() === User("foo", 27) 49 | } 50 | 51 | step { 52 | server.shutdown.unsafeRunSync() 53 | } 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /http-support-tests/src/test/scala/http/support/tests/client/ScalajHttpClientSupportSpec.scala: -------------------------------------------------------------------------------- 1 | package http.support.tests.client 2 | 3 | import http.support.tests.{UserCoding, User, Api} 4 | import typedapi.client._ 5 | import typedapi.client.scalajhttp._ 6 | import scalaj.http.Http 7 | import io.circe.parser._ 8 | import io.circe.syntax._ 9 | import org.specs2.mutable.Specification 10 | 11 | final class ScalajHttpClientSupportSpec extends Specification { 12 | 13 | import UserCoding._ 14 | 15 | sequential 16 | 17 | case class DecodeException(msg: String) extends Exception 18 | 19 | implicit val decoder = typedapi.util.Decoder[Id, User](json => decode[User](json).fold( 20 | error => Left(DecodeException(error.toString())), 21 | user => Right(user) 22 | )) 23 | implicit val encoder = typedapi.util.Encoder[Id, User](user => user.asJson.noSpaces) 24 | 25 | val cm = ClientManager(Http, "http://localhost", 9001) 26 | 27 | val server = TestServer.start() 28 | 29 | "http4s client support" >> { 30 | val (p, s, q, header, fixed, clInH, clFixH, clColl, serMatchH, serSendH, m0, m1, m2, m3, m4, m5, _, _, _) = deriveAll(Api) 31 | 32 | "paths and segments" >> { 33 | p().run[Blocking](cm) === Right(User("foo", 27)) 34 | s("jim").run[Blocking](cm) === Right(User("jim", 27)) 35 | } 36 | 37 | "queries" >> { 38 | q(42).run[Blocking](cm) === Right(User("foo", 42)) 39 | } 40 | 41 | "headers" >> { 42 | header(42).run[Blocking](cm) === Right(User("foo", 42)) 43 | fixed().run[Blocking](cm) === Right(User("joe", 27)) 44 | clInH("jim").run[Blocking](cm) === Right(User("jim", 27)) 45 | clFixH().run[Blocking](cm) === Right(User("joe", 27)) 46 | clColl(Map("coll" -> "joe", "collect" -> "jim")).run[Blocking](cm) === Right(User("collect: jim,coll: joe", 27)) 47 | serMatchH().run[Blocking](cm) === Right(User("joe", 27)) 48 | serSendH().run[Blocking](cm) === Right(User("joe", 27)) 49 | } 50 | 51 | "methods" >> { 52 | m0().run[Blocking](cm) === Right(User("foo", 27)) 53 | m1().run[Blocking](cm) === Right(User("foo", 27)) 54 | m2(User("jim", 42)).run[Blocking](cm) === Right(User("jim", 42)) 55 | m3().run[Blocking](cm) === Right(User("foo", 27)) 56 | m4(User("jim", 42)).run[Blocking](cm) === Right(User("jim", 42)) 57 | m5(List("because")).run[Blocking](cm) === Right(User("foo", 27)) 58 | } 59 | 60 | "raw" >> { 61 | m0().run[Id].raw(cm).body === """{"name":"foo","age":27}""" 62 | } 63 | 64 | step { 65 | server.shutdown.unsafeRunSync() 66 | } 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /http-support-tests/src/test/scala/http/support/tests/client/TestServer.scala: -------------------------------------------------------------------------------- 1 | package http.support.tests.client 2 | 3 | import http.support.tests.{User, UserCoding} 4 | import cats.effect.IO 5 | import io.circe.syntax._ 6 | import org.http4s._ 7 | import org.http4s.circe._ 8 | import org.http4s.dsl.io._ 9 | import org.http4s.server.Server 10 | import org.http4s.server.blaze._ 11 | 12 | object Age extends QueryParamDecoderMatcher[Int]("age") 13 | 14 | object Reasons { 15 | def unapplySeq(params: Map[String, Seq[String]]) = params.get("reasons") 16 | def unapply(params: Map[String, Seq[String]]) = unapplySeq(params) 17 | } 18 | 19 | object TestServer { 20 | 21 | import UserCoding._ 22 | 23 | val service = HttpService[IO] { 24 | case GET -> Root / "path" => Ok(User("foo", 27)) 25 | case GET -> Root / "segment" / name => Ok(User(name, 27)) 26 | 27 | case GET -> Root / "query" :? Age(age) => Ok(User("foo", age)) 28 | 29 | case req @ GET -> Root / "header" => 30 | val headers = req.headers.toList 31 | val age = headers.find(_.name.value == "age").get.value.toInt 32 | 33 | Ok(User("foo", age)) 34 | 35 | case req @ GET -> Root / "header" / "fixed" => 36 | val headers = req.headers.toList 37 | val value = headers.find(_.name.value == "Hello").get.value 38 | 39 | if (value != "*") 40 | throw new IllegalArgumentException("unexpected header value " + value) 41 | 42 | Ok(User("joe", 27)) 43 | 44 | case req @ GET -> Root / "header" / "client" => 45 | val headers = req.headers.toList 46 | val value = headers.find(_.name.value == "Hello").get.value 47 | 48 | if (value != "*") 49 | throw new IllegalArgumentException("unexpected header value " + value) 50 | 51 | Ok(User("joe", 27)) 52 | 53 | case req @ GET -> Root / "header" / "client" / "coll" => 54 | val headers = req.headers.toList 55 | val values = headers.filter(_.name.value.contains("coll")) 56 | 57 | if (values.isEmpty) 58 | throw new IllegalArgumentException("no header collection ") 59 | 60 | Ok(User(values.mkString(","), 27)) 61 | 62 | case req @ GET -> Root / "header" / "input" / "client" => 63 | val headers = req.headers.toList 64 | val value = headers.find(_.name.value == "Hello").get.value 65 | 66 | Ok(User(value, 27)) 67 | 68 | case req @ GET -> Root / "header" / "server" / "send" => 69 | Ok(User("joe", 27)).map(resp => resp.copy(headers = resp.headers put Header("Hello", "*"))) 70 | 71 | case req @ GET -> Root / "header" / "server" / "match" => 72 | Ok(User("joe", 27)) 73 | 74 | case GET -> Root => Ok(User("foo", 27)) 75 | case PUT -> Root => Ok(User("foo", 27)) 76 | case req @ PUT -> Root / "body" => Ok(User("foo", 27)) 77 | for { 78 | user <- req.as[User] 79 | resp <- Ok(user.asJson) 80 | } yield resp 81 | 82 | case POST -> Root => Ok(User("foo", 27)) 83 | case req @ POST -> Root / "body" => Ok(User("foo", 27)) 84 | for { 85 | user <- req.as[User] 86 | resp <- Ok(user.asJson) 87 | } yield resp 88 | 89 | case DELETE -> Root :? Reasons(reasons) => 90 | println(reasons) 91 | Ok(User("foo", 27)) 92 | } 93 | 94 | def start(): Server[IO] = { 95 | val builder = BlazeBuilder[IO] 96 | .bindHttp(9001, "localhost") 97 | .mountService(service, "/") 98 | .start 99 | 100 | builder.unsafeRunSync() 101 | } 102 | } 103 | -------------------------------------------------------------------------------- /http-support-tests/src/test/scala/http/support/tests/package.scala: -------------------------------------------------------------------------------- 1 | package http.support 2 | 3 | import typedapi.dsl._ 4 | 5 | package object tests { 6 | 7 | val Api = 8 | (:= :> "path" :> Get[Json, User]) :|: 9 | (:= :> "segment" :> Segment[String]('name) :> Get[Json, User]) :|: 10 | (:= :> "query" :> Query[Int]('age) :> Get[Json, User]) :|: 11 | (:= :> "header" :> Header[Int]('age) :> Get[Json, User]) :|: 12 | (:= :> "header" :> "fixed" :> Header("Hello", "*") :> Get[Json, User]) :|: 13 | (:= :> "header" :> "input" :> "client" :> Client.Header[String]("Hello") :> Get[Json, User]) :|: 14 | (:= :> "header" :> "client" :> Client.Header("Hello", "*") :> Get[Json, User]) :|: 15 | (:= :> "header" :> "client" :> "coll" :> Client.Coll[String] :> Get[Json, User]) :|: 16 | (:= :> "header" :> "server" :> "match" :> Server.Match[String]("test") :> Get[Json, User]) :|: 17 | (:= :> "header" :> "server" :> "send" :> Server.Send("Hello", "*") :> Get[Json, User]) :|: 18 | (:= :> Get[Json, User]) :|: 19 | (:= :> Put[Json, User]) :|: 20 | (:= :> "body" :> ReqBody[Json, User] :> Put[Json, User]) :|: 21 | (:= :> Post[Json, User]) :|: 22 | (:= :> "body" :> ReqBody[Json, User] :> Post[Json, User]) :|: 23 | (:= :> Query[List[String]]('reasons) :> Delete[Json, User]) :|: 24 | (:= :> "status" :> "200" :> Get[Plain, String]) :|: 25 | (:= :> "status" :> "400" :> Get[Plain, String]) :|: 26 | (:= :> "status" :> "500" :> Get[Plain, String]) 27 | } 28 | -------------------------------------------------------------------------------- /http-support-tests/src/test/scala/http/support/tests/server/AkkaHttpServerSupportSpec.scala: -------------------------------------------------------------------------------- 1 | package http.support.tests.server 2 | 3 | import http.support.tests.{Api, UserCoding} 4 | import typedapi.server._ 5 | import typedapi.server.akkahttp._ 6 | import akka.actor.ActorSystem 7 | import akka.stream.ActorMaterializer 8 | import akka.http.scaladsl.Http 9 | import de.heikoseeberger.akkahttpcirce.FailFastCirceSupport 10 | import cats.implicits._ 11 | import org.specs2.concurrent.ExecutionEnv 12 | 13 | import scala.concurrent.{Future, Await} 14 | import scala.concurrent.duration._ 15 | 16 | final class AkkaHttpServerSupportSpec(implicit ee: ExecutionEnv) extends ServerSupportSpec[Future]()(catsStdInstancesForFuture(ee.ec)) { 17 | 18 | import UserCoding._ 19 | import FailFastCirceSupport._ 20 | 21 | implicit val timeout = 5.second 22 | implicit val system = ActorSystem("akka-http-server-spec", defaultExecutionContext = Some(ee.ec)) 23 | implicit val mat = ActorMaterializer() 24 | 25 | import system.dispatcher 26 | 27 | val endpoints = deriveAll[Future](Api).from( 28 | path, 29 | segment, 30 | query, 31 | header, 32 | fixed, 33 | input, 34 | clientHdr, 35 | coll, 36 | matching, 37 | send, 38 | get, 39 | put, 40 | putB, 41 | post, 42 | postB, 43 | delete, 44 | code200, 45 | code400, 46 | code500 47 | ) 48 | val sm = ServerManager(Http(), "localhost", 9000) 49 | val server = mount(sm, endpoints) 50 | 51 | "akka http implements TypedApi's server interface" >> { 52 | tests(9000) 53 | 54 | step { 55 | Await.ready(server.map(_.unbind()), timeout) 56 | Await.ready(system.terminate, timeout) 57 | } 58 | } 59 | 60 | } 61 | -------------------------------------------------------------------------------- /http-support-tests/src/test/scala/http/support/tests/server/Http4sServerSupportSpec.scala: -------------------------------------------------------------------------------- 1 | package http.support.tests.server 2 | 3 | import http.support.tests.{UserCoding, Api} 4 | import typedapi.server._ 5 | import typedapi.server.http4s._ 6 | import cats.effect.IO 7 | import org.http4s.server.blaze.BlazeBuilder 8 | 9 | final class Http4sServerSupportSpec extends ServerSupportSpec[IO] { 10 | 11 | import UserCoding._ 12 | 13 | val endpoints = deriveAll[IO](Api).from( 14 | path, 15 | segment, 16 | query, 17 | header, 18 | fixed, 19 | input, 20 | clientHdr, 21 | coll, 22 | matching, 23 | send, 24 | get, 25 | put, 26 | putB, 27 | post, 28 | postB, 29 | delete, 30 | code200, 31 | code400, 32 | code500 33 | ) 34 | val sm = ServerManager(BlazeBuilder[IO], "localhost", 9000) 35 | val server = mount(sm, endpoints).unsafeRunSync() 36 | 37 | "http4s implements TypedApi's server interface" >> { 38 | tests(9000) 39 | 40 | step { 41 | server.shutdown.unsafeRunSync() 42 | } 43 | } 44 | 45 | } 46 | -------------------------------------------------------------------------------- /http-support-tests/src/test/scala/http/support/tests/server/ServerSupportSpec.scala: -------------------------------------------------------------------------------- 1 | package http.support.tests.server 2 | 3 | import typedapi.server._ 4 | import http.support.tests.{User, UserCoding} 5 | import org.http4s._ 6 | import org.http4s.dsl.io._ 7 | import org.http4s.client.blaze._ 8 | import org.http4s.client.dsl.io._ 9 | import cats.Applicative 10 | import cats.effect.IO 11 | import org.specs2.mutable.Specification 12 | 13 | import scala.language.higherKinds 14 | 15 | abstract class ServerSupportSpec[F[_]: Applicative] extends Specification { 16 | 17 | import StatusCodes._ 18 | 19 | sequential 20 | 21 | val client = Http1Client[IO]().unsafeRunSync 22 | 23 | def tests(port: Int) = { 24 | import UserCoding._ 25 | 26 | "paths and segments" >> { 27 | client.expect[User](s"http://localhost:$port/path").unsafeRunSync() === User("path", 27) 28 | client.expect[User](s"http://localhost:$port/segment/jim").unsafeRunSync() === User("jim", 27) 29 | } 30 | 31 | "queries" >> { 32 | client.expect[User](s"http://localhost:$port/query?age=42").unsafeRunSync() === User("query", 42) 33 | } 34 | 35 | "headers" >> { 36 | client.expect[User](Request[IO]( 37 | method = GET, 38 | uri = Uri.fromString(s"http://localhost:$port/header").right.get, 39 | headers = Headers(Header("age", "42")) 40 | )).unsafeRunSync() === User("header", 42) 41 | client.expect[User](Request[IO]( 42 | method = GET, 43 | uri = Uri.fromString(s"http://localhost:$port/header/fixed").right.get, 44 | headers = Headers(Header("Hello", "*")) 45 | )).unsafeRunSync() === User("fixed", 27) 46 | client.expect[User](Request[IO]( 47 | method = GET, 48 | uri = Uri.fromString(s"http://localhost:$port/header/client").right.get 49 | )).unsafeRunSync() === User("client header", 27) 50 | client.expect[User](Request[IO]( 51 | method = GET, 52 | uri = Uri.fromString(s"http://localhost:$port/header/input/client").right.get 53 | )).unsafeRunSync() === User("input", 27) 54 | client.fetch[Option[Header]]( 55 | Request[IO]( 56 | method = GET, 57 | uri = Uri.fromString(s"http://localhost:$port/header/server/send").right.get 58 | ) 59 | )( 60 | resp => IO { 61 | resp.headers.toList.find(_.name.toString == "Hello") 62 | } 63 | ).unsafeRunSync() === Some(Header("Hello", "*")) 64 | client.expect[User](Request[IO]( 65 | method = GET, 66 | uri = Uri.fromString(s"http://localhost:$port/header/server/match").right.get, 67 | headers = Headers(Header("test", "foo"), Header("testy", "bar"), Header("meh", "NONO")) 68 | )).unsafeRunSync() === User("test -> foo,testy -> bar", 27) 69 | client.fetch[Option[Header]]( 70 | Request[IO]( 71 | method = OPTIONS, 72 | uri = Uri.fromString(s"http://localhost:$port/header/fixed").right.get, 73 | headers = Headers(Header("Hello", "*")) 74 | ) 75 | )( 76 | resp => IO { 77 | resp.headers.toList.find(_.name.toString == "Access-Control-Allow-Methods") 78 | } 79 | ).unsafeRunSync() === Some(Header("Access-Control-Allow-Methods", "GET")) 80 | } 81 | 82 | "methods" >> { 83 | client.expect[User](s"http://localhost:$port/").unsafeRunSync() === User("get", 27) 84 | client.expect[User](PUT(Uri.fromString(s"http://localhost:$port/").right.get)).unsafeRunSync() === User("put", 27) 85 | client.expect[User](PUT(Uri.fromString(s"http://localhost:$port/body").right.get, User("joe", 27))).unsafeRunSync() === User("joe", 27) 86 | client.expect[User](POST(Uri.fromString(s"http://localhost:$port/").right.get)).unsafeRunSync() === User("post", 27) 87 | client.expect[User](POST(Uri.fromString(s"http://localhost:$port/body").right.get, User("joe", 27))).unsafeRunSync() === User("joe", 27) 88 | client.expect[User](DELETE(Uri.fromString(s"http://localhost:$port/?reasons=because").right.get)).unsafeRunSync() === User("because", 27) 89 | } 90 | 91 | "status codes" >> { 92 | client.fetch[Int](GET(Uri.fromString(s"http://localhost:$port/status/200").right.get))(resp => IO.pure(resp.status.code)).unsafeRunSync === 200 93 | client.fetch[Int](GET(Uri.fromString(s"http://localhost:$port/status/400").right.get))(resp => IO.pure(resp.status.code)).unsafeRunSync === 400 94 | client.fetch[Int](GET(Uri.fromString(s"http://localhost:$port/status/500").right.get))(resp => IO.pure(resp.status.code)).unsafeRunSync === 500 95 | } 96 | } 97 | 98 | val path: () => F[Result[User]] = () => Applicative[F].pure(successWith(Ok)(User("path", 27))) 99 | val segment: String => F[Result[User]] = name => Applicative[F].pure(successWith(Ok)(User(name, 27))) 100 | val query: Int => F[Result[User]] = age => Applicative[F].pure(successWith(Ok)(User("query", age))) 101 | val header: Int => F[Result[User]] = age => Applicative[F].pure(successWith(Ok)(User("header", age))) 102 | val fixed: () => F[Result[User]] = () => Applicative[F].pure(successWith(Ok)(User("fixed", 27))) 103 | val input: () => F[Result[User]] = () => Applicative[F].pure(successWith(Ok)(User("input", 27))) 104 | val clientHdr: () => F[Result[User]] = () => Applicative[F].pure(successWith(Ok)(User("client header", 27))) 105 | val coll: () => F[Result[User]] = () => Applicative[F].pure(successWith(Ok)(User("coll", 27))) 106 | val matching: Map[String, String] => F[Result[User]] = matches => Applicative[F].pure(successWith(Ok)(User(matches.mkString(","), 27))) 107 | val send: () => F[Result[User]] = () => Applicative[F].pure(successWith(Ok)(User("send", 27))) 108 | val get: () => F[Result[User]] = () => Applicative[F].pure(successWith(Ok)(User("get", 27))) 109 | val put: () => F[Result[User]] = () => Applicative[F].pure(successWith(Ok)(User("put", 27))) 110 | val putB: User => F[Result[User]] = user => Applicative[F].pure(successWith(Ok)(user)) 111 | val post: () => F[Result[User]] = () => Applicative[F].pure(successWith(Ok)(User("post", 27))) 112 | val postB: User => F[Result[User]] = user => Applicative[F].pure(successWith(Ok)(user)) 113 | val delete: List[String] => F[Result[User]] = reasons => { 114 | Applicative[F].pure(successWith(Ok)(User(reasons.mkString(","), 27))) 115 | } 116 | val code200: () => F[Result[String]] = () => Applicative[F].pure(successWith(Ok)("")) 117 | val code400: () => F[Result[String]] = () => Applicative[F].pure(errorWith(BadRequest, "meh")) 118 | val code500: () => F[Result[String]] = () => Applicative[F].pure(errorWith(InternalServerError, "boom")) 119 | } 120 | -------------------------------------------------------------------------------- /http4s-client/src/main/scala/typedapi/client/http4s/package.scala: -------------------------------------------------------------------------------- 1 | package typedapi.client 2 | 3 | import cats.{Monad, MonadError, Applicative} 4 | import org.http4s._ 5 | import org.http4s.client._ 6 | import org.http4s.Status.Successful 7 | 8 | import scala.language.higherKinds 9 | 10 | package object http4s { 11 | 12 | private implicit class Http4sRequestOps[F[_]](req: Request[F]) { 13 | 14 | def withQuery(queries: Map[String, List[String]]): Request[F] = { 15 | if (queries.nonEmpty) { 16 | val q = org.http4s.Query.fromMap(queries) 17 | val uri = Uri(req.uri.scheme, req.uri.authority, req.uri.path, q, req.uri.fragment) 18 | 19 | req.withUri(uri) 20 | } 21 | else req 22 | } 23 | 24 | def withHeaders(headers: Map[String, String]): Request[F] = { 25 | if (headers.nonEmpty) { 26 | val h: List[Header] = headers.map { case (k, v) => org.http4s.Header(k, v) }(collection.breakOut) 27 | 28 | req.withHeaders(Headers(h)) 29 | } 30 | else req 31 | } 32 | 33 | def run(cm: ClientManager[Client[F]])(implicit F: Applicative[F]): F[Response[F]] = 34 | cm.client.fetch(req)(resp => F.pure(resp)) 35 | } 36 | 37 | private implicit class Http4sResponseOps[F[_]](resp: Response[F]) { 38 | 39 | def decode[A](implicit d: EntityDecoder[F, A], F: MonadError[F, Throwable]): F[A] = resp match { 40 | case Successful(_resp) => 41 | d.decode(_resp, strict = false).fold(throw _, identity) 42 | case failedResponse => 43 | F.raiseError(UnexpectedStatus(failedResponse.status)) 44 | } 45 | } 46 | 47 | implicit def rawGetRequest[F[_]](implicit F: Applicative[F]) = new RawGetRequest[Client[F], F] { 48 | type Resp = Response[F] 49 | 50 | def apply(uri: List[String], queries: Map[String, List[String]], headers: Map[String, String], cm: ClientManager[Client[F]]): F[Resp] = { 51 | val request = Request[F](Method.GET, Uri.unsafeFromString(deriveUriString(cm, uri))) 52 | .withQuery(queries) 53 | .withHeaders(headers) 54 | 55 | request.run(cm) 56 | } 57 | } 58 | 59 | implicit def getRequest[F[_], A](implicit decoder: EntityDecoder[F, A], F: MonadError[F, Throwable]) = new GetRequest[Client[F], F, A] { 60 | private val raw = rawGetRequest[F] 61 | 62 | def apply(uri: List[String], queries: Map[String, List[String]], headers: Map[String, String], cm: ClientManager[Client[F]]): F[A] = 63 | F.flatMap(raw(uri, queries, headers, cm))(_.decode[A]) 64 | } 65 | 66 | implicit def rawPutRequest[F[_]](implicit F: Applicative[F]) = new RawPutRequest[Client[F], F] { 67 | type Resp = Response[F] 68 | 69 | def apply(uri: List[String], queries: Map[String, List[String]], headers: Map[String, String], cm: ClientManager[Client[F]]): F[Resp] = { 70 | val request = Request[F](Method.PUT, Uri.unsafeFromString(deriveUriString(cm, uri))) 71 | .withQuery(queries) 72 | .withHeaders(headers) 73 | 74 | request.run(cm) 75 | } 76 | } 77 | 78 | implicit def putRequest[F[_], A](implicit decoder: EntityDecoder[F, A], F: MonadError[F, Throwable]) = new PutRequest[Client[F], F, A] { 79 | private val raw = rawPutRequest[F] 80 | 81 | def apply(uri: List[String], queries: Map[String, List[String]], headers: Map[String, String], cm: ClientManager[Client[F]]): F[A] = 82 | F.flatMap(raw(uri, queries, headers, cm))(_.decode[A]) 83 | } 84 | 85 | implicit def rawPutBodyRequest[F[_], Bd](implicit encoder: EntityEncoder[F, Bd], F: Monad[F]) = new RawPutWithBodyRequest[Client[F], F, Bd] { 86 | type Resp = Response[F] 87 | 88 | def apply(uri: List[String], queries: Map[String, List[String]], headers: Map[String, String], body: Bd, cm: ClientManager[Client[F]]): F[Resp] = { 89 | val requestF = Request[F](Method.PUT, Uri.unsafeFromString(deriveUriString(cm, uri))) 90 | .withQuery(queries) 91 | .withHeaders(headers) 92 | .withBody(body) 93 | 94 | F.flatMap(requestF)(_.run(cm)) 95 | } 96 | } 97 | 98 | implicit def putBodyRequest[F[_], Bd, A](implicit encoder: EntityEncoder[F, Bd], 99 | decoder: EntityDecoder[F, A], 100 | F: MonadError[F, Throwable]) = new PutWithBodyRequest[Client[F], F, Bd, A] { 101 | private val raw = rawPutBodyRequest[F, Bd] 102 | 103 | def apply(uri: List[String], queries: Map[String, List[String]], headers: Map[String, String], body: Bd, cm: ClientManager[Client[F]]): F[A] = 104 | F.flatMap(raw(uri, queries, headers, body, cm))(_.decode[A]) 105 | } 106 | 107 | implicit def rawPostRequest[F[_]](implicit F: Applicative[F]) = new RawPostRequest[Client[F], F] { 108 | type Resp = Response[F] 109 | 110 | def apply(uri: List[String], queries: Map[String, List[String]], headers: Map[String, String], cm: ClientManager[Client[F]]): F[Resp] = { 111 | val request = Request[F](Method.POST, Uri.unsafeFromString(deriveUriString(cm, uri))) 112 | .withQuery(queries) 113 | .withHeaders(headers) 114 | 115 | request.run(cm) 116 | } 117 | } 118 | 119 | implicit def postRequest[F[_], A](implicit decoder: EntityDecoder[F, A], F: MonadError[F, Throwable]) = new PostRequest[Client[F], F, A] { 120 | private val raw = rawPostRequest[F] 121 | 122 | def apply(uri: List[String], queries: Map[String, List[String]], headers: Map[String, String], cm: ClientManager[Client[F]]): F[A] = 123 | F.flatMap(raw(uri, queries, headers, cm))(_.decode[A]) 124 | } 125 | 126 | implicit def rawPostBodyRequest[F[_], Bd](implicit encoder: EntityEncoder[F, Bd], 127 | F: Monad[F]) = new RawPostWithBodyRequest[Client[F], F, Bd] { 128 | type Resp = Response[F] 129 | 130 | def apply(uri: List[String], queries: Map[String, List[String]], headers: Map[String, String], body: Bd, cm: ClientManager[Client[F]]): F[Resp] = { 131 | val requestF = Request[F](Method.POST, Uri.unsafeFromString(deriveUriString(cm, uri))) 132 | .withQuery(queries) 133 | .withHeaders(headers) 134 | .withBody(body) 135 | 136 | F.flatMap(requestF)(_.run(cm)) 137 | } 138 | } 139 | 140 | implicit def postBodyRequest[F[_], Bd, A](implicit encoder: EntityEncoder[F, Bd], 141 | decoder: EntityDecoder[F, A], 142 | F: MonadError[F, Throwable]) = new PostWithBodyRequest[Client[F], F, Bd, A] { 143 | private val raw = rawPostBodyRequest[F, Bd] 144 | 145 | def apply(uri: List[String], queries: Map[String, List[String]], headers: Map[String, String], body: Bd, cm: ClientManager[Client[F]]): F[A] = 146 | F.flatMap(raw(uri, queries, headers, body, cm))(_.decode[A]) 147 | } 148 | 149 | implicit def rawDeleteRequest[F[_]](implicit F: Applicative[F]) = new RawDeleteRequest[Client[F], F] { 150 | type Resp = Response[F] 151 | 152 | def apply(uri: List[String], queries: Map[String, List[String]], headers: Map[String, String], cm: ClientManager[Client[F]]): F[Resp] = { 153 | val request = Request[F](Method.DELETE, Uri.unsafeFromString(deriveUriString(cm, uri))) 154 | .withQuery(queries) 155 | .withHeaders(headers) 156 | 157 | request.run(cm) 158 | } 159 | } 160 | 161 | implicit def deleteRequest[F[_], A](implicit decoder: EntityDecoder[F, A], F: MonadError[F, Throwable]) = new DeleteRequest[Client[F], F, A] { 162 | private val raw = rawDeleteRequest[F] 163 | 164 | def apply(uri: List[String], queries: Map[String, List[String]], headers: Map[String, String], cm: ClientManager[Client[F]]): F[A] = 165 | F.flatMap(raw(uri, queries, headers, cm))(_.decode[A]) 166 | } 167 | } 168 | -------------------------------------------------------------------------------- /http4s-server/src/main/scala/typedapi/server/htt4ps/package.scala: -------------------------------------------------------------------------------- 1 | package typedapi.server 2 | 3 | import typedapi.shared.MethodType 4 | import cats.Monad 5 | import cats.implicits._ 6 | import cats.effect.IO 7 | import org.http4s._ 8 | import org.http4s.dsl._ 9 | import org.http4s.dsl.impl.EntityResponseGenerator 10 | import org.http4s.server.Server 11 | import org.http4s.server.blaze.BlazeBuilder 12 | import shapeless._ 13 | import shapeless.ops.hlist.Prepend 14 | 15 | import scala.language.higherKinds 16 | 17 | package object http4s { 18 | 19 | private def getHeaders(raw: Map[String, String]): List[Header.Raw] = 20 | raw.map { case (key, value) => Header(key, value) }(collection.breakOut) 21 | 22 | implicit def noReqBodyExecutor[El <: HList, KIn <: HList, VIn <: HList, M <: MethodType, F[_]: Monad, FOut](implicit encoder: EntityEncoder[F, FOut]) = 23 | new NoReqBodyExecutor[El, KIn, VIn, M, F, FOut] { 24 | type R = Request[F] 25 | type Out = F[Response[F]] 26 | 27 | private val dsl = Http4sDsl[F] 28 | import dsl._ 29 | 30 | private def respGenerator(code: Status) = new EntityResponseGenerator[F] { val status = code } 31 | 32 | def apply(req: R, eReq: EndpointRequest, endpoint: Endpoint[El, KIn, VIn, M, VIn, F, FOut]): Either[ExtractionError, Out] = { 33 | extract(eReq, endpoint).map { extracted => 34 | Monad[F].flatMap(execute(extracted, endpoint)) { 35 | case Right((code, response)) => 36 | respGenerator(Status(code.statusCode))(response, getHeaders(endpoint.headers): _*) 37 | 38 | case Left(HttpError(code, msg)) => 39 | respGenerator(Status(code.statusCode))(msg, getHeaders(endpoint.headers): _*) 40 | } 41 | } 42 | } 43 | } 44 | 45 | implicit def withReqBodyExecutor[El <: HList, KIn <: HList, VIn <: HList, Bd, M <: MethodType, ROut <: HList, POut <: HList, F[_]: Monad, FOut] 46 | (implicit encoder: EntityEncoder[F, FOut], 47 | decoder: EntityDecoder[F, Bd], 48 | _prepend: Prepend.Aux[ROut, Bd :: HNil, POut], 49 | _eqProof: POut =:= VIn) = new ReqBodyExecutor[El, KIn, VIn, Bd, M, ROut, POut, F, FOut] { 50 | type R = Request[F] 51 | type Out = F[Response[F]] 52 | 53 | implicit val prepend = _prepend 54 | implicit val eqProof = _eqProof 55 | 56 | private val dsl = Http4sDsl[F] 57 | import dsl._ 58 | 59 | private def respGenerator(code: Status) = new EntityResponseGenerator[F] { val status = code } 60 | 61 | def apply(req: R, eReq: EndpointRequest, endpoint: Endpoint[El, KIn, VIn, M, (BodyType[Bd], ROut), F, FOut]): Either[ExtractionError, Out] = { 62 | extract(eReq, endpoint).map { case (_, extracted) => 63 | for { 64 | body <- req.as[Bd] 65 | response <- execute(extracted, body, endpoint).flatMap { 66 | case Right((code, response)) => 67 | respGenerator(Status(code.statusCode))(response, getHeaders(endpoint.headers): _*) 68 | 69 | case Left(HttpError(code, msg)) => 70 | respGenerator(Status(code.statusCode))(msg, getHeaders(endpoint.headers): _*) 71 | } 72 | } yield response 73 | } 74 | } 75 | } 76 | 77 | implicit val mountEndpoints = new MountEndpoints[BlazeBuilder[IO], Request[IO], IO[Response[IO]]] { 78 | 79 | import io._ 80 | 81 | type Out = IO[Server[IO]] 82 | 83 | def apply(server: ServerManager[BlazeBuilder[IO]], endpoints: List[Serve[Request[IO], IO[Response[IO]]]]): Out = { 84 | val service = HttpService[IO] { 85 | case request => 86 | def execute(eps: List[Serve[Request[IO], IO[Response[IO]]]], eReq: EndpointRequest): IO[Response[IO]] = eps match { 87 | case collection.immutable.::(endpoint, tail) => endpoint(request, eReq) match { 88 | case Right(response) => response 89 | case Left(RouteNotFound) => execute(tail, eReq) 90 | case Left(BadRouteRequest(msg)) => io.BadRequest(msg) 91 | } 92 | 93 | case Nil => io.NotFound("uri = " + request.uri) 94 | } 95 | 96 | val eReq = EndpointRequest( 97 | request.method.name, 98 | { 99 | val path = request.uri.path.split("/") 100 | 101 | if (path.isEmpty) List.empty 102 | else path.tail.toList 103 | }, 104 | request.uri.multiParams.map { case (key, value) => key -> value.toList }, 105 | request.headers.toList.map(header => header.name.toString.toLowerCase -> header.value)(collection.breakOut) 106 | ) 107 | 108 | if (request.method.name == "OPTIONS") { 109 | IO(Response(headers = Headers(getHeaders(optionsHeaders(endpoints, eReq))))) 110 | } 111 | else 112 | execute(endpoints, eReq) 113 | } 114 | 115 | server.server.bindHttp(server.port, server.host).mountService(service, "/").start 116 | } 117 | } 118 | } 119 | -------------------------------------------------------------------------------- /js-client/src/main/scala/typedapi/client/js/package.scala: -------------------------------------------------------------------------------- 1 | package typedapi.client 2 | 3 | import typedapi.util._ 4 | import org.scalajs.dom.XMLHttpRequest 5 | import org.scalajs.dom.ext.Ajax 6 | 7 | import scala.concurrent.{Future, ExecutionContext} 8 | 9 | package object js { 10 | 11 | private def renderQueries(queries: Map[String, List[String]]): String = 12 | if (queries.nonEmpty) 13 | queries 14 | .map { case (key, values) => s"$key=${values.mkString(",")}" } 15 | .mkString("?", "&", "") 16 | else 17 | "" 18 | 19 | private def flatten[A](decoded: Future[Either[Exception, A]])(implicit ec: ExecutionContext): Future[A] = decoded.flatMap { 20 | case Right(a) => Future.successful(a) 21 | case Left(error) => Future.failed(error) 22 | } 23 | 24 | implicit def rawGetRequest(implicit ec: ExecutionContext) = new RawGetRequest[Ajax.type, Future] { 25 | type Resp = XMLHttpRequest 26 | 27 | def apply(uri: List[String], queries: Map[String, List[String]], headers: Map[String, String], cm: ClientManager[Ajax.type]): Future[Resp] = 28 | cm.client 29 | .get( 30 | url = deriveUriString(cm, uri) + renderQueries(queries), 31 | headers = headers 32 | ) 33 | } 34 | 35 | implicit def getRequest[A](implicit decoder: Decoder[Future, A], ec: ExecutionContext) = new GetRequest[Ajax.type, Future, A] { 36 | private val raw = rawGetRequest 37 | 38 | def apply(uri: List[String], queries: Map[String, List[String]], headers: Map[String, String], cm: ClientManager[Ajax.type]): Future[A] = 39 | raw(uri, queries, headers, cm).flatMap(response => flatten(decoder(response.responseText))) 40 | } 41 | 42 | implicit def rawPutRequest(implicit ec: ExecutionContext) = new RawPutRequest[Ajax.type, Future] { 43 | type Resp = XMLHttpRequest 44 | 45 | def apply(uri: List[String], queries: Map[String, List[String]], headers: Map[String, String], cm: ClientManager[Ajax.type]): Future[Resp] = 46 | cm.client 47 | .put( 48 | url = deriveUriString(cm, uri) + renderQueries(queries), 49 | headers = headers 50 | ) 51 | } 52 | 53 | implicit def putRequest[A](implicit decoder: Decoder[Future, A], ec: ExecutionContext) = new PutRequest[Ajax.type, Future, A] { 54 | private val raw = rawPutRequest 55 | 56 | def apply(uri: List[String], queries: Map[String, List[String]], headers: Map[String, String], cm: ClientManager[Ajax.type]): Future[A] = 57 | raw(uri, queries, headers, cm).flatMap(response => flatten(decoder(response.responseText))) 58 | } 59 | 60 | implicit def rawPutBodyRequest[Bd](implicit encoder: Encoder[Future, Bd], ec: ExecutionContext) = new RawPutWithBodyRequest[Ajax.type, Future, Bd] { 61 | type Resp = XMLHttpRequest 62 | 63 | def apply(uri: List[String], queries: Map[String, List[String]], headers: Map[String, String], body: Bd, cm: ClientManager[Ajax.type]): Future[Resp] = 64 | encoder(body).flatMap { encoded => 65 | cm.client 66 | .put( 67 | url = deriveUriString(cm, uri) + renderQueries(queries), 68 | headers = headers, 69 | data = encoded 70 | ) 71 | } 72 | } 73 | 74 | implicit def putBodyRequest[Bd, A](implicit encoder: Encoder[Future, Bd], decoder: Decoder[Future, A], ec: ExecutionContext) = new PutWithBodyRequest[Ajax.type, Future, Bd, A] { 75 | private val raw = rawPutBodyRequest[Bd] 76 | 77 | def apply(uri: List[String], queries: Map[String, List[String]], headers: Map[String, String], body: Bd, cm: ClientManager[Ajax.type]): Future[A] = 78 | raw(uri, queries, headers, body, cm).flatMap(response => flatten(decoder(response.responseText))) 79 | } 80 | 81 | implicit def rawPostRequest(implicit ec: ExecutionContext) = new RawPostRequest[Ajax.type, Future] { 82 | type Resp = XMLHttpRequest 83 | 84 | def apply(uri: List[String], queries: Map[String, List[String]], headers: Map[String, String], cm: ClientManager[Ajax.type]): Future[Resp] = 85 | cm.client 86 | .post( 87 | url = deriveUriString(cm, uri) + renderQueries(queries), 88 | headers = headers 89 | ) 90 | } 91 | 92 | implicit def postRequest[A](implicit decoder: Decoder[Future, A], ec: ExecutionContext) = new PostRequest[Ajax.type, Future, A] { 93 | private val raw = rawPostRequest 94 | 95 | def apply(uri: List[String], queries: Map[String, List[String]], headers: Map[String, String], cm: ClientManager[Ajax.type]): Future[A] = 96 | raw(uri, queries, headers, cm).flatMap(response => flatten(decoder(response.responseText))) 97 | } 98 | 99 | implicit def rawPostBodyRequest[Bd](implicit encoder: Encoder[Future, Bd], ec: ExecutionContext) = new RawPostWithBodyRequest[Ajax.type, Future, Bd] { 100 | type Resp = XMLHttpRequest 101 | 102 | def apply(uri: List[String], queries: Map[String, List[String]], headers: Map[String, String], body: Bd, cm: ClientManager[Ajax.type]): Future[Resp] = 103 | encoder(body).flatMap { encoded => 104 | cm.client 105 | .post( 106 | url = deriveUriString(cm, uri) + renderQueries(queries), 107 | headers = headers, 108 | data = encoded 109 | ) 110 | } 111 | } 112 | 113 | implicit def postBodyRequest[Bd, A](implicit encoder: Encoder[Future, Bd], decoder: Decoder[Future, A], ec: ExecutionContext) = new PostWithBodyRequest[Ajax.type, Future, Bd, A] { 114 | private val raw = rawPostBodyRequest[Bd] 115 | 116 | def apply(uri: List[String], queries: Map[String, List[String]], headers: Map[String, String], body: Bd, cm: ClientManager[Ajax.type]): Future[A] = 117 | raw(uri, queries, headers, body, cm).flatMap(response => flatten(decoder(response.responseText))) 118 | } 119 | 120 | implicit def rawDeleteRequest(implicit ec: ExecutionContext) = new RawDeleteRequest[Ajax.type, Future] { 121 | type Resp = XMLHttpRequest 122 | 123 | def apply(uri: List[String], queries: Map[String, List[String]], headers: Map[String, String], cm: ClientManager[Ajax.type]): Future[Resp] = 124 | cm.client 125 | .delete( 126 | url = deriveUriString(cm, uri) + renderQueries(queries), 127 | headers = headers 128 | ) 129 | } 130 | 131 | implicit def deleteRequest[A](implicit decoder: Decoder[Future, A], ec: ExecutionContext) = new DeleteRequest[Ajax.type, Future, A] { 132 | private val raw = rawDeleteRequest 133 | 134 | def apply(uri: List[String], queries: Map[String, List[String]], headers: Map[String, String], cm: ClientManager[Ajax.type]): Future[A] = 135 | raw(uri, queries, headers, cm).flatMap(response => flatten(decoder(response.responseText))) 136 | } 137 | } 138 | -------------------------------------------------------------------------------- /project/build.properties: -------------------------------------------------------------------------------- 1 | sbt.version=1.0.4 -------------------------------------------------------------------------------- /project/build.scala: -------------------------------------------------------------------------------- 1 | import sbt._ 2 | 3 | object Dependencies { 4 | 5 | private val specs2V = "3.9.4" 6 | 7 | val shared = Seq( 8 | "com.chuusai" %% "shapeless" % "2.3.3" % Compile, 9 | 10 | "org.specs2" %% "specs2-core" % specs2V % Test 11 | ) 12 | 13 | val client = Seq( 14 | "org.specs2" %% "specs2-core" % specs2V % Test 15 | ) 16 | 17 | val server = Seq( 18 | "org.specs2" %% "specs2-core" % specs2V % Test 19 | ) 20 | 21 | private val http4sV = "0.18.0" 22 | 23 | val http4sClient = Seq( 24 | "org.http4s" %% "http4s-blaze-client" % http4sV % Provided, 25 | ) 26 | 27 | val http4sServer = Seq( 28 | "org.http4s" %% "http4s-blaze-server" % http4sV % Provided, 29 | "org.http4s" %% "http4s-dsl" % http4sV % Provided 30 | ) 31 | 32 | private val akkaHttpV = "10.0.13" 33 | 34 | val akkaHttpClient = Seq( 35 | "com.typesafe.akka" %% "akka-http" % akkaHttpV % Provided, 36 | "com.typesafe.akka" %% "akka-http-core" % akkaHttpV % Provided 37 | ) 38 | 39 | val akkaHttpServer = Seq( 40 | "com.typesafe.akka" %% "akka-http" % akkaHttpV % Provided, 41 | "com.typesafe.akka" %% "akka-http-core" % akkaHttpV % Provided 42 | ) 43 | 44 | private val scalajHttpV = "2.4.1" 45 | 46 | val scalajHttpClient = Seq( 47 | "org.scalaj" %% "scalaj-http" % scalajHttpV % Provided 48 | ) 49 | 50 | private val circeV = "0.9.1" 51 | 52 | val httpSupportTests = Seq( 53 | "org.specs2" %% "specs2-core" % specs2V % Test, 54 | 55 | "org.http4s" %% "http4s-blaze-client" % http4sV % Test, 56 | "org.http4s" %% "http4s-blaze-server" % http4sV % Test, 57 | "org.http4s" %% "http4s-dsl" % http4sV % Test, 58 | "org.http4s" %% "http4s-circe" % http4sV % Test, 59 | "com.typesafe.akka" %% "akka-http" % akkaHttpV % Test, 60 | "org.scalaj" %% "scalaj-http" % scalajHttpV % Test, 61 | 62 | "io.circe" %% "circe-core" % circeV % Test, 63 | "io.circe" %% "circe-parser" % circeV % Test, 64 | "io.circe" %% "circe-generic" % circeV % Test, 65 | "de.heikoseeberger" %% "akka-http-circe" % "1.21.0" % Test, 66 | 67 | "org.specs2" %% "specs2-core" % specs2V % Test 68 | ) 69 | 70 | val ammoniteSupport = Seq( 71 | "org.scalaj" %% "scalaj-http" % scalajHttpV % Compile 72 | ) 73 | } 74 | -------------------------------------------------------------------------------- /project/plugins.sbt: -------------------------------------------------------------------------------- 1 | addSbtPlugin("org.xerial.sbt" % "sbt-sonatype" % "2.3") 2 | 3 | addSbtPlugin("com.jsuereth" % "sbt-pgp" % "1.1.0") 4 | 5 | addSbtPlugin("org.scala-js" % "sbt-scalajs" % "0.6.22") 6 | -------------------------------------------------------------------------------- /scalaj-http-client/src/main/scala/typedapi/client/scalajhttp/package.scala: -------------------------------------------------------------------------------- 1 | package typedapi.client 2 | 3 | import typedapi.util._ 4 | import scalaj.http._ 5 | 6 | package object scalajhttp { 7 | 8 | type Id[A] = A 9 | type Blocking[A] = Either[Exception, A] 10 | 11 | private def reduceQueries(queries: Map[String, List[String]]): Map[String, String] = 12 | queries.map { case (key, values) => key -> values.mkString(",") }(collection.breakOut) 13 | 14 | implicit def rawGetRequest = new RawGetRequest[Http.type, Id] { 15 | type Resp = HttpResponse[String] 16 | 17 | def apply(uri: List[String], queries: Map[String, List[String]], headers: Map[String, String], cm: ClientManager[Http.type]): Id[Resp] = { 18 | val req = cm.client(deriveUriString(cm, uri)).params(reduceQueries(queries)).headers(headers).method("GET") 19 | 20 | req.asString 21 | } 22 | } 23 | 24 | implicit def getRequest[A](implicit decoder: Decoder[Id, A]) = new GetRequest[Http.type, Blocking, A] { 25 | private val raw = rawGetRequest 26 | 27 | def apply(uri: List[String], queries: Map[String, List[String]], headers: Map[String, String], cm: ClientManager[Http.type]): Blocking[A] = 28 | decoder(raw(uri, queries, headers, cm).body) 29 | } 30 | 31 | implicit def rawPutRequest = new RawPutRequest[Http.type, Id] { 32 | type Resp = HttpResponse[String] 33 | 34 | def apply(uri: List[String], queries: Map[String, List[String]], headers: Map[String, String], cm: ClientManager[Http.type]): Id[Resp] = { 35 | val req = cm.client(deriveUriString(cm, uri)).params(reduceQueries(queries)).headers(headers).method("PUT") 36 | 37 | req.asString 38 | } 39 | } 40 | 41 | implicit def putRequest[A](implicit decoder: Decoder[Id, A]) = new PutRequest[Http.type, Blocking, A] { 42 | private val raw = rawPutRequest 43 | 44 | def apply(uri: List[String], queries: Map[String, List[String]], headers: Map[String, String], cm: ClientManager[Http.type]): Blocking[A] = 45 | decoder(raw(uri, queries, headers, cm).body) 46 | } 47 | 48 | implicit def rawPutBodyRequest[Bd](implicit encoder: Encoder[Id, Bd]) = new RawPutWithBodyRequest[Http.type, Id, Bd] { 49 | type Resp = HttpResponse[String] 50 | 51 | def apply(uri: List[String], queries: Map[String, List[String]], headers: Map[String, String], body: Bd, cm: ClientManager[Http.type]): Id[Resp] = { 52 | val req = cm.client(deriveUriString(cm, uri)).params(reduceQueries(queries)).headers(headers).put(encoder(body)) 53 | 54 | req.asString 55 | } 56 | } 57 | 58 | implicit def putBodyRequest[Bd, A](implicit encoder: Encoder[Id, Bd], decoder: Decoder[Id, A]) = new PutWithBodyRequest[Http.type, Blocking, Bd, A] { 59 | private val raw = rawPutBodyRequest[Bd] 60 | 61 | def apply(uri: List[String], queries: Map[String, List[String]], headers: Map[String, String], body: Bd, cm: ClientManager[Http.type]): Blocking[A] = 62 | decoder(raw(uri, queries, headers, body, cm).body) 63 | } 64 | 65 | implicit def rawPostRequest = new RawPostRequest[Http.type, Id] { 66 | type Resp = HttpResponse[String] 67 | 68 | def apply(uri: List[String], queries: Map[String, List[String]], headers: Map[String, String], cm: ClientManager[Http.type]): Id[Resp] = { 69 | val req = cm.client(deriveUriString(cm, uri)).params(reduceQueries(queries)).headers(headers).method("POST") 70 | 71 | req.asString 72 | } 73 | } 74 | 75 | implicit def postRequest[A](implicit decoder: Decoder[Id, A]) = new PostRequest[Http.type, Blocking, A] { 76 | private val raw = rawPostRequest 77 | 78 | def apply(uri: List[String], queries: Map[String, List[String]], headers: Map[String, String], cm: ClientManager[Http.type]): Blocking[A] = 79 | decoder(raw(uri, queries, headers, cm).body) 80 | } 81 | 82 | implicit def rawPostBodyRequest[Bd](implicit encoder: Encoder[Id, Bd]) = new RawPostWithBodyRequest[Http.type, Id, Bd] { 83 | type Resp = HttpResponse[String] 84 | 85 | def apply(uri: List[String], queries: Map[String, List[String]], headers: Map[String, String], body: Bd, cm: ClientManager[Http.type]): Id[Resp] = { 86 | val req = cm.client(deriveUriString(cm, uri)).params(reduceQueries(queries)).headers(headers).postData(encoder(body)) 87 | 88 | req.asString 89 | } 90 | } 91 | 92 | implicit def postBodyRequest[Bd, A](implicit encoder: Encoder[Id, Bd], decoder: Decoder[Id, A]) = new PostWithBodyRequest[Http.type, Blocking, Bd, A] { 93 | private val raw = rawPostBodyRequest[Bd] 94 | 95 | def apply(uri: List[String], queries: Map[String, List[String]], headers: Map[String, String], body: Bd, cm: ClientManager[Http.type]): Blocking[A] = 96 | decoder(raw(uri, queries, headers, body, cm).body) 97 | } 98 | 99 | implicit def rawDeleteRequest = new RawDeleteRequest[Http.type, Id] { 100 | type Resp = HttpResponse[String] 101 | 102 | def apply(uri: List[String], queries: Map[String, List[String]], headers: Map[String, String], cm: ClientManager[Http.type]): Id[Resp] = { 103 | val req = cm.client(deriveUriString(cm, uri)).params(reduceQueries(queries)).headers(headers).method("DELETE") 104 | 105 | req.asString 106 | } 107 | } 108 | 109 | implicit def deleteRequest[A](implicit decoder: Decoder[Id, A]) = new DeleteRequest[Http.type, Blocking, A] { 110 | private val raw = rawDeleteRequest 111 | 112 | def apply(uri: List[String], queries: Map[String, List[String]], headers: Map[String, String], cm: ClientManager[Http.type]): Blocking[A] = 113 | decoder(raw(uri, queries, headers, cm).body) 114 | } 115 | } 116 | -------------------------------------------------------------------------------- /server/src/main/scala/typedapi/server/Endpoint.scala: -------------------------------------------------------------------------------- 1 | package typedapi.server 2 | 3 | import typedapi.shared._ 4 | import shapeless._ 5 | import shapeless.labelled.FieldType 6 | import shapeless.ops.function._ 7 | 8 | import scala.language.higherKinds 9 | 10 | /** Represents a server endpoint and is basically a function which gets the expected input `VIn` and returns the expected output. */ 11 | abstract class Endpoint[El <: HList, KIn <: HList, VIn <: HList, M <: MethodType, ROut, F[_], Out] 12 | (val method: String, val extractor: RouteExtractor.Aux[El, KIn, VIn, M, HNil, ROut], val headers: Map[String, String]) { 13 | 14 | def apply(in: VIn): F[Result[Out]] 15 | } 16 | 17 | /** Request representation which every server implementation has to provide. */ 18 | final case class EndpointRequest(method: String, 19 | uri: List[String], 20 | queries: Map[String, List[String]], 21 | headers: Map[String, String]) 22 | 23 | final class ExecutableDerivation[F[_]] { 24 | 25 | final class Derivation[El <: HList, KIn <: HList, VIn <: HList, M <: MethodType, ROut, Fn, Out] 26 | (extractor: RouteExtractor.Aux[El, KIn, VIn, M, HNil, ROut], 27 | method: String, 28 | headers: Map[String, String], 29 | fnToVIn: FnToProduct.Aux[Fn, VIn => F[Result[Out]]]) { 30 | 31 | /** Restricts type of parameter `fn` to a function defined by the given API: 32 | * 33 | * {{{ 34 | * val Api = := :> Segment[String]('name) :> Get[User] 35 | * 36 | * derive[IO](Api).from(name: String => IO.pure(User(name))) 37 | * }}} 38 | */ 39 | def from(fn: Fn): Endpoint[El, KIn, VIn, M, ROut, F, Out] = 40 | new Endpoint[El, KIn, VIn, M, ROut, F, Out](method, extractor, headers) { 41 | private val fin = fnToVIn(fn) 42 | 43 | def apply(in: VIn): F[Result[Out]] = fin(in) 44 | } 45 | } 46 | 47 | def apply[H <: HList, FH <: HList, El <: HList, KIn <: HList, VIn <: HList, ROut, Fn, M <: MethodType, MT <: MediaType, Out] 48 | (apiList: ApiTypeCarrier[H]) 49 | (implicit filter: FilterClientElements.Aux[H, FH], 50 | folder: Lazy[TypeLevelFoldLeft.Aux[FH, Unit, (El, KIn, VIn, M, FieldType[MT, Out])]], 51 | extractor: RouteExtractor.Aux[El, KIn, VIn, M, HNil, ROut], 52 | methodShow: MethodToString[M], 53 | serverHeaders: ServerHeaderExtractor[El], 54 | inToFn: Lazy[FnFromProduct.Aux[VIn => F[Result[Out]], Fn]], 55 | fnToVIn: Lazy[FnToProduct.Aux[Fn, VIn => F[Result[Out]]]]): Derivation[El, KIn, VIn, M, ROut, Fn, Out] = 56 | new Derivation[El, KIn, VIn, M, ROut, Fn, Out](extractor, methodShow.show, serverHeaders(Map.empty), fnToVIn.value) 57 | } 58 | -------------------------------------------------------------------------------- /server/src/main/scala/typedapi/server/EndpointComposition.scala: -------------------------------------------------------------------------------- 1 | package typedapi.server 2 | 3 | import typedapi.shared._ 4 | import shapeless._ 5 | import shapeless.labelled.FieldType 6 | import shapeless.ops.function._ 7 | 8 | import scala.language.higherKinds 9 | import scala.annotation.implicitNotFound 10 | 11 | /** Fuses [[RouteExtractor]] and the endpoint function into an [[Endpoint]]. */ 12 | trait EndpointConstructor[F[_], Fn, El <: HList, KIn <: HList, VIn <: HList, M <: MethodType, ROut, Out] { 13 | 14 | def apply(fn: Fn): Endpoint[El, KIn, VIn, M, ROut, F, Out] 15 | } 16 | 17 | /** Compiles RouteExtractor and FunApply for every API endpoint and generates expected list of endpoint functions. */ 18 | @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) 19 | 20 | transformed: ${H}""") 21 | sealed trait PrecompileEndpoint[F[_], H <: HList] { 22 | 23 | // list of expected endpoint functions 24 | type Fns <: HList 25 | // list of endpoint constructors 26 | type Consts <: HList 27 | 28 | def constructors: Consts 29 | } 30 | 31 | object PrecompileEndpoint extends PrecompileEndpointLowPrio { 32 | 33 | type Aux[F[_], H <: HList, Fns0 <: HList, Consts0 <: HList] = PrecompileEndpoint[F, H] { 34 | type Fns = Fns0 35 | type Consts = Consts0 36 | } 37 | } 38 | 39 | trait PrecompileEndpointLowPrio { 40 | 41 | implicit def hnilPrecompiledCase[F[_]] = new PrecompileEndpoint[F, HNil] { 42 | type Fns = HNil 43 | type Consts = HNil 44 | 45 | val constructors = HNil 46 | } 47 | 48 | implicit def constructorsCase[F[_], Fn, El <: HList, KIn <: HList, VIn <: HList, MT <: MediaType, Out, M <: MethodType, ROut, T <: HList] 49 | (implicit extractor: RouteExtractor.Aux[El, KIn, VIn, M, HNil, ROut], 50 | methodShow: MethodToString[M], 51 | serverHeaders: ServerHeaderExtractor[El], 52 | vinToFn: FnFromProduct.Aux[VIn => F[Result[Out]], Fn], 53 | fnToVIn: Lazy[FnToProduct.Aux[Fn, VIn => F[Result[Out]]]], 54 | next: PrecompileEndpoint[F, T]) = 55 | new PrecompileEndpoint[F, (El, KIn, VIn, M, FieldType[MT, Out]) :: T] { 56 | type Fns = Fn :: next.Fns 57 | type Consts = EndpointConstructor[F, Fn, El, KIn, VIn, M, ROut, Out] :: next.Consts 58 | 59 | val constructor = new EndpointConstructor[F, Fn, El, KIn, VIn, M, ROut, Out] { 60 | 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)) { 61 | private val fin = fnToVIn.value(fn) 62 | 63 | def apply(in: VIn): F[Result[Out]] = fin(in) 64 | } 65 | } 66 | 67 | val constructors = constructor :: next.constructors 68 | } 69 | } 70 | 71 | @implicitNotFound("""Whoops, you should not be here. This seems to be a bug. 72 | 73 | constructors: ${Consts} 74 | functions: ${Fns}""") 75 | sealed trait MergeToEndpoint[F[_], Consts <: HList, Fns <: HList] { 76 | 77 | type Out <: HList 78 | 79 | def apply(constructors: Consts, fns: Fns): Out 80 | } 81 | 82 | object MergeToEndpoint extends MergeToEndpointLowPrio { 83 | 84 | type Aux[F[_], Consts <: HList, Fns <: HList, Out0 <: HList] = MergeToEndpoint[F, Consts, Fns] { type Out = Out0 } 85 | } 86 | 87 | trait MergeToEndpointLowPrio { 88 | 89 | implicit def hnilMergeCase[F[_]] = new MergeToEndpoint[F, HNil, HNil] { 90 | type Out = HNil 91 | 92 | def apply(constructors: HNil, fns: HNil): Out = HNil 93 | } 94 | 95 | implicit def mergeCase[F[_], El <: HList, KIn <: HList, VIn <: HList, Out0, M <: MethodType, ROut, Consts <: HList, Fn, Fns <: HList] 96 | (implicit next: MergeToEndpoint[F, Consts, Fns]) = 97 | new MergeToEndpoint[F, EndpointConstructor[F, Fn, El, KIn, VIn, M, ROut, Out0] :: Consts, Fn :: Fns] { 98 | type Out = Endpoint[El, KIn, VIn, M, ROut, F, Out0] :: next.Out 99 | 100 | def apply(constructors: EndpointConstructor[F, Fn, El, KIn, VIn, M, ROut, Out0] :: Consts, fns: Fn :: Fns): Out = { 101 | val endpoint = constructors.head(fns.head) 102 | 103 | endpoint :: next(constructors.tail, fns.tail) 104 | } 105 | } 106 | } 107 | 108 | final class ExecutableCompositionDerivation[F[_]] { 109 | 110 | final class Derivation[H <: HList, Fns <: HList, Consts <: HList, Out <: HList, Drv](pre: PrecompileEndpoint.Aux[F, H, Fns, Consts], 111 | merge: MergeToEndpoint.Aux[F, Consts, Fns, Out], 112 | derived: FnFromProduct.Aux[Fns => Out, Drv]) { 113 | 114 | /** Restricts type of input parameter to a composition of functions defined by the precompile-stage. 115 | * 116 | * {{{ 117 | * val Api = 118 | * (:= :> Segment[String]('name) :> Get[User]) :|: 119 | * (:= :> "foo" :> Segment[String]('name) :> Get[User]) 120 | * 121 | * val f0: String => IO[User] = name => IO.pure(User(name)) 122 | * val f1: String => IO[User] = name => IO.pure(User(name)) 123 | * deriveAll[IO](Api).from(f0 _, f1 _) 124 | * }}} 125 | */ 126 | val from: Drv = derived.apply(fns => merge(pre.constructors, fns)) 127 | } 128 | 129 | def apply[H <: HList, FH <: HList, Fold <: HList, Fns <: HList, FnsTup, Consts <: HList, Out <: HList, Drv](apiLists: CompositionCons[H]) 130 | (implicit filter: FilterClientElementsList.Aux[H, FH], 131 | folder: TypeLevelFoldLeftList.Aux[FH, Fold], 132 | pre: PrecompileEndpoint.Aux[F, Fold, Fns, Consts], 133 | merge: MergeToEndpoint.Aux[F, Consts, Fns, Out], 134 | derived: FnFromProduct.Aux[Fns => Out, Drv]): Derivation[Fold, Fns, Consts, Out, Drv] = 135 | new Derivation[Fold, Fns, Consts, Out, Drv](pre, merge, derived) 136 | } 137 | -------------------------------------------------------------------------------- /server/src/main/scala/typedapi/server/EndpointExecutor.scala: -------------------------------------------------------------------------------- 1 | package typedapi.server 2 | 3 | import typedapi.shared.MethodType 4 | import shapeless._ 5 | import shapeless.ops.hlist.Prepend 6 | 7 | import scala.language.higherKinds 8 | import scala.annotation.implicitNotFound 9 | 10 | @implicitNotFound("""Cannot find EndpointExecutor. Do you miss some implicit values e.g. encoder/decoder? 11 | 12 | elements: ${El} 13 | input keys: ${KIn} 14 | input values: ${VIn}""") 15 | sealed trait EndpointExecutor[El <: HList, KIn <: HList, VIn <: HList, M <: MethodType, ROut, F[_], FOut] { 16 | 17 | type R 18 | type Out 19 | 20 | def extract(eReq: EndpointRequest, endpoint: Endpoint[El, KIn, VIn, M, ROut, F, FOut]): Either[ExtractionError, ROut] = 21 | endpoint.extractor(eReq, HNil) 22 | 23 | def apply(req: R, eReq: EndpointRequest, endpoint: Endpoint[El, KIn, VIn, M, ROut, F, FOut]): Either[ExtractionError, Out] 24 | } 25 | 26 | object EndpointExecutor { 27 | 28 | type Aux[R0, El <: HList, KIn <: HList, VIn <: HList, M <: MethodType, ROut, F[_], FOut, Out0] = EndpointExecutor[El, KIn, VIn, M, ROut, F, FOut] { 29 | type R = R0 30 | type Out = Out0 31 | } 32 | } 33 | 34 | trait NoReqBodyExecutor[El <: HList, KIn <: HList, VIn <: HList, M <: MethodType, F[_], FOut] extends EndpointExecutor[El, KIn, VIn, M, VIn, F, FOut] { 35 | 36 | protected def execute(input: VIn, endpoint: Endpoint[El, KIn, VIn, M, VIn, F, FOut]): F[Result[FOut]] = 37 | endpoint.apply(input) 38 | } 39 | 40 | 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] { 41 | 42 | implicit def prepend: Prepend.Aux[ROut, Bd :: HNil, POut] 43 | implicit def eqProof: POut =:= VIn 44 | 45 | protected def execute(input: ROut, body: Bd, endpoint: Endpoint[El, KIn, VIn, M, (BodyType[Bd], ROut), F, FOut]): F[Result[FOut]] = 46 | endpoint.apply(input :+ body) 47 | } 48 | -------------------------------------------------------------------------------- /server/src/main/scala/typedapi/server/FilterClientElements.scala: -------------------------------------------------------------------------------- 1 | package typedapi.server 2 | 3 | import typedapi.shared.{ClientHeaderElement, ClientHeaderParam, ClientHeaderCollParam} 4 | import shapeless._ 5 | 6 | sealed trait FilterClientElements[H <: HList] { 7 | 8 | type Out <: HList 9 | } 10 | 11 | sealed trait FilterClientElementsLowPrio { 12 | 13 | implicit val filterClientResult = new FilterClientElements[HNil] { 14 | type Out = HNil 15 | } 16 | 17 | implicit def filterClientKeep[El, T <: HList](implicit next: FilterClientElements[T]) = new FilterClientElements[El :: T] { 18 | type Out = El :: next.Out 19 | } 20 | } 21 | 22 | object FilterClientElements extends FilterClientElementsLowPrio { 23 | 24 | type Aux[H <: HList, Out0 <: HList] = FilterClientElements[H] { type Out = Out0 } 25 | 26 | implicit def filterClientEl[K, V, T <: HList](implicit next: FilterClientElements[T]) = new FilterClientElements[ClientHeaderElement[K, V] :: T] { 27 | type Out = next.Out 28 | } 29 | 30 | implicit def filterClientParam[K, V, T <: HList](implicit next: FilterClientElements[T]) = new FilterClientElements[ClientHeaderParam[K, V] :: T] { 31 | type Out = next.Out 32 | } 33 | 34 | implicit def filterClientCollParam[V, T <: HList](implicit next: FilterClientElements[T]) = new FilterClientElements[ClientHeaderCollParam[V] :: T] { 35 | type Out = next.Out 36 | } 37 | } 38 | 39 | sealed trait FilterClientElementsList[H <: HList] { 40 | 41 | type Out <: HList 42 | } 43 | 44 | object FilterClientElementsList { 45 | 46 | type Aux[H <: HList, Out0 <: HList] = FilterClientElementsList[H] { type Out = Out0 } 47 | 48 | implicit val filterClientListResult = new FilterClientElementsList[HNil] { 49 | type Out = HNil 50 | } 51 | 52 | implicit def filterClientListStep[Api <: HList, T <: HList](implicit filtered: FilterClientElements[Api], next: FilterClientElementsList[T]) = 53 | new FilterClientElementsList[Api :: T] { 54 | type Out = filtered.Out :: next.Out 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /server/src/main/scala/typedapi/server/Serve.scala: -------------------------------------------------------------------------------- 1 | package typedapi.server 2 | 3 | /** Reduces an Endpoint and its EndpointExecutor to a simple Request => Response function. */ 4 | trait Serve[Req, Resp] { 5 | 6 | def options(eReq: EndpointRequest): Option[(String, Map[String, String])] 7 | def apply(req: Req, eReq: EndpointRequest): Either[ExtractionError, Resp] 8 | } 9 | -------------------------------------------------------------------------------- /server/src/main/scala/typedapi/server/ServeToList.scala: -------------------------------------------------------------------------------- 1 | package typedapi.server 2 | 3 | import shapeless._ 4 | 5 | import scala.annotation.implicitNotFound 6 | 7 | @implicitNotFound("""Cannot find ServeToList instance to map Serve HList to List. Maybe you have different Request, Response types defined?. 8 | 9 | serves: ${H} 10 | request: ${Req} 11 | response: ${Resp}""") 12 | sealed trait ServeToList[H <: HList, Req, Resp] { 13 | 14 | def apply(h: H): List[Serve[Req, Resp]] 15 | } 16 | 17 | object ServeToList extends ServeToListLowPrio 18 | 19 | trait ServeToListLowPrio { 20 | 21 | implicit def hnilToList[Req, Resp] = new ServeToList[HNil, Req, Resp] { 22 | def apply(h: HNil): List[Serve[Req, Resp]] = Nil 23 | } 24 | 25 | implicit def serveToList[Req, Resp, T <: HList](implicit next: ServeToList[T, Req, Resp]) = new ServeToList[Serve[Req, Resp] :: T, Req, Resp] { 26 | def apply(h: Serve[Req, Resp] :: T): List[Serve[Req, Resp]] = h.head :: next(h.tail) 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /server/src/main/scala/typedapi/server/ServerHeaderExtractor.scala: -------------------------------------------------------------------------------- 1 | package typedapi.server 2 | 3 | import typedapi.shared._ 4 | import shapeless._ 5 | 6 | import scala.annotation.implicitNotFound 7 | 8 | @implicitNotFound("""Whoops, you should not be here. This seems to be a bug. 9 | 10 | elements: ${El}""") 11 | sealed trait ServerHeaderExtractor[El <: HList] { 12 | 13 | def apply(agg: Map[String, String]): Map[String, String] 14 | } 15 | 16 | sealed trait ServerHeaderExtractorLowPrio { 17 | 18 | implicit val serverHeaderReturnCase = new ServerHeaderExtractor[HNil] { 19 | def apply(agg: Map[String, String]): Map[String, String] = agg 20 | } 21 | 22 | implicit def serverHeaderIgnoreCase[H, T <: HList](implicit next: ServerHeaderExtractor[T]) = new ServerHeaderExtractor[H :: T] { 23 | def apply(agg: Map[String, String]): Map[String, String] = next(agg) 24 | } 25 | } 26 | 27 | object ServerHeaderExtractor extends ServerHeaderExtractorLowPrio { 28 | 29 | implicit def serverHeaderExtractCase[K, V, T <: HList] 30 | (implicit kWit: Witness.Aux[K], kShow: WitnessToString[K], vWit: Witness.Aux[V], vShow: WitnessToString[V], next: ServerHeaderExtractor[T]) = 31 | new ServerHeaderExtractor[ServerHeaderSend[K, V] :: T] { 32 | def apply(agg: Map[String, String]): Map[String, String] = { 33 | val key = kShow.show(kWit.value) 34 | val value = vShow.show(vWit.value) 35 | 36 | next(agg + (key -> value)) 37 | } 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /server/src/main/scala/typedapi/server/ServerManager.scala: -------------------------------------------------------------------------------- 1 | package typedapi.server 2 | 3 | import scala.annotation.tailrec 4 | 5 | final case class ServerManager[S](server: S, host: String, port: Int) 6 | 7 | object ServerManager { 8 | 9 | def mount[S, Req, Resp, Out](server: ServerManager[S], endpoints: List[Serve[Req, Resp]])(implicit mounting: MountEndpoints.Aux[S, Req, Resp, Out]): Out = 10 | mounting(server, endpoints) 11 | } 12 | 13 | trait MountEndpoints[S, Req, Resp] { 14 | 15 | type Out 16 | 17 | final def optionsHeaders(eps: List[Serve[Req, Resp]], eReq: EndpointRequest): Map[String, String] = { 18 | @tailrec 19 | def collect(serve: List[Serve[Req, Resp]], methods: List[String], headers: Map[String, String]): (List[String], Map[String, String]) = serve match { 20 | case collection.immutable.::(endpoint, tail) => endpoint.options(eReq) match { 21 | case Some((method, hds)) => collect(tail, method :: methods, hds ++ headers) 22 | case _ => collect(tail, methods, headers) 23 | } 24 | 25 | case Nil => (methods, headers) 26 | } 27 | 28 | val (methods, headers) = collect(eps, Nil, Map.empty) 29 | 30 | headers + (("Access-Control-Allow-Methods", methods.mkString(","))) 31 | } 32 | 33 | def apply(server: ServerManager[S], endpoints: List[Serve[Req, Resp]]): Out 34 | } 35 | 36 | object MountEndpoints { 37 | 38 | type Aux[S, Req, Resp, Out0] = MountEndpoints[S, Req, Resp] { type Out = Out0 } 39 | } 40 | -------------------------------------------------------------------------------- /server/src/main/scala/typedapi/server/StatusCodes.scala: -------------------------------------------------------------------------------- 1 | package typedapi.server 2 | 3 | final case class SuccessCode(statusCode: Int) extends AnyVal 4 | final case class ErrorCode(statusCode: Int) extends AnyVal 5 | 6 | final case class HttpError(code: ErrorCode, message: String) 7 | 8 | object StatusCodes { 9 | 10 | // success codes 11 | final val Continue = SuccessCode(100) 12 | final val SwitchingProtocols = SuccessCode(101) 13 | final val Processing = SuccessCode(102) 14 | 15 | final val Ok = SuccessCode(200) 16 | final val Created = SuccessCode(201) 17 | final val Accepted = SuccessCode(202) 18 | final val NonAuthoritativeInformation = SuccessCode(203) 19 | final val NoContent = SuccessCode(204) 20 | final val ResetContent = SuccessCode(205) 21 | final val PartialContent = SuccessCode(206) 22 | final val MultiStatus = SuccessCode(207) 23 | final val AlreadyReported = SuccessCode(208) 24 | final val IMUsed = SuccessCode(226) 25 | 26 | final val MultipleChoices = SuccessCode(300) 27 | final val MovedPermanently = SuccessCode(301) 28 | final val Found = SuccessCode(302) 29 | final val SeeOther = SuccessCode(303) 30 | final val NotModified = SuccessCode(304) 31 | final val UseProxy = SuccessCode(305) 32 | final val TemporaryRedirect = SuccessCode(307) 33 | final val PermanentRedirect = SuccessCode(308) 34 | 35 | // error codes 36 | final val BadRequest = ErrorCode(400) 37 | final val Unauthorized = ErrorCode(401) 38 | final val PaymentRequired = ErrorCode(402) 39 | final val Forbidden = ErrorCode(403) 40 | final val NotFound = ErrorCode(404) 41 | final val MethodNotAllowed = ErrorCode(405) 42 | final val NotAcceptable = ErrorCode(406) 43 | final val ProxyAuthenticationRequired = ErrorCode(407) 44 | final val RequestTimeout = ErrorCode(408) 45 | final val Conflict = ErrorCode(409) 46 | final val Gone = ErrorCode(410) 47 | final val LengthRequired = ErrorCode(411) 48 | final val PreconditionFailed = ErrorCode(412) 49 | final val PayloadTooLarge = ErrorCode(413) 50 | final val RequestURITooLong = ErrorCode(414) 51 | final val UnsupportedMediaType = ErrorCode(415) 52 | final val RequestedRangeNotSatisfiable = ErrorCode(416) 53 | final val ExpectationFailed = ErrorCode(417) 54 | final val ImAteapot = ErrorCode(418) 55 | final val MisdirectedRequest = ErrorCode(421) 56 | final val UnprocessableEntity = ErrorCode(422) 57 | final val Locked = ErrorCode(423) 58 | final val FailedDependency = ErrorCode(424) 59 | final val UpgradeRequired = ErrorCode(426) 60 | final val PreconditionRequired = ErrorCode(428) 61 | final val TooManyRequests = ErrorCode(429) 62 | final val RequestHeaderFieldsTooLarge = ErrorCode(431) 63 | final val ConnectionClosedWithoutResult = ErrorCode(444) 64 | final val UnavailableForLegalReasons = ErrorCode(451) 65 | final val ClientClosedRequest = ErrorCode(499) 66 | 67 | final val InternalServerError = ErrorCode(500) 68 | final val NotImplemented = ErrorCode(501) 69 | final val BadGateway = ErrorCode(502) 70 | final val ServiceUnavailable = ErrorCode(503) 71 | final val GatewayTimeout = ErrorCode(504) 72 | final val HTTPVersionNotSupported = ErrorCode(505) 73 | final val VariantAlsoNegotiates = ErrorCode(506) 74 | final val InsufficientStorage = ErrorCode(507) 75 | final val LoopDetected = ErrorCode(508) 76 | final val NotExtended = ErrorCode(510) 77 | final val NetworkAuthenticationRequired = ErrorCode(511) 78 | final val NetworkConnectTimeoutError = ErrorCode(599) 79 | } 80 | -------------------------------------------------------------------------------- /server/src/main/scala/typedapi/server/package.scala: -------------------------------------------------------------------------------- 1 | package typedapi 2 | 3 | import typedapi.shared._ 4 | import shapeless._ 5 | import shapeless.ops.hlist.Mapper 6 | 7 | import scala.language.higherKinds 8 | 9 | package object server extends TypeLevelFoldLeftLowPrio 10 | with TypeLevelFoldLeftListLowPrio 11 | with WitnessToStringLowPrio 12 | with ApiTransformer { 13 | 14 | val SC = StatusCodes 15 | 16 | type Result[A] = Either[HttpError, (SuccessCode, A)] 17 | 18 | def successWith[A](code: SuccessCode)(a: A): Result[A] = Right(code -> a) 19 | def success[A](a: A): Result[A] = successWith(StatusCodes.Ok)(a) 20 | 21 | def errorWith[A](code: ErrorCode, message: String): Result[A] = Left(HttpError(code, message)) 22 | 23 | def derive[F[_]]: ExecutableDerivation[F] = new ExecutableDerivation[F] 24 | 25 | def mount[S, El <: HList, KIn <: HList, VIn <: HList, M <: MethodType, ROut, F[_], FOut, Req, Resp, Out] 26 | (server: ServerManager[S], endpoint: Endpoint[El, KIn, VIn, M, ROut, F, FOut]) 27 | (implicit executor: EndpointExecutor.Aux[Req, El, KIn, VIn, M, ROut, F, FOut, Resp], mounting: MountEndpoints.Aux[S, Req, Resp, Out]): Out = 28 | mounting(server, List(new Serve[executor.R, executor.Out] { 29 | def options(eReq: EndpointRequest): Option[(String, Map[String, String])] = { 30 | endpoint.extractor(eReq, HNil) match { 31 | case Right(_) => Some((endpoint.method, endpoint.headers)) 32 | case _ => None 33 | } 34 | } 35 | 36 | def apply(req: executor.R, eReq: EndpointRequest): Either[ExtractionError, executor.Out] = executor(req, eReq, endpoint) 37 | })) 38 | 39 | def deriveAll[F[_]]: ExecutableCompositionDerivation[F] = new ExecutableCompositionDerivation[F] 40 | 41 | object endpointToServe extends Poly1 { 42 | 43 | implicit def default[El <: HList, KIn <: HList, VIn <: HList, M <: MethodType, ROut, F[_], FOut](implicit executor: EndpointExecutor[El, KIn, VIn, M, ROut, F, FOut]) = 44 | at[Endpoint[El, KIn, VIn, M, ROut, F, FOut]] { endpoint => 45 | new Serve[executor.R, executor.Out] { 46 | def options(eReq: EndpointRequest): Option[(String, Map[String, String])] = { 47 | endpoint.extractor(eReq, HNil) match { 48 | case Right(_) => Some((endpoint.method, endpoint.headers)) 49 | case _ => None 50 | } 51 | } 52 | 53 | def apply(req: executor.R, eReq: EndpointRequest): Either[ExtractionError, executor.Out] = executor(req, eReq, endpoint) 54 | } 55 | } 56 | } 57 | 58 | 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 = 59 | mounting(server, toList(end.map(endpointToServe))) 60 | } 61 | -------------------------------------------------------------------------------- /server/src/test/scala/typedapi/server/ApiToEndpointLinkSpec.scala: -------------------------------------------------------------------------------- 1 | package typedapi.server 2 | 3 | import typedapi.dsl._ 4 | import shapeless._ 5 | 6 | import org.specs2.mutable.Specification 7 | 8 | final class ApiToEndpointLinkSpec extends Specification { 9 | 10 | import StatusCodes._ 11 | 12 | case class Foo(name: String) 13 | 14 | "link api definitions to endpoint functions" >> { 15 | val Api = := :> "find" :> typedapi.dsl.Segment[String]('name) :> 16 | Query[Int]('limit) :> 17 | Client.Header('hello, 'world) :> 18 | Server.Send('foo, 'bar) :> Server.Match[String]("hi") :> 19 | Get[Json, List[Foo]] 20 | 21 | val endpoint0 = derive[Option](Api).from((name, limit, hi) => Some(successWith(Ok)(List(Foo(name)).take(limit)))) 22 | endpoint0("john" :: 10 :: Map("hi" -> "whats", "hi-ho" -> "up") :: HNil) === Some(Right(Ok -> List(Foo("john")))) 23 | endpoint0.headers == Map("foo" -> "bar") 24 | endpoint0.method == "GET" 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /server/src/test/scala/typedapi/server/RouteExtractorSpec.scala: -------------------------------------------------------------------------------- 1 | package typedapi.server 2 | 3 | import typedapi.dsl._ 4 | import typedapi.shared._ 5 | import shapeless.{HList, HNil, Lazy} 6 | import org.specs2.mutable.Specification 7 | 8 | final class RouteExtractorSpec extends Specification { 9 | 10 | case class Foo(name: String) 11 | 12 | def extract[H <: HList, El <: HList, KIn <: HList, VIn <: HList, M <: MethodType, ROut, Out] 13 | (api: ApiTypeCarrier[H]) 14 | (implicit folder: Lazy[TypeLevelFoldLeft.Aux[H, Unit, (El, KIn, VIn, M, Out)]], 15 | extractor: RouteExtractor.Aux[El, KIn, VIn, M, HNil, ROut]): RouteExtractor.Aux[El, KIn, VIn, M, HNil, ROut] = extractor 16 | 17 | "determine routes defined by requests and extract included data (segments, queries, headers)" >> { 18 | "no data" >> { 19 | val ext = extract(:= :> "hello" :> "world" :> Get[Json, Foo]) 20 | 21 | ext(EndpointRequest("GET", List("hello", "world"), Map.empty, Map.empty), HNil) === Right(HNil) 22 | ext(EndpointRequest("GET", List("hello", "wrong"), Map.empty, Map.empty), HNil) === RouteExtractor.NotFoundE 23 | ext(EndpointRequest("GET", List("hello"), Map.empty, Map.empty), HNil) === RouteExtractor.NotFoundE 24 | ext(EndpointRequest("GET", Nil, Map.empty, Map.empty), HNil) === RouteExtractor.NotFoundE 25 | } 26 | 27 | "segments" >> { 28 | val ext = extract(:= :> "foo" :> Segment[Int]('age) :> Get[Json, Foo]) 29 | 30 | ext(EndpointRequest("GET", List("foo", "0"), Map.empty, Map.empty), HNil) === Right(0 :: HNil) 31 | ext(EndpointRequest("GET", List("foo", "wrong"), Map.empty, Map.empty), HNil) === RouteExtractor.NotFoundE 32 | ext(EndpointRequest("GET", List("foo"), Map.empty, Map.empty), HNil) === RouteExtractor.NotFoundE 33 | ext(EndpointRequest("GET", Nil, Map.empty, Map.empty), HNil) === RouteExtractor.NotFoundE 34 | } 35 | 36 | "queries" >> { 37 | val ext0 = extract(:= :> "foo" :> Query[Int]('age) :> Get[Json, Foo]) 38 | 39 | ext0(EndpointRequest("GET", List("foo"), Map("age" -> List("0")), Map.empty), HNil) === Right(0 :: HNil) 40 | ext0(EndpointRequest("GET", List("foo"), Map("age" -> List("wrong")), Map.empty), HNil) === RouteExtractor.BadRequestE("query 'age' has not type Int") 41 | ext0(EndpointRequest("GET", List("foo"), Map("wrong" -> List("0")), Map.empty), HNil) === RouteExtractor.BadRequestE("missing query 'age'") 42 | ext0(EndpointRequest("GET", List("foo"), Map.empty, Map.empty), HNil) === RouteExtractor.BadRequestE("missing query 'age'") 43 | ext0(EndpointRequest("GET", List("foo", "bar"), Map.empty, Map.empty), HNil) === RouteExtractor.NotFoundE 44 | 45 | val ext1 = extract(:= :> "foo" :> Query[Option[Int]]('age) :> Get[Json, Foo]) 46 | 47 | ext1(EndpointRequest("GET", List("foo"), Map("age" -> List("0")), Map.empty), HNil) === Right(Some(0) :: HNil) 48 | ext1(EndpointRequest("GET", List("foo"), Map("wrong" -> List("0")), Map.empty), HNil) === Right(None :: HNil) 49 | 50 | val ext2 = extract(:= :> "foo" :> Query[List[Int]]('age) :> Get[Json, Foo]) 51 | 52 | ext2(EndpointRequest("GET", List("foo"), Map("age" -> List("0", "1")), Map.empty), HNil) === Right(List(0, 1) :: HNil) 53 | ext2(EndpointRequest("GET", List("foo"), Map.empty, Map.empty), HNil) === Right(Nil :: HNil) 54 | } 55 | 56 | "headers" >> { 57 | val ext0 = extract(:= :> "foo" :> Header[Int]('age) :> Get[Json, Foo]) 58 | 59 | ext0(EndpointRequest("GET", List("foo"), Map.empty, Map("age" -> "0")), HNil) === Right(0 :: HNil) 60 | ext0(EndpointRequest("GET", List("foo"), Map.empty, Map("age" -> "wrong")), HNil) === RouteExtractor.BadRequestE("header 'age' has not type Int") 61 | ext0(EndpointRequest("GET", List("foo"), Map.empty, Map("wrong" -> "0")), HNil) === RouteExtractor.BadRequestE("missing header 'age'") 62 | ext0(EndpointRequest("GET", List("foo"), Map.empty, Map.empty), HNil) === RouteExtractor.BadRequestE("missing header 'age'") 63 | ext0(EndpointRequest("GET", List("foo", "bar"), Map.empty, Map.empty), HNil) === RouteExtractor.NotFoundE 64 | 65 | val ext2 = extract(:= :> "foo" :> Header[Option[Int]]('age) :> Get[Json, Foo]) 66 | 67 | ext2(EndpointRequest("GET", List("foo"), Map.empty, Map("age" -> "0")), HNil) === Right(Some(0) :: HNil) 68 | ext2(EndpointRequest("GET", List("foo"), Map.empty, Map.empty), HNil) === Right(None :: HNil) 69 | 70 | val ext3 = extract(:= :> "foo" :> Header("Accept", "*") :> Get[Json, Foo]) 71 | 72 | ext3(EndpointRequest("GET", List("foo"), Map.empty, Map("accept" -> "*")), HNil) === Right(HNil) 73 | ext3(EndpointRequest("GET", List("foo"), Map.empty, Map("wrong" -> "*")), HNil) === RouteExtractor.BadRequestE("missing header 'Accept'") 74 | ext3(EndpointRequest("GET", List("foo"), Map.empty, Map("accept" -> "wrong")), HNil) === RouteExtractor.BadRequestE("header 'Accept' has unexpected value 'wrong' - expected '*'") 75 | 76 | val ext4 = extract(:= :> "foo" :> Server.Send("Accept", "*") :> Get[Json, Foo]) 77 | 78 | ext4(EndpointRequest("GET", List("foo"), Map.empty, Map.empty), HNil) === Right(HNil) 79 | 80 | val ext6 = extract(:= :> "foo" :> Server.Match[Int]("age") :> Get[Json, Foo]) 81 | 82 | ext6(EndpointRequest("GET", List("foo"), Map.empty, Map("age" -> "0")), HNil) === Right(Map("age" ->0) :: HNil) 83 | ext6(EndpointRequest("GET", List("foo"), Map.empty, Map("age-and-what-not" -> "0")), HNil) === Right(Map("age-and-what-not" -> 0) :: HNil) 84 | ext6(EndpointRequest("GET", List("foo"), Map.empty, Map("nope" -> "0")), HNil) === Right(Map.empty :: HNil) 85 | ext6(EndpointRequest("GET", List("foo"), Map.empty, Map("age-" -> "hello")), HNil) === RouteExtractor.BadRequestE("header 'age-' has not type Int") 86 | } 87 | 88 | "body type" >> { 89 | val ext0 = extract(:= :> ReqBody[Json, Foo] :> Put[Json, Foo]) 90 | 91 | ext0(EndpointRequest("PUT", Nil, Map.empty, Map.empty), HNil) === Right((BodyType[Foo], HNil)) 92 | ext0(EndpointRequest("PUT", List("foo", "bar"), Map.empty, Map.empty), HNil) === RouteExtractor.NotFoundE 93 | ext0(EndpointRequest("OPTIONS", List("foo", "bar"), Map.empty, Map.empty), HNil) === RouteExtractor.NotFoundE 94 | 95 | val ext1 = extract(:= :> ReqBody[Json, Foo] :> Post[Json, Foo]) 96 | 97 | ext1(EndpointRequest("POST", Nil, Map.empty, Map.empty), HNil) === Right((BodyType[Foo], HNil)) 98 | ext1(EndpointRequest("OPTIONS", Nil, Map.empty, Map.empty), HNil) === Right((BodyType[Foo], HNil)) 99 | } 100 | 101 | "methods" >> { 102 | val ext0 = extract(:= :> Get[Json, Foo]) 103 | 104 | ext0(EndpointRequest("GET", Nil, Map.empty, Map.empty), HNil) === Right(HNil) 105 | ext0(EndpointRequest("WRONG", Nil, Map.empty, Map.empty), HNil) === RouteExtractor.NotFoundE 106 | ext0(EndpointRequest("GET", List("foo"), Map.empty, Map.empty), HNil) === RouteExtractor.NotFoundE 107 | ext0(EndpointRequest("OPTIONS", Nil, Map.empty, Map.empty), HNil) === Right(HNil) 108 | 109 | val ext1 = extract(:= :> Put[Json, Foo]) 110 | 111 | ext1(EndpointRequest("PUT", Nil, Map.empty, Map.empty), HNil) === Right(HNil) 112 | ext1(EndpointRequest("OPTIONS", Nil, Map.empty, Map.empty), HNil) === Right(HNil) 113 | 114 | val ext2 = extract(:= :> Post[Json, Foo]) 115 | 116 | ext2(EndpointRequest("POST", Nil, Map.empty, Map.empty), HNil) === Right(HNil) 117 | ext2(EndpointRequest("OPTIONS", Nil, Map.empty, Map.empty), HNil) === Right(HNil) 118 | 119 | val ext3 = extract(:= :> Delete[Json, Foo]) 120 | 121 | ext3(EndpointRequest("DELETE", Nil, Map.empty, Map.empty), HNil) === Right(HNil) 122 | ext3(EndpointRequest("OPTIONS", Nil, Map.empty, Map.empty), HNil) === Right(HNil) 123 | } 124 | 125 | "combinations" >> { 126 | val ext0 = extract(:= :> "foo" :> Query[Int]('age) :> Header[String]('id) :> Get[Json, Foo]) 127 | 128 | ext0(EndpointRequest("GET", List("foo"), Map("age" -> List("0")), Map("id" -> "john")), HNil) === Right(0 :: "john" :: HNil) 129 | 130 | val ext1 = extract(:= :> Get[Json, Foo]) 131 | 132 | ext1(EndpointRequest("GET", Nil, Map.empty, Map.empty), HNil) === Right(HNil) 133 | } 134 | } 135 | } 136 | -------------------------------------------------------------------------------- /server/src/test/scala/typedapi/server/ServeAndMountSpec.scala: -------------------------------------------------------------------------------- 1 | package typedapi.server 2 | 3 | import typedapi.dsl._ 4 | import typedapi.shared.MethodType 5 | import shapeless.{HList, HNil, ::} 6 | import shapeless.ops.hlist.{Prepend, Mapper} 7 | import org.specs2.mutable.Specification 8 | 9 | import scala.language.higherKinds 10 | 11 | final class ServeAndMountSpec extends Specification { 12 | 13 | import StatusCodes._ 14 | 15 | case class Foo(name: String) 16 | 17 | sealed trait Req 18 | case class TestRequest(uri: List[String], queries: Map[String, List[String]], headers: Map[String, String]) extends Req 19 | case class TestRequestWithBody[Bd](uri: List[String], queries: Map[String, List[String]], headers: Map[String, String], body: Bd) extends Req 20 | 21 | case class TestResponse(raw: Any) 22 | 23 | implicit def execNoBodyId[El <: HList, KIn <: HList, VIn <: HList, M <: MethodType, FOut] = 24 | new NoReqBodyExecutor[El, KIn, VIn, M, Option, FOut] { 25 | type R = Req 26 | type Out = TestResponse 27 | 28 | def apply(req: Req, eReq: EndpointRequest, endpoint: Endpoint[El, KIn, VIn, M, VIn, Option, FOut]): Either[ExtractionError, Out] = 29 | extract(eReq, endpoint).right.map { extracted => 30 | TestResponse(execute(extracted, endpoint)) 31 | } 32 | } 33 | 34 | 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) = 35 | new ReqBodyExecutor[El, KIn, VIn, Bd, M, ROut, POut, Option, FOut] { 36 | type R = Req 37 | type Out = TestResponse 38 | 39 | implicit val prepend = _prepend 40 | implicit def eqProof = _eqProof 41 | 42 | def apply(req: Req, eReq: EndpointRequest, endpoint: Endpoint[El, KIn, VIn, M, (BodyType[Bd], ROut), Option, FOut]): Either[ExtractionError, Out] = 43 | extract(eReq, endpoint).right.map { case (_, extracted) => 44 | TestResponse(execute(extracted, req.asInstanceOf[TestRequestWithBody[Bd]].body, endpoint)) 45 | } 46 | } 47 | 48 | def toList[El <: HList, KIn <: HList, VIn <: HList, M <: MethodType, ROut, F[_], FOut](endpoint: Endpoint[El, KIn, VIn, M, ROut, F, FOut]) 49 | (implicit executor: EndpointExecutor[El, KIn, VIn, M, ROut, F, FOut]): List[Serve[executor.R, executor.Out]] = 50 | List(new Serve[executor.R, executor.Out] { 51 | def options(eReq: EndpointRequest): Option[(String, Map[String, String])] = { 52 | endpoint.extractor(eReq, HNil) match { 53 | case Right(_) => Some((endpoint.method, endpoint.headers)) 54 | case _ => None 55 | } 56 | } 57 | 58 | def apply(req: executor.R, eReq: EndpointRequest): Either[ExtractionError, executor.Out] = executor(req, eReq, endpoint) 59 | }) 60 | 61 | 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]] = 62 | s(end.map(endpointToServe)) 63 | 64 | "serve endpoints as simple Request -> Response functions and mount them into a server" >> { 65 | "serve single endpoint and no body" >> { 66 | val Api = := :> "find" :> "user" :> Segment[String]('name) :> Query[Int]('sortByAge) :> Get[Json, List[Foo]] 67 | val endpoint = derive[Option](Api).from((name, sortByAge) => Some(successWith(Ok)(List(Foo(name))))) 68 | val served = toList(endpoint) 69 | 70 | val req = TestRequest(List("find", "user", "joe"), Map("sortByAge" -> List("1")), Map.empty) 71 | val eReq = EndpointRequest("GET", req.uri, req.queries, req.headers) 72 | 73 | served.head(req, eReq) === Right(TestResponse(Some(Right(Ok -> List(Foo("joe")))))) 74 | } 75 | 76 | "check if route exists and return method" >> { 77 | val Api = := :> "find" :> "user" :> Segment[String]('name) :> Query[Int]('sortByAge) :> Server.Send("Hello", "*") :> Get[Json, List[Foo]] 78 | val endpoint = derive[Option](Api).from((name, sortByAge) => Some(successWith(Ok)(List(Foo(name))))) 79 | val served = toList(endpoint) 80 | 81 | val req0 = TestRequest(List("find", "user", "joe"), Map("sortByAge" -> List("1")), Map.empty) 82 | val eReq0 = EndpointRequest("GET", req0.uri, req0.queries, req0.headers) 83 | 84 | served.head.options(eReq0) === Some(("GET", Map(("Hello", "*")))) 85 | 86 | val eReq1 = EndpointRequest("POST", req0.uri, req0.queries, req0.headers) 87 | 88 | served.head.options(eReq1) === None 89 | } 90 | 91 | "serve single endpoint and with body" >> { 92 | val Api = := :> "find" :> "user" :> Segment[String]('name) :> ReqBody[Json, Foo] :> Post[Json, List[Foo]] 93 | val endpoint = derive[Option](Api).from((name, body) => Some(successWith(Ok)(List(Foo(name), body)))) 94 | val served = toList(endpoint) 95 | 96 | val req = TestRequestWithBody(List("find", "user", "joe"), Map.empty, Map.empty, Foo("jim")) 97 | val eReq = EndpointRequest("POST", req.uri, req.queries, req.headers) 98 | 99 | served.head(req, eReq) === Right(TestResponse(Some(Right(Ok -> List(Foo("joe"), Foo("jim")))))) 100 | } 101 | 102 | "serve multiple endpoints" >> { 103 | val Api = 104 | (:= :> "find" :> "user" :> Segment[String]('name) :> Query[Int]('sortByAge) :> Get[Json, List[Foo]]) :|: 105 | (:= :> "create" :> "user" :> ReqBody[Json, Foo] :> Post[Json, Foo]) 106 | 107 | def find(name: String, age: Int): Option[Result[List[Foo]]] = Some(successWith(Ok)(List(Foo(name)))) 108 | def create(foo: Foo): Option[Result[Foo]] = Some(successWith(Ok)(foo)) 109 | 110 | val endpoints = deriveAll[Option](Api).from(find _, create _) 111 | 112 | val served = toList(endpoints) 113 | 114 | val req = TestRequest(List("find", "user", "joe"), Map("sortByAge" -> List("1")), Map.empty) 115 | val eReq = EndpointRequest("GET", req.uri, req.queries, req.headers) 116 | 117 | served.head(req, eReq) === Right(TestResponse(Some(Right(Ok -> List(Foo("joe")))))) 118 | } 119 | } 120 | } 121 | -------------------------------------------------------------------------------- /shared/src/main/scala/typedapi/dsl/ApiDsl.scala: -------------------------------------------------------------------------------- 1 | package typedapi.dsl 2 | 3 | import typedapi.shared._ 4 | import shapeless._ 5 | 6 | sealed trait ApiList[H <: HList] 7 | 8 | /** Basic operations. */ 9 | sealed trait MethodOps[H <: HList] { 10 | 11 | def :>[MT <: MediaType, A](body: TypeCarrier[ReqBodyElement[MT, A]]): WithBodyCons[MT, A, H] = WithBodyCons() 12 | def :>[M <: MethodElement](method: TypeCarrier[M]): ApiTypeCarrier[M :: H] = ApiTypeCarrier() 13 | } 14 | 15 | sealed trait PathOps[H <: HList] { 16 | 17 | def :>[S](path: Witness.Lt[S]): PathCons[PathElement[S] :: H] = PathCons() 18 | def :>[K, V](segment: TypeCarrier[SegmentParam[K, V]]): SegmentCons[SegmentParam[K, V] :: H] = SegmentCons() 19 | } 20 | 21 | sealed trait HeaderOps[H <: HList] { 22 | 23 | def :>[K, V](header: TypeCarrier[HeaderParam[K, V]]): InputHeaderCons[HeaderParam[K, V] :: H] = InputHeaderCons() 24 | def :>[K, V](fixed: TypeCarrier[FixedHeaderElement[K, V]]): FixedHeaderCons[FixedHeaderElement[K, V] :: H] = FixedHeaderCons() 25 | 26 | def :>[K, V](client: TypeCarrier[ClientHeaderElement[K, V]]): ClientHeaderElCons[ClientHeaderElement[K, V] :: H] = ClientHeaderElCons() 27 | def :>[K, V](client: TypeCarrier[ClientHeaderParam[K, V]]): ClientHeaderParamCons[ClientHeaderParam[K, V] :: H] = ClientHeaderParamCons() 28 | def :>[V](client: TypeCarrier[ClientHeaderCollParam[V]]): ClientHeaderCollParamCons[ClientHeaderCollParam[V] :: H] = ClientHeaderCollParamCons() 29 | 30 | def :>[K, V](server: TypeCarrier[ServerHeaderMatchParam[K, V]]): ServerHeaderMatchParamCons[ServerHeaderMatchParam[K, V] :: H] = ServerHeaderMatchParamCons() 31 | def :>[K, V](server: TypeCarrier[ServerHeaderSendElement[K, V]]): ServerHeaderSendElCons[ServerHeaderSendElement[K, V] :: H] = ServerHeaderSendElCons() 32 | } 33 | 34 | /** Initial element with empty api description. */ 35 | case object EmptyCons extends PathOps[HNil] with HeaderOps[HNil] with MethodOps[HNil] with ApiList[HNil] { 36 | 37 | def :>[K, V](query: TypeCarrier[QueryParam[K, V]]): QueryCons[QueryParam[K, V] :: HNil] = QueryCons() 38 | } 39 | 40 | /** Last set element is a path. */ 41 | final case class PathCons[H <: HList]() extends PathOps[H] with HeaderOps[H] with MethodOps[H] with ApiList[H] { 42 | 43 | def :>[K, V](query: TypeCarrier[QueryParam[K, V]]): QueryCons[QueryParam[K, V] :: H] = QueryCons() 44 | } 45 | 46 | /** Last set element is a segment. */ 47 | final case class SegmentCons[H <: HList]() extends PathOps[H] with HeaderOps[H] with MethodOps[H] with ApiList[H] { 48 | 49 | def :>[K, V](query: TypeCarrier[QueryParam[K, V]]): QueryCons[QueryParam[K, V] :: H] = QueryCons() 50 | } 51 | 52 | /** Last set element is a query parameter. */ 53 | final case class QueryCons[H <: HList]() extends HeaderOps[H] with MethodOps[H] with ApiList[H] { 54 | 55 | def :>[K, V](query: TypeCarrier[QueryParam[K, V]]): QueryCons[QueryParam[K, V] :: H] = QueryCons() 56 | } 57 | 58 | /** Last set element is a header. */ 59 | sealed trait HeaderCons[H <: HList] extends HeaderOps[H] with MethodOps[H] with ApiList[H] 60 | 61 | final case class InputHeaderCons[H <: HList]() extends HeaderCons[H] 62 | final case class FixedHeaderCons[H <: HList]() extends HeaderCons[H] 63 | final case class ClientHeaderElCons[H <: HList]() extends HeaderCons[H] 64 | final case class ClientHeaderParamCons[H <: HList]() extends HeaderCons[H] 65 | final case class ClientHeaderCollParamCons[H <: HList]() extends HeaderCons[H] 66 | final case class ServerHeaderMatchParamCons[H <: HList]() extends HeaderCons[H] 67 | final case class ServerHeaderSendElCons[H <: HList]() extends HeaderCons[H] 68 | 69 | /** Last set element is a request body. */ 70 | final case class WithBodyCons[BMT <: MediaType, Bd, H <: HList]() extends ApiList[H] { 71 | 72 | def :>[M <: MethodElement](method: TypeCarrier[M])(implicit out: MethodToReqBody[M, BMT, Bd]): ApiTypeCarrier[out.Out :: H] = ApiTypeCarrier() 73 | } 74 | -------------------------------------------------------------------------------- /shared/src/main/scala/typedapi/dsl/package.scala: -------------------------------------------------------------------------------- 1 | package typedapi 2 | 3 | import typedapi.shared._ 4 | 5 | package object dsl extends MethodToReqBodyLowPrio with MethodToStringLowPrio { 6 | 7 | val MediaTypes = typedapi.shared.MediaTypes 8 | val MT = typedapi.shared.MediaTypes 9 | 10 | def := = EmptyCons 11 | 12 | def Segment[V] = new PairTypeFromWitnessKey[SegmentParam, V] 13 | def Query[V] = new PairTypeFromWitnessKey[QueryParam, V] 14 | def Header[V] = new PairTypeFromWitnessKey[HeaderParam, V] 15 | def Header = new PairTypeFromWitnesses[FixedHeaderElement] 16 | 17 | object Client { 18 | 19 | def Header = new PairTypeFromWitnesses[ClientHeaderElement] 20 | def Header[V] = new PairTypeFromWitnessKey[ClientHeaderParam, V] 21 | def Coll[V] = TypeCarrier[ClientHeaderCollParam[V]]() 22 | } 23 | 24 | object Server { 25 | 26 | def Send = new PairTypeFromWitnesses[ServerHeaderSendElement] 27 | def Match[V] = new PairTypeFromWitnessKey[ServerHeaderMatchParam, V] 28 | } 29 | 30 | type Json = MT.`Application/json` 31 | type Plain = MT.`Text/plain` 32 | 33 | def ReqBody[MT <: MediaType, A] = TypeCarrier[ReqBodyElement[MT, A]]() 34 | def Get[MT <: MediaType, A] = TypeCarrier[GetElement[MT, A]]() 35 | def Put[MT <: MediaType, A] = TypeCarrier[PutElement[MT, A]]() 36 | def Post[MT <: MediaType, A] = TypeCarrier[PostElement[MT, A]]() 37 | def Delete[MT <: MediaType, A] = TypeCarrier[DeleteElement[MT, A]]() 38 | } 39 | -------------------------------------------------------------------------------- /shared/src/main/scala/typedapi/package.scala: -------------------------------------------------------------------------------- 1 | 2 | import typedapi.shared._ 3 | import shapeless._ 4 | import shapeless.ops.hlist.Prepend 5 | 6 | package object typedapi extends MethodToReqBodyLowPrio with MethodToStringLowPrio { 7 | 8 | val MediaTypes = typedapi.shared.MediaTypes 9 | val MT = typedapi.shared.MediaTypes 10 | 11 | val Root = PathListBuilder[HNil]() 12 | def Segment[V] = new PairTypeFromWitnessKey[SegmentParam, V] 13 | 14 | val Queries = QueryListBuilder[HNil]() 15 | val NoQueries = Queries 16 | 17 | val Headers = HeaderListBuilder[HNil]() 18 | val NoHeaders = Headers 19 | 20 | def ReqBody[MT <: MediaType, A] = TypeCarrier[ReqBodyElement[MT, A]]() 21 | def Get[MT <: MediaType, A] = TypeCarrier[GetElement[MT, A]]() 22 | def Put[MT <: MediaType, A] = TypeCarrier[PutElement[MT, A]]() 23 | def Post[MT <: MediaType, A] = TypeCarrier[PostElement[MT, A]]() 24 | def Delete[MT <: MediaType, A] = TypeCarrier[DeleteElement[MT, A]]() 25 | 26 | type Json = MT.`Application/json` 27 | type Plain = MT.`Text/plain` 28 | 29 | def api[M <: MethodElement, P <: HList, Q <: HList, H <: HList, Prep <: HList, Api <: HList] 30 | (method: TypeCarrier[M], path: PathListBuilder[P] = Root, queries: QueryListBuilder[Q] = NoQueries, headers: HeaderListBuilder[H] = NoHeaders) 31 | (implicit prepQP: Prepend.Aux[Q, P, Prep], prepH: Prepend.Aux[H, Prep, Api]): ApiTypeCarrier[M :: Api] = ApiTypeCarrier() 32 | 33 | def apiWithBody[M <: MethodElement, P <: HList, Q <: HList, H <: HList, Prep <: HList, Api <: HList, BMT <: MediaType, Bd] 34 | (method: TypeCarrier[M], body: TypeCarrier[ReqBodyElement[BMT, Bd]], path: PathListBuilder[P] = Root, queries: QueryListBuilder[Q] = NoQueries, headers: HeaderListBuilder[H] = NoHeaders) 35 | (implicit prepQP: Prepend.Aux[Q, P, Prep], prepH: Prepend.Aux[H, Prep, Api], m: MethodToReqBody[M, BMT, Bd]): ApiTypeCarrier[m.Out :: Api] = ApiTypeCarrier() 36 | } 37 | -------------------------------------------------------------------------------- /shared/src/main/scala/typedapi/shared/ApiElement.scala: -------------------------------------------------------------------------------- 1 | package typedapi.shared 2 | 3 | import scala.annotation.implicitNotFound 4 | 5 | sealed trait ApiElement 6 | 7 | /** Type-container providing the singleton-type of an static path element */ 8 | sealed trait PathElement[P] 9 | 10 | /** Type-container providing the name (singleton) and value type for a path parameter. */ 11 | sealed trait SegmentParam[K, V] extends ApiElement 12 | 13 | /** Type-container providing the name (singleton) and value type for a query parameter. */ 14 | sealed trait QueryParam[K, V] extends ApiElement 15 | 16 | /** Type-container providing the name (singleton) and value type for a header parameter. */ 17 | sealed trait HeaderParam[K, V] extends ApiElement 18 | 19 | /** Type-container providing the name (singleton) and value type for a static header element. */ 20 | sealed trait FixedHeaderElement[K, V] extends ApiElement 21 | /** Type-container providing the name (singleton) and value type for a static header element only used for the client. */ 22 | sealed trait ClientHeaderElement[K, V] extends ApiElement 23 | /** Type-container providing the name (singleton) and value type for a header parameter only used for the client. */ 24 | sealed trait ClientHeaderParam[K, V] extends ApiElement 25 | /** Type-container providing a collection of headers (Map[String, V]) only used for the client. */ 26 | sealed trait ClientHeaderCollParam[V] extends ApiElement 27 | /** Type-container providing the name (singleton) and value type for a static header element sent by server. */ 28 | sealed trait ServerHeaderSendElement[K, V] extends ApiElement 29 | /** Type-container providing the name (singleton) and value type describing a sub-string headers have to match only used for the server. */ 30 | sealed trait ServerHeaderMatchParam[K, V] extends ApiElement 31 | 32 | /** Type-container providing the media-type and value type for a request body. */ 33 | sealed trait ReqBodyElement[MT <: MediaType, A] extends ApiElement 34 | 35 | trait MethodElement extends ApiElement 36 | /** Type-container representing a GET operation with a media-type and value type for the result. */ 37 | sealed trait GetElement[MT <: MediaType, A] extends MethodElement 38 | /** Type-container representing a PUT operation with a media-type and value type for the result. */ 39 | sealed trait PutElement[MT <: MediaType, A] extends MethodElement 40 | /** Type-container representing a PUT operation with a media-type and value type for the result and a body. */ 41 | sealed trait PutWithBodyElement[BMT <: MediaType, Bd, MT <: MediaType, A] extends MethodElement 42 | /** Type-container representing a POST operation with a media-type and value type for the result. */ 43 | sealed trait PostElement[MT <: MediaType, A] extends MethodElement 44 | /** Type-container representing a POST operation with a media-type and value type for the result and a body. */ 45 | sealed trait PostWithBodyElement[BMT <: MediaType, Bd, MT <: MediaType, A] extends MethodElement 46 | /** Type-container representing a DELETE operation with a media-type and value type for the result. */ 47 | sealed trait DeleteElement[MT <: MediaType, A] extends MethodElement 48 | 49 | @implicitNotFound("""You try to add a request body to a method which doesn't expect one. 50 | 51 | method: ${M} 52 | """) 53 | trait MethodToReqBody[M <: MethodElement, MT <: MediaType, Bd] { 54 | 55 | type Out <: MethodElement 56 | } 57 | 58 | trait MethodToReqBodyLowPrio { 59 | 60 | implicit def reqBodyForPut[MT <: MediaType, A, BMT <: MediaType, Bd] = new MethodToReqBody[PutElement[MT, A], BMT, Bd] { 61 | type Out = PutWithBodyElement[BMT, Bd, MT, A] 62 | } 63 | 64 | implicit def reqBodyForPost[MT <: MediaType, A, BMT <: MediaType, Bd] = new MethodToReqBody[PostElement[MT, A], BMT, Bd] { 65 | type Out = PostWithBodyElement[BMT, Bd, MT, A] 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /shared/src/main/scala/typedapi/shared/ApiList.scala: -------------------------------------------------------------------------------- 1 | package typedapi.shared 2 | 3 | import shapeless._ 4 | 5 | /** Typecarrier to construct a complete path description from [[PathElement]]s and [[SegmentParam]]s. */ 6 | final case class PathListBuilder[P <: HList]() { 7 | 8 | def /[S](path: Witness.Lt[S]): PathListBuilder[PathElement[S] :: P] = PathListBuilder() 9 | def /[K, V](segment: TypeCarrier[SegmentParam[K, V]]): PathListBuilder[SegmentParam[K, V] :: P] = PathListBuilder() 10 | } 11 | 12 | /** Typecarrier to construct a set of queries from [[QueryParam]]s. */ 13 | final case class QueryListBuilder[Q <: HList]() { 14 | 15 | final class WitnessDerivation[V] { 16 | def apply[K](wit: Witness.Lt[K]): QueryListBuilder[QueryParam[K, V] :: Q] = QueryListBuilder() 17 | } 18 | 19 | def add[V]: WitnessDerivation[V] = new WitnessDerivation[V] 20 | } 21 | 22 | /** Typecarrier to construct a set of headers from [[HeaderParam]]s, [[FixedHeaderElement]]s, [[ClientHeaderElement]]s, 23 | [[ServerHeaderSendElement]]s and [ServerHeaderMatchParam]]s. */ 24 | final case class HeaderListBuilder[H <: HList]() { 25 | 26 | final class WitnessDerivation[V] { 27 | def apply[K](wit: Witness.Lt[K]): HeaderListBuilder[HeaderParam[K, V] :: H] = HeaderListBuilder() 28 | } 29 | def add[V]: WitnessDerivation[V] = new WitnessDerivation[V] 30 | 31 | def add[K, V](kWit: Witness.Lt[K], vWit: Witness.Lt[V]): HeaderListBuilder[FixedHeaderElement[K, V] :: H] = HeaderListBuilder() 32 | 33 | final class ClientWitnessDerivation[V] { 34 | def apply[K](wit: Witness.Lt[K]): HeaderListBuilder[ClientHeaderParam[K, V] :: H] = HeaderListBuilder() 35 | } 36 | def client[V]: ClientWitnessDerivation[V] = new ClientWitnessDerivation[V] 37 | 38 | def client[K, V](kWit: Witness.Lt[K], vWit: Witness.Lt[V]): HeaderListBuilder[ClientHeaderElement[K, V] :: H] = HeaderListBuilder() 39 | 40 | def clientColl[V]: HeaderListBuilder[ClientHeaderCollParam[V] :: H] = HeaderListBuilder() 41 | 42 | final class ServerMatchWitnessDerivation[V] { 43 | def apply[K](wit: Witness.Lt[K]): HeaderListBuilder[ServerHeaderMatchParam[K, V] :: H] = HeaderListBuilder() 44 | } 45 | def serverMatch[V]: ServerMatchWitnessDerivation[V] = new ServerMatchWitnessDerivation[V] 46 | 47 | def serverSend[K, V](kWit: Witness.Lt[K], vWit: Witness.Lt[V]): HeaderListBuilder[ServerHeaderSendElement[K, V] :: H] = HeaderListBuilder() 48 | } 49 | -------------------------------------------------------------------------------- /shared/src/main/scala/typedapi/shared/ApiTransformer.scala: -------------------------------------------------------------------------------- 1 | package typedapi.shared 2 | 3 | import shapeless._ 4 | import shapeless.labelled.FieldType 5 | 6 | import scala.annotation.implicitNotFound 7 | 8 | trait ApiOp 9 | 10 | sealed trait SegmentInput extends ApiOp 11 | sealed trait QueryInput extends ApiOp 12 | sealed trait HeaderInput extends ApiOp 13 | 14 | sealed trait FixedHeader[K, V] extends ApiOp 15 | sealed trait ClientHeader[K, V] extends ApiOp 16 | sealed trait ClientHeaderInput extends ApiOp 17 | sealed trait ClientHeaderCollInput extends ApiOp 18 | sealed trait ServerHeaderSend[K, V] extends ApiOp 19 | sealed trait ServerHeaderMatchInput extends ApiOp 20 | 21 | trait MethodType extends ApiOp 22 | sealed trait GetCall extends MethodType 23 | sealed trait PutCall extends MethodType 24 | sealed trait PutWithBodyCall extends MethodType 25 | sealed trait PostCall extends MethodType 26 | sealed trait PostWithBodyCall extends MethodType 27 | sealed trait DeleteCall extends MethodType 28 | 29 | /** Transforms a [[MethodType]] to a `String`. */ 30 | @implicitNotFound("Missing String transformation for this method = ${M}.") 31 | trait MethodToString[M <: MethodType] { 32 | 33 | def show: String 34 | } 35 | 36 | trait MethodToStringLowPrio { 37 | 38 | implicit val getToStr = new MethodToString[GetCall] { val show = "GET" } 39 | implicit val putToStr = new MethodToString[PutCall] { val show = "PUT" } 40 | implicit val putBodyToStr = new MethodToString[PutWithBodyCall] { val show = "PUT" } 41 | implicit val postToStr = new MethodToString[PostCall] { val show = "POST" } 42 | implicit val postBodyToStr = new MethodToString[PostWithBodyCall] { val show = "POST" } 43 | implicit val deleteToStr = new MethodToString[DeleteCall] { val show = "DELETE" } 44 | } 45 | 46 | /** Tranforms API type shape into five distinct types: 47 | * - El: elements of the API (path elements, segment/query/header input placeholder, etc.) 48 | * - KIn: expected input key types (from parameters) 49 | * - VIn: expected input value types (from parameters) 50 | * - M: method type 51 | * - Out: output type 52 | * 53 | * ``` 54 | * val api: TypeCarrier[Get[Json, Foo] :: Segment["name".type, String] :: "find".type :: HNil] 55 | * val trans: ("name".type :: SegmentInput :: HNil, "name".type :: HNil, String :: HNil], Field[Json, GetCall], Foo) 56 | * ``` 57 | */ 58 | trait ApiTransformer { 59 | 60 | import TypeLevelFoldFunction.at 61 | 62 | implicit def pathElementTransformer[S, El <: HList, KIn <: HList, VIn <: HList, M <: MethodType, Out] = 63 | at[PathElement[S], (El, KIn, VIn, M, Out), (S :: El, KIn, VIn, M, Out)] 64 | 65 | implicit def segmentParamTransformer[S, A, El <: HList, KIn <: HList, VIn <: HList, M <: MethodType, Out] = 66 | at[SegmentParam[S, A], (El, KIn, VIn, M, Out), (SegmentInput :: El, S :: KIn, A :: VIn, M, Out)] 67 | 68 | implicit def queryParamTransformer[S, A, El <: HList, KIn <: HList, VIn <: HList, M <: MethodType, Out] = 69 | at[QueryParam[S, A], (El, KIn, VIn, M, Out), (QueryInput :: El, S :: KIn, A :: VIn, M, Out)] 70 | 71 | implicit def queryListParamTransformer[S, A, El <: HList, KIn <: HList, VIn <: HList, M <: MethodType, Out] = 72 | at[QueryParam[S, List[A]], (El, KIn, VIn, M, Out), (QueryInput :: El, S :: KIn, List[A] :: VIn, M, Out)] 73 | 74 | implicit def headerParamTransformer[S, A, El <: HList, KIn <: HList, VIn <: HList, M <: MethodType, Out] = 75 | at[HeaderParam[S, A], (El, KIn, VIn, M, Out), (HeaderInput :: El, S :: KIn, A :: VIn, M, Out)] 76 | 77 | implicit def fixedHeaderElementTransformer[K, V, El <: HList, KIn <: HList, VIn <: HList, M <: MethodType, Out] = 78 | at[FixedHeaderElement[K, V], (El, KIn, VIn, M, Out), (FixedHeader[K, V] :: El, KIn, VIn, M, Out)] 79 | 80 | implicit def clientHeaderElementTransformer[K, V, El <: HList, KIn <: HList, VIn <: HList, M <: MethodType, Out] = 81 | at[ClientHeaderElement[K, V], (El, KIn, VIn, M, Out), (ClientHeader[K, V] :: El, KIn, VIn, M, Out)] 82 | 83 | implicit def clientHeaderParamTransformer[K, V, El <: HList, KIn <: HList, VIn <: HList, M <: MethodType, Out] = 84 | at[ClientHeaderParam[K, V], (El, KIn, VIn, M, Out), (ClientHeaderInput :: El, K :: KIn, V :: VIn, M, Out)] 85 | 86 | implicit def clientHeaderCollParamTransformer[V, El <: HList, KIn <: HList, VIn <: HList, M <: MethodType, Out] = 87 | at[ClientHeaderCollParam[V], (El, KIn, VIn, M, Out), (ClientHeaderCollInput :: El, KIn, Map[String, V] :: VIn, M, Out)] 88 | 89 | implicit def serverHeaderSendElementTransformer[K, V, El <: HList, KIn <: HList, VIn <: HList, M <: MethodType, Out] = 90 | at[ServerHeaderSendElement[K, V], (El, KIn, VIn, M, Out), (ServerHeaderSend[K, V] :: El, KIn, VIn, M, Out)] 91 | 92 | implicit def serverHeaderMatchParamTransformer[K, V, El <: HList, KIn <: HList, VIn <: HList, M <: MethodType, Out] = 93 | at[ServerHeaderMatchParam[K, V], (El, KIn, VIn, M, Out), (ServerHeaderMatchInput :: El, K :: KIn, Map[String, V] :: VIn, M, Out)] 94 | 95 | implicit def getTransformer[MT <: MediaType, A] = at[GetElement[MT, A], Unit, (HNil, HNil, HNil, GetCall, FieldType[MT, A])] 96 | 97 | implicit def putTransformer[MT <: MediaType, A] = at[PutElement[MT, A], Unit, (HNil, HNil, HNil, PutCall, FieldType[MT, A])] 98 | 99 | implicit def putWithBodyTransformer[BMT <: MediaType, Bd, MT <: MediaType, A] = 100 | at[PutWithBodyElement[BMT, Bd, MT, A], Unit, (HNil, FieldType[BMT, BodyField.T] :: HNil, Bd :: HNil, PutWithBodyCall, FieldType[MT, A])] 101 | 102 | implicit def postTransformer[MT <: MediaType, A] = at[PostElement[MT, A], Unit, (HNil, HNil, HNil, PostCall, FieldType[MT, A])] 103 | 104 | implicit def postWithBodyTransformer[BMT <: MediaType, Bd, MT <: MediaType, A] = 105 | at[PostWithBodyElement[BMT, Bd, MT, A], Unit, (HNil, FieldType[BMT, BodyField.T] :: HNil, Bd :: HNil, PostWithBodyCall, FieldType[MT, A])] 106 | 107 | implicit def deleteTransformer[MT <: MediaType, A] = at[DeleteElement[MT, A], Unit, (HNil, HNil, HNil, DeleteCall, FieldType[MT, A])] 108 | } 109 | -------------------------------------------------------------------------------- /shared/src/main/scala/typedapi/shared/TypeCarrier.scala: -------------------------------------------------------------------------------- 1 | package typedapi.shared 2 | 3 | import shapeless._ 4 | 5 | import scala.language.higherKinds 6 | 7 | /** As the name says this case class is only there it pass types around on the value level. */ 8 | final case class TypeCarrier[A]() 9 | 10 | /** Derive a [[TypeCarrier]] from a type parameter and a singleton type. */ 11 | final class PairTypeFromWitnessKey[F[_, _], V] { 12 | 13 | def apply[K](wit: Witness.Lt[K]): TypeCarrier[F[K, V]] = TypeCarrier() 14 | } 15 | 16 | /** Derive a [[TypeCarrier]] from two singleton types. */ 17 | final class PairTypeFromWitnesses[F[_, _]] { 18 | 19 | def apply[K, V](kWit: Witness.Lt[K], vWit: Witness.Lt[V]): TypeCarrier[F[K, V]] = TypeCarrier() 20 | } 21 | 22 | /** Specific [[TypeCarrier]] for complete API types. */ 23 | final case class ApiTypeCarrier[H <: HList]() { 24 | 25 | def :|:[H1 <: HList](next: ApiTypeCarrier[H1]): CompositionCons[H1 :: H :: HNil] = CompositionCons() 26 | } 27 | 28 | /** Specific [[TypeCarrier]] for multiple API types. */ 29 | final case class CompositionCons[H <: HList]() { 30 | 31 | def :|:[H1 <: HList](next: ApiTypeCarrier[H1]): CompositionCons[H1 :: H] = CompositionCons() 32 | } 33 | -------------------------------------------------------------------------------- /shared/src/main/scala/typedapi/shared/TypeLevelFoldLeft.scala: -------------------------------------------------------------------------------- 1 | package typedapi.shared 2 | 3 | import shapeless._ 4 | 5 | import scala.annotation.implicitNotFound 6 | 7 | // INTERNAL API 8 | 9 | /** Reimplements shapeles Case2 but on the type level (no real HList instance). */ 10 | @implicitNotFound("""Woops, you shouldn't be here. We cannot find TypeLevelFoldFunction instance. 11 | 12 | input: ${In} 13 | aggregation: ${Agg}""") 14 | sealed trait TypeLevelFoldFunction[In, Agg] { 15 | 16 | type Out 17 | } 18 | 19 | object TypeLevelFoldFunction { 20 | 21 | type Aux[In, Agg, Out0] = TypeLevelFoldFunction[In, Agg] { type Out = Out0 } 22 | 23 | def at[In, Agg, Out0]: Aux[In, Agg, Out0] = new TypeLevelFoldFunction[In, Agg] { 24 | type Out = Out0 25 | } 26 | } 27 | 28 | /** Reimplements shapeless LeftFolder but on the type level (no real HList instance) */ 29 | @implicitNotFound("""Woops, you shouldn't be here. We cannot find TypeLevelFold instance. 30 | 31 | list: ${H} 32 | aggregation: ${Agg}""") 33 | sealed trait TypeLevelFoldLeft[H <: HList, Agg] extends Serializable { 34 | 35 | type Out 36 | } 37 | 38 | object TypeLevelFoldLeft { 39 | 40 | type Aux[H <: HList, Agg, Out0] = TypeLevelFoldLeft[H, Agg] { 41 | type Out = Out0 42 | } 43 | } 44 | 45 | trait TypeLevelFoldLeftLowPrio { 46 | 47 | implicit def hnilCase[Agg]: TypeLevelFoldLeft.Aux[HNil, Agg, Agg] = new TypeLevelFoldLeft[HNil, Agg] { 48 | type Out = Agg 49 | } 50 | 51 | implicit def foldCase[H, T <: HList, Agg, FtOut, FOut](implicit f: TypeLevelFoldFunction.Aux[H, Agg, FtOut], 52 | next: Lazy[TypeLevelFoldLeft.Aux[T, FtOut, FOut]]): TypeLevelFoldLeft.Aux[H :: T, Agg, FOut] = new TypeLevelFoldLeft[H :: T, Agg] { 53 | type Out = next.value.Out 54 | } 55 | } 56 | 57 | /** Helper to work on a composition of HLists we want to fold over. */ 58 | @implicitNotFound("""Woops, you shouldn't be here. We cannot find TypeLevelFoldList instance. 59 | 60 | apis: ${H}""") 61 | trait TypeLevelFoldLeftList[H <: HList] { 62 | 63 | type Out <: HList 64 | } 65 | 66 | object TypeLevelFoldLeftList { 67 | 68 | type Aux[H <: HList, Out0 <: HList] = TypeLevelFoldLeftList[H] { 69 | type Out = Out0 70 | } 71 | } 72 | 73 | trait TypeLevelFoldLeftListLowPrio { 74 | 75 | implicit def lastFoldLeftList[H <: HList, Agg](implicit folder0: TypeLevelFoldLeft[H, Agg]) = new TypeLevelFoldLeftList[H :: HNil] { 76 | type Out = folder0.Out :: HNil 77 | } 78 | 79 | implicit def folderLeftList[H <: HList, Agg, T <: HList](implicit folder0: TypeLevelFoldLeft[H, Agg], list: TypeLevelFoldLeftList[T]) = new TypeLevelFoldLeftList[H :: T] { 80 | type Out = folder0.Out :: list.Out 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /shared/src/main/scala/typedapi/shared/WitnessToString.scala: -------------------------------------------------------------------------------- 1 | package typedapi.shared 2 | 3 | import scala.annotation.implicitNotFound 4 | 5 | @implicitNotFound("Couldn't find transformation for witness ${K} to String.") 6 | sealed trait WitnessToString[K] { 7 | 8 | def show(key: K): String 9 | } 10 | 11 | trait WitnessToStringLowPrio { 12 | 13 | implicit def symbolKey[K <: Symbol] = new WitnessToString[K] { 14 | def show(key: K): String = key.name 15 | } 16 | 17 | implicit def stringKey[K <: String] = new WitnessToString[K] { 18 | def show(key: K): String = key 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /shared/src/main/scala/typedapi/shared/package.scala: -------------------------------------------------------------------------------- 1 | package typedapi 2 | 3 | import shapeless.Witness 4 | 5 | package object shared { 6 | 7 | final val BodyField = Witness('body) 8 | final val RawHeadersField = Witness('rawHeaders) 9 | } 10 | -------------------------------------------------------------------------------- /shared/src/main/scala/typedapi/util/Decoder.scala: -------------------------------------------------------------------------------- 1 | package typedapi.util 2 | 3 | import scala.language.higherKinds 4 | 5 | trait Decoder[F[_], A] extends (String => F[Either[Exception, A]]) 6 | 7 | object Decoder { 8 | 9 | def apply[F[_], A](decoder: String => F[Either[Exception, A]]): Decoder[F, A] = new Decoder[F, A] { 10 | def apply(raw: String): F[Either[Exception, A]] = decoder(raw) 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /shared/src/main/scala/typedapi/util/Encoder.scala: -------------------------------------------------------------------------------- 1 | package typedapi.util 2 | 3 | import scala.language.higherKinds 4 | 5 | trait Encoder[F[_], A] extends (A => F[String]) 6 | 7 | object Encoder { 8 | 9 | def apply[F[_], A](encoder: A => F[String]): Encoder[F, A] = new Encoder[F, A] { 10 | def apply(a: A): F[String] = encoder(a) 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /shared/src/test/scala/typedapi/ApiDefinitionSpec.scala: -------------------------------------------------------------------------------- 1 | package typedapi 2 | 3 | import typedapi.shared._ 4 | import shapeless._ 5 | 6 | import SpecUtil._ 7 | 8 | // compilation-only test 9 | object ApiDefinitionSpec { 10 | 11 | import MediaTypes._ 12 | 13 | case class Foo() 14 | 15 | val testW = Witness("test") 16 | val test2W = Witness("test2") 17 | val fooW = Witness('foo) 18 | val barW = Witness('bah) 19 | 20 | type Base = PathElement[testW.T] :: HNil 21 | 22 | // path lists 23 | testCompile(Root)[HNil] 24 | testCompile(Root / "test")[PathElement[testW.T] :: HNil] 25 | testCompile(Root / "test" / "test2")[PathElement[test2W.T] :: PathElement[testW.T] :: HNil] 26 | testCompile(Root / "test" / Segment[Int]('foo))[SegmentParam[fooW.T, Int] :: PathElement[testW.T] :: HNil] 27 | testCompile(Root / Segment[Int]('foo) / "test")[PathElement[testW.T] :: SegmentParam[fooW.T, Int] :: HNil] 28 | 29 | // query lists 30 | testCompile(Queries)[HNil] 31 | testCompile(Queries add[Int](fooW))[QueryParam[fooW.T, Int] :: HNil] 32 | testCompile(Queries add[Int](fooW) add[Int](barW))[QueryParam[barW.T, Int] :: QueryParam[fooW.T, Int] :: HNil] 33 | 34 | // header lists 35 | testCompile(Headers)[HNil] 36 | testCompile(Headers add[Int](fooW))[HeaderParam[fooW.T, Int] :: HNil] 37 | testCompile(Headers add[Int](fooW) add[Int](barW))[HeaderParam[barW.T, Int] :: HeaderParam[fooW.T, Int] :: HNil] 38 | testCompile(Headers add(fooW, testW))[FixedHeaderElement[fooW.T, testW.T] :: HNil] 39 | testCompile(Headers client(fooW, testW))[ClientHeaderElement[fooW.T, testW.T] :: HNil] 40 | testCompile(Headers client[String](fooW))[ClientHeaderParam[fooW.T, String] :: HNil] 41 | testCompile(Headers.clientColl[String])[ClientHeaderCollParam[String] :: HNil] 42 | testCompile(Headers serverSend(fooW, testW))[ServerHeaderSendElement[fooW.T, testW.T] :: HNil] 43 | testCompile(Headers serverMatch[String](fooW))[ServerHeaderMatchParam[fooW.T, String] :: HNil] 44 | testCompile(Headers serverMatch[String](fooW))[ServerHeaderMatchParam[fooW.T, String] :: HNil] 45 | 46 | // methods 47 | testCompile(api(Get[Json, Foo]))[GetElement[`Application/json`, Foo] :: HNil] 48 | test.illTyped("apiWothBody(Get[Foo], ReqBody[Foo])") 49 | testCompile(api(Put[Json, Foo]))[PutElement[`Application/json`, Foo] :: HNil] 50 | testCompile(apiWithBody(Put[Json, Foo], ReqBody[Plain, Foo]))[PutWithBodyElement[`Text/plain`, Foo, `Application/json`, Foo] :: HNil] 51 | testCompile(api(Post[Json, Foo]))[PostElement[`Application/json`, Foo] :: HNil] 52 | testCompile(apiWithBody(Post[Json, Foo], ReqBody[Plain, Foo]))[PostWithBodyElement[`Text/plain`, Foo, `Application/json`, Foo] :: HNil] 53 | testCompile(api(Delete[Json, Foo]))[DeleteElement[`Application/json`, Foo] :: HNil] 54 | test.illTyped("apiWothBody(Delete[Json, Foo], ReqBody[Plain, Foo])") 55 | 56 | // whole api 57 | testCompile( 58 | api(Get[Json, Foo], Root / "test" / Segment[Int]('foo), Queries add[String]('foo), Headers add[Double]('foo)) 59 | )[GetElement[`Application/json`, Foo] :: HeaderParam[fooW.T, Double] :: QueryParam[fooW.T, String] :: SegmentParam[fooW.T, Int] :: PathElement[testW.T] :: HNil] 60 | } 61 | -------------------------------------------------------------------------------- /shared/src/test/scala/typedapi/SpecUtil.scala: -------------------------------------------------------------------------------- 1 | package typedapi 2 | 3 | import shapeless.HList 4 | 5 | import scala.language.higherKinds 6 | 7 | object SpecUtil { 8 | 9 | class TestHelper[Act <: HList] { 10 | 11 | def apply[Exp <: HList](implicit ev: Act =:= Exp) = Unit 12 | } 13 | 14 | def testCompile[F[_ <: HList], Act <: HList](cons: F[Act]) = new TestHelper[Act] 15 | } 16 | -------------------------------------------------------------------------------- /shared/src/test/scala/typedapi/dsl/ApiDslSpec.scala: -------------------------------------------------------------------------------- 1 | package typedapi.dsl 2 | 3 | import typedapi.SpecUtil._ 4 | import typedapi.shared._ 5 | import shapeless._ 6 | 7 | // compilation-only test 8 | object ApiDslSpec { 9 | 10 | import MediaTypes._ 11 | 12 | case class Foo() 13 | 14 | val testW = Witness("test") 15 | val fooW = Witness('foo) 16 | val base = := :> "test" 17 | 18 | type Base = PathElement[testW.T] :: HNil 19 | 20 | val a = Query[Int].apply(fooW) 21 | 22 | // empty path 23 | testCompile(:= :> Segment[Int](fooW))[SegmentParam[fooW.T, Int] :: HNil] 24 | testCompile(:= :> Query[Int](fooW))[QueryParam[fooW.T, Int] :: HNil] 25 | testCompile(:= :> Header[Int](fooW))[HeaderParam[fooW.T, Int] :: HNil] 26 | testCompile(:= :> Get[Json, Foo])[GetElement[`Application/json`, Foo] :: HNil] 27 | 28 | // path: add every element 29 | testCompile(base :> Segment[Int](fooW))[SegmentParam[fooW.T, Int] :: Base] 30 | testCompile(base :> Query[Int](fooW))[QueryParam[fooW.T, Int] :: Base] 31 | testCompile(base :> Header[Int](fooW))[HeaderParam[fooW.T, Int] :: Base] 32 | testCompile(base :> Get[Json, Foo])[GetElement[`Application/json`, Foo] :: Base] 33 | 34 | // segment: add every element 35 | val _baseSeg = base :> Segment[Int](fooW) 36 | 37 | type _BaseSeg = SegmentParam[fooW.T, Int] :: Base 38 | 39 | testCompile(_baseSeg :> Segment[Int](fooW))[SegmentParam[fooW.T, Int] :: _BaseSeg] 40 | testCompile(_baseSeg :> Query[Int](fooW))[QueryParam[fooW.T, Int] :: _BaseSeg] 41 | testCompile(_baseSeg :> Header[Int](fooW))[HeaderParam[fooW.T, Int] :: _BaseSeg] 42 | testCompile(_baseSeg :> Get[Json, Foo])[GetElement[`Application/json`, Foo] :: _BaseSeg] 43 | 44 | // query: add queries, headers, body and final 45 | val _baseQ = base :> Query[Int](fooW) 46 | 47 | type _BaseQ = QueryParam[fooW.T, Int] :: Base 48 | 49 | test.illTyped("_baseQ :> \"fail\"") 50 | test.illTyped("_baseQ :> Segment[Int](fooW)") 51 | testCompile(_baseQ :> Query[Int](fooW))[QueryParam[fooW.T, Int] :: _BaseQ] 52 | testCompile(_baseQ :> Header[Int](fooW))[HeaderParam[fooW.T, Int] :: _BaseQ] 53 | testCompile(_baseQ :> Get[Json, Foo])[GetElement[`Application/json`, Foo] :: _BaseQ] 54 | 55 | // header: add header, final 56 | val _baseH = base :> Header[Int](fooW) 57 | 58 | type _BaseH = HeaderParam[fooW.T, Int] :: Base 59 | 60 | test.illTyped("_baseH :> \"fail\"") 61 | test.illTyped("_baseH :> Segment[Int](fooW)") 62 | test.illTyped("_baseH :> Query[Int](fooW)") 63 | testCompile(_baseH :> Header[Int](fooW))[HeaderParam[fooW.T, Int] :: _BaseH] 64 | testCompile(_baseH :> Header(fooW, testW) :> Header[Int](fooW))[HeaderParam[fooW.T, Int] :: FixedHeaderElement[fooW.T, testW.T] :: _BaseH] 65 | testCompile(_baseH :> Client.Header[String](fooW))[ClientHeaderParam[fooW.T, String] :: _BaseH] 66 | testCompile(_baseH :> Client.Header(fooW, testW))[ClientHeaderElement[fooW.T, testW.T] :: _BaseH] 67 | testCompile(_baseH :> Server.Send(fooW, testW))[ServerHeaderSendElement[fooW.T, testW.T] :: _BaseH] 68 | testCompile(_baseH :> Server.Match[String](fooW))[ServerHeaderMatchParam[fooW.T, String] :: _BaseH] 69 | testCompile(_baseH :> Get[Json, Foo])[GetElement[`Application/json`, Foo] :: _BaseH] 70 | 71 | // request body: add put or post 72 | val _baseRB = base :> ReqBody[Plain, Foo] 73 | 74 | type _BaseRB = Base 75 | 76 | test.illTyped("_baseRB :> Segment[Int](fooW)") 77 | test.illTyped("_baseRB :> Query[Int](fooW)") 78 | test.illTyped("_baseRB :> Header[Int](fooW)") 79 | test.illTyped("_baseRB :> Get[Json, Foo]") 80 | testCompile(_baseRB :> Put[Json, Foo])[PutWithBodyElement[`Text/plain`, Foo, `Application/json`, Foo] :: _BaseRB] 81 | testCompile(_baseRB :> Post[Json, Foo])[PostWithBodyElement[`Text/plain`, Foo, `Application/json`, Foo] :: _BaseRB] 82 | 83 | // method: nothing at all 84 | val _baseF = base :> Get[Json, Foo] 85 | 86 | test.illTyped("_baseF :> Segment[Int](fooW)") 87 | test.illTyped("_baseF :> Query[Int](fooW)") 88 | test.illTyped("_baseF :> Header[Int](fooW)") 89 | test.illTyped("_baseF :> Get[Json, Foo]") 90 | } 91 | -------------------------------------------------------------------------------- /shared/src/test/scala/typedapi/shared/ApiTransformerSpec.scala: -------------------------------------------------------------------------------- 1 | package typedapi.shared 2 | 3 | import typedapi.Json 4 | import shapeless._ 5 | import shapeless.labelled.FieldType 6 | 7 | // compilation-only test 8 | final class ApiTransformerSpec extends TypeLevelFoldLeftLowPrio with ApiTransformer { 9 | 10 | case class Foo() 11 | 12 | def testCompile[H <: HList, Out](implicit folder: TypeLevelFoldLeft.Aux[H, Unit, Out]): TypeLevelFoldLeft.Aux[H, Unit, Out] = 13 | folder 14 | 15 | val pathW = Witness("test") 16 | val fooW = Witness('foo) 17 | val barW = Witness('bar) 18 | 19 | testCompile[GetElement[Json, Foo] :: HNil, (HNil, HNil, HNil, GetCall, FieldType[Json, Foo])] 20 | testCompile[PutElement[Json, Foo] :: HNil, (HNil, HNil, HNil, PutCall, FieldType[Json, Foo])] 21 | testCompile[PutWithBodyElement[Json, Foo, Json, Foo] :: HNil, (HNil, FieldType[Json, BodyField.T] :: HNil, Foo :: HNil, PutWithBodyCall, FieldType[Json, Foo])] 22 | testCompile[PostElement[Json, Foo] :: HNil, (HNil, HNil, HNil, PostCall, FieldType[Json, Foo])] 23 | testCompile[PostWithBodyElement[Json, Foo, Json, Foo] :: HNil, (HNil, FieldType[Json, BodyField.T] :: HNil, Foo :: HNil, PostWithBodyCall, FieldType[Json, Foo])] 24 | testCompile[DeleteElement[Json, Foo] :: HNil, (HNil, HNil, HNil, DeleteCall, FieldType[Json, Foo])] 25 | testCompile[GetElement[Json, Foo] :: PathElement[pathW.T] :: HNil, (pathW.T :: HNil, HNil, HNil, GetCall, FieldType[Json, Foo])] 26 | testCompile[GetElement[Json, Foo] :: SegmentParam[fooW.T, String] :: HNil, (SegmentInput :: HNil, fooW.T :: HNil, String :: HNil, GetCall, FieldType[Json, Foo])] 27 | testCompile[GetElement[Json, Foo] :: QueryParam[fooW.T, String] :: HNil, (QueryInput :: HNil, fooW.T :: HNil, String :: HNil, GetCall, FieldType[Json, Foo])] 28 | testCompile[GetElement[Json, Foo] :: QueryParam[fooW.T, List[String]] :: HNil, (QueryInput :: HNil, fooW.T :: HNil, List[String] :: HNil, GetCall, FieldType[Json, Foo])] 29 | testCompile[GetElement[Json, Foo] :: HeaderParam[fooW.T, String] :: HNil, (HeaderInput :: HNil, fooW.T :: HNil, String :: HNil, GetCall, FieldType[Json, Foo])] 30 | testCompile[GetElement[Json, Foo] :: FixedHeaderElement[fooW.T, barW.T] :: HNil, (FixedHeader[fooW.T, barW.T] :: HNil, HNil, HNil, GetCall, FieldType[Json, Foo])] 31 | testCompile[GetElement[Json, Foo] :: ClientHeaderParam[fooW.T, String] :: HNil, (ClientHeaderInput :: HNil, fooW.T :: HNil, String :: HNil, GetCall, FieldType[Json, Foo])] 32 | testCompile[GetElement[Json, Foo] :: ClientHeaderElement[fooW.T, barW.T] :: HNil, (ClientHeader[fooW.T, barW.T] :: HNil, HNil, HNil, GetCall, FieldType[Json, Foo])] 33 | testCompile[GetElement[Json, Foo] :: ClientHeaderCollParam[Int] :: HNil, (ClientHeaderCollInput :: HNil, HNil, Map[String, Int] :: HNil, GetCall, FieldType[Json, Foo])] 34 | testCompile[GetElement[Json, Foo] :: ServerHeaderMatchParam[fooW.T, String] :: HNil, (ServerHeaderMatchInput :: HNil, fooW.T :: HNil, Map[String, String] :: HNil, GetCall, FieldType[Json, Foo])] 35 | testCompile[GetElement[Json, Foo] :: ServerHeaderSendElement[fooW.T, barW.T] :: HNil, (ServerHeaderSend[fooW.T, barW.T] :: HNil, HNil, HNil, GetCall, FieldType[Json, Foo])] 36 | 37 | testCompile[ 38 | GetElement[Json, Foo] :: HeaderParam[fooW.T, Boolean] :: QueryParam[fooW.T, Int] :: SegmentParam[fooW.T, String] :: PathElement[pathW.T] :: HNil, 39 | (pathW.T :: SegmentInput :: QueryInput :: HeaderInput :: HNil, fooW.T :: fooW.T :: fooW.T :: HNil, String :: Int :: Boolean :: HNil, GetCall, FieldType[Json, Foo]) 40 | ] 41 | } 42 | --------------------------------------------------------------------------------