Showing preview only (275K chars total). Download the full file or copy to clipboard to get everything.
Repository: pheymann/typedapi
Branch: master
Commit: c13558a527f2
Files: 81
Total size: 247.9 KB
Directory structure:
gitextract_555nqngw/
├── .gitignore
├── .travis.yml
├── CHANGELOG.md
├── LICENSE
├── README.md
├── akka-http-client/
│ └── src/
│ └── main/
│ └── scala/
│ └── typedapi/
│ └── client/
│ └── akkahttp/
│ └── package.scala
├── akka-http-server/
│ └── src/
│ └── main/
│ └── scala/
│ └── typedapi/
│ └── server/
│ └── akkahttp/
│ └── package.scala
├── ammonite-client-support/
│ └── src/
│ └── main/
│ └── scala/
│ └── typedapi/
│ └── client/
│ └── package.scala
├── build.sbt
├── client/
│ └── src/
│ ├── main/
│ │ └── scala/
│ │ └── typedapi/
│ │ └── client/
│ │ ├── ApiRequest.scala
│ │ ├── ClientManager.scala
│ │ ├── ExecutableDerivation.scala
│ │ ├── ExecutablesFromHList.scala
│ │ ├── FilterServerElements.scala
│ │ ├── RequestDataBuilder.scala
│ │ ├── package.scala
│ │ └── test/
│ │ ├── RequestInput.scala
│ │ └── package.scala
│ └── test/
│ └── scala/
│ └── typedapi/
│ └── client/
│ ├── ClientManagerSpec.scala
│ └── RequestDataBuilderSpec.scala
├── docs/
│ ├── ApiDefinition.md
│ ├── ClientCreation.md
│ ├── ExtendIt.md
│ ├── ServerCreation.md
│ └── example/
│ ├── ammonite_client_example.sc
│ ├── build.sbt
│ ├── client-js/
│ │ ├── index.html
│ │ └── src/
│ │ └── main/
│ │ └── scala/
│ │ └── Client.scala
│ ├── client-jvm/
│ │ └── src/
│ │ └── main/
│ │ └── scala/
│ │ └── Client.scala
│ ├── project/
│ │ ├── build.properties
│ │ └── plugins.sbt
│ ├── server/
│ │ └── src/
│ │ └── main/
│ │ └── scala/
│ │ └── Server.scala
│ └── shared/
│ └── src/
│ └── main/
│ └── scala/
│ ├── Apis.scala
│ └── User.scala
├── http-support-tests/
│ └── src/
│ └── test/
│ └── scala/
│ └── http/
│ └── support/
│ └── tests/
│ ├── User.scala
│ ├── client/
│ │ ├── AkkaHttpClientSupportSpec.scala
│ │ ├── Http4sClientSupportSpec.scala
│ │ ├── ScalajHttpClientSupportSpec.scala
│ │ └── TestServer.scala
│ ├── package.scala
│ └── server/
│ ├── AkkaHttpServerSupportSpec.scala
│ ├── Http4sServerSupportSpec.scala
│ └── ServerSupportSpec.scala
├── http4s-client/
│ └── src/
│ └── main/
│ └── scala/
│ └── typedapi/
│ └── client/
│ └── http4s/
│ └── package.scala
├── http4s-server/
│ └── src/
│ └── main/
│ └── scala/
│ └── typedapi/
│ └── server/
│ └── htt4ps/
│ └── package.scala
├── js-client/
│ └── src/
│ └── main/
│ └── scala/
│ └── typedapi/
│ └── client/
│ └── js/
│ └── package.scala
├── project/
│ ├── build.properties
│ ├── build.scala
│ └── plugins.sbt
├── scalaj-http-client/
│ └── src/
│ └── main/
│ └── scala/
│ └── typedapi/
│ └── client/
│ └── scalajhttp/
│ └── package.scala
├── server/
│ └── src/
│ ├── main/
│ │ └── scala/
│ │ └── typedapi/
│ │ └── server/
│ │ ├── Endpoint.scala
│ │ ├── EndpointComposition.scala
│ │ ├── EndpointExecutor.scala
│ │ ├── FilterClientElements.scala
│ │ ├── RouteExtractor.scala
│ │ ├── Serve.scala
│ │ ├── ServeToList.scala
│ │ ├── ServerHeaderExtractor.scala
│ │ ├── ServerManager.scala
│ │ ├── StatusCodes.scala
│ │ └── package.scala
│ └── test/
│ └── scala/
│ └── typedapi/
│ └── server/
│ ├── ApiToEndpointLinkSpec.scala
│ ├── RouteExtractorSpec.scala
│ └── ServeAndMountSpec.scala
└── shared/
└── src/
├── main/
│ └── scala/
│ └── typedapi/
│ ├── dsl/
│ │ ├── ApiDsl.scala
│ │ └── package.scala
│ ├── package.scala
│ ├── shared/
│ │ ├── ApiElement.scala
│ │ ├── ApiList.scala
│ │ ├── ApiTransformer.scala
│ │ ├── MediaTypes.scala
│ │ ├── TypeCarrier.scala
│ │ ├── TypeLevelFoldLeft.scala
│ │ ├── WitnessToString.scala
│ │ └── package.scala
│ └── util/
│ ├── Decoder.scala
│ └── Encoder.scala
└── test/
└── scala/
└── typedapi/
├── ApiDefinitionSpec.scala
├── SpecUtil.scala
├── dsl/
│ └── ApiDslSpec.scala
└── shared/
└── ApiTransformerSpec.scala
================================================
FILE CONTENTS
================================================
================================================
FILE: .gitignore
================================================
# ensime
*.*~
.ensime
.ensime_cache
project/ensime-plugin.sbt
# sbt
target/
project/target/
project/project/
================================================
FILE: .travis.yml
================================================
language: scala
scala:
- 2.11.11
- 2.12.3
jdk:
- oraclejdk8
script:
- sbt ++$TRAVIS_SCALA_VERSION clean test
- sbt "project sharedJS" "fastOptJS"
- sbt "project js-client" "fastOptJS"
================================================
FILE: CHANGELOG.md
================================================
### 0.2.0
- `StatusCodes` and `MediaTypes` are in distinct `object`s, thus, have to be imported explicitly
- improved Ammonite integration
- internal: separated decoded and raw requests with `RawApiRequest` and `ApiRequest`
- fixed `implicitNotFound` message for `ApiRequest`
- implemented [#31](https://github.com/pheymann/typedapi/issues/31) [#35](https://github.com/pheymann/typedapi/issues/35) [#36](https://github.com/pheymann/typedapi/issues/36)
- server endpoints have to define a response code:
```Scala
derive[IO](Api) { input =>
success(/*...*/)
^
}
```
### 0.1.0
- internal cleanups and refactorings
- extended example project and added ScalaJS client
- centralized http-support specs
- added akka-http support on server and client-side
- added scalaj-http support on the client-side
- added ScalaJS compilation support for shared and client code
- implemented basic ScalaJS client
- added body encoding types and made them mandatory (several hundred Mediatypes supported)
```Scala
:= :> ReqBody[Json, User] :> Get[Json, User]
_______________^__________________^
```
- `RawHeaders` was removed
- fixed headers were added; a fixed header is a statically known key-value pair, therefore, no input is required
```Scala
// dsl
:= :> Header("Access-Control-Allow-Origin", "*") :> Get[Json, User]
// function
api(Get[Json, User], headers = Headers add("Access-Control-Allow-Origin", "*"))
```
- changes to the server API:
- `NoReqBodyExecutor` and `ReqBodyExecutor` now expect a `MethodType`:
```Scala
new NoReqBodyExecutor[El, KIn, VIn, M, F, FOut] {
____________________________________^
new ReqBodyExecutor[El, KIn, VIn, Bd, M, ROut, POut, F, FOut] {
______________________________________^
```
- fixed header only sent by the server
```Scala
:= :> Server.Send("Access-Control-Allow-Origin", "*") :> Get[Json, User]
api(Get[Json, User], Headers.serverSend("Access-Control-Allow-Origin", "*"))
```
- extract headers which have keys that match a `String`
```Scala
:= :> Server.Match[String]("Control") :> Get[Json, User]
api(Get[Json, User], Headers.serverMatch[String]("Control"))
```
- changes to the client API:
- new encoding types add `Content-Type` and `Accept` headers
- fixed header only sent by the client
```Scala
:= :> Client.Header("Access-Control-Allow-Origin", "*") :> Get[Json, User]
api(Get[Json, User], Headers.client("Access-Control-Allow-Origin", "*"))
```
- send dynamic header ignore it on the server-side
```Scala
:= :> Client.Header[String]("Foo") :> Get[Json, User]
api(Get[Json, User], Headers.client[String]("Foo"))
```
### 0.1.0-RC5 / Almost there
- changes to the client API:
```Scala
val ApiList =
(:= :> Get[Foo]) :|:
(:= :> RequestBody[Foo] :> Put[Foo])
// `:|:` removed for API compositions
val (get, put) = deriveAll(ApiList)
```
- changes to the server API:
```Scala
// same for endpoint compositions
val e = deriveAll[IO](ApiList).from(get, put)
```
### 0.1.0-RC4 / Towards a stable API
- changes to the client API:
```Scala
val Api = := :> Get[Foo]
val ApiList =
(:= :> Get[Foo]) :|:
(:= :> RequestBody[Foo] :> Put[Foo])
// not `compile`, but
val foo = derive(Api)
val (foo2 :|: bar :|: =:) = deriveAll(ApiList)
...
// explicitly pass ClientManager
foo().run[IO](cm)
_______________^
```
- changes to the server API
```Scala
// not `link.to`, but
val endpoint = derive[IO](Api).from(...)
val endpoints = deriveAll[IO](ApiList).from(...)
```
- major changes were applied to the internal code to reach a stable state (see [this PR](https://github.com/pheymann/typedapi/pull/13))
================================================
FILE: LICENSE
================================================
MIT License
Copyright (c) 2018 Paul Heymann
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
================================================
FILE: README.md
================================================
[](https://travis-ci.org/pheymann/typedapi)
[](https://maven-badges.herokuapp.com/maven-central/com.github.pheymann/typedapi-shared_2.12)
[](https://gitter.im/pheymann/Lobby)
[](https://www.scala-js.org/)
*experimental project*: see issues [#39](https://github.com/pheymann/typedapi/issues/39) and [#41](https://github.com/pheymann/typedapi/issues/41)
# Typedapi
Define type safe APIs and let the Scala compiler do the rest:
### Api definition
```Scala
import typedapi._
val MyApi =
// GET {body: User} /fetch/user?{name: String}
api(method = Get[MT.`application/json`, User],
path = Root / "fetch" / "user",
queries = Queries add Query[String]('name)) :|:
// POST {body: User} /create/user
apiWithBody(method = Post[MT.`application/json`, User],
body = ReqBody[Json, User],
path = Root / "create" / "user")
```
And for the Servant lovers:
```Scala
import typedapi.dsl._
val MyApi =
// GET {body: User} /fetch/user?{name: String}
(:= :> "fetch" :> "user" :> Query[String]('name) :> Get[MT.`application/json`, User]) :|:
// POST {body: User} /create/user
(:= :> "create" :> "user" :> ReqBody[Json, User] :> Post[MT.`application/json`, User])
```
### Client side
```Scala
import typedapi.client._
val (fetch, create) = deriveAll(MyApi)
import typedapi.client.http4s._; import cats.effect.IO; import org.http4s.client.blaze.Http1Client
val cm = ClientManager(Http1Client[IO]().unsafeRunSync, "http://my-host", 8080)
fetch("joe").run[IO](cm): IO[User]
```
### Server side
```Scala
import typedapi.server._
val fetch: String => IO[Result[User]] = name => findUserIO(name).map(success)
val create: User => IO[Result[User]] = user => createUserIO(user).map(success)
val endpoints = deriveAll[IO](MyApi).from(fetch, create)
import typedapi.server.http4s._; import cats.effect.IO; import org.http4s.server.blaze.BlazeBuilder
val sm = ServerManager(BlazeBuilder[IO], "http://my-host", 8080)
val server = mount(sm, endpoints)
server.unsafeRunSync()
```
This is all you have to do to define an API with multiple endpoints and to create a working client and server for them.
You can find the above code as a complete project [here](https://github.com/pheymann/typedapi/tree/master/docs/example).
## Motivation
This library is the result of the following questions:
> How much can we encode on the type level? Are we able to describe a whole API and generate the call functions from that without using Macros?
It is inspired by [Servant](https://github.com/haskell-servant/servant) and it provides an API layer which is independent of the underlying server/client implementation. Right now Typedapi supports:
- [http4s](https://github.com/http4s/http4s)
- [akka-http](https://github.com/akka/akka-http)
- [scalaj-http](https://github.com/scalaj/scalaj-http) on the client-side
- ScalaJS on the client-side
If you need something else take a look at this [doc](https://github.com/pheymann/typedapi/blob/master/docs/ExtendIt.md#write-your-own-client-backend).
## Get this library
It is available for Scala 2.11, 2.12 and ScalaJS and can be downloaded as Maven artifact:
```
// dsl
"com.github.pheymann" %% "typedapi-client" % <version>
"com.github.pheymann" %% "typedapi-server" % <version>
// http4s support
"com.github.pheymann" %% "typedapi-http4s-client" % <version>
"com.github.pheymann" %% "typedapi-http4s-server" % <version>
// akka-http support
"com.github.pheymann" %% "typedapi-akka-http-client" % <version>
"com.github.pheymann" %% "typedapi-akka-http-server" % <version>
// Scalaj-Http client support
"com.github.pheymann" %% "typedapi-scalaj-http-client" % <version>
// ScalaJS client support
"com.github.pheymann" %% "typedapi-js-client" % <version>
```
You can also build it on your machine:
```
git clone https://github.com/pheymann/typedapi.git
cd typedapi
sbt "+ publishLocal"
```
## Ammonite
Typedapi also offers an improved experience for [Ammonite](http://ammonite.io/#Ammonite-REPL) and [ScalaScripts](http://ammonite.io/#ScalaScripts):
```Scala
import $ivy.`com.github.pheymann::typedapi-ammonite-client:<version>`
import typedapi._
import client._
import amm._
val Readme = api(Get[MT.`text/html`, String], Root / "pheymann" / "typedapi" / "master" / "README.md")
val readme = derive(Readme)
// gives you the raw scalaj-http response
val cm = clientManager("https://raw.githubusercontent.com")
val response = get().run[Id].raw(cm)
response.body
response.headers
...
```
In case Ammonite cannot resolve `com.dwijnand:sbt-compat:1.0.0`, follow [this](https://github.com/pheymann/typedapi/blob/master/docs/ClientCreation.md#ammonite) solution.
## Documentation
The documentation is located in [docs](https://github.com/pheymann/typedapi/blob/master/docs) and covers the following topics so far:
- [How to define an API](https://github.com/pheymann/typedapi/blob/master/docs/ApiDefinition.md)
- [How to create a client](https://github.com/pheymann/typedapi/blob/master/docs/ClientCreation.md)
- [How to create a server](https://github.com/pheymann/typedapi/blob/master/docs/ServerCreation.md)
- [Extend the library](https://github.com/pheymann/typedapi/blob/master/docs/ExtendIt.md)
- Typelevel Summit 2018 Berlin Talk [Typedapi: Define your API on the type-level](https://github.com/pheymann/typelevel-summit-2018)
- and a [post](https://typelevel.org/blog/2018/06/15/typedapi.html) on the Typelevel Blog describing the basic concept behind this library.
## Dependencies
- [shapeless 2.3.3](https://github.com/milessabin/shapeless/)
## Contribution
Contributions are highly appreciated. If you find a bug or you are missing the support for a specific client/server library consider opening a PR with your solution.
================================================
FILE: akka-http-client/src/main/scala/typedapi/client/akkahttp/package.scala
================================================
package typedapi.client
import akka.http.scaladsl.HttpExt
import akka.http.scaladsl.model.RequestEntity
import akka.http.scaladsl.model.headers.RawHeader
import akka.http.scaladsl.model.{ HttpMethods, HttpRequest, HttpResponse, Uri }
import akka.http.scaladsl.unmarshalling.{Unmarshal, FromEntityUnmarshaller}
import akka.http.scaladsl.marshalling.{Marshal, ToEntityMarshaller}
import akka.stream.Materializer
import scala.concurrent.duration.FiniteDuration
import scala.concurrent.{ ExecutionContext, Future }
package object akkahttp {
private def mkRequest(uri: String, queries: Map[String, List[String]], headers: Map[String, String]): HttpRequest =
HttpRequest(
uri = Uri(uri).withQuery(Uri.Query(queries.mapValues(_.mkString(",")))),
headers = headers.map { case (key, value) => RawHeader(key, value) }(collection.breakOut)
)
private def execRequest(client: HttpExt, request: HttpRequest, bodyConsumerTimeout: FiniteDuration)
(implicit ec: ExecutionContext,
mat: Materializer): Future[HttpResponse] =
client.singleRequest(request)
.flatMap { response =>
response.toStrict(bodyConsumerTimeout)
}
def rawGetRequest(bodyConsumerTimeout: FiniteDuration)(implicit ec: ExecutionContext,
mat: Materializer) = new RawGetRequest[HttpExt, Future] {
type Resp = HttpResponse
def apply(uri: List[String], queries: Map[String, List[String]], headers: Map[String, String], cm: ClientManager[HttpExt]): Future[Resp] = {
val request = mkRequest(deriveUriString(cm, uri), queries, headers).copy(HttpMethods.GET)
execRequest(cm.client, request, bodyConsumerTimeout)
}
}
implicit def rawGetRequestImpl(implicit bodyConsumerTimeout: FiniteDuration,
ec: ExecutionContext,
mat: Materializer): RawGetRequest[HttpExt, Future] = rawGetRequest(bodyConsumerTimeout)
def getRequest[A](bodyConsumerTimeout: FiniteDuration)(implicit decoder: FromEntityUnmarshaller[A],
ec: ExecutionContext,
mat: Materializer) = new GetRequest[HttpExt, Future, A] {
private val raw = rawGetRequest(bodyConsumerTimeout)
def apply(uri: List[String], queries: Map[String, List[String]], headers: Map[String, String], cm: ClientManager[HttpExt]): Future[A] =
raw(uri, queries, headers, cm).flatMap { response =>
Unmarshal(response.entity).to[A]
}
}
implicit def getRequestImpl[A](implicit bodyConsumerTimeout: FiniteDuration,
decoder: FromEntityUnmarshaller[A],
ec: ExecutionContext,
mat: Materializer): GetRequest[HttpExt, Future, A] = getRequest(bodyConsumerTimeout)
def rawPutRequest(bodyConsumerTimeout: FiniteDuration)(implicit ec: ExecutionContext,
mat: Materializer) = new RawPutRequest[HttpExt, Future] {
type Resp = HttpResponse
def apply(uri: List[String], queries: Map[String, List[String]], headers: Map[String, String], cm: ClientManager[HttpExt]): Future[Resp] = {
val request = mkRequest(deriveUriString(cm, uri), queries, headers).copy(HttpMethods.PUT)
execRequest(cm.client, request, bodyConsumerTimeout)
}
}
implicit def rawPutRequestImpl(implicit bodyConsumerTimeout: FiniteDuration,
ec: ExecutionContext,
mat: Materializer): RawPutRequest[HttpExt, Future] = rawPutRequest(bodyConsumerTimeout)
def putRequest[A](bodyConsumerTimeout: FiniteDuration)(implicit decoder: FromEntityUnmarshaller[A],
ec: ExecutionContext,
mat: Materializer) = new PutRequest[HttpExt, Future, A] {
private val raw = rawPutRequest(bodyConsumerTimeout)
def apply(uri: List[String], queries: Map[String, List[String]], headers: Map[String, String], cm: ClientManager[HttpExt]): Future[A] =
raw(uri, queries, headers, cm).flatMap { response =>
Unmarshal(response.entity).to[A]
}
}
implicit def putRequestImpl[A](implicit bodyConsumerTimeout: FiniteDuration,
decoder: FromEntityUnmarshaller[A],
ec: ExecutionContext,
mat: Materializer): PutRequest[HttpExt, Future, A] = putRequest(bodyConsumerTimeout)
def rawPutBodyRequest[Bd](bodyConsumerTimeout: FiniteDuration)(implicit encoder: ToEntityMarshaller[Bd],
ec: ExecutionContext,
mat: Materializer) = new RawPutWithBodyRequest[HttpExt, Future, Bd] {
type Resp = HttpResponse
def apply(uri: List[String], queries: Map[String, List[String]], headers: Map[String, String], body: Bd, cm: ClientManager[HttpExt]): Future[Resp] = {
Marshal(body).to[RequestEntity].flatMap { marshalledBody =>
val request = mkRequest(deriveUriString(cm, uri), queries, headers - "Content-Type").copy(HttpMethods.PUT, entity = marshalledBody)
execRequest(cm.client, request, bodyConsumerTimeout)
}
}
}
implicit def rawPutBodyRequestImpl[Bd](implicit bodyConsumerTimeout: FiniteDuration,
encoder: ToEntityMarshaller[Bd],
ec: ExecutionContext,
mat: Materializer): RawPutWithBodyRequest[HttpExt, Future, Bd] = rawPutBodyRequest(bodyConsumerTimeout)
def putBodyRequest[Bd, A](bodyConsumerTimeout: FiniteDuration)(implicit encoder: ToEntityMarshaller[Bd],
decoder: FromEntityUnmarshaller[A],
ec: ExecutionContext,
mat: Materializer) = new PutWithBodyRequest[HttpExt, Future, Bd, A] {
private val raw = rawPutBodyRequest(bodyConsumerTimeout)
def apply(uri: List[String], queries: Map[String, List[String]], headers: Map[String, String], body: Bd, cm: ClientManager[HttpExt]): Future[A] =
raw(uri, queries, headers, body, cm).flatMap { response =>
Unmarshal(response.entity).to[A]
}
}
implicit def putBodyRequestImpl[Bd, A](implicit bodyConsumerTimeout: FiniteDuration,
encoder: ToEntityMarshaller[Bd],
decoder: FromEntityUnmarshaller[A],
ec: ExecutionContext,
mat: Materializer): PutWithBodyRequest[HttpExt, Future, Bd, A] = putBodyRequest(bodyConsumerTimeout)
def rawPostRequest(bodyConsumerTimeout: FiniteDuration)(implicit ec: ExecutionContext,
mat: Materializer) = new RawPostRequest[HttpExt, Future] {
type Resp = HttpResponse
def apply(uri: List[String], queries: Map[String, List[String]], headers: Map[String, String], cm: ClientManager[HttpExt]): Future[Resp] = {
val request = mkRequest(deriveUriString(cm, uri), queries, headers).copy(HttpMethods.POST)
execRequest(cm.client, request, bodyConsumerTimeout)
}
}
implicit def rawPostRequestImpl(implicit bodyConsumerTimeout: FiniteDuration,
ec: ExecutionContext,
mat: Materializer): RawPostRequest[HttpExt, Future] = rawPostRequest(bodyConsumerTimeout)
def postRequest[A](bodyConsumerTimeout: FiniteDuration)(implicit decoder: FromEntityUnmarshaller[A],
ec: ExecutionContext,
mat: Materializer) = new PostRequest[HttpExt, Future, A] {
private val raw = rawPostRequest(bodyConsumerTimeout)
def apply(uri: List[String], queries: Map[String, List[String]], headers: Map[String, String], cm: ClientManager[HttpExt]): Future[A] =
raw(uri, queries, headers, cm).flatMap { response =>
Unmarshal(response.entity).to[A]
}
}
implicit def postRequestImpl[A](implicit bodyConsumerTimeout: FiniteDuration,
decoder: FromEntityUnmarshaller[A],
ec: ExecutionContext,
mat: Materializer): PostRequest[HttpExt, Future, A] = postRequest(bodyConsumerTimeout)
def rawPostBodyRequest[Bd](bodyConsumerTimeout: FiniteDuration)(implicit encoder: ToEntityMarshaller[Bd],
ec: ExecutionContext,
mat: Materializer) = new RawPostWithBodyRequest[HttpExt, Future, Bd] {
type Resp = HttpResponse
def apply(uri: List[String], queries: Map[String, List[String]], headers: Map[String, String], body: Bd, cm: ClientManager[HttpExt]): Future[Resp] = {
Marshal(body).to[RequestEntity].flatMap { marshalledBody =>
val request = mkRequest(deriveUriString(cm, uri), queries, headers - "Content-Type").copy(HttpMethods.POST, entity = marshalledBody)
execRequest(cm.client, request, bodyConsumerTimeout)
}
}
}
implicit def rawPostBodyRequestImpl[Bd](implicit bodyConsumerTimeout: FiniteDuration,
encoder: ToEntityMarshaller[Bd],
ec: ExecutionContext,
mat: Materializer): RawPostWithBodyRequest[HttpExt, Future, Bd] = rawPostBodyRequest(bodyConsumerTimeout)
def postBodyRequest[Bd, A](bodyConsumerTimeout: FiniteDuration)(implicit encoder: ToEntityMarshaller[Bd],
decoder: FromEntityUnmarshaller[A],
ec: ExecutionContext,
mat: Materializer) = new PostWithBodyRequest[HttpExt, Future, Bd, A] {
private val raw = rawPostBodyRequest(bodyConsumerTimeout)
def apply(uri: List[String], queries: Map[String, List[String]], headers: Map[String, String], body: Bd, cm: ClientManager[HttpExt]): Future[A] =
raw(uri, queries, headers, body, cm).flatMap { response =>
Unmarshal(response.entity).to[A]
}
}
implicit def postBodyRequestImpl[Bd, A](implicit bodyConsumerTimeout: FiniteDuration,
encoder: ToEntityMarshaller[Bd],
decoder: FromEntityUnmarshaller[A],
ec: ExecutionContext,
mat: Materializer): PostWithBodyRequest[HttpExt, Future, Bd, A] = postBodyRequest(bodyConsumerTimeout)
def rawDeleteRequest(bodyConsumerTimeout: FiniteDuration)(implicit ec: ExecutionContext,
mat: Materializer) = new RawDeleteRequest[HttpExt, Future] {
type Resp = HttpResponse
def apply(uri: List[String], queries: Map[String, List[String]], headers: Map[String, String], cm: ClientManager[HttpExt]): Future[Resp] = {
val request = mkRequest(deriveUriString(cm, uri), queries, headers).copy(HttpMethods.DELETE)
execRequest(cm.client, request, bodyConsumerTimeout)
}
}
implicit def rawDeleteRequestImpl(implicit bodyConsumerTimeout: FiniteDuration,
ec: ExecutionContext,
mat: Materializer): RawDeleteRequest[HttpExt, Future] = rawDeleteRequest(bodyConsumerTimeout)
def deleteRequest[A](bodyConsumerTimeout: FiniteDuration)(implicit decoder: FromEntityUnmarshaller[A],
ec: ExecutionContext,
mat: Materializer) = new DeleteRequest[HttpExt, Future, A] {
private val raw = rawDeleteRequest(bodyConsumerTimeout)
def apply(uri: List[String], queries: Map[String, List[String]], headers: Map[String, String], cm: ClientManager[HttpExt]): Future[A] =
raw(uri, queries, headers, cm).flatMap { response =>
Unmarshal(response.entity).to[A]
}
}
implicit def deleteRequestImpl[A](implicit bodyConsumerTimeout: FiniteDuration,
decoder: FromEntityUnmarshaller[A],
ec: ExecutionContext,
mat: Materializer): DeleteRequest[HttpExt, Future, A] = deleteRequest(bodyConsumerTimeout)
}
================================================
FILE: akka-http-server/src/main/scala/typedapi/server/akkahttp/package.scala
================================================
package typedapi.server
import typedapi.shared.MethodType
import shapeless._
import shapeless.ops.hlist.Prepend
import akka.http.scaladsl.{HttpExt, Http}
import akka.http.scaladsl.model._
import akka.http.scaladsl.unmarshalling.{Unmarshal, FromEntityUnmarshaller}
import akka.http.scaladsl.marshalling.{Marshal, ToEntityMarshaller}
import akka.stream.Materializer
import akka.stream.scaladsl.Sink
import scala.collection.mutable.Builder
import scala.concurrent.{Future, ExecutionContext}
import scala.annotation.tailrec
package object akkahttp {
case class AkkaHttpHeaderParseException(msg: String) extends Exception(msg)
private def getHeaders(raw: Map[String, String]): List[HttpHeader] =
raw.map { case (key, value) => HttpHeader.parse(key, value) match {
case HttpHeader.ParsingResult.Ok(header, _) => header
case HttpHeader.ParsingResult.Error(cause) => throw AkkaHttpHeaderParseException(cause.formatPretty)
}}(collection.breakOut)
implicit def noReqBodyExecutor[El <: HList, KIn <: HList, VIn <: HList, M <: MethodType, FOut](implicit encoder: ToEntityMarshaller[FOut], ec: ExecutionContext) =
new NoReqBodyExecutor[El, KIn, VIn, M, Future, FOut] {
type R = HttpRequest
type Out = Future[HttpResponse]
def apply(req: R, eReq: EndpointRequest, endpoint: Endpoint[El, KIn, VIn, M, VIn, Future, FOut]): Either[ExtractionError, Out] = {
extract(eReq, endpoint).right.map { extracted =>
execute(extracted, endpoint).flatMap {
case Right((code, response)) =>
Marshal(response).to[ResponseEntity].map { marshalledBody =>
HttpResponse(status = StatusCode.int2StatusCode(code.statusCode), entity = marshalledBody, headers = getHeaders(endpoint.headers))
}
case Left(HttpError(code, msg)) =>
Future.successful(HttpResponse(status = StatusCode.int2StatusCode(code.statusCode), entity = msg, headers = getHeaders(endpoint.headers)))
}
}
}
}
implicit def withReqBodyExecutor[El <: HList, KIn <: HList, VIn <: HList, Bd, M <: MethodType, ROut <: HList, POut <: HList, FOut]
(implicit encoder: ToEntityMarshaller[FOut],
decoder: FromEntityUnmarshaller[Bd],
_prepend: Prepend.Aux[ROut, Bd :: HNil, POut],
_eqProof: POut =:= VIn,
mat: Materializer,
ec: ExecutionContext) = new ReqBodyExecutor[El, KIn, VIn, Bd, M, ROut, POut, Future, FOut] {
type R = HttpRequest
type Out = Future[HttpResponse]
implicit val prepend = _prepend
implicit val eqProof = _eqProof
def apply(req: R, eReq: EndpointRequest, endpoint: Endpoint[El, KIn, VIn, M, (BodyType[Bd], ROut), Future, FOut]): Either[ExtractionError, Out] = {
extract(eReq, endpoint).right.map { case (_, extracted) =>
for {
body <- Unmarshal(req.entity).to[Bd]
response <- execute(extracted, body, endpoint).flatMap {
case Right((code, response)) =>
Marshal(response).to[ResponseEntity].map { marshalledBody =>
HttpResponse(status = StatusCode.int2StatusCode(code.statusCode), entity = marshalledBody, headers = getHeaders(endpoint.headers))
}
case Left(HttpError(code, msg)) =>
Future.successful(HttpResponse(status = StatusCode.int2StatusCode(code.statusCode), entity = msg, headers = getHeaders(endpoint.headers)))
}
} yield response
}
}
}
implicit def mountEndpoints(implicit mat: Materializer) = new MountEndpoints[HttpExt, HttpRequest, Future[HttpResponse]] {
type Out = Future[Http.ServerBinding]
def apply(server: ServerManager[HttpExt], endpoints: List[Serve[HttpRequest, Future[HttpResponse]]]): Out = {
val service: HttpRequest => Future[HttpResponse] = request => {
def execute(eps: List[Serve[HttpRequest, Future[HttpResponse]]], eReq: EndpointRequest): Future[HttpResponse] = eps match {
case collection.immutable.::(endpoint, tail) => endpoint(request, eReq) match {
case Right(response) => response
case Left(RouteNotFound) => execute(tail, eReq)
case Left(BadRouteRequest(msg)) => Future.successful(HttpResponse(400, entity = msg))
}
case Nil => Future.successful(HttpResponse(404, entity = "uri = " + request.uri))
}
@tailrec
def toListPath(path: Uri.Path, agg: Builder[String, List[String]]): List[String] = path match {
case Uri.Path.Slash(tail) => toListPath(tail, agg)
case Uri.Path.Segment(p, tail) => toListPath(tail, agg += p)
case Uri.Path.Empty => agg.result()
}
val eReq = EndpointRequest(
request.method.name,
toListPath(request.uri.path, List.newBuilder),
request.uri.query().toMultiMap,
request.headers.map(header => header.lowercaseName -> header.value)(collection.breakOut)
)
if (request.method.name == "OPTIONS") {
Future.successful(HttpResponse(headers = getHeaders(optionsHeaders(endpoints, eReq))))
}
else
execute(endpoints, eReq)
}
server.server.bind(server.host, server.port).to(Sink.foreach { connection =>
connection.handleWithAsyncHandler(service)
}).run()
}
}
}
================================================
FILE: ammonite-client-support/src/main/scala/typedapi/client/package.scala
================================================
package typedapi.client
import typedapi.util._
import scalaj.http._
package object amm {
type Id[A] = scalajhttp.Id[A]
type Blocking[A] = scalajhttp.Blocking[A]
def clientManager(host: String, port: Int): ClientManager[Http.type] = ClientManager(Http, host, port)
def clientManager(host: String): ClientManager[Http.type] = ClientManager(Http, host)
implicit def rawGetRequestAmm = scalajhttp.rawGetRequest
implicit def getRequestAmm[A](implicit decoder: Decoder[Id, A]) = scalajhttp.getRequest[A]
implicit def rawPutRequestAmm = scalajhttp.rawPutRequest
implicit def putRequestAmm[A](implicit decoder: Decoder[Id, A]) = scalajhttp.putRequest[A]
implicit def rawPutBodyRequestAmm[Bd](implicit encoder: Encoder[Id, Bd]) = scalajhttp.rawPutBodyRequest[Bd]
implicit def putBodyRequestAmm[Bd, A](implicit encoder: Encoder[Id, Bd], decoder: Decoder[Id, A]) = scalajhttp.putBodyRequest[Bd, A]
implicit def rawPostRequestAmm = scalajhttp.rawPostRequest
implicit def postRequest[A](implicit decoder: Decoder[Id, A]) = scalajhttp.postRequest[A]
implicit def rawPostBodyRequestAm[Bd](implicit encoder: Encoder[Id, Bd]) = scalajhttp.rawPostBodyRequest[Bd]
implicit def postBodyRequestAmm[Bd, A](implicit encoder: Encoder[Id, Bd], decoder: Decoder[Id, A]) = scalajhttp.postBodyRequest[Bd, A]
implicit def rawDeleteRequestAmm = scalajhttp.rawDeleteRequest
implicit def deleteRequestAmm[A](implicit decoder: Decoder[Id, A]) = scalajhttp.deleteRequest[A]
}
================================================
FILE: build.sbt
================================================
import sbt.Keys._
val `compiler-2.12` = Seq(
"-deprecation",
"-encoding", "utf-8",
"-explaintypes",
"-feature",
"-unchecked",
"-Xfatal-warnings",
"-Xfuture",
"-Xlint:inaccessible",
"-Xlint:infer-any",
"-Xlint:missing-interpolator",
"-Xlint:option-implicit",
"-Xlint:type-parameter-shadow",
"-Xlint:unsound-match",
"-Ywarn-dead-code",
"-Ywarn-inaccessible",
"-Ywarn-infer-any",
"-Ywarn-numeric-widen",
// "-Ywarn-unused:implicits", -> get errors for implicit evidence
"-Ywarn-unused:imports",
// "-Ywarn-unused:locals",
"-Ywarn-unused:privates"
)
val `compiler-2.11` = Seq(
"-deprecation",
"-encoding", "utf-8",
"-explaintypes",
"-feature",
"-unchecked",
"-Xfatal-warnings",
"-Xfuture",
"-Xlint:inaccessible",
"-Xlint:infer-any",
"-Xlint:missing-interpolator",
"-Xlint:option-implicit",
"-Xlint:type-parameter-shadow",
"-Xlint:unsound-match",
"-Ywarn-dead-code",
"-Ywarn-numeric-widen",
"-Ywarn-unused",
"-Ywarn-inaccessible",
"-Ywarn-infer-any"
)
lazy val commonSettings = Seq(
organization := "com.github.pheymann",
version := "0.2.0",
crossScalaVersions := Seq("2.11.11", "2.12.4"),
scalaVersion := "2.12.4",
scalacOptions ++= { CrossVersion.partialVersion(scalaVersion.value) match {
case Some((2, 12)) => `compiler-2.12`
case Some((2, 11)) => `compiler-2.11`
case _ => Seq.empty[String]
}},
publishTo := sonatypePublishTo.value
)
lazy val mavenSettings = Seq(
sonatypeProfileName := "pheymann",
publishMavenStyle := true,
pomExtra in Global := {
<url>https://github.com/pheymann/typedapi</url>
<licenses>
<license>
<name>MIT</name>
<url>https://github.com/pheymann/typedapi/blob/master/LICENSE</url>
</license>
</licenses>
<scm>
<connection>scm:git:github.com/pheymann/typedapi</connection>
<developerConnection>scm:git:git@github.com:pheymann/typedapi</developerConnection>
<url>github.com/pheymann/typedapi</url>
</scm>
<developers>
<developer>
<id>pheymann</id>
<name>Paul Heymann</name>
<url>https://github.com/pheymann</url>
</developer>
</developers>
}
)
lazy val typedapi = project
.in(file("."))
.settings(commonSettings: _*)
.aggregate(
`shared-js`,
`shared-jvm`,
`client-js`,
`client-jvm`,
server,
`http4s-client`,
`http4s-server`,
`akka-http-client`,
`akka-http-server`,
`js-client`,
`scalaj-http-client`,
`http-support-tests`,
`ammonite-client-support`
)
lazy val shared = crossProject.crossType(CrossType.Pure)
.in(file("shared"))
.settings(
commonSettings,
mavenSettings,
name := "typedapi-shared",
libraryDependencies ++= Dependencies.shared
)
lazy val `shared-js` = shared.js
lazy val `shared-jvm` = shared.jvm
lazy val client = crossProject.crossType(CrossType.Pure)
.in(file("client"))
.settings(
commonSettings,
mavenSettings,
name := "typedapi-client",
libraryDependencies ++= Dependencies.client
)
.dependsOn(shared)
lazy val `client-js` = client.js
lazy val `client-jvm` = client.jvm
lazy val server = project
.in(file("server"))
.settings(
commonSettings,
mavenSettings,
name := "typedapi-server",
libraryDependencies ++= Dependencies.server
)
.dependsOn(`shared-jvm`)
lazy val `http4s-client` = project
.in(file("http4s-client"))
.settings(
commonSettings,
mavenSettings,
name := "typedapi-http4s-client",
libraryDependencies ++= Dependencies.http4sClient,
)
.dependsOn(`client-jvm`)
lazy val `http4s-server` = project
.in(file("http4s-server"))
.settings(
commonSettings,
mavenSettings,
name := "typedapi-http4s-server",
libraryDependencies ++= Dependencies.http4sServer,
)
.dependsOn(server)
lazy val `akka-http-client` = project
.in(file("akka-http-client"))
.settings(
commonSettings,
mavenSettings,
name := "typedapi-akka-http-client",
libraryDependencies ++= Dependencies.akkaHttpClient
)
.dependsOn(`client-jvm`)
lazy val `akka-http-server` = project
.in(file("akka-http-server"))
.settings(
commonSettings,
mavenSettings,
name := "typedapi-akka-http-server",
libraryDependencies ++= Dependencies.akkaHttpServer
)
.dependsOn(server)
lazy val `js-client` = project
.in(file("js-client"))
.enablePlugins(ScalaJSPlugin)
.settings(
commonSettings,
mavenSettings,
name := "typedapi-js-client",
libraryDependencies ++= Seq(
"org.scala-js" %%% "scalajs-dom" % "0.9.6" % Compile
)
)
.dependsOn(`client-js`)
lazy val `scalaj-http-client` = project
.in(file("scalaj-http-client"))
.settings(
commonSettings,
mavenSettings,
name := "typedapi-scalaj-http-client",
libraryDependencies ++= Dependencies.scalajHttpClient
)
.dependsOn(`client-jvm`)
lazy val `http-support-tests` = project
.in(file("http-support-tests"))
.settings(
commonSettings,
parallelExecution in Test := false,
libraryDependencies ++= Dependencies.httpSupportTests
)
.dependsOn(`http4s-client`, `http4s-server`, `akka-http-client`, `akka-http-server`, `scalaj-http-client`)
lazy val `ammonite-client-support` = project
.in(file("ammonite-client-support"))
.settings(
commonSettings,
mavenSettings,
name := "typedapi-ammonite-client",
libraryDependencies ++= Dependencies.ammoniteSupport
)
.dependsOn(`scalaj-http-client`)
================================================
FILE: client/src/main/scala/typedapi/client/ApiRequest.scala
================================================
package typedapi.client
import RequestDataBuilder.{Data, DataWithBody}
import typedapi.shared._
import shapeless._
import scala.language.higherKinds
import scala.annotation.implicitNotFound
/** Basic api request element. Provides a function to create an effect representing the actual request. */
@implicitNotFound("""Cannot find RawApiRequest instance for ${M}.
context: ${F}""")
trait RawApiRequest[M <: MethodType, D <: HList, C, F[_]] {
type Resp
def apply(data: D, cm: ClientManager[C]): F[Resp]
}
@implicitNotFound("""Cannot find ApiRequest instance for ${M}. Do you miss some implicit value e.g. encoders/decoders?
ouput: ${Out}
context: ${F}""")
trait ApiRequest[M <: MethodType, D <: HList, C, F[_], Out] {
def apply(data: D, cm: ClientManager[C]): F[Out]
}
trait RawApiWithoutBodyRequest[M <: MethodType, C, F[_]] extends RawApiRequest[M, Data, C, F] {
def apply(data: Data, cm: ClientManager[C]): F[Resp] = {
val (uri :: queries :: headers :: HNil): Data = data
apply(uri, queries, headers, cm)
}
def apply(uri: List[String], queries: Map[String, List[String]], headers: Map[String, String], cm: ClientManager[C]): F[Resp]
}
trait ApiWithoutBodyRequest[M <: MethodType, C, F[_], Out] extends ApiRequest[M, Data, C, F, Out] {
def apply(data: Data, cm: ClientManager[C]): F[Out] = {
val (uri :: queries :: headers :: HNil): Data = data
apply(uri, queries, headers, cm)
}
def apply(uri: List[String], queries: Map[String, List[String]], headers: Map[String, String], cm: ClientManager[C]): F[Out]
}
trait RawApiWithBodyRequest[M <: MethodType, C, F[_], Bd] extends RawApiRequest[M, DataWithBody[Bd], C, F] {
def apply(data: DataWithBody[Bd], cm: ClientManager[C]): F[Resp] = {
val (uri :: queries :: headers :: body :: HNil): DataWithBody[Bd] = data
apply(uri, queries, headers, body, cm)
}
def apply(uri: List[String], queries: Map[String, List[String]], headers: Map[String, String], body: Bd, cm: ClientManager[C]): F[Resp]
}
trait ApiWithBodyRequest[M <: MethodType, C, F[_], Bd, Out] extends ApiRequest[M, DataWithBody[Bd], C, F, Out] {
def apply(data: DataWithBody[Bd], cm: ClientManager[C]): F[Out] = {
val (uri :: queries :: headers :: body :: HNil): DataWithBody[Bd] = data
apply(uri, queries, headers, body, cm)
}
def apply(uri: List[String], queries: Map[String, List[String]], headers: Map[String, String], body: Bd, cm: ClientManager[C]): F[Out]
}
trait RawGetRequest[C, F[_]] extends RawApiWithoutBodyRequest[GetCall, C, F]
trait GetRequest[C, F[_], A] extends ApiWithoutBodyRequest[GetCall, C, F, A]
trait RawPutRequest[C, F[_]] extends RawApiWithoutBodyRequest[PutCall, C, F]
trait PutRequest[C, F[_], A] extends ApiWithoutBodyRequest[PutCall, C, F, A]
trait RawPutWithBodyRequest[C, F[_], Bd] extends RawApiWithBodyRequest[PutWithBodyCall, C, F, Bd]
trait PutWithBodyRequest[C, F[_], Bd, A] extends ApiWithBodyRequest[PutWithBodyCall, C, F, Bd, A]
trait RawPostRequest[C, F[_]] extends RawApiWithoutBodyRequest[PostCall, C, F]
trait PostRequest[C, F[_], A] extends ApiWithoutBodyRequest[PostCall, C, F, A]
trait RawPostWithBodyRequest[C, F[_], Bd] extends RawApiWithBodyRequest[PostWithBodyCall, C, F, Bd]
trait PostWithBodyRequest[C, F[_], Bd, A] extends ApiWithBodyRequest[PostWithBodyCall, C, F, Bd, A]
trait RawDeleteRequest[C, F[_]] extends RawApiWithoutBodyRequest[DeleteCall, C, F]
trait DeleteRequest[C, F[_], A] extends ApiWithoutBodyRequest[DeleteCall, C, F, A]
================================================
FILE: client/src/main/scala/typedapi/client/ClientManager.scala
================================================
package typedapi.client
/** Provides a supported client instance and some basic configuration. */
final case class ClientManager[C](client: C, host: String, portO: Option[Int]) {
val base = portO match {
case Some(p) => s"$host:$p"
case None => host
}
}
object ClientManager {
def apply[C](client: C, host: String, port: Int): ClientManager[C] = ClientManager(client, host, Some(port))
def apply[C](client: C, host: String): ClientManager[C] = ClientManager(client, host, None)
}
================================================
FILE: client/src/main/scala/typedapi/client/ExecutableDerivation.scala
================================================
package typedapi.client
import typedapi.shared.{MethodType, MediaType}
import shapeless._
import shapeless.labelled.FieldType
import scala.language.higherKinds
/** Helper class to match the [[RequestDataBuilder]] with an [[ApiRequest]] instance. */
final class ExecutableDerivation[El <: HList, KIn <: HList, VIn <: HList, M <: MethodType, MT <: MediaType, O, D <: HList]
(builder: RequestDataBuilder.Aux[El, KIn, VIn, M, FieldType[MT, O], D], input: VIn) {
final class Derivation[F[_]] {
def apply[C](cm: ClientManager[C])(implicit req: ApiRequest[M, D, C, F, O]): F[O] = {
val data = builder(input, List.newBuilder, Map.empty, Map.empty)
req(data, cm)
}
def raw[C](cm: ClientManager[C])(implicit req: RawApiRequest[M, D, C, F]): F[req.Resp] = {
val data = builder(input, List.newBuilder, Map.empty, Map.empty)
req(data, cm)
}
}
def run[F[_]]: Derivation[F] = new Derivation[F]
}
================================================
FILE: client/src/main/scala/typedapi/client/ExecutablesFromHList.scala
================================================
package typedapi.client
import typedapi.shared.{MethodType, MediaType}
import shapeless._
import shapeless.labelled.FieldType
import shapeless.ops.function.FnFromProduct
/** Derives executables from list of RequestBuilders. */
sealed trait ExecutablesFromHList[H <: HList] {
type Out <: HList
def apply(h: H): Out
}
object ExecutablesFromHList extends ExecutablesFromHListLowPrio {
type Aux[H <: HList, Out0 <: HList] = ExecutablesFromHList[H] { type Out = Out0 }
}
trait ExecutablesFromHListLowPrio {
implicit val noExecutable = new ExecutablesFromHList[HNil] {
type Out = HNil
def apply(h: HNil): Out = HNil
}
implicit def deriveExcutable[El <: HList, KIn <: HList, VIn <: HList, M <: MethodType, MT <: MediaType, O, D <: HList, T <: HList]
(implicit next: ExecutablesFromHList[T],
vinToFn: FnFromProduct[VIn => ExecutableDerivation[El, KIn, VIn, M, MT, O, D]]) =
new ExecutablesFromHList[RequestDataBuilder.Aux[El, KIn, VIn, M, FieldType[MT, O], D] :: T] {
type Out = vinToFn.Out :: next.Out
def apply(comps: RequestDataBuilder.Aux[El, KIn, VIn, M, FieldType[MT, O], D] :: T): Out = {
val fn = vinToFn.apply(input => new ExecutableDerivation[El, KIn, VIn, M, MT, O, D](comps.head, input))
fn :: next(comps.tail)
}
}
}
================================================
FILE: client/src/main/scala/typedapi/client/FilterServerElements.scala
================================================
package typedapi.client
import typedapi.shared._
import shapeless._
//TODO replace with Typelevelfoldleft
sealed trait FilterServerElements[H <: HList] {
type Out <: HList
}
sealed trait FilterServerElementsLowPrio {
implicit val filterServerResult = new FilterServerElements[HNil] {
type Out = HNil
}
implicit def filterServerKeep[El, T <: HList](implicit next: FilterServerElements[T]) = new FilterServerElements[El :: T] {
type Out = El :: next.Out
}
}
object FilterServerElements extends FilterServerElementsLowPrio {
type Aux[H <: HList, Out0 <: HList] = FilterServerElements[H] { type Out = Out0 }
implicit def filterServerHeaderMatch[K, V, T <: HList](implicit next: FilterServerElements[T]) = new FilterServerElements[ServerHeaderMatchParam[K, V] :: T] {
type Out = next.Out
}
implicit def filterServerHeaderSend[K, V, T <: HList](implicit next: FilterServerElements[T]) = new FilterServerElements[ServerHeaderSendElement[K, V] :: T] {
type Out = next.Out
}
}
sealed trait FilterServerElementsList[H <: HList] {
type Out <: HList
}
object FilterServerElementsList {
type Aux[H <: HList, Out0 <: HList] = FilterServerElementsList[H] { type Out = Out0 }
implicit val filterServerListResult = new FilterServerElementsList[HNil] {
type Out = HNil
}
implicit def filterServerListStep[Api <: HList, T <: HList](implicit filtered: FilterServerElements[Api], next: FilterServerElementsList[T]) =
new FilterServerElementsList[Api :: T] {
type Out = filtered.Out :: next.Out
}
}
================================================
FILE: client/src/main/scala/typedapi/client/RequestDataBuilder.scala
================================================
package typedapi.client
import typedapi.shared._
import shapeless._
import shapeless.labelled.FieldType
import scala.collection.mutable.Builder
import scala.annotation.implicitNotFound
/** Compiles type level api description into a function returning request data (uri, query, header, body) and return-type `A` which are used for a request. */
@implicitNotFound("""Woops, you shouldn't be here. We cannot find the RequestDataBuilder. This seems to be a bug.
elements: ${El}
input keys: ${KIn}
input values: ${VIn}
method: ${M}
expected result: ${O}""")
trait RequestDataBuilder[El <: HList, KIn <: HList, VIn <: HList, M <: MethodType, O] {
type Out <: HList
def apply(inputs: VIn,
uri: Builder[String, List[String]],
queries: Map[String, List[String]],
headers: Map[String, String]): Out
}
object RequestDataBuilder extends RequestDataBuilderMediumPrio {
type Aux[El <: HList, KIn <: HList, VIn <: HList, M <: MethodType, O, Out0 <: HList] = RequestDataBuilder[El, KIn, VIn, M, O] { type Out = Out0 }
}
trait RequestDataBuilderLowPrio {
implicit def pathCompiler[S, T <: HList, KIn <: HList, VIn <: HList, M <: MethodType, O](implicit wit: Witness.Aux[S], compiler: RequestDataBuilder[T, KIn, VIn, M, O]) =
new RequestDataBuilder[S :: T, KIn, VIn, M, O] {
type Out = compiler.Out
def apply(inputs: VIn, uri: Builder[String, List[String]], queries: Map[String, List[String]], headers: Map[String, String]): Out = {
compiler(inputs, uri += wit.value.toString(), queries, headers)
}
}
implicit def segmentInputCompiler[T <: HList, K, V, KIn <: HList, VIn <: HList, M <: MethodType, O](implicit compiler: RequestDataBuilder[T, KIn, VIn, M, O]) =
new RequestDataBuilder[SegmentInput :: T, K :: KIn, V :: VIn, M, O] {
type Out = compiler.Out
def apply(inputs: V :: VIn, uri: Builder[String, List[String]], queries: Map[String, List[String]], headers: Map[String, String]): Out = {
val segValue = inputs.head
compiler(inputs.tail, uri += segValue.toString(), queries, headers)
}
}
implicit def queryInputCompiler[T <: HList, K, V, KIn <: HList, VIn <: HList, M <: MethodType, O]
(implicit wit: Witness.Aux[K], show: WitnessToString[K], compiler: RequestDataBuilder[T, KIn, VIn, M, O]) =
new RequestDataBuilder[QueryInput :: T, K :: KIn, V :: VIn, M, O] {
type Out = compiler.Out
def apply(inputs: V :: VIn, uri: Builder[String, List[String]], queries: Map[String, List[String]], headers: Map[String, String]): Out = {
val queryName = show.show(wit.value)
val queryValue = inputs.head
compiler(inputs.tail, uri, Map((queryName, List(queryValue.toString()))) ++ queries, headers)
}
}
implicit def headerInputCompiler[T <: HList, K, V, KIn <: HList, VIn <: HList, M <: MethodType, O]
(implicit wit: Witness.Aux[K], show: WitnessToString[K], compiler: RequestDataBuilder[T, KIn, VIn, M, O]) =
new RequestDataBuilder[HeaderInput :: T, K :: KIn, V :: VIn, M, O] {
type Out = compiler.Out
def apply(inputs: V :: VIn, uri: Builder[String, List[String]], queries: Map[String, List[String]], headers: Map[String, String]): Out = {
val headerName = show.show(wit.value)
val headerValue = inputs.head
compiler(inputs.tail, uri, queries, Map((headerName, headerValue.toString())) ++ headers)
}
}
implicit def fixedHeaderCompiler[K, V, T <: HList, KIn <: HList, VIn <: HList, M <: MethodType, O]
(implicit kWit: Witness.Aux[K], vWit: Witness.Aux[V], kShow: WitnessToString[K], vShow: WitnessToString[V], compiler: RequestDataBuilder[T, KIn, VIn, M, O]) =
new RequestDataBuilder[FixedHeader[K, V] :: T, KIn, VIn, M, O] {
type Out = compiler.Out
def apply(inputs: VIn, uri: Builder[String, List[String]], queries: Map[String, List[String]], headers: Map[String, String]): Out = {
val key = kShow.show(kWit.value)
val value = vShow.show(vWit.value)
compiler(inputs, uri, queries, Map((key, value)) ++ headers)
}
}
implicit def clientHeaderCompiler[K, V, T <: HList, KIn <: HList, VIn <: HList, M <: MethodType, O]
(implicit kWit: Witness.Aux[K], vWit: Witness.Aux[V], kShow: WitnessToString[K], vShow: WitnessToString[V], compiler: RequestDataBuilder[T, KIn, VIn, M, O]) =
new RequestDataBuilder[ClientHeader[K, V] :: T, KIn, VIn, M, O] {
type Out = compiler.Out
def apply(inputs: VIn, uri: Builder[String, List[String]], queries: Map[String, List[String]], headers: Map[String, String]): Out = {
val key = kShow.show(kWit.value)
val value = vShow.show(vWit.value)
compiler(inputs, uri, queries, Map((key, value)) ++ headers)
}
}
implicit def clientHeaderInputCompiler[K, V, T <: HList, KIn <: HList, VIn <: HList, M <: MethodType, O]
(implicit kWit: Witness.Aux[K], kShow: WitnessToString[K], compiler: RequestDataBuilder[T, KIn, VIn, M, O]) =
new RequestDataBuilder[ClientHeaderInput :: T, K :: KIn, V :: VIn, M, O] {
type Out = compiler.Out
def apply(inputs: V :: VIn, uri: Builder[String, List[String]], queries: Map[String, List[String]], headers: Map[String, String]): Out = {
val key = kShow.show(kWit.value)
val value = inputs.head.toString
compiler(inputs.tail, uri, queries, Map((key, value)) ++ headers)
}
}
implicit def clientHeaderCollInputCompiler[V, T <: HList, KIn <: HList, VIn <: HList, M <: MethodType, O]
(implicit compiler: RequestDataBuilder[T, KIn, VIn, M, O]) =
new RequestDataBuilder[ClientHeaderCollInput :: T, KIn, Map[String, V] :: VIn, M, O] {
type Out = compiler.Out
def apply(inputs: Map[String, V] :: VIn, uri: Builder[String, List[String]], queries: Map[String, List[String]], headers: Map[String, String]): Out = {
val coll = inputs.head.mapValues(_.toString)
compiler(inputs.tail, uri, queries, coll ++ headers)
}
}
type Data = List[String] :: Map[String, List[String]] :: Map[String, String] :: HNil
type DataWithBody[Bd] = List[String] :: Map[String, List[String]] :: Map[String, String] :: Bd :: HNil
private def accept[MT <: MediaType](headers: Map[String, String], media: MT): Map[String, String] =
Map(("Accept", media.value)) ++ headers
implicit def getCompiler[MT <: MediaType, A](implicit media: Witness.Aux[MT]) = new RequestDataBuilder[HNil, HNil, HNil, GetCall, FieldType[MT, A]] {
type Out = Data
def apply(inputs: HNil, uri: Builder[String, List[String]], queries: Map[String, List[String]], headers: Map[String, String]): Out = {
val out = uri.result() :: queries :: accept(headers, media.value) :: HNil
out
}
}
implicit def putCompiler[MT <: MediaType, A](implicit media: Witness.Aux[MT]) = new RequestDataBuilder[HNil, HNil, HNil, PutCall, FieldType[MT, A]] {
type Out = Data
def apply(inputs: HNil, uri: Builder[String, List[String]], queries: Map[String, List[String]], headers: Map[String, String]): Out = {
val out = uri.result() :: queries :: accept(headers, media.value) :: HNil
out
}
}
implicit def putWithBodyCompiler[BMT <: MediaType, Bd, MT <: MediaType, A](implicit media: Witness.Aux[MT]) =
new RequestDataBuilder[HNil, FieldType[BMT, BodyField.T] :: HNil, Bd :: HNil, PutWithBodyCall, FieldType[MT, A]] {
type Out = DataWithBody[Bd]
def apply(inputs: Bd :: HNil, uri: Builder[String, List[String]], queries: Map[String, List[String]], headers: Map[String, String]): Out = {
val out = uri.result() :: queries :: accept(headers, media.value) :: inputs.head :: HNil
out
}
}
implicit def postCompiler[MT <: MediaType, A](implicit media: Witness.Aux[MT]) = new RequestDataBuilder[HNil, HNil, HNil, PostCall, FieldType[MT, A]] {
type Out = Data
def apply(inputs: HNil, uri: Builder[String, List[String]], queries: Map[String, List[String]], headers: Map[String, String]): Out = {
val out = uri.result() :: queries :: accept(headers, media.value) :: HNil
out
}
}
implicit def postWithBodyCompiler[BMT <: MediaType, Bd, MT <: MediaType, A](implicit media: Witness.Aux[MT]) =
new RequestDataBuilder[HNil, FieldType[BMT, BodyField.T] :: HNil, Bd :: HNil, PostWithBodyCall, FieldType[MT, A]] {
type Out = DataWithBody[Bd]
def apply(inputs: Bd :: HNil, uri: Builder[String, List[String]], queries: Map[String, List[String]], headers: Map[String, String]): Out = {
val out = uri.result() :: queries :: accept(headers, media.value) :: inputs.head :: HNil
out
}
}
implicit def deleteCompiler[MT <: MediaType, A](implicit media: Witness.Aux[MT]) = new RequestDataBuilder[HNil, HNil, HNil, DeleteCall, FieldType[MT, A]] {
type Out = Data
def apply(inputs: HNil, uri: Builder[String, List[String]], queries: Map[String, List[String]], headers: Map[String, String]): Out = {
val out = uri.result() :: queries :: accept(headers, media.value) :: HNil
out
}
}
}
trait RequestDataBuilderMediumPrio extends RequestDataBuilderLowPrio {
implicit def queryOptInputCompiler[T <: HList, K, V, KIn <: HList, VIn <: HList, M <: MethodType, O]
(implicit wit: Witness.Aux[K], show: WitnessToString[K], compiler: RequestDataBuilder[T, KIn, VIn, M, O]) =
new RequestDataBuilder[QueryInput :: T, K :: KIn, Option[V] :: VIn, M, O] {
type Out = compiler.Out
def apply(inputs: Option[V] :: VIn, uri: Builder[String, List[String]], queries: Map[String, List[String]], headers: Map[String, String]): Out = {
val queryName = show.show(wit.value)
val queryValue = inputs.head
val updatedQueries = queryValue.fold(queries)(q => Map(queryName -> List(q.toString())) ++ queries)
compiler(inputs.tail, uri, updatedQueries, headers)
}
}
implicit def queryListInputCompiler[T <: HList, K, V, KIn <: HList, VIn <: HList, M <: MethodType, O]
(implicit wit: Witness.Aux[K], show: WitnessToString[K], compiler: RequestDataBuilder[T, KIn, VIn, M, O]) =
new RequestDataBuilder[QueryInput :: T, K :: KIn, List[V] :: VIn, M, O] {
type Out = compiler.Out
def apply(inputs: List[V] :: VIn, uri: Builder[String, List[String]], queries: Map[String, List[String]], headers: Map[String, String]): Out = {
val queryName = show.show(wit.value)
val queryValue = inputs.head
if (queryValue.isEmpty)
compiler(inputs.tail, uri, queries, headers)
else
compiler(inputs.tail, uri, Map((queryName, queryValue.map(_.toString()))) ++ queries, headers)
}
}
implicit def headersOptInputCompiler[T <: HList, K, V, KIn <: HList, VIn <: HList, M <: MethodType, O]
(implicit wit: Witness.Aux[K], show: WitnessToString[K], compiler: RequestDataBuilder[T, KIn, VIn, M, O]) =
new RequestDataBuilder[HeaderInput :: T, K :: KIn, Option[V] :: VIn, M, O] {
type Out = compiler.Out
def apply(inputs: Option[V] :: VIn, uri: Builder[String, List[String]], queries: Map[String, List[String]], headers: Map[String, String]): Out = {
val headerName = show.show(wit.value)
val headerValue = inputs.head
val updatedHeaders = headerValue.fold(headers)(h => Map(headerName -> h.toString()) ++ headers)
compiler(inputs.tail, uri, queries, updatedHeaders)
}
}
}
@implicitNotFound("""Woops, you shouldn't be here. We cannot find the RequestDataBuilderList. This seems to be a bug.
list: ${H}""")
trait RequestDataBuilderList[H <: HList] {
type Out <: HList
def builders: Out
}
object RequestDataBuilderList extends RequestDataBuilderListLowPrio {
type Aux[H <: HList, Out0 <: HList] = RequestDataBuilderList[H] { type Out = Out0 }
}
trait RequestDataBuilderListLowPrio {
implicit def lastCompilerList[El <: HList, KIn <: HList, VIn <: HList, M <: MethodType, O, D <: HList](implicit builder: RequestDataBuilder.Aux[El, KIn, VIn, M, O, D]) =
new RequestDataBuilderList[(El, KIn, VIn, M, O) :: HNil] {
type Out = RequestDataBuilder.Aux[El, KIn, VIn, M, O, D] :: HNil
val builders = builder :: HNil
}
implicit def builderList[El <: HList, KIn <: HList, VIn <: HList, M <: MethodType, O, D <: HList, T <: HList](implicit builder: RequestDataBuilder.Aux[El, KIn, VIn, M, O, D], next: RequestDataBuilderList[T]) =
new RequestDataBuilderList[(El, KIn, VIn, M, O) :: T] {
type Out = RequestDataBuilder.Aux[El, KIn, VIn, M, O, D] :: next.Out
val builders = builder :: next.builders
}
}
================================================
FILE: client/src/main/scala/typedapi/client/package.scala
================================================
package typedapi
import typedapi.shared._
import shapeless._
import shapeless.labelled.FieldType
import shapeless.ops.hlist.Tupler
import shapeless.ops.function.FnFromProduct
package object client extends TypeLevelFoldLeftLowPrio
with TypeLevelFoldLeftListLowPrio
with WitnessToStringLowPrio
with ApiTransformer {
def deriveUriString(cm: ClientManager[_], uri: List[String]): String = cm.base + "/" + uri.mkString("/")
def derive[H <: HList, FH <: HList, El <: HList, KIn <: HList, VIn <: HList, M <: MethodType, MT <: MediaType, Out, D <: HList]
(apiList: ApiTypeCarrier[H])
(implicit filter: FilterServerElements.Aux[H, FH],
folder: Lazy[TypeLevelFoldLeft.Aux[FH, Unit, (El, KIn, VIn, M, FieldType[MT, Out])]],
builder: RequestDataBuilder.Aux[El, KIn, VIn, M, FieldType[MT, Out], D],
inToFn: FnFromProduct[VIn => ExecutableDerivation[El, KIn, VIn, M, MT, Out, D]]): inToFn.Out =
inToFn.apply(input => new ExecutableDerivation[El, KIn, VIn, M, MT, Out, D](builder, input))
def deriveAll[H <: HList, FH <: HList, In <: HList, Fold <: HList, B <: HList, Ex <: HList]
(apiLists: CompositionCons[H])
(implicit filter: FilterServerElementsList.Aux[H, FH],
folders: TypeLevelFoldLeftList.Aux[FH, Fold],
builderList: RequestDataBuilderList.Aux[Fold, B],
executables: ExecutablesFromHList.Aux[B, Ex],
tupler: Tupler[Ex]): tupler.Out =
executables(builderList.builders).tupled
}
================================================
FILE: client/src/main/scala/typedapi/client/test/RequestInput.scala
================================================
package typedapi.client.test
final case class ReqInput(method: String,
uri: List[String],
queries: Map[String, List[String]],
headers: Map[String, String])
final case class ReqInputWithBody[Bd](method: String,
uri: List[String],
queries: Map[String, List[String]],
headers: Map[String, String],
body: Bd)
================================================
FILE: client/src/main/scala/typedapi/client/test/package.scala
================================================
package typedapi.client
import scala.language.higherKinds
package object test {
type TestClientM = ClientManager[Unit]
val clientManager: TestClientM = ClientManager((), "", 0)
def testRawGet[F[_]](pure: ReqInput => F[ReqInput]) = new RawGetRequest[Unit, F] {
type Resp = ReqInput
def apply(uri: List[String], queries: Map[String, List[String]], headers: Map[String, String], cm: TestClientM): F[Resp] =
pure(ReqInput("GET", uri, queries, headers))
}
def testGet[F[_], A](f: ReqInput => F[A]) = new GetRequest[Unit, F, A] {
def apply(uri: List[String], queries: Map[String, List[String]], headers: Map[String, String], cm: TestClientM): F[A] =
f(ReqInput("GET", uri, queries, headers))
}
def testRawPut[F[_]](pure: ReqInput => F[ReqInput]) = new RawPutRequest[Unit, F] {
type Resp = ReqInput
def apply(uri: List[String], queries: Map[String, List[String]], headers: Map[String, String], cm: TestClientM): F[Resp] =
pure(ReqInput("PUT", uri, queries, headers))
}
def testPut[F[_], A](f: ReqInput => F[A]) = new PutRequest[Unit, F, A] {
def apply(uri: List[String], queries: Map[String, List[String]], headers: Map[String, String], cm: TestClientM): F[A] =
f(ReqInput("PUT", uri, queries, headers))
}
def testRawPutWithBody[F[_], Bd](pure: ReqInputWithBody[Bd] => F[ReqInputWithBody[Bd]]) =
new RawPutWithBodyRequest[Unit, F, Bd] {
type Resp = ReqInputWithBody[Bd]
def apply(uri: List[String], queries: Map[String, List[String]], headers: Map[String, String], body: Bd, cm: TestClientM): F[Resp] =
pure(ReqInputWithBody("PUT", uri, queries, headers, body))
}
def testPutWithBody[F[_], Bd, A](f: ReqInputWithBody[Bd] => F[A]) =
new PutWithBodyRequest[Unit, F, Bd, A] {
def apply(uri: List[String], queries: Map[String, List[String]], headers: Map[String, String], body: Bd, cm: TestClientM): F[A] =
f(ReqInputWithBody("PUT", uri, queries, headers, body))
}
def testRawPost[F[_]](pure: ReqInput => F[ReqInput]) = new RawPostRequest[Unit, F] {
type Resp = ReqInput
def apply(uri: List[String], queries: Map[String, List[String]], headers: Map[String, String], cm: TestClientM): F[Resp] =
pure(ReqInput("POST", uri, queries, headers))
}
def testPost[F[_], A](f: ReqInput => F[A]) = new PostRequest[Unit, F, A] {
def apply(uri: List[String], queries: Map[String, List[String]], headers: Map[String, String], cm: TestClientM): F[A] =
f(ReqInput("POST", uri, queries, headers))
}
def testRawPostWithBody[F[_], Bd](pure: ReqInputWithBody[Bd] => F[ReqInputWithBody[Bd]]) =
new RawPostWithBodyRequest[Unit, F, Bd] {
type Resp = ReqInputWithBody[Bd]
def apply(uri: List[String], queries: Map[String, List[String]], headers: Map[String, String], body: Bd, cm: TestClientM): F[Resp] =
pure(ReqInputWithBody("POST", uri, queries, headers, body))
}
def testPostWithBody[F[_], Bd, A](f: ReqInputWithBody[Bd] => F[A]) =
new PostWithBodyRequest[Unit, F, Bd, A] {
def apply(uri: List[String], queries: Map[String, List[String]], headers: Map[String, String], body: Bd, cm: TestClientM): F[A] =
f(ReqInputWithBody("POST", uri, queries, headers, body))
}
def testRawDelete[F[_]](pure: ReqInput => F[ReqInput]) = new RawDeleteRequest[Unit, F] {
type Resp = ReqInput
def apply(uri: List[String], queries: Map[String, List[String]], headers: Map[String, String], cm: TestClientM): F[Resp] =
pure(ReqInput("DELETE", uri, queries, headers))
}
def testDelete[F[_], A](f: ReqInput => F[A]) = new DeleteRequest[Unit, F, A] {
def apply(uri: List[String], queries: Map[String, List[String]], headers: Map[String, String], cm: TestClientM): F[A] =
f(ReqInput("DELETE", uri, queries, headers))
}
}
================================================
FILE: client/src/test/scala/typedapi/client/ClientManagerSpec.scala
================================================
package typedapi.client
import org.specs2.mutable.Specification
final class ClientManagerSpec extends Specification {
"optional port definition" >> {
ClientManager((), "my-host", 80).base === "my-host:80"
ClientManager((), "my-host").base === "my-host"
}
}
================================================
FILE: client/src/test/scala/typedapi/client/RequestDataBuilderSpec.scala
================================================
package typedapi.client
import typedapi.dsl._
import typedapi.client.test._
import shapeless.Id
import org.specs2.mutable.Specification
final class RequestDataBuilderSpec extends Specification {
case class Foo()
type Result = (String, List[String], Map[String, String], Map[String, String], Option[Foo])
implicit val get = testGet[Id, ReqInput](identity)
implicit val put = testPut[Id, ReqInput](identity)
implicit def putB[Bd] = testPutWithBody[Id, Bd, ReqInputWithBody[Bd]](identity)
implicit val post = testPost[Id, ReqInput](identity)
implicit def postB[Bd] = testPostWithBody[Id, Bd, ReqInputWithBody[Bd]](identity)
implicit val delete = testDelete[Id, ReqInput](identity)
"executes compiled api" >> {
val cm = clientManager
"single api" >> {
"method" >> {
val api0 = derive(:= :> Get[Json, ReqInput])
api0().run[Id](cm) === ReqInput("GET", Nil, Map(), Map(("Accept", "application/json")))
val api1 = derive(:= :> Put[Json, ReqInput])
api1().run[Id](cm) === ReqInput("PUT", Nil, Map(), Map(("Accept", "application/json")))
val api2 = derive(:= :> Post[Json, ReqInput])
api2().run[Id](cm) === ReqInput("POST", Nil, Map(), Map(("Accept", "application/json")))
val api3 = derive(:= :> Delete[Json, ReqInput])
api3().run[Id](cm) === ReqInput("DELETE", Nil, Map(), Map(("Accept", "application/json")))
}
"segment" >> {
val api0 = derive(:= :> Segment[Int]('i0) :> Get[Json, ReqInput])
api0(0).run[Id](cm) === ReqInput("GET", "0" :: Nil, Map(), Map(("Accept", "application/json")))
val api1 = derive(:= :> Segment[Int]('i0) :> Segment[Int]('i1) :> Get[Json, ReqInput])
api1(0, 1).run[Id](cm) === ReqInput("GET", "0" :: "1" :: Nil, Map(), Map(("Accept", "application/json")))
}
"query" >> {
val api0 = derive(:= :> Query[Int]('i0) :> Get[Json, ReqInput])
api0(0).run[Id](cm) === ReqInput("GET", Nil, Map("i0" -> List("0")), Map(("Accept", "application/json")))
val api1 = derive(:= :> Query[Int]('i0) :> Query[Int]('i1) :> Get[Json, ReqInput])
api1(0, 1).run[Id](cm) === ReqInput("GET", Nil, Map("i0" -> List("0"), "i1" -> List("1")), Map(("Accept", "application/json")))
val api2 = derive(:= :> Query[List[Int]]('i0) :> Get[Json, ReqInput])
api2(List(0, 1)).run[Id](cm) === ReqInput("GET", Nil, Map("i0" -> List("0", "1")), Map(("Accept", "application/json")))
api2(Nil).run[Id](cm) === ReqInput("GET", Nil, Map.empty, Map(("Accept", "application/json")))
val api3 = derive(:= :> Query[Option[Int]]('i0) :> Get[Json, ReqInput])
api3(Some(0)).run[Id](cm) === ReqInput("GET", Nil, Map("i0" -> List("0")), Map(("Accept", "application/json")))
api3(None).run[Id](cm) === ReqInput("GET", Nil, Map.empty, Map(("Accept", "application/json")))
}
"header" >> {
val api0 = derive(:= :> Header[Int]('i0) :> Get[Json, ReqInput])
api0(0).run[Id](cm) === ReqInput("GET", Nil, Map(), Map("Accept" -> "application/json", "i0" -> "0"))
val api1 = derive(:= :> Header[Int]('i0) :> Header[Int]('i1) :> Get[Json, ReqInput])
api1(0, 1).run[Id](cm) === ReqInput("GET", Nil, Map(), Map("Accept" -> "application/json", "i0" -> "0", "i1" -> "1"))
val api2 = derive(:= :> Header[Option[Int]]('i0) :> Get[Json, ReqInput])
api2(Some(0)).run[Id](cm) === ReqInput("GET", Nil, Map(), Map("Accept" -> "application/json", "i0" -> "0"))
api2(None).run[Id](cm) === ReqInput("GET", Nil, Map(), Map("Accept" -> "application/json"))
val api3 = derive(:= :> Header('i0, 'i1) :> Get[Json, ReqInput])
api3().run[Id](cm) === ReqInput("GET", Nil, Map(), Map("Accept" -> "application/json", "i0" -> "i1"))
val api4 = derive(:= :> Client.Header[Int]('i0) :> Get[Json, ReqInput])
api4(0).run[Id](cm) === ReqInput("GET", Nil, Map(), Map("Accept" -> "application/json", "i0" -> "0"))
val api5 = derive(:= :> Client.Header('i0, 'i1) :> Get[Json, ReqInput])
api5().run[Id](cm) === ReqInput("GET", Nil, Map(), Map("Accept" -> "application/json", "i0" -> "i1"))
val api6 = derive(:= :> Client.Coll[Int] :> Get[Json, ReqInput])
api6(Map("hello" -> 5)).run[Id](cm) === ReqInput("GET", Nil, Map(), Map("Accept" -> "application/json", "hello" -> "5"))
}
"ignore server elements" >> {
val api0 = derive(:= :> Server.Match[String]("Hello-") :> Server.Send("a", "b") :> Client.Header[Int]('i0) :> Get[Json, ReqInput])
api0(0).run[Id](cm) === ReqInput("GET", Nil, Map(), Map("Accept" -> "application/json", "i0" -> "0"))
}
"request body" >> {
val api0 = derive(:= :> ReqBody[Json, Int] :> Put[Json, ReqInputWithBody[Int]])
api0(0).run[Id](cm) === ReqInputWithBody("PUT", Nil, Map(), Map(("Accept", "application/json")), 0)
}
"path" >> {
val api0 = derive(:= :> "hello" :> "world" :> Get[Json, ReqInput])
api0().run[Id](cm) === ReqInput("GET", "hello" :: "world" :: Nil, Map(), Map(("Accept", "application/json")))
}
}
"raw" >> {
implicit def rawPutB[Bd] = testRawPutWithBody[Id, Bd](identity)
val api0 = derive(:= :> ReqBody[Json, Int] :> Put[Json, ReqInputWithBody[Int]])
api0(0).run[Id].raw(cm) === ReqInputWithBody("PUT", Nil, Map(), Map(("Accept", "application/json")), 0)
}
"composition" >> {
val api =
(:= :> "find" :> Server.Match[String]("Hello-") :> Server.Send("a", "b") :> Get[Json, ReqInput]) :|:
(:= :> "fetch" :> Segment[String]('type) :> Get[Json, ReqInput]) :|:
(:= :> "store" :> ReqBody[Json, Int] :> Post[Json, ReqInputWithBody[Int]])
val (find, fetch, store) = deriveAll(api)
find().run[Id](cm) === ReqInput("GET", "find" :: Nil, Map(), Map(("Accept", "application/json")))
fetch("all").run[Id](cm) === ReqInput("GET", "fetch" :: "all" :: Nil, Map(), Map(("Accept", "application/json")))
store(0).run[Id](cm) === ReqInputWithBody("POST", "store" :: Nil, Map(), Map(("Accept", "application/json")), 0)
}
}
}
================================================
FILE: docs/ApiDefinition.md
================================================
## How to define an API
The central idea behind Typedapi is to make client and server implementation as boilerplate-free, typesafe and simple as possible.
- On the client-side you only define what you expect from an API provided by a server. In other words, you define a contract between the client and the server.
- The server-side then has to comply with that contract by implementing proper endpoint functions.
But how do you create this API definitions/contracts? This document will show you two ways provided by Typedapi:
- use the DSL (`import typedapi.dsl._`)
- or function-call-like definition (`import typedapi._`)
### Base case
Every API has to fullfil the base case, meaning it has to have a root path and a method description:
```Scala
// dsl
:= :> Get[MediaTypes.`application/json`, A]
// or
:= :> Get[MT.`application/json`, A]
// or in case of JSON
:= :> Get[Json, A]
// function
api(Get[Json, A])
// or
api(method = Get[Json, A], path = Root)
```
This translates to `GET /` returning some `Json A`.
### Methods
So far Typedapi supports the following methods:
```Scala
// dsl
:= :> Get[Json, A]
:= :> Put[Json, A]
:= :> Post[Json, A]
:= :> Delete[Json, A]
// function
api(Get[Json, A])
api(Put[Json, A])
api(Post[Json, A])
api(Delete[Json, A])
```
### Request Body
You may noticed that `Put` and `Post` don't have a field to describe a request body. To add that you have to explicitly define it with an element in your Api:
```Scala
// PUT {body: User} /
// dsl
:= :> ReqBody[Json, B] :> Put[Json, A]
// function
apiWithBody(Put[Json, A], ReqBody[Json, B])
// POST {body: User} /
// dsl
:= :> ReqBody[Json, B] :> Post[Json, A]
// function
apiWithBody(Post[Json, A], ReqBody[Json, B])
```
By the way, you can only add `Put` and `Post` as the next element of `ReqBody`. Everything else will not compile. Thus, you end up with a valid API description and not something like `:= :> ReqBody[Json, B] :> Get[Json, A]` or `api(Get[Json, A], ReqBody[Json, B])`.
### One word to encodings
You can find a list of provided encodings [here](https://github.com/pheymann/typedapi/blob/master/shared/src/main/scala/typedapi/shared/MediaTypes.scala). If you need something else implement `trait MediaType`.
### Path
```Scala
// GET /hello/world
// dsl
:= :> "hello" :> "world" :> Get[Json, A]
// function
api(Get[Json, A], Root / "hello" / "world")
```
All path elements are translated to singleton types and therefore encoded in the type of the API.
### Segment
Have a dynamic path element:
```Scala
// GET /user/{name: String}
// dsl
:= :> "user" :> Segment[String]("name") :> Get[Json, A]
// function
api(Get[Json, A], Root / "user" / Segment[String]("name"))
```
Every segment gets a name which is again encoded as singleton type in the API type.
### Query Parameter
```Scala
// GET /query?{id: Int}
// dsl
:= :> "query" :> Query[Int]("id") :> Get[Json, A]
// function
api(Get[Json, A], Root / "query", Queries.add[Int]("id"))
```
Every query gets a name which is again encoded as singleton type in the API type.
#### Optional Query
```Scala
// GET /query/opt?{id: Option[Int]}
// dsl
:= :> "query" :> "opt" :> Query[Option[Int]]("id") :> Get[Json, A]
// function
api(Get[Json, A], Root / "query" / "opt", Queries.add[Option[Int]]("id"))
```
#### Query with a List of elements
```Scala
// GET /query/list?{id: List[Int]}
// dsl
:= :> "query" :> "list" :> Query[List[Int]]("id") :> Get[Json, A]
// function
api(Get[Json, A], Root / "query" / "list", Queries.add[List[Int]]("id"))
```
### Header
```Scala
// GET /header {headers: id: Int}
// dsl
:= :> "header" :> Header[Int]("id") :> Get[Json, A]
// function
api(Get[Json, A], Root / "header", headers = Headers.add[Int]("id"))
```
This header is an expected input parameter.
Every header gets a name which is again encoded as singleton type in the API type.
#### Optional Header
```Scala
// GET /header/opt {headers: id: Option[Int]}
// dsl
:= :> "header" :> "opt" :> Header[Option[Int]]("id") :> Get[Json, A]
// function
api(Get[Json, A], Root / "header" / "opt", headers = Headers.add[Option[Int]]("id"))
```
#### Fixed Headers aka static headers
If you have a set of headers which are statically known and have to be provided by all sides you can add them as follows:
```Scala
// GET /header/fixed {headers: consumer=me}
// dsl
:= :> "header" :> "fixed" :> Header("consumer", "me") :> Get[Json, A]
// function
api(Get[Json, A], Root / "header" / "fixed", headers = Headers.add("consumer", "me"))
```
#### Client-Side: Headers
You have to send headers from the client-side but not server side? Here you go:
```Scala
// GET /header/client {header: consumer: String}
// dsl
:= :> "header" :> "client" :> Client.Header[String]("consumer") :> Get[Json, A]
// function
api(Get[Json, A], Root / "header" / "client", headers = Headers.client[String]("consumer"))
```
#### Client-Side: fixed/static Headers
You have to send static headers from the client-side but not server side? Here you go:
```Scala
// GET /header/client/fixed {header: consumer=me}
// dsl
:= :> "header" :> "client" :> "fixed" :> Client.Header("consumer", "me") :> Get[Json, A]
// function
api(Get[Json, A], Root / "header" / "client" / "fixed", headers = Headers.client("consumer", "me"))
```
#### Client-Side: Header collections
You can send header collections (`Map[String, V]`) as a single argument:
```Scala
// GET /header/client/coll {headers: a:b:...}
//dsl
:= :> "header" :> "client" :> "coll" :> Client.Coll[Int] :> Get[Json, A]
//function
api(Get[Json, A], Root / "header" / "client" / "coll", headers = Headers.clientColl[Int])
```
#### Server-Side: send Headers
You have to send headers from the server-side, e.g. for CORS? Here you go:
```Scala
// GET /header/server/send => {header: consumer=me}
// dsl
:= :> "header" :> "server" :> "send" :> Server.Send("consumer", "me") :> Get[Json, A]
// function
api(Get[Json, A], Root / "header" / "server" / "send", headers = Headers.serverSend("consumer", "me"))
```
#### Server-Side: extract matching Headers keys
You want to extract all headers which contain a certain `String`? Here you go:
```Scala
// GET /header/server/match {header: test1=me, test2=you}
// dsl
:= :> "header" :> "server" :> "match" :> Server.Match[String]("test") :> Get[Json, A]
// function
api(Get[Json, A], Root / "header" / "server" / "match", headers = Headers.serverMatch[String]("test"))
```
This will give you a `Set[V]` with `V = String` in this example.
### Multiple definitions in a single API
You can put multiple definitions into a single API element:
```Scala
val Api =
(:= :> "hello" :> Get[Json, A]) :|:
(:= :> "world" :> Query[Int]('foo) :> Delete[B])
```
================================================
FILE: docs/ClientCreation.md
================================================
## Create a client from your API
After we [defined](https://github.com/pheymann/typedapi/blob/master/docs/ApiDefinition.md) our API we have to derive a function/set of functions we can use to make our calls.
```Scala
val Api =
(api(Get[Json, User], Root / "user" / Segment[String]("name"))) :|:
(apiWithBody(Put[Json, User], ReqBody[Json, User], Root / "user"))
```
### First thing first, derive your functions
Lets derive our functions:
```Scala
import typedapi.client._
final case class User(name: String)
// implicit encoders and decoders
val (get, create) = deriveAll(Api)
```
### Http4s
If you want to use [http4s](https://github.com/http4s/http4s) as your client backend you have to add the following code:
```Scala
import typedapi.client.http4s._
import org.http4s.client.blaze.Http1Client
val client = Http1Client[IO]().unsafeRunSync
val cm = ClientManager(client, "http://my-host", myPort)
```
### Akka-Http
If you want to use [akka-http](https://github.com/akka/akka-http) as your client backend you have to add the following code:
```Scala
import typedapi.client.akkahttp._
import akka.actor.ActorSystem
import akka.stream.ActorMaterializer
import akka.http.scaladsl.Http
implicit val timeout = 5.second
implicit val system = ActorSystem("akka-http-client")
implicit val mat = ActorMaterializer()
import system.dispatcher
val cm = ClientManager(Http(), "http://my-host", myPort)
```
### Scalaj-Http
If you want to use [scalaj-http](https://github.com/scalaj/scalaj-http) as your client backend you have to add the following code:
```Scala
import typedapi.client.scalajhttp._
import scalaj.http._
val cm = ClientManager(Http, "http://my-host", myPort)
```
Be aware that `typedapi.util` provides an `Encoder[F[_], A]` and `Decoder[F[_], A]` trait to marshall and unmarhsall bodies. You have to provide implementations for your types.
```Scala
implicit val decoder = Decoder[Future, User] { json =>
// unmarshall the json using some known lib like circe
}
implicit val encoder = Encoder[Future, User] { user =>
// marshall the user using some known lib like circe
}
```
### Ammonite
There is special support for [Ammonite](http://ammonite.io/#Ammonite-REPL) and [ScalaScripts](http://ammonite.io/#ScalaScripts). It lets you tinker with the raw response and reduces the amount of imports you have to do:
```Scala
import $ivy.`com.github.pheymann::typedapi-ammonite-client:<version>`
import typedapi._
import client._
import amm._
val cm = clientManager("http://localhost", 9000)
final case class User(name: String, age: Int)
val Api = api(Get[Json, User], Root / "user" / "url")
val get = derive(Api)
// gives you the raw scalaj-http response
val response = get().run[Id].raw(cm)
response.body
response.headers
...
```
No `Decoder` needed if you use `raw(cm)`. Under the covers it uses scalaj-http as a client library.
It can be, that Ammonite isn't able to load `com.dwijnand:sbt-compat:1.0.0`. If that is the case execute the following command:
```Scala
interp.repositories() ++= Seq(coursier.ivy.IvyRepository.fromPattern(
"https://dl.bintray.com/dwijnand/sbt-plugins/" +:
coursier.ivy.Pattern.default
))
```
### ScalaJS
If you want to compile to [ScalaJS](https://www.scala-js.org/) you have to use the [Ajax](https://github.com/scala-js/scala-js-dom/blob/master/src/main/scala/org/scalajs/dom/ext/Extensions.scala#L253) with:
```Scala
import typedapi.client.js._
import org.scalajs.dom.ext.Ajax
val cm = ClientManager(Ajax, "http://my-host", myPort)
```
Be aware that `typedapi.util` provides an `Encoder[F[_], A]` and `Decoder[F[_], A]` trait to marshall and unmarhsall bodies. You have to provide implementations for your types.
```Scala
implicit val decoder = Decoder[Future, User] { json =>
// unmarshall the json using some known lib like circe
}
implicit val encoder = Encoder[Future, User] { user =>
// marshall the user using some known lib like circe
}
```
### Usage
Now we can to use our client functions:
```Scala
for {
_ <- create(User("Joe", 42)).run[IO](cm)
user <- get("Joe").run[IO](cm)
} yield user
//F[User]
```
**Make sure** you have the proper encoders and decoders in place.
================================================
FILE: docs/ExtendIt.md
================================================
## Extend Typedapi to fit your needs
You ended up in this file if:
- the default implements for a HTTP framework doesn't fit your needs
- if the framework you want to use is not supported
- you need more specilized [RequestDataBuilder](https://github.com/pheymann/typedapi/blob/master/client/src/main/scala/typedapi/client/RequestDataBuilder.scala), [RouteExtractors](https://github.com/pheymann/typedapi/blob/master/server/src/main/scala/typedapi/server/RouteExtractor.scala), [MediaTypes](https://github.com/pheymann/typedapi/blob/master/shared/src/main/scala/typedapi/shared/ApiElement.scala#L58), or the like
### General remark
I kept most of the type-classes open. That means you can override them as you like. If a certain class like *RequestDataBuilder* doesn't fullfil your needs just add another instance. Take a look at available implementations to get an idea how it works or ask a question in Gitter.
### Write your own Client backend
To write your own client backend you have to implement the [ApiRequest](https://github.com/pheymann/typedapi/blob/master/client/src/main/scala/typedapi/client/ApiRequest.scala) type-classes:
- `GetRequest`
- `PutRequest` and `PutWithBodyRequest`
- `PostRequest` and `PostWithBodyRequest`
- `DeleteRequest`
Take a look into [http4s-client](https://github.com/pheymann/typedapi/blob/master/http4s-client/src/main/scala/typedapi/client/http4s/package.scala) to get an idea how to do it.
You can implement all type-classes or just a subset to override implementations provided by TypedApi.
### Write your own Server backend
To write your own server backend you have to implement the [EndpointExecutor](https://github.com/pheymann/typedapi/blob/master/server/src/main/scala/typedapi/server/EndpointExecutor.scala) and [MountEndpoints](https://github.com/pheymann/typedapi/blob/master/server/src/main/scala/typedapi/server/ServerManager.scala) type-classes
Take a look into [http4s-server](https://github.com/pheymann/typedapi/blob/master/http4s-server/src/main/scala/typedapi/server/http4s/package.scala) to get an idea how to do it.
You can implement all type-classes or just a subset to override implementations provided by TypedApi.
================================================
FILE: docs/ServerCreation.md
================================================
## Create a server from your API
After we [defined](https://github.com/pheymann/typedapi/blob/master/docs/ApiDefinition.md) our API we have to derive the endpoint/set of endpoints we can mount and serve to the world.
```Scala
val Api =
(api(Get[Json, User], Root / "user" / Segment[String]("name"))) :|:
(apiWithBody(Put[Json, User], ReqBody[Json, User], Root / "user"))
```
### First things first, derive the endpoints
```Scala
import typedapi.server._
final case class User(name: String)
// implicit encoders and decoders
val endpoints = deriveAll[IO](Api).from(
name => // retrieve and return user
user => // store user
)
```
### Set Status Codes
#### Success
```Scala
deriveAll[IO](Api).from(
name =>
val user: User = ???
success(user) // creates a 200
...
}
```
or
```Scala
deriveAll[IO](Api).from(
name =>
val user: User = ???
successWith(StatusCodes.Ok)(user) // set code
...
}
```
#### Error
```Scala
deriveAll[IO](Api).from(
name =>
val user: Option[User] = ???
user.fold(errorWith(StatusCodes.NotFound, s"no user $id")(user => success(user))
...
}
```
### Http4s
If you want to use [http4s](https://github.com/http4s/http4s) as your server backend you have to add the following code:
```Scala
import typedapi.server.http4s._
import org.http4s.server.blaze.BlazeBuilder
val sm = ServerManager(BlazeBuilder[IO], "http://my-host", myPort)
```
### Akka-Http
If you want to use [akka-http](https://github.com/akka/akka-http) as your server backend you have to add the following code:
```Scala
implicit val timeout = 5.second
implicit val system = ActorSystem("akka-http-server")
implicit val mat = ActorMaterializer()
import system.dispatcher
val sm = ServerManager(Http(), "http://my-host", myPort)
```
### Start server
Now we can mount `endpoints` and serve to to the world:
```Scala
val server = mount(sm, endpoints)
server.unsafeRunSync()
```
**Make sure** you have the proper encoders and decoders in place.
================================================
FILE: docs/example/ammonite_client_example.sc
================================================
import $ivy.`com.github.pheymann::typedapi-ammonite-client:0.2.0-M1`
import typedapi._
import client._
import amm._
val cm = clientManager("http://localhost", 9000)
final case class User(name: String, age: Int)
val Api = api(Get[Json, User], Root, headers = Headers.serverSend("Access-Control-Allow-Origin", "*"))
val get = derive(Api)
val response = get().run[Id].raw(cm)
================================================
FILE: docs/example/build.sbt
================================================
val typedapiVersion = "0.2.0"
val http4sVersion = "0.18.0"
val commonSettings = Seq(
scalaVersion := "2.12.4"
)
lazy val root = project
.in(file("."))
.aggregate(`shared-jvm`, `shared-js`, `client-jvm`, `client-js`, server)
lazy val shared = crossProject.crossType(CrossType.Pure)
.in(file("shared"))
.settings(commonSettings: _*)
.settings(
libraryDependencies ++= Seq(
"com.github.pheymann" %%% "typedapi-shared" % typedapiVersion,
"io.circe" %%% "circe-core" % "0.9.1",
"io.circe" %%% "circe-parser" % "0.9.1",
"io.circe" %%% "circe-generic" % "0.9.1"
)
)
lazy val `shared-js` = shared.js
lazy val `shared-jvm` = shared.jvm
lazy val server = project
.in(file("server"))
.settings(commonSettings: _*)
.settings(
libraryDependencies ++= Seq(
"com.github.pheymann" %% "typedapi-http4s-server" % typedapiVersion,
"org.http4s" %% "http4s-circe" % http4sVersion,
"org.http4s" %% "http4s-blaze-server" % http4sVersion,
"org.http4s" %% "http4s-dsl" % http4sVersion
)
)
.dependsOn(`shared-jvm`)
lazy val `client-jvm` = project
.in(file("client-jvm"))
.settings(commonSettings: _*)
.settings(
libraryDependencies ++= Seq(
"com.github.pheymann" %% "typedapi-http4s-client" % typedapiVersion,
"org.http4s" %% "http4s-circe" % http4sVersion,
"org.http4s" %% "http4s-blaze-client" % http4sVersion,
"org.http4s" %% "http4s-dsl" % http4sVersion
)
)
.dependsOn(`shared-jvm`)
lazy val `client-js` = project
.in(file("client-js"))
.enablePlugins(ScalaJSPlugin)
.settings(commonSettings: _*)
.settings(
libraryDependencies ++= Seq(
"com.github.pheymann" %%% "typedapi-js-client" % typedapiVersion,
),
scalaJSUseMainModuleInitializer := true
)
.dependsOn(`shared-js`)
================================================
FILE: docs/example/client-js/index.html
================================================
<!DOCTYPE html>
<html lang="en">
<body>
<script src="target/scala-2.12/client-js-fastopt.js"></script>
</body>
</html>
================================================
FILE: docs/example/client-js/src/main/scala/Client.scala
================================================
import typedapi.client._
import typedapi.client.js._
import org.scalajs.dom.ext.Ajax
import io.circe._
import io.circe.generic.auto._
import io.circe.parser._
import io.circe.syntax._
import scala.concurrent.Future
import scala.concurrent.ExecutionContext.Implicits.global
import scala.concurrent.duration._
object Client {
type Id[A] = A
final case class DecodeException(msg: String) extends Exception
implicit val decoder = typedapi.util.Decoder[Future, User](json => decode[User](json).fold(
error => Future.successful(Left(DecodeException(error.toString()))),
user => Future.successful(Right(user))
))
implicit val encoder = typedapi.util.Encoder[Future, User](user => Future.successful(user.asJson.noSpaces))
val (get, put, post, delete, path, putBody, segment, search, header, fixed, client, coll, matches) = deriveAll(FromDsl.MyApi)
def main(args: Array[String]): Unit = {
val cm = ClientManager(Ajax, "http://localhost", 9000)
(for {
u0 <- putBody(User("joe", 27)).run[Future](cm)
u1 <- search("joe").run[Future](cm)
} yield (u0, u1)).foreach { case (u0, u1) =>
println(u0)
println(u1)
}
}
}
================================================
FILE: docs/example/client-jvm/src/main/scala/Client.scala
================================================
import typedapi.client._
import typedapi.client.http4s._
import cats.effect.IO
import org.http4s._
import org.http4s.circe._
object Client {
implicit val decoder = jsonOf[IO, User]
implicit val encoder = jsonEncoderOf[IO, User]
val (get, put, post, delete, path, putBody, segment, search, header, fixed, client, coll, matches) = deriveAll(FromDsl.MyApi)
def main(args: Array[String]): Unit = {
import User._
import cats.effect.IO
import org.http4s.client.blaze.Http1Client
val cm = ClientManager(Http1Client[IO]().unsafeRunSync, "http://localhost", 9000)
(for {
u0 <- putBody(User("joe", 27)).run[IO](cm)
u1 <- search("joe").run[IO](cm)
} yield {
println(u0)
println(u1)
()
}).unsafeRunSync()
}
}
================================================
FILE: docs/example/project/build.properties
================================================
sbt.version=1.0.4
================================================
FILE: docs/example/project/plugins.sbt
================================================
addSbtPlugin("org.scala-js" % "sbt-scalajs" % "0.6.22")
================================================
FILE: docs/example/server/src/main/scala/Server.scala
================================================
import typedapi.server._
import typedapi.server.http4s._
import cats.effect.IO
import org.http4s._
import org.http4s.circe._
object Server {
implicit val decoder = jsonOf[IO, User]
implicit val encoder = jsonEncoderOf[IO, User]
val get: () => IO[Result[User]] = () => IO.pure(success(User("Joe", 42)))
val put: () => IO[Result[User]] = get
val post: () => IO[Result[User]] = get
val delete: () => IO[Result[User]] = get
val path: () => IO[Result[User]] = get
val putBody: User => IO[Result[User]] = user => IO.pure(success(user))
val segment: String => IO[Result[User]] = name => IO.pure(success(User(name, 42)))
val search: String => IO[Result[User]] = segment
val header: String => IO[Result[User]] = consumer => IO.pure(success(User("found: " + consumer, 42)))
val fixed: () => IO[Result[User]] = get
val client: () => IO[Result[User]] = get
val coll: () => IO[Result[User]] = get
val matching: Map[String, String] => IO[Result[User]] = matches => IO.pure(success(User(matches.mkString(","), 42)))
val endpoints = deriveAll[IO](FromDefinition.MyApi).from(
get,
put,
post,
delete,
path,
putBody,
segment,
search,
header,
fixed,
client,
coll,
matching
)
def main(args: Array[String]): Unit = {
import org.http4s.server.blaze.BlazeBuilder
val sm = ServerManager(BlazeBuilder[IO], "localhost", 9000)
mount(sm, endpoints).unsafeRunSync()
scala.io.StdIn.readLine("Press 'Enter' to stop ...")
}
}
================================================
FILE: docs/example/shared/src/main/scala/Apis.scala
================================================
object FromDsl {
import typedapi.dsl._
/* NOTE: we have to add the 'Access-Control-Allow-Origin' header to the server-side to allow the
* browser (ScalaJS) to access the server (CORS)
*/
val MyApi =
// basic GET request
(:= :> Server.Send("Access-Control-Allow-Origin", "*") :> Get[Json, User]) :|:
// basic PUT request
(:= :> Server.Send("Access-Control-Allow-Origin", "*") :> Put[Json, User]) :|:
// basic POST request
(:= :> Server.Send("Access-Control-Allow-Origin", "*") :> Post[Json, User]) :|:
// basic DELETE request
(:= :> Server.Send("Access-Control-Allow-Origin", "*") :> Delete[Json, User]) :|:
// define a path
(:= :> "my" :> "path" :> Server.Send("Access-Control-Allow-Origin", "*") :> Get[Json, User]) :|:
// add a request body
(:= :> "with" :> "body" :> Server.Send("Access-Control-Allow-Origin", "*") :> ReqBody[Json, User] :> Put[Json, User]) :|:
// add segments
(:= :> "name" :> Segment[String]("name") :> Server.Send("Access-Control-Allow-Origin", "*") :> Get[Json, User]) :|:
// add query
(:= :> "search" :> "user" :> Query[String]("name") :> Server.Send("Access-Control-Allow-Origin", "*") :> Get[Json, User]) :|:
// add header
(:= :> "header" :> Header[String]("consumer") :> Server.Send("Access-Control-Allow-Origin", "*") :> Get[Json, User]) :|:
(:= :> "header" :> "fixed" :> Header("consumer", "me") :> Server.Send("Access-Control-Allow-Origin", "*") :> Get[Json, User]) :|:
(:= :> "header" :> "client" :> Client.Header("client", "me") :> Server.Send("Access-Control-Allow-Origin", "*") :> Get[Json, User]) :|:
(:= :> "header" :> "client" :> "coll" :> Client.Coll[Int] :> Server.Send("Access-Control-Allow-Origin", "*") :> Get[Json, User]) :|:
(:= :> "header" :> "server" :> Server.Match[String]("Control-") :> Server.Send("Access-Control-Allow-Origin", "*") :> Get[Json, User])
}
object FromDefinition {
import typedapi._
val MyApi =
// basic GET request
api(Get[Json, User], headers = Headers.serverSend("Access-Control-Allow-Origin", "*")) :|:
// basic PUT request
api(Put[Json, User], headers = Headers.serverSend("Access-Control-Allow-Origin", "*")) :|:
// basic POST request
api(Post[Json, User], headers = Headers.serverSend("Access-Control-Allow-Origin", "*")) :|:
// basic Delete request
api(Delete[Json, User], headers = Headers.serverSend("Access-Control-Allow-Origin", "*")) :|:
// define a path
api(Get[Json, User], Root / "my" / "path", headers = Headers.serverSend("Access-Control-Allow-Origin", "*")) :|:
// add a request body
apiWithBody(Put[Json, User], ReqBody[Json, User], Root / "with" / "body", headers = Headers.serverSend("Access-Control-Allow-Origin", "*")) :|:
// add segments
api(Get[Json, User], Root / "name" / Segment[String]("name"), headers = Headers.serverSend("Access-Control-Allow-Origin", "*")) :|:
// add query
api(Get[Json, User], Root / "search" / "user", Queries.add[String]("name"), Headers.serverSend("Access-Control-Allow-Origin", "*")) :|:
// add header
api(Get[Json, User], Root / "header", headers = Headers.add[String]("consumer").serverSend("Access-Control-Allow-Origin", "*")) :|:
api(Get[Json, User], Root / "header" / "fixed", headers = Headers.add("consumer", "me").serverSend("Access-Control-Allow-Origin", "*")) :|:
api(Get[Json, User], Root / "header" / "client", headers = Headers.client("client", "me").serverSend("Access-Control-Allow-Origin", "*")) :|:
api(Get[Json, User], Root / "header" / "client" / "coll", headers = Headers.clientColl[Int].serverSend("Access-Control-Allow-Origin", "*")) :|:
api(Get[Json, User], Root / "header" / "server", headers = Headers.serverMatch[String]("Control-").serverSend("Access-Control-Allow-Origin", "*"))
}
================================================
FILE: docs/example/shared/src/main/scala/User.scala
================================================
import io.circe.syntax._
import io.circe.generic.JsonCodec
final case class User(name: String, age: Int)
object User {
implicit val enc = io.circe.generic.semiauto.deriveEncoder[User]
implicit val dec = io.circe.generic.semiauto.deriveDecoder[User]
}
================================================
FILE: http-support-tests/src/test/scala/http/support/tests/User.scala
================================================
package http.support.tests
import cats.effect.IO
import io.circe.generic.semiauto._
import org.http4s.circe._
import org.http4s.dsl.io._
final case class User(name: String, age: Int)
object UserCoding {
implicit val enc = deriveEncoder[User]
implicit val dec = deriveDecoder[User]
implicit val decoderIO = jsonOf[IO, User]
implicit val encoderIO = jsonEncoderOf[IO, User]
}
================================================
FILE: http-support-tests/src/test/scala/http/support/tests/client/AkkaHttpClientSupportSpec.scala
================================================
package http.support.tests.client
import http.support.tests.{User, UserCoding, Api}
import typedapi.client._
import typedapi.client.akkahttp._
import akka.actor.ActorSystem
import akka.stream.ActorMaterializer
import akka.http.scaladsl.Http
import de.heikoseeberger.akkahttpcirce.FailFastCirceSupport
import org.specs2.mutable.Specification
import org.specs2.concurrent.ExecutionEnv
import scala.concurrent.duration._
import scala.concurrent.{Future, Await}
final class AkkaHttpClientSupportSpec(implicit ee: ExecutionEnv) extends Specification {
import UserCoding._
import FailFastCirceSupport._
sequential
implicit val timeout = 5.second
implicit val system = ActorSystem("akka-http-client-spec", defaultExecutionContext = Some(ee.ec))
implicit val mat = ActorMaterializer()
import system.dispatcher
val cm = ClientManager(Http(), "http://localhost", 9001)
val server = TestServer.start()
"akka http client support" >> {
val (p, s, q, header, fixed, clInH, clFixH, clColl, serMatchH, serSendH, m0, m1, m2, m3, m4, m5, _, _, _) = deriveAll(Api)
"paths and segments" >> {
p().run[Future](cm) must beEqualTo(User("foo", 27)).awaitFor(timeout)
s("jim").run[Future](cm) must beEqualTo(User("jim", 27)).awaitFor(timeout)
}
"queries" >> {
q(42).run[Future](cm) must beEqualTo(User("foo", 42)).awaitFor(timeout)
}
"headers" >> {
header(42).run[Future](cm) must beEqualTo(User("foo", 42)).awaitFor(timeout)
fixed().run[Future](cm) must beEqualTo(User("joe", 27)).awaitFor(timeout)
clInH("jim").run[Future](cm) must beEqualTo(User("jim", 27)).awaitFor(timeout)
clFixH().run[Future](cm) must beEqualTo(User("joe", 27)).awaitFor(timeout)
clColl(Map("coll" -> "joe", "collect" -> "jim")).run[Future](cm) must beEqualTo(User("coll: joe,collect: jim", 27)).awaitFor(timeout)
serMatchH().run[Future](cm) must beEqualTo(User("joe", 27)).awaitFor(timeout)
serSendH().run[Future](cm) must beEqualTo(User("joe", 27)).awaitFor(timeout)
}
"methods" >> {
m0().run[Future](cm) must beEqualTo(User("foo", 27)).awaitFor(timeout)
m1().run[Future](cm) must beEqualTo(User("foo", 27)).awaitFor(timeout)
m2(User("jim", 42)).run[Future](cm) must beEqualTo(User("jim", 42)).awaitFor(timeout)
m3().run[Future](cm) must beEqualTo(User("foo", 27)).awaitFor(timeout)
m4(User("jim", 42)).run[Future](cm) must beEqualTo(User("jim", 42)).awaitFor(timeout)
m5(List("because")).run[Future](cm) must beEqualTo(User("foo", 27)).awaitFor(timeout)
}
step {
server.shutdown.unsafeRunSync()
Await.ready(system.terminate, timeout)
}
}
}
================================================
FILE: http-support-tests/src/test/scala/http/support/tests/client/Http4sClientSupportSpec.scala
================================================
package http.support.tests.client
import http.support.tests.{UserCoding, User, Api}
import typedapi.client._
import typedapi.client.http4s._
import cats.effect.IO
import org.http4s.client.blaze.Http1Client
import org.specs2.mutable.Specification
final class Http4sClientSupportSpec extends Specification {
import UserCoding._
sequential
val cm = ClientManager(Http1Client[IO]().unsafeRunSync, "http://localhost", 9001)
val server = TestServer.start()
"http4s client support" >> {
val (p, s, q, header, fixed, clInH, clFixH, clColl, serMatchH, serSendH, m0, m1, m2, m3, m4, m5, _, _, _) = deriveAll(Api)
"paths and segments" >> {
p().run[IO](cm).unsafeRunSync() === User("foo", 27)
s("jim").run[IO](cm).unsafeRunSync() === User("jim", 27)
}
"queries" >> {
q(42).run[IO](cm).unsafeRunSync() === User("foo", 42)
}
"headers" >> {
header(42).run[IO](cm).unsafeRunSync() === User("foo", 42)
fixed().run[IO](cm).unsafeRunSync() === User("joe", 27)
clInH("jim").run[IO](cm).unsafeRunSync === User("jim", 27)
clFixH().run[IO](cm).unsafeRunSync() === User("joe", 27)
clColl(Map("coll" -> "joe", "collect" -> "jim")).run[IO](cm).unsafeRunSync === User("coll: joe,collect: jim", 27)
serMatchH().run[IO](cm).unsafeRunSync() === User("joe", 27)
serSendH().run[IO](cm).unsafeRunSync() === User("joe", 27)
}
"methods" >> {
m0().run[IO](cm).unsafeRunSync() === User("foo", 27)
m1().run[IO](cm).unsafeRunSync() === User("foo", 27)
m2(User("jim", 42)).run[IO](cm).unsafeRunSync() === User("jim", 42)
m3().run[IO](cm).unsafeRunSync() === User("foo", 27)
m4(User("jim", 42)).run[IO](cm).unsafeRunSync() === User("jim", 42)
m5(List("because")).run[IO](cm).unsafeRunSync() === User("foo", 27)
}
step {
server.shutdown.unsafeRunSync()
}
}
}
================================================
FILE: http-support-tests/src/test/scala/http/support/tests/client/ScalajHttpClientSupportSpec.scala
================================================
package http.support.tests.client
import http.support.tests.{UserCoding, User, Api}
import typedapi.client._
import typedapi.client.scalajhttp._
import scalaj.http.Http
import io.circe.parser._
import io.circe.syntax._
import org.specs2.mutable.Specification
final class ScalajHttpClientSupportSpec extends Specification {
import UserCoding._
sequential
case class DecodeException(msg: String) extends Exception
implicit val decoder = typedapi.util.Decoder[Id, User](json => decode[User](json).fold(
error => Left(DecodeException(error.toString())),
user => Right(user)
))
implicit val encoder = typedapi.util.Encoder[Id, User](user => user.asJson.noSpaces)
val cm = ClientManager(Http, "http://localhost", 9001)
val server = TestServer.start()
"http4s client support" >> {
val (p, s, q, header, fixed, clInH, clFixH, clColl, serMatchH, serSendH, m0, m1, m2, m3, m4, m5, _, _, _) = deriveAll(Api)
"paths and segments" >> {
p().run[Blocking](cm) === Right(User("foo", 27))
s("jim").run[Blocking](cm) === Right(User("jim", 27))
}
"queries" >> {
q(42).run[Blocking](cm) === Right(User("foo", 42))
}
"headers" >> {
header(42).run[Blocking](cm) === Right(User("foo", 42))
fixed().run[Blocking](cm) === Right(User("joe", 27))
clInH("jim").run[Blocking](cm) === Right(User("jim", 27))
clFixH().run[Blocking](cm) === Right(User("joe", 27))
clColl(Map("coll" -> "joe", "collect" -> "jim")).run[Blocking](cm) === Right(User("collect: jim,coll: joe", 27))
serMatchH().run[Blocking](cm) === Right(User("joe", 27))
serSendH().run[Blocking](cm) === Right(User("joe", 27))
}
"methods" >> {
m0().run[Blocking](cm) === Right(User("foo", 27))
m1().run[Blocking](cm) === Right(User("foo", 27))
m2(User("jim", 42)).run[Blocking](cm) === Right(User("jim", 42))
m3().run[Blocking](cm) === Right(User("foo", 27))
m4(User("jim", 42)).run[Blocking](cm) === Right(User("jim", 42))
m5(List("because")).run[Blocking](cm) === Right(User("foo", 27))
}
"raw" >> {
m0().run[Id].raw(cm).body === """{"name":"foo","age":27}"""
}
step {
server.shutdown.unsafeRunSync()
}
}
}
================================================
FILE: http-support-tests/src/test/scala/http/support/tests/client/TestServer.scala
================================================
package http.support.tests.client
import http.support.tests.{User, UserCoding}
import cats.effect.IO
import io.circe.syntax._
import org.http4s._
import org.http4s.circe._
import org.http4s.dsl.io._
import org.http4s.server.Server
import org.http4s.server.blaze._
object Age extends QueryParamDecoderMatcher[Int]("age")
object Reasons {
def unapplySeq(params: Map[String, Seq[String]]) = params.get("reasons")
def unapply(params: Map[String, Seq[String]]) = unapplySeq(params)
}
object TestServer {
import UserCoding._
val service = HttpService[IO] {
case GET -> Root / "path" => Ok(User("foo", 27))
case GET -> Root / "segment" / name => Ok(User(name, 27))
case GET -> Root / "query" :? Age(age) => Ok(User("foo", age))
case req @ GET -> Root / "header" =>
val headers = req.headers.toList
val age = headers.find(_.name.value == "age").get.value.toInt
Ok(User("foo", age))
case req @ GET -> Root / "header" / "fixed" =>
val headers = req.headers.toList
val value = headers.find(_.name.value == "Hello").get.value
if (value != "*")
throw new IllegalArgumentException("unexpected header value " + value)
Ok(User("joe", 27))
case req @ GET -> Root / "header" / "client" =>
val headers = req.headers.toList
val value = headers.find(_.name.value == "Hello").get.value
if (value != "*")
throw new IllegalArgumentException("unexpected header value " + value)
Ok(User("joe", 27))
case req @ GET -> Root / "header" / "client" / "coll" =>
val headers = req.headers.toList
val values = headers.filter(_.name.value.contains("coll"))
if (values.isEmpty)
throw new IllegalArgumentException("no header collection ")
Ok(User(values.mkString(","), 27))
case req @ GET -> Root / "header" / "input" / "client" =>
val headers = req.headers.toList
val value = headers.find(_.name.value == "Hello").get.value
Ok(User(value, 27))
case req @ GET -> Root / "header" / "server" / "send" =>
Ok(User("joe", 27)).map(resp => resp.copy(headers = resp.headers put Header("Hello", "*")))
case req @ GET -> Root / "header" / "server" / "match" =>
Ok(User("joe", 27))
case GET -> Root => Ok(User("foo", 27))
case PUT -> Root => Ok(User("foo", 27))
case req @ PUT -> Root / "body" => Ok(User("foo", 27))
for {
user <- req.as[User]
resp <- Ok(user.asJson)
} yield resp
case POST -> Root => Ok(User("foo", 27))
case req @ POST -> Root / "body" => Ok(User("foo", 27))
for {
user <- req.as[User]
resp <- Ok(user.asJson)
} yield resp
case DELETE -> Root :? Reasons(reasons) =>
println(reasons)
Ok(User("foo", 27))
}
def start(): Server[IO] = {
val builder = BlazeBuilder[IO]
.bindHttp(9001, "localhost")
.mountService(service, "/")
.start
builder.unsafeRunSync()
}
}
================================================
FILE: http-support-tests/src/test/scala/http/support/tests/package.scala
================================================
package http.support
import typedapi.dsl._
package object tests {
val Api =
(:= :> "path" :> Get[Json, User]) :|:
(:= :> "segment" :> Segment[String]('name) :> Get[Json, User]) :|:
(:= :> "query" :> Query[Int]('age) :> Get[Json, User]) :|:
(:= :> "header" :> Header[Int]('age) :> Get[Json, User]) :|:
(:= :> "header" :> "fixed" :> Header("Hello", "*") :> Get[Json, User]) :|:
(:= :> "header" :> "input" :> "client" :> Client.Header[String]("Hello") :> Get[Json, User]) :|:
(:= :> "header" :> "client" :> Client.Header("Hello", "*") :> Get[Json, User]) :|:
(:= :> "header" :> "client" :> "coll" :> Client.Coll[String] :> Get[Json, User]) :|:
(:= :> "header" :> "server" :> "match" :> Server.Match[String]("test") :> Get[Json, User]) :|:
(:= :> "header" :> "server" :> "send" :> Server.Send("Hello", "*") :> Get[Json, User]) :|:
(:= :> Get[Json, User]) :|:
(:= :> Put[Json, User]) :|:
(:= :> "body" :> ReqBody[Json, User] :> Put[Json, User]) :|:
(:= :> Post[Json, User]) :|:
(:= :> "body" :> ReqBody[Json, User] :> Post[Json, User]) :|:
(:= :> Query[List[String]]('reasons) :> Delete[Json, User]) :|:
(:= :> "status" :> "200" :> Get[Plain, String]) :|:
(:= :> "status" :> "400" :> Get[Plain, String]) :|:
(:= :> "status" :> "500" :> Get[Plain, String])
}
================================================
FILE: http-support-tests/src/test/scala/http/support/tests/server/AkkaHttpServerSupportSpec.scala
================================================
package http.support.tests.server
import http.support.tests.{Api, UserCoding}
import typedapi.server._
import typedapi.server.akkahttp._
import akka.actor.ActorSystem
import akka.stream.ActorMaterializer
import akka.http.scaladsl.Http
import de.heikoseeberger.akkahttpcirce.FailFastCirceSupport
import cats.implicits._
import org.specs2.concurrent.ExecutionEnv
import scala.concurrent.{Future, Await}
import scala.concurrent.duration._
final class AkkaHttpServerSupportSpec(implicit ee: ExecutionEnv) extends ServerSupportSpec[Future]()(catsStdInstancesForFuture(ee.ec)) {
import UserCoding._
import FailFastCirceSupport._
implicit val timeout = 5.second
implicit val system = ActorSystem("akka-http-server-spec", defaultExecutionContext = Some(ee.ec))
implicit val mat = ActorMaterializer()
import system.dispatcher
val endpoints = deriveAll[Future](Api).from(
path,
segment,
query,
header,
fixed,
input,
clientHdr,
coll,
matching,
send,
get,
put,
putB,
post,
postB,
delete,
code200,
code400,
code500
)
val sm = ServerManager(Http(), "localhost", 9000)
val server = mount(sm, endpoints)
"akka http implements TypedApi's server interface" >> {
tests(9000)
step {
Await.ready(server.map(_.unbind()), timeout)
Await.ready(system.terminate, timeout)
}
}
}
================================================
FILE: http-support-tests/src/test/scala/http/support/tests/server/Http4sServerSupportSpec.scala
================================================
package http.support.tests.server
import http.support.tests.{UserCoding, Api}
import typedapi.server._
import typedapi.server.http4s._
import cats.effect.IO
import org.http4s.server.blaze.BlazeBuilder
final class Http4sServerSupportSpec extends ServerSupportSpec[IO] {
import UserCoding._
val endpoints = deriveAll[IO](Api).from(
path,
segment,
query,
header,
fixed,
input,
clientHdr,
coll,
matching,
send,
get,
put,
putB,
post,
postB,
delete,
code200,
code400,
code500
)
val sm = ServerManager(BlazeBuilder[IO], "localhost", 9000)
val server = mount(sm, endpoints).unsafeRunSync()
"http4s implements TypedApi's server interface" >> {
tests(9000)
step {
server.shutdown.unsafeRunSync()
}
}
}
================================================
FILE: http-support-tests/src/test/scala/http/support/tests/server/ServerSupportSpec.scala
================================================
package http.support.tests.server
import typedapi.server._
import http.support.tests.{User, UserCoding}
import org.http4s._
import org.http4s.dsl.io._
import org.http4s.client.blaze._
import org.http4s.client.dsl.io._
import cats.Applicative
import cats.effect.IO
import org.specs2.mutable.Specification
import scala.language.higherKinds
abstract class ServerSupportSpec[F[_]: Applicative] extends Specification {
import StatusCodes._
sequential
val client = Http1Client[IO]().unsafeRunSync
def tests(port: Int) = {
import UserCoding._
"paths and segments" >> {
client.expect[User](s"http://localhost:$port/path").unsafeRunSync() === User("path", 27)
client.expect[User](s"http://localhost:$port/segment/jim").unsafeRunSync() === User("jim", 27)
}
"queries" >> {
client.expect[User](s"http://localhost:$port/query?age=42").unsafeRunSync() === User("query", 42)
}
"headers" >> {
client.expect[User](Request[IO](
method = GET,
uri = Uri.fromString(s"http://localhost:$port/header").right.get,
headers = Headers(Header("age", "42"))
)).unsafeRunSync() === User("header", 42)
client.expect[User](Request[IO](
method = GET,
uri = Uri.fromString(s"http://localhost:$port/header/fixed").right.get,
headers = Headers(Header("Hello", "*"))
)).unsafeRunSync() === User("fixed", 27)
client.expect[User](Request[IO](
method = GET,
uri = Uri.fromString(s"http://localhost:$port/header/client").right.get
)).unsafeRunSync() === User("client header", 27)
client.expect[User](Request[IO](
method = GET,
uri = Uri.fromString(s"http://localhost:$port/header/input/client").right.get
)).unsafeRunSync() === User("input", 27)
client.fetch[Option[Header]](
Request[IO](
method = GET,
uri = Uri.fromString(s"http://localhost:$port/header/server/send").right.get
)
)(
resp => IO {
resp.headers.toList.find(_.name.toString == "Hello")
}
).unsafeRunSync() === Some(Header("Hello", "*"))
client.expect[User](Request[IO](
method = GET,
uri = Uri.fromString(s"http://localhost:$port/header/server/match").right.get,
headers = Headers(Header("test", "foo"), Header("testy", "bar"), Header("meh", "NONO"))
)).unsafeRunSync() === User("test -> foo,testy -> bar", 27)
client.fetch[Option[Header]](
Request[IO](
method = OPTIONS,
uri = Uri.fromString(s"http://localhost:$port/header/fixed").right.get,
headers = Headers(Header("Hello", "*"))
)
)(
resp => IO {
resp.headers.toList.find(_.name.toString == "Access-Control-Allow-Methods")
}
).unsafeRunSync() === Some(Header("Access-Control-Allow-Methods", "GET"))
}
"methods" >> {
client.expect[User](s"http://localhost:$port/").unsafeRunSync() === User("get", 27)
client.expect[User](PUT(Uri.fromString(s"http://localhost:$port/").right.get)).unsafeRunSync() === User("put", 27)
client.expect[User](PUT(Uri.fromString(s"http://localhost:$port/body").right.get, User("joe", 27))).unsafeRunSync() === User("joe", 27)
client.expect[User](POST(Uri.fromString(s"http://localhost:$port/").right.get)).unsafeRunSync() === User("post", 27)
client.expect[User](POST(Uri.fromString(s"http://localhost:$port/body").right.get, User("joe", 27))).unsafeRunSync() === User("joe", 27)
client.expect[User](DELETE(Uri.fromString(s"http://localhost:$port/?reasons=because").right.get)).unsafeRunSync() === User("because", 27)
}
"status codes" >> {
client.fetch[Int](GET(Uri.fromString(s"http://localhost:$port/status/200").right.get))(resp => IO.pure(resp.status.code)).unsafeRunSync === 200
client.fetch[Int](GET(Uri.fromString(s"http://localhost:$port/status/400").right.get))(resp => IO.pure(resp.status.code)).unsafeRunSync === 400
client.fetch[Int](GET(Uri.fromString(s"http://localhost:$port/status/500").right.get))(resp => IO.pure(resp.status.code)).unsafeRunSync === 500
}
}
val path: () => F[Result[User]] = () => Applicative[F].pure(successWith(Ok)(User("path", 27)))
val segment: String => F[Result[User]] = name => Applicative[F].pure(successWith(Ok)(User(name, 27)))
val query: Int => F[Result[User]] = age => Applicative[F].pure(successWith(Ok)(User("query", age)))
val header: Int => F[Result[User]] = age => Applicative[F].pure(successWith(Ok)(User("header", age)))
val fixed: () => F[Result[User]] = () => Applicative[F].pure(successWith(Ok)(User("fixed", 27)))
val input: () => F[Result[User]] = () => Applicative[F].pure(successWith(Ok)(User("input", 27)))
val clientHdr: () => F[Result[User]] = () => Applicative[F].pure(successWith(Ok)(User("client header", 27)))
val coll: () => F[Result[User]] = () => Applicative[F].pure(successWith(Ok)(User("coll", 27)))
val matching: Map[String, String] => F[Result[User]] = matches => Applicative[F].pure(successWith(Ok)(User(matches.mkString(","), 27)))
val send: () => F[Result[User]] = () => Applicative[F].pure(successWith(Ok)(User("send", 27)))
val get: () => F[Result[User]] = () => Applicative[F].pure(successWith(Ok)(User("get", 27)))
val put: () => F[Result[User]] = () => Applicative[F].pure(successWith(Ok)(User("put", 27)))
val putB: User => F[Result[User]] = user => Applicative[F].pure(successWith(Ok)(user))
val post: () => F[Result[User]] = () => Applicative[F].pure(successWith(Ok)(User("post", 27)))
val postB: User => F[Result[User]] = user => Applicative[F].pure(successWith(Ok)(user))
val delete: List[String] => F[Result[User]] = reasons => {
Applicative[F].pure(successWith(Ok)(User(reasons.mkString(","), 27)))
}
val code200: () => F[Result[String]] = () => Applicative[F].pure(successWith(Ok)(""))
val code400: () => F[Result[String]] = () => Applicative[F].pure(errorWith(BadRequest, "meh"))
val code500: () => F[Result[String]] = () => Applicative[F].pure(errorWith(InternalServerError, "boom"))
}
================================================
FILE: http4s-client/src/main/scala/typedapi/client/http4s/package.scala
================================================
package typedapi.client
import cats.{Monad, MonadError, Applicative}
import org.http4s._
import org.http4s.client._
import org.http4s.Status.Successful
import scala.language.higherKinds
package object http4s {
private implicit class Http4sRequestOps[F[_]](req: Request[F]) {
def withQuery(queries: Map[String, List[String]]): Request[F] = {
if (queries.nonEmpty) {
val q = org.http4s.Query.fromMap(queries)
val uri = Uri(req.uri.scheme, req.uri.authority, req.uri.path, q, req.uri.fragment)
req.withUri(uri)
}
else req
}
def withHeaders(headers: Map[String, String]): Request[F] = {
if (headers.nonEmpty) {
val h: List[Header] = headers.map { case (k, v) => org.http4s.Header(k, v) }(collection.breakOut)
req.withHeaders(Headers(h))
}
else req
}
def run(cm: ClientManager[Client[F]])(implicit F: Applicative[F]): F[Response[F]] =
cm.client.fetch(req)(resp => F.pure(resp))
}
private implicit class Http4sResponseOps[F[_]](resp: Response[F]) {
def decode[A](implicit d: EntityDecoder[F, A], F: MonadError[F, Throwable]): F[A] = resp match {
case Successful(_resp) =>
d.decode(_resp, strict = false).fold(throw _, identity)
case failedResponse =>
F.raiseError(UnexpectedStatus(failedResponse.status))
}
}
implicit def rawGetRequest[F[_]](implicit F: Applicative[F]) = new RawGetRequest[Client[F], F] {
type Resp = Response[F]
def apply(uri: List[String], queries: Map[String, List[String]], headers: Map[String, String], cm: ClientManager[Client[F]]): F[Resp] = {
val request = Request[F](Method.GET, Uri.unsafeFromString(deriveUriString(cm, uri)))
.withQuery(queries)
.withHeaders(headers)
request.run(cm)
}
}
implicit def getRequest[F[_], A](implicit decoder: EntityDecoder[F, A], F: MonadError[F, Throwable]) = new GetRequest[Client[F], F, A] {
private val raw = rawGetRequest[F]
def apply(uri: List[String], queries: Map[String, List[String]], headers: Map[String, String], cm: ClientManager[Client[F]]): F[A] =
F.flatMap(raw(uri, queries, headers, cm))(_.decode[A])
}
implicit def rawPutRequest[F[_]](implicit F: Applicative[F]) = new RawPutRequest[Client[F], F] {
type Resp = Response[F]
def apply(uri: List[String], queries: Map[String, List[String]], headers: Map[String, String], cm: ClientManager[Client[F]]): F[Resp] = {
val request = Request[F](Method.PUT, Uri.unsafeFromString(deriveUriString(cm, uri)))
.withQuery(queries)
.withHeaders(headers)
request.run(cm)
}
}
implicit def putRequest[F[_], A](implicit decoder: EntityDecoder[F, A], F: MonadError[F, Throwable]) = new PutRequest[Client[F], F, A] {
private val raw = rawPutRequest[F]
def apply(uri: List[String], queries: Map[String, List[String]], headers: Map[String, String], cm: ClientManager[Client[F]]): F[A] =
F.flatMap(raw(uri, queries, headers, cm))(_.decode[A])
}
implicit def rawPutBodyRequest[F[_], Bd](implicit encoder: EntityEncoder[F, Bd], F: Monad[F]) = new RawPutWithBodyRequest[Client[F], F, Bd] {
type Resp = Response[F]
def apply(uri: List[String], queries: Map[String, List[String]], headers: Map[String, String], body: Bd, cm: ClientManager[Client[F]]): F[Resp] = {
val requestF = Request[F](Method.PUT, Uri.unsafeFromString(deriveUriString(cm, uri)))
.withQuery(queries)
.withHeaders(headers)
.withBody(body)
F.flatMap(requestF)(_.run(cm))
}
}
implicit def putBodyRequest[F[_], Bd, A](implicit encoder: EntityEncoder[F, Bd],
decoder: EntityDecoder[F, A],
F: MonadError[F, Throwable]) = new PutWithBodyRequest[Client[F], F, Bd, A] {
private val raw = rawPutBodyRequest[F, Bd]
def apply(uri: List[String], queries: Map[String, List[String]], headers: Map[String, String], body: Bd, cm: ClientManager[Client[F]]): F[A] =
F.flatMap(raw(uri, queries, headers, body, cm))(_.decode[A])
}
implicit def rawPostRequest[F[_]](implicit F: Applicative[F]) = new RawPostRequest[Client[F], F] {
type Resp = Response[F]
def apply(uri: List[String], queries: Map[String, List[String]], headers: Map[String, String], cm: ClientManager[Client[F]]): F[Resp] = {
val request = Request[F](Method.POST, Uri.unsafeFromString(deriveUriString(cm, uri)))
.withQuery(queries)
.withHeaders(headers)
request.run(cm)
}
}
implicit def postRequest[F[_], A](implicit decoder: EntityDecoder[F, A], F: MonadError[F, Throwable]) = new PostRequest[Client[F], F, A] {
private val raw = rawPostRequest[F]
def apply(uri: List[String], queries: Map[String, List[String]], headers: Map[String, String], cm: ClientManager[Client[F]]): F[A] =
F.flatMap(raw(uri, queries, headers, cm))(_.decode[A])
}
implicit def rawPostBodyRequest[F[_], Bd](implicit encoder: EntityEncoder[F, Bd],
F: Monad[F]) = new RawPostWithBodyRequest[Client[F], F, Bd] {
type Resp = Response[F]
def apply(uri: List[String], queries: Map[String, List[String]], headers: Map[String, String], body: Bd, cm: ClientManager[Client[F]]): F[Resp] = {
val requestF = Request[F](Method.POST, Uri.unsafeFromString(deriveUriString(cm, uri)))
.withQuery(queries)
.withHeaders(headers)
.withBody(body)
F.flatMap(requestF)(_.run(cm))
}
}
implicit def postBodyRequest[F[_], Bd, A](implicit encoder: EntityEncoder[F, Bd],
decoder: EntityDecoder[F, A],
F: MonadError[F, Throwable]) = new PostWithBodyRequest[Client[F], F, Bd, A] {
private val raw = rawPostBodyRequest[F, Bd]
def apply(uri: List[String], queries: Map[String, List[String]], headers: Map[String, String], body: Bd, cm: ClientManager[Client[F]]): F[A] =
F.flatMap(raw(uri, queries, headers, body, cm))(_.decode[A])
}
implicit def rawDeleteRequest[F[_]](implicit F: Applicative[F]) = new RawDeleteRequest[Client[F], F] {
type Resp = Response[F]
def apply(uri: List[String], queries: Map[String, List[String]], headers: Map[String, String], cm: ClientManager[Client[F]]): F[Resp] = {
val request = Request[F](Method.DELETE, Uri.unsafeFromString(deriveUriString(cm, uri)))
.withQuery(queries)
.withHeaders(headers)
request.run(cm)
}
}
implicit def deleteRequest[F[_], A](implicit decoder: EntityDecoder[F, A], F: MonadError[F, Throwable]) = new DeleteRequest[Client[F], F, A] {
private val raw = rawDeleteRequest[F]
def apply(uri: List[String], queries: Map[String, List[String]], headers: Map[String, String], cm: ClientManager[Client[F]]): F[A] =
F.flatMap(raw(uri, queries, headers, cm))(_.decode[A])
}
}
================================================
FILE: http4s-server/src/main/scala/typedapi/server/htt4ps/package.scala
================================================
package typedapi.server
import typedapi.shared.MethodType
import cats.Monad
import cats.implicits._
import cats.effect.IO
import org.http4s._
import org.http4s.dsl._
import org.http4s.dsl.impl.EntityResponseGenerator
import org.http4s.server.Server
import org.http4s.server.blaze.BlazeBuilder
import shapeless._
import shapeless.ops.hlist.Prepend
import scala.language.higherKinds
package object http4s {
private def getHeaders(raw: Map[String, String]): List[Header.Raw] =
raw.map { case (key, value) => Header(key, value) }(collection.breakOut)
implicit def noReqBodyExecutor[El <: HList, KIn <: HList, VIn <: HList, M <: MethodType, F[_]: Monad, FOut](implicit encoder: EntityEncoder[F, FOut]) =
new NoReqBodyExecutor[El, KIn, VIn, M, F, FOut] {
type R = Request[F]
type Out = F[Response[F]]
private val dsl = Http4sDsl[F]
import dsl._
private def respGenerator(code: Status) = new EntityResponseGenerator[F] { val status = code }
def apply(req: R, eReq: EndpointRequest, endpoint: Endpoint[El, KIn, VIn, M, VIn, F, FOut]): Either[ExtractionError, Out] = {
extract(eReq, endpoint).map { extracted =>
Monad[F].flatMap(execute(extracted, endpoint)) {
case Right((code, response)) =>
respGenerator(Status(code.statusCode))(response, getHeaders(endpoint.headers): _*)
case Left(HttpError(code, msg)) =>
respGenerator(Status(code.statusCode))(msg, getHeaders(endpoint.headers): _*)
}
}
}
}
implicit def withReqBodyExecutor[El <: HList, KIn <: HList, VIn <: HList, Bd, M <: MethodType, ROut <: HList, POut <: HList, F[_]: Monad, FOut]
(implicit encoder: EntityEncoder[F, FOut],
decoder: EntityDecoder[F, Bd],
_prepend: Prepend.Aux[ROut, Bd :: HNil, POut],
_eqProof: POut =:= VIn) = new ReqBodyExecutor[El, KIn, VIn, Bd, M, ROut, POut, F, FOut] {
type R = Request[F]
type Out = F[Response[F]]
implicit val prepend = _prepend
implicit val eqProof = _eqProof
private val dsl = Http4sDsl[F]
import dsl._
private def respGenerator(code: Status) = new EntityResponseGenerator[F] { val status = code }
def apply(req: R, eReq: EndpointRequest, endpoint: Endpoint[El, KIn, VIn, M, (BodyType[Bd], ROut), F, FOut]): Either[ExtractionError, Out] = {
extract(eReq, endpoint).map { case (_, extracted) =>
for {
body <- req.as[Bd]
response <- execute(extracted, body, endpoint).flatMap {
case Right((code, response)) =>
respGenerator(Status(code.statusCode))(response, getHeaders(endpoint.headers): _*)
case Left(HttpError(code, msg)) =>
respGenerator(Status(code.statusCode))(msg, getHeaders(endpoint.headers): _*)
}
} yield response
}
}
}
implicit val mountEndpoints = new MountEndpoints[BlazeBuilder[IO], Request[IO], IO[Response[IO]]] {
import io._
type Out = IO[Server[IO]]
def apply(server: ServerManager[BlazeBuilder[IO]], endpoints: List[Serve[Request[IO], IO[Response[IO]]]]): Out = {
val service = HttpService[IO] {
case request =>
def execute(eps: List[Serve[Request[IO], IO[Response[IO]]]], eReq: EndpointRequest): IO[Response[IO]] = eps match {
case collection.immutable.::(endpoint, tail) => endpoint(request, eReq) match {
case Right(response) => response
case Left(RouteNotFound) => execute(tail, eReq)
case Left(BadRouteRequest(msg)) => io.BadRequest(msg)
}
case Nil => io.NotFound("uri = " + request.uri)
}
val eReq = EndpointRequest(
request.method.name,
{
val path = request.uri.path.split("/")
if (path.isEmpty) List.empty
else path.tail.toList
},
request.uri.multiParams.map { case (key, value) => key -> value.toList },
request.headers.toList.map(header => header.name.toString.toLowerCase -> header.value)(collection.breakOut)
)
if (request.method.name == "OPTIONS") {
IO(Response(headers = Headers(getHeaders(optionsHeaders(endpoints, eReq)))))
}
else
execute(endpoints, eReq)
}
server.server.bindHttp(server.port, server.host).mountService(service, "/").start
}
}
}
================================================
FILE: js-client/src/main/scala/typedapi/client/js/package.scala
================================================
package typedapi.client
import typedapi.util._
import org.scalajs.dom.XMLHttpRequest
import org.scalajs.dom.ext.Ajax
import scala.concurrent.{Future, ExecutionContext}
package object js {
private def renderQueries(queries: Map[String, List[String]]): String =
if (queries.nonEmpty)
queries
.map { case (key, values) => s"$key=${values.mkString(",")}" }
.mkString("?", "&", "")
else
""
private def flatten[A](decoded: Future[Either[Exception, A]])(implicit ec: ExecutionContext): Future[A] = decoded.flatMap {
case Right(a) => Future.successful(a)
case Left(error) => Future.failed(error)
}
implicit def rawGetRequest(implicit ec: ExecutionContext) = new RawGetRequest[Ajax.type, Future] {
type Resp = XMLHttpRequest
def apply(uri: List[String], queries: Map[String, List[String]], headers: Map[String, String], cm: ClientManager[Ajax.type]): Future[Resp] =
cm.client
.get(
url = deriveUriString(cm, uri) + renderQueries(queries),
headers = headers
)
}
implicit def getRequest[A](implicit decoder: Decoder[Future, A], ec: ExecutionContext) = new GetRequest[Ajax.type, Future, A] {
private val raw = rawGetRequest
def apply(uri: List[String], queries: Map[String, List[String]], headers: Map[String, String], cm: ClientManager[Ajax.type]): Future[A] =
raw(uri, queries, headers, cm).flatMap(response => flatten(decoder(response.responseText)))
}
implicit def rawPutRequest(implicit ec: ExecutionContext) = new RawPutRequest[Ajax.type, Future] {
type Resp = XMLHttpRequest
def apply(uri: List[String], queries: Map[String, List[String]], headers: Map[String, String], cm: ClientManager[Ajax.type]): Future[Resp] =
cm.client
.put(
url = deriveUriString(cm, uri) + renderQueries(queries),
headers = headers
)
}
implicit def putRequest[A](implicit decoder: Decoder[Future, A], ec: ExecutionContext) = new PutRequest[Ajax.type, Future, A] {
private val raw = rawPutRequest
def apply(uri: List[String], queries: Map[String, List[String]], headers: Map[String, String], cm: ClientManager[Ajax.type]): Future[A] =
raw(uri, queries, headers, cm).flatMap(response => flatten(decoder(response.responseText)))
}
implicit def rawPutBodyRequest[Bd](implicit encoder: Encoder[Future, Bd], ec: ExecutionContext) = new RawPutWithBodyRequest[Ajax.type, Future, Bd] {
type Resp = XMLHttpRequest
def apply(uri: List[String], queries: Map[String, List[String]], headers: Map[String, String], body: Bd, cm: ClientManager[Ajax.type]): Future[Resp] =
encoder(body).flatMap { encoded =>
cm.client
.put(
url = deriveUriString(cm, uri) + renderQueries(queries),
headers = headers,
data = encoded
)
}
}
implicit def putBodyRequest[Bd, A](implicit encoder: Encoder[Future, Bd], decoder: Decoder[Future, A], ec: ExecutionContext) = new PutWithBodyRequest[Ajax.type, Future, Bd, A] {
private val raw = rawPutBodyRequest[Bd]
def apply(uri: List[String], queries: Map[String, List[String]], headers: Map[String, String], body: Bd, cm: ClientManager[Ajax.type]): Future[A] =
raw(uri, queries, headers, body, cm).flatMap(response => flatten(decoder(response.responseText)))
}
implicit def rawPostRequest(implicit ec: ExecutionContext) = new RawPostRequest[Ajax.type, Future] {
type Resp = XMLHttpRequest
def apply(uri: List[String], queries: Map[String, List[String]], headers: Map[String, String], cm: ClientManager[Ajax.type]): Future[Resp] =
cm.client
.post(
url = deriveUriString(cm, uri) + renderQueries(queries),
headers = headers
)
}
implicit def postRequest[A](implicit decoder: Decoder[Future, A], ec: ExecutionContext) = new PostRequest[Ajax.type, Future, A] {
private val raw = rawPostRequest
def apply(uri: List[String], queries: Map[String, List[String]], headers: Map[String, String], cm: ClientManager[Ajax.type]): Future[A] =
raw(uri, queries, headers, cm).flatMap(response => flatten(decoder(response.responseText)))
}
implicit def rawPostBodyRequest[Bd](implicit encoder: Encoder[Future, Bd], ec: ExecutionContext) = new RawPostWithBodyRequest[Ajax.type, Future, Bd] {
type Resp = XMLHttpRequest
def apply(uri: List[String], queries: Map[String, List[String]], headers: Map[String, String], body: Bd, cm: ClientManager[Ajax.type]): Future[Resp] =
encoder(body).flatMap { encoded =>
cm.client
.post(
url = deriveUriString(cm, uri) + renderQueries(queries),
headers = headers,
data = encoded
)
}
}
implicit def postBodyRequest[Bd, A](implicit encoder: Encoder[Future, Bd], decoder: Decoder[Future, A], ec: ExecutionContext) = new PostWithBodyRequest[Ajax.type, Future, Bd, A] {
private val raw = rawPostBodyRequest[Bd]
def apply(uri: List[String], queries: Map[String, List[String]], headers: Map[String, String], body: Bd, cm: ClientManager[Ajax.type]): Future[A] =
raw(uri, queries, headers, body, cm).flatMap(response => flatten(decoder(response.responseText)))
}
implicit def rawDeleteRequest(implicit ec: ExecutionContext) = new RawDeleteRequest[Ajax.type, Future] {
type Resp = XMLHttpRequest
def apply(uri: List[String], queries: Map[String, List[String]], headers: Map[String, String], cm: ClientManager[Ajax.type]): Future[Resp] =
cm.client
.delete(
url = deriveUriString(cm, uri) + renderQueries(queries),
headers = headers
)
}
implicit def deleteRequest[A](implicit decoder: Decoder[Future, A], ec: ExecutionContext) = new DeleteRequest[Ajax.type, Future, A] {
private val raw = rawDeleteRequest
def apply(uri: List[String], queries: Map[String, List[String]], headers: Map[String, String], cm: ClientManager[Ajax.type]): Future[A] =
raw(uri, queries, headers, cm).flatMap(response => flatten(decoder(response.responseText)))
}
}
================================================
FILE: project/build.properties
================================================
sbt.version=1.0.4
================================================
FILE: project/build.scala
================================================
import sbt._
object Dependencies {
private val specs2V = "3.9.4"
val shared = Seq(
"com.chuusai" %% "shapeless" % "2.3.3" % Compile,
"org.specs2" %% "specs2-core" % specs2V % Test
)
val client = Seq(
"org.specs2" %% "specs2-core" % specs2V % Test
)
val server = Seq(
"org.specs2" %% "specs2-core" % specs2V % Test
)
private val http4sV = "0.18.0"
val http4sClient = Seq(
"org.http4s" %% "http4s-blaze-client" % http4sV % Provided,
)
val http4sServer = Seq(
"org.http4s" %% "http4s-blaze-server" % http4sV % Provided,
"org.http4s" %% "http4s-dsl" % http4sV % Provided
)
private val akkaHttpV = "10.0.13"
val akkaHttpClient = Seq(
"com.typesafe.akka" %% "akka-http" % akkaHttpV % Provided,
"com.typesafe.akka" %% "akka-http-core" % akkaHttpV % Provided
)
val akkaHttpServer = Seq(
"com.typesafe.akka" %% "akka-http" % akkaHttpV % Provided,
"com.typesafe.akka" %% "akka-http-core" % akkaHttpV % Provided
)
private val scalajHttpV = "2.4.1"
val scalajHttpClient = Seq(
"org.scalaj" %% "scalaj-http" % scalajHttpV % Provided
)
private val circeV = "0.9.1"
val httpSupportTests = Seq(
"org.specs2" %% "specs2-core" % specs2V % Test,
"org.http4s" %% "http4s-blaze-client" % http4sV % Test,
"org.http4s" %% "http4s-blaze-server" % http4sV % Test,
"org.http4s" %% "http4s-dsl" % http4sV % Test,
"org.http4s" %% "http4s-circe" % http4sV % Test,
"com.typesafe.akka" %% "akka-http" % akkaHttpV % Test,
"org.scalaj" %% "scalaj-http" % scalajHttpV % Test,
"io.circe" %% "circe-core" % circeV % Test,
"io.circe" %% "circe-parser" % circeV % Test,
"io.circe" %% "circe-generic" % circeV % Test,
"de.heikoseeberger" %% "akka-http-circe" % "1.21.0" % Test,
"org.specs2" %% "specs2-core" % specs2V % Test
)
val ammoniteSupport = Seq(
"org.scalaj" %% "scalaj-http" % scalajHttpV % Compile
)
}
================================================
FILE: project/plugins.sbt
================================================
addSbtPlugin("org.xerial.sbt" % "sbt-sonatype" % "2.3")
addSbtPlugin("com.jsuereth" % "sbt-pgp" % "1.1.0")
addSbtPlugin("org.scala-js" % "sbt-scalajs" % "0.6.22")
================================================
FILE: scalaj-http-client/src/main/scala/typedapi/client/scalajhttp/package.scala
================================================
package typedapi.client
import typedapi.util._
import scalaj.http._
package object scalajhttp {
type Id[A] = A
type Blocking[A] = Either[Exception, A]
private def reduceQueries(queries: Map[String, List[String]]): Map[String, String] =
queries.map { case (key, values) => key -> values.mkString(",") }(collection.breakOut)
implicit def rawGetRequest = new RawGetRequest[Http.type, Id] {
type Resp = HttpResponse[String]
def apply(uri: List[String], queries: Map[String, List[String]], headers: Map[String, String], cm: ClientManager[Http.type]): Id[Resp] = {
val req = cm.client(deriveUriString(cm, uri)).params(reduceQueries(queries)).headers(headers).method("GET")
req.asString
}
}
implicit def getRequest[A](implicit decoder: Decoder[Id, A]) = new GetRequest[Http.type, Blocking, A] {
private val raw = rawGetRequest
def apply(uri: List[String], queries: Map[String, List[String]], headers: Map[String, String], cm: ClientManager[Http.type]): Blocking[A] =
decoder(raw(uri, queries, headers, cm).body)
}
implicit def rawPutRequest = new RawPutRequest[Http.type, Id] {
type Resp = HttpResponse[String]
def apply(uri: List[String], queries: Map[String, List[String]], headers: Map[String, String], cm: ClientManager[Http.type]): Id[Resp] = {
val req = cm.client(deriveUriString(cm, uri)).params(reduceQueries(queries)).headers(headers).method("PUT")
req.asString
}
}
implicit def putRequest[A](implicit decoder: Decoder[Id, A]) = new PutRequest[Http.type, Blocking, A] {
private val raw = rawPutRequest
def apply(uri: List[String], queries: Map[String, List[String]], headers: Map[String, String], cm: ClientManager[Http.type]): Blocking[A] =
decoder(raw(uri, queries, headers, cm).body)
}
implicit def rawPutBodyRequest[Bd](implicit encoder: Encoder[Id, Bd]) = new RawPutWithBodyRequest[Http.type, Id, Bd] {
type Resp = HttpResponse[String]
def apply(uri: List[String], queries: Map[String, List[String]], headers: Map[String, String], body: Bd, cm: ClientManager[Http.type]): Id[Resp] = {
val req = cm.client(deriveUriString(cm, uri)).params(reduceQueries(queries)).headers(headers).put(encoder(body))
req.asString
}
}
implicit def putBodyRequest[Bd, A](implicit encoder: Encoder[Id, Bd], decoder: Decoder[Id, A]) = new PutWithBodyRequest[Http.type, Blocking, Bd, A] {
private val raw = rawPutBodyRequest[Bd]
def apply(uri: List[String], queries: Map[String, List[String]], headers: Map[String, String], body: Bd, cm: ClientManager[Http.type]): Blocking[A] =
decoder(raw(uri, queries, headers, body, cm).body)
}
implicit def rawPostRequest = new RawPostRequest[Http.type, Id] {
type Resp = HttpResponse[String]
def apply(uri: List[String], queries: Map[String, List[String]], headers: Map[String, String], cm: ClientManager[Http.type]): Id[Resp] = {
val req = cm.client(deriveUriString(cm, uri)).params(reduceQueries(queries)).headers(headers).method("POST")
req.asString
}
}
implicit def postRequest[A](implicit decoder: Decoder[Id, A]) = new PostRequest[Http.type, Blocking, A] {
private val raw = rawPostRequest
def apply(uri: List[String], queries: Map[String, List[String]], headers: Map[String, String], cm: ClientManager[Http.type]): Blocking[A] =
decoder(raw(uri, queries, headers, cm).body)
}
implicit def rawPostBodyRequest[Bd](implicit encoder: Encoder[Id, Bd]) = new RawPostWithBodyRequest[Http.type, Id, Bd] {
type Resp = HttpResponse[String]
def apply(uri: List[String], queries: Map[String, List[String]], headers: Map[String, String], body: Bd, cm: ClientManager[Http.type]): Id[Resp] = {
val req = cm.client(deriveUriString(cm, uri)).params(reduceQueries(queries)).headers(headers).postData(encoder(body))
req.asString
}
}
implicit def postBodyRequest[Bd, A](implicit encoder: Encoder[Id, Bd], decoder: Decoder[Id, A]) = new PostWithBodyRequest[Http.type, Blocking, Bd, A] {
private val raw = rawPostBodyRequest[Bd]
def apply(uri: List[String], queries: Map[String, List[String]], headers: Map[String, String], body: Bd, cm: ClientManager[Http.type]): Blocking[A] =
decoder(raw(uri, queries, headers, body, cm).body)
}
implicit def rawDeleteRequest = new RawDeleteRequest[Http.type, Id] {
type Resp = HttpResponse[String]
def apply(uri: List[String], queries: Map[String, List[String]], headers: Map[String, String], cm: ClientManager[Http.type]): Id[Resp] = {
val req = cm.client(deriveUriString(cm, uri)).params(reduceQueries(queries)).headers(headers).method("DELETE")
req.asString
}
}
implicit def deleteRequest[A](implicit decoder: Decoder[Id, A]) = new DeleteRequest[Http.type, Blocking, A] {
private val raw = rawDeleteRequest
def apply(uri: List[String], queries: Map[String, List[String]], headers: Map[String, String], cm: ClientManager[Http.type]): Blocking[A] =
decoder(raw(uri, queries, headers, cm).body)
}
}
================================================
FILE: server/src/main/scala/typedapi/server/Endpoint.scala
================================================
package typedapi.server
import typedapi.shared._
import shapeless._
import shapeless.labelled.FieldType
import shapeless.ops.function._
import scala.language.higherKinds
/** Represents a server endpoint and is basically a function which gets the expected input `VIn` and returns the expected output. */
abstract class Endpoint[El <: HList, KIn <: HList, VIn <: HList, M <: MethodType, ROut, F[_], Out]
(val method: String, val extractor: RouteExtractor.Aux[El, KIn, VIn, M, HNil, ROut], val headers: Map[String, String]) {
def apply(in: VIn): F[Result[Out]]
}
/** Request representation which every server implementation has to provide. */
final case class EndpointRequest(method: String,
uri: List[String],
queries: Map[String, List[String]],
headers: Map[String, String])
final class ExecutableDerivation[F[_]] {
final class Derivation[El <: HList, KIn <: HList, VIn <: HList, M <: MethodType, ROut, Fn, Out]
(extractor: RouteExtractor.Aux[El, KIn, VIn, M, HNil, ROut],
method: String,
headers: Map[String, String],
fnToVIn: FnToProduct.Aux[Fn, VIn => F[Result[Out]]]) {
/** Restricts type of parameter `fn` to a function defined by the given API:
*
* {{{
* val Api = := :> Segment[String]('name) :> Get[User]
*
* derive[IO](Api).from(name: String => IO.pure(User(name)))
* }}}
*/
def from(fn: Fn): Endpoint[El, KIn, VIn, M, ROut, F, Out] =
new Endpoint[El, KIn, VIn, M, ROut, F, Out](method, extractor, headers) {
private val fin = fnToVIn(fn)
def apply(in: VIn): F[Result[Out]] = fin(in)
}
}
def apply[H <: HList, FH <: HList, El <: HList, KIn <: HList, VIn <: HList, ROut, Fn, M <: MethodType, MT <: MediaType, Out]
(apiList: ApiTypeCarrier[H])
(implicit filter: FilterClientElements.Aux[H, FH],
folder: Lazy[TypeLevelFoldLeft.Aux[FH, Unit, (El, KIn, VIn, M, FieldType[MT, Out])]],
extractor: RouteExtractor.Aux[El, KIn, VIn, M, HNil, ROut],
methodShow: MethodToString[M],
serverHeaders: ServerHeaderExtractor[El],
inToFn: Lazy[FnFromProduct.Aux[VIn => F[Result[Out]], Fn]],
fnToVIn: Lazy[FnToProduct.Aux[Fn, VIn => F[Result[Out]]]]): Derivation[El, KIn, VIn, M, ROut, Fn, Out] =
new Derivation[El, KIn, VIn, M, ROut, Fn, Out](extractor, methodShow.show, serverHeaders(Map.empty), fnToVIn.value)
}
================================================
FILE: server/src/main/scala/typedapi/server/EndpointComposition.scala
================================================
package typedapi.server
import typedapi.shared._
import shapeless._
import shapeless.labelled.FieldType
import shapeless.ops.function._
import scala.language.higherKinds
import scala.annotation.implicitNotFound
/** Fuses [[RouteExtractor]] and the endpoint function into an [[Endpoint]]. */
trait EndpointConstructor[F[_], Fn, El <: HList, KIn <: HList, VIn <: HList, M <: MethodType, ROut, Out] {
def apply(fn: Fn): Endpoint[El, KIn, VIn, M, ROut, F, Out]
}
/** Compiles RouteExtractor and FunApply for every API endpoint and generates expected list of endpoint functions. */
@implicitNotFound("""Could not precompile your API. This can happen when you try to extract an value from the route which is not supported (ValueExtractor in RouteExtractor.scala)
transformed: ${H}""")
sealed trait PrecompileEndpoint[F[_], H <: HList] {
// list of expected endpoint functions
type Fns <: HList
// list of endpoint constructors
type Consts <: HList
def constructors: Consts
}
object PrecompileEndpoint extends PrecompileEndpointLowPrio {
type Aux[F[_], H <: HList, Fns0 <: HList, Consts0 <: HList] = PrecompileEndpoint[F, H] {
type Fns = Fns0
type Consts = Consts0
}
}
trait PrecompileEndpointLowPrio {
implicit def hnilPrecompiledCase[F[_]] = new PrecompileEndpoint[F, HNil] {
type Fns = HNil
type Consts = HNil
val constructors = HNil
}
implicit def constructorsCase[F[_], Fn, El <: HList, KIn <: HList, VIn <: HList, MT <: MediaType, Out, M <: MethodType, ROut, T <: HList]
(implicit extractor: RouteExtractor.Aux[El, KIn, VIn, M, HNil, ROut],
methodShow: MethodToString[M],
serverHeaders: ServerHeaderExtractor[El],
vinToFn: FnFromProduct.Aux[VIn => F[Result[Out]], Fn],
fnToVIn: Lazy[FnToProduct.Aux[Fn, VIn => F[Result[Out]]]],
next: PrecompileEndpoint[F, T]) =
new PrecompileEndpoint[F, (El, KIn, VIn, M, FieldType[MT, Out]) :: T] {
type Fns = Fn :: next.Fns
type Consts = EndpointConstructor[F, Fn, El, KIn, VIn, M, ROut, Out] :: next.Consts
val constructor = new EndpointConstructor[F, Fn, El, KIn, VIn, M, ROut, Out] {
def apply(fn: Fn): Endpoint[El, KIn, VIn, M, ROut, F, Out] = new Endpoint[El, KIn, VIn, M, ROut, F, Out](methodShow.show, extractor, serverHeaders(Map.empty)) {
private val fin = fnToVIn.value(fn)
def apply(in: VIn): F[Result[Out]] = fin(in)
}
}
val constructors = constructor :: next.constructors
}
}
@implicitNotFound("""Whoops, you should not be here. This seems to be a bug.
constructors: ${Consts}
functions: ${Fns}""")
sealed trait MergeToEndpoint[F[_], Consts <: HList, Fns <: HList] {
type Out <: HList
def apply(constructors: Consts, fns: Fns): Out
}
object MergeToEndpoint extends MergeToEndpointLowPrio {
type Aux[F[_], Consts <: HList, Fns <: HList, Out0 <: HList] = MergeToEndpoint[F, Consts, Fns] { type Out = Out0 }
}
trait MergeToEndpointLowPrio {
implicit def hnilMergeCase[F[_]] = new MergeToEndpoint[F, HNil, HNil] {
type Out = HNil
def apply(constructors: HNil, fns: HNil): Out = HNil
}
implicit def mergeCase[F[_], El <: HList, KIn <: HList, VIn <: HList, Out0, M <: MethodType, ROut, Consts <: HList, Fn, Fns <: HList]
(implicit next: MergeToEndpoint[F, Consts, Fns]) =
new MergeToEndpoint[F, EndpointConstructor[F, Fn, El, KIn, VIn, M, ROut, Out0] :: Consts, Fn :: Fns] {
type Out = Endpoint[El, KIn, VIn, M, ROut, F, Out0] :: next.Out
def apply(constructors: EndpointConstructor[F, Fn, El, KIn, VIn, M, ROut, Out0] :: Consts, fns: Fn :: Fns): Out = {
val endpoint = constructors.head(fns.head)
endpoint :: next(constructors.tail, fns.tail)
}
}
}
final class ExecutableCompositionDerivation[F[_]] {
final class Derivation[H <: HList, Fns <: HList, Consts <: HList, Out <: HList, Drv](pre: PrecompileEndpoint.Aux[F, H, Fns, Consts],
merge: MergeToEndpoint.Aux[F, Consts, Fns, Out],
derived: FnFromProduct.Aux[Fns => Out, Drv]) {
/** Restricts type of input parameter to a composition of functions defined by the precompile-stage.
*
* {{{
* val Api =
* (:= :> Segment[String]('name) :> Get[User]) :|:
* (:= :> "foo" :> Segment[String]('name) :> Get[User])
*
* val f0: String => IO[User] = name => IO.pure(User(name))
* val f1: String => IO[User] = name => IO.pure(User(name))
* deriveAll[IO](Api).from(f0 _, f1 _)
* }}}
*/
val from: Drv = derived.apply(fns => merge(pre.constructors, fns))
}
def apply[H <: HList, FH <: HList, Fold <: HList, Fns <: HList, FnsTup, Consts <: HList, Out <: HList, Drv](apiLists: CompositionCons[H])
(implicit filter: FilterClientElementsList.Aux[H, FH],
folder: TypeLevelFoldLeftList.Aux[FH, Fold],
pre: PrecompileEndpoint.Aux[F, Fold, Fns, Consts],
merge: MergeToEndpoint.Aux[F, Consts, Fns, Out],
derived: FnFromProduct.Aux[Fns => Out, Drv]): Derivation[Fold, Fns, Consts, Out, Drv] =
new Derivation[Fold, Fns, Consts, Out, Drv](pre, merge, derived)
}
================================================
FILE: server/src/main/scala/typedapi/server/EndpointExecutor.scala
================================================
package typedapi.server
import typedapi.shared.MethodType
import shapeless._
import shapeless.ops.hlist.Prepend
import scala.language.higherKinds
import scala.annotation.implicitNotFound
@implicitNotFound("""Cannot find EndpointExecutor. Do you miss some implicit values e.g. encoder/decoder?
elements: ${El}
input keys: ${KIn}
input values: ${VIn}""")
sealed trait EndpointExecutor[El <: HList, KIn <: HList, VIn <: HList, M <: MethodType, ROut, F[_], FOut] {
type R
type Out
def extract(eReq: EndpointRequest, endpoint: Endpoint[El, KIn, VIn, M, ROut, F, FOut]): Either[ExtractionError, ROut] =
endpoint.extractor(eReq, HNil)
def apply(req: R, eReq: EndpointRequest, endpoint: Endpoint[El, KIn, VIn, M, ROut, F, FOut]): Either[ExtractionError, Out]
}
object EndpointExecutor {
type Aux[R0, El <: HList, KIn <: HList, VIn <: HList, M <: MethodType, ROut, F[_], FOut, Out0] = EndpointExecutor[El, KIn, VIn, M, ROut, F, FOut] {
type R = R0
type Out = Out0
}
}
trait NoReqBodyExecutor[El <: HList, KIn <: HList, VIn <: HList, M <: MethodType, F[_], FOut] extends EndpointExecutor[El, KIn, VIn, M, VIn, F, FOut] {
protected def execute(input: VIn, endpoint: Endpoint[El, KIn, VIn, M, VIn, F, FOut]): F[Result[FOut]] =
endpoint.apply(input)
}
trait ReqBodyExecutor[El <: HList, KIn <: HList, VIn <: HList, Bd, M <: MethodType, ROut <: HList, POut <: HList, F[_], FOut] extends EndpointExecutor[El, KIn, VIn, M, (BodyType[Bd], ROut), F, FOut] {
implicit def prepend: Prepend.Aux[ROut, Bd :: HNil, POut]
implicit def eqProof: POut =:= VIn
protected def execute(input: ROut, body: Bd, endpoint: Endpoint[El, KIn, VIn, M, (BodyType[Bd], ROut), F, FOut]): F[Result[FOut]] =
endpoint.apply(input :+ body)
}
================================================
FILE: server/src/main/scala/typedapi/server/FilterClientElements.scala
================================================
package typedapi.server
import typedapi.shared.{ClientHeaderElement, ClientHeaderParam, ClientHeaderCollParam}
import shapeless._
sealed trait FilterClientElements[H <: HList] {
type Out <: HList
}
sealed trait FilterClientElementsLowPrio {
implicit val filterClientResult = new FilterClientElements[HNil] {
type Out = HNil
}
implicit def filterClientKeep[El, T <: HList](implicit next: FilterClientElements[T]) = new FilterClientElements[El :: T] {
type Out = El :: next.Out
}
}
object FilterClientElements extends FilterClientElementsLowPrio {
type Aux[H <: HList, Out0 <: HList] = FilterClientElements[H] { type Out = Out0 }
implicit def filterClientEl[K, V, T <: HList](implicit next: FilterClientElements[T]) = new FilterClientElements[ClientHeaderElement[K, V] :: T] {
type Out = next.Out
}
implicit def filterClientParam[K, V, T <: HList](implicit next: FilterClientElements[T]) = new FilterClientElements[ClientHeaderParam[K, V] :: T] {
type Out = next.Out
}
implicit def filterClientCollParam[V, T <: HList](implicit next: FilterClientElements[T]) = new FilterClientElements[ClientHeaderCollParam[V] :: T] {
type Out = next.Out
}
}
sealed trait FilterClientElementsList[H <: HList] {
type Out <: HList
}
object FilterClientElementsList {
type Aux[H <: HList, Out0 <: HList] = FilterClientElementsList[H] { type Out = Out0 }
implicit val filterClientListResult = new FilterClientElementsList[HNil] {
type Out = HNil
}
implicit def filterClientListStep[Api <: HList, T <: HList](implicit filtered: FilterClientElements[Api], next: FilterClientElementsList[T]) =
new FilterClientElementsList[Api :: T] {
type Out = filtered.Out :: next.Out
}
}
================================================
FILE: server/src/main/scala/typedapi/server/RouteExtractor.scala
================================================
package typedapi.server
import typedapi.shared._
import shapeless.{HList, HNil, Witness}
import shapeless.labelled.FieldType
import shapeless.ops.hlist.Reverse
import scala.util.Try
import scala.annotation.implicitNotFound
sealed trait ExtractionError
case object RouteNotFound extends ExtractionError
final case class BadRouteRequest(msg: String) extends ExtractionError
/** Builds a function which extracts inputs from a given requests based on the API.
* - if a request path does not fit the API definition `RouteNotFound` is returned
* - if a query, header, body, etc is missing `BadRouteRequest` is returned
*/
@implicitNotFound("""Cannot find RouteExtractor. Maybe a ValueExtractor could not be found.
elements: ${El}
input keys: ${KIn}
inout values: ${VIn}
method: ${M}""")
trait RouteExtractor[El <: HList, KIn <: HList, VIn <: HList, M <: MethodType, EIn <: HList] {
type Out
def apply(request: EndpointRequest, inAgg: EIn): Either[ExtractionError, Out]
}
object RouteExtractor extends RouteExtractorMediumPrio {
type Aux[El <: HList, KIn <: HList, VIn <: HList, M <: MethodType, EIn <: HList, Out0] = RouteExtractor[El, KIn, VIn, M, EIn] { type Out = Out0 }
type Extract[Out] = Either[ExtractionError, Out]
def NotFoundE[Out]: Extract[Out] = Left(RouteNotFound)
def BadRequestE[Out](msg: String): Extract[Out] = Left(BadRouteRequest(msg))
}
trait RouteExtractorLowPrio {
import RouteExtractor._
implicit def pathExtractor[S, El <: HList, KIn <: HList, VIn <: HList, M <: MethodType, EIn <: HList]
(implicit wit: Witness.Aux[S], show: WitnessToString[S], next: RouteExtractor[El, KIn, VIn, M, EIn]) =
new RouteExtractor[shapeless.::[S, El], KIn, VIn, M, EIn] {
type Out = next.Out
def apply(request: EndpointRequest, inAgg: EIn): Extract[Out] = request.uri match {
case p :: tail =>
if (p == show.show(wit.value))
next(request.copy(uri = tail), inAgg)
else
NotFoundE
case Nil => NotFoundE
}
}
}
trait RouteExtractorMediumPrio extends RouteExtractorLowPrio {
import RouteExtractor._
private def checkEmptyPath[Out](request: EndpointRequest)(f: EndpointRequest => Extract[Out]): Extract[Out] =
if (request.uri.isEmpty)
f(request)
else
NotFoundE
implicit def segmentExtractor[El <: HList, K, V, KIn <: HList, VIn <: HList, M <: MethodType, EIn <: HList](implicit value: ValueExtractor[V], next: RouteExtractor[El, KIn, VIn, M, shapeless.::[V, EIn]]) =
new RouteExtractor[shapeless.::[SegmentInput, El], shapeless.::[K, KIn], shapeless.::[V, VIn], M, EIn] {
type Out = next.Out
def apply(request: EndpointRequest, inAgg: EIn): Extract[Out] = request.uri match {
case p :: tail => value(p).fold(NotFoundE[Out])(v => next(request.copy(uri = tail), v :: inAgg))
case Nil => NotFoundE
}
}
implicit def queryExtractor[El <: HList, K, V, KIn <: HList, VIn <: HList, M <: MethodType, EIn <: HList]
(implicit wit: Witness.Aux[K], show: WitnessToString[K], value: ValueExtractor[V], next: RouteExtractor[El, KIn, VIn, M, shapeless.::[V, EIn]]) =
new RouteExtractor[shapeless.::[QueryInput, El], shapeless.::[K, KIn], shapeless.::[V, VIn], M, EIn] {
type Out = next.Out
def apply(request: EndpointRequest, inAgg: EIn): Extract[Out] = checkEmptyPath(request) { req =>
val key = show.show(wit.value)
req.queries.get(key).fold(BadRequestE[Out](s"missing query '$key'")) { raw =>
raw.headOption.flatMap(value.apply).fold(BadRequestE[Out](s"query '$key' has not type ${value.typeDesc}")) { v =>
next(request, v :: inAgg)
}
}
}
}
implicit def queryOptListExtractor[El <: HList, K, V, KIn <: HList, VIn <: HList, M <: MethodType, EIn <: HList]
(implicit wit: Witness.Aux[K], show: WitnessToString[K], value: ValueExtractor[V], next: RouteExtractor[El, KIn, VIn, M, shapeless.::[Option[V], EIn]]) =
new RouteExtractor[shapeless.::[QueryInput, El], shapeless.::[K, KIn], shapeless.::[Option[V], VIn], M, EIn] {
type Out = next.Out
def apply(request: EndpointRequest, inAgg: EIn): Extract[Out] = checkEmptyPath(request) { req =>
val key = show.show(wit.value)
req.queries.get(key).fold(next(request, None :: inAgg)) { raw =>
raw.headOption.flatMap(value.apply).fold(BadRequestE[Out](s"query '$key' has not type ${value.typeDesc}")) { v =>
next(request, Some(v) :: inAgg)
}
}
}
}
implicit def queryListExtractor[El <: HList, K, V, KIn <: HList, VIn <: HList, M <: MethodType, EIn <: HList]
(implicit wit: Witness.Aux[K], show: WitnessToString[K], value: ValueExtractor[V], next: RouteExtractor[El, KIn, VIn, M, shapeless.::[List[V], EIn]]) =
new RouteExtractor[shapeless.::[QueryInput, El], shapeless.::[K, KIn], shapeless.::[List[V], VIn], M, EIn] {
type Out = next.Out
def apply(request: EndpointRequest, inAgg: EIn): Extract[Out] = checkEmptyPath(request) { req =>
val key = show.show(wit.value)
req.queries.get(key).fold(next(request, Nil :: inAgg)) { raw =>
val vs = raw.flatMap(value.apply)
if (vs.length < raw.length)
BadRequestE(s"some values of query '$key' are no ${value.typeDesc}")
else
next(request, vs :: inAgg)
}
}
}
implicit def headerExtractor[El <: HList, K, V, KIn <: HList, VIn <: HList, M <: MethodType, EIn <: HList]
(implicit wit: Witness.Aux[K], show: WitnessToString[K], value: ValueExtractor[V], next: RouteExtractor[El, KIn, VIn, M, shapeless.::[V, EIn]]) =
new RouteExtractor[shapeless.::[HeaderInput, El], shapeless.::[K, KIn], shapeless.::[V, VIn], M, EIn] {
type Out = next.Out
def apply(request: EndpointRequest, inAgg: EIn): Extract[Out] = checkEmptyPath(request) { req =>
val key = show.show(wit.value).toLowerCase
req.headers.get(key).fold(BadRequestE[Out](s"missing header '$key'")) { raw =>
value(raw).fold(BadRequestE[Out](s"header '$key' has not type ${value.typeDesc}")) { v =>
next(request, v :: inAgg)
}
}
}
}
implicit def headerOptExtractor[El <: HList, K, V, KIn <: HList, VIn <: HList, M <: MethodType, EIn <: HList]
(implicit wit: Witness.Aux[K], show: WitnessToString[K], value: ValueExtractor[V], next: RouteExtractor[El, KIn, VIn, M, shapeless.::[Option[V], EIn]]) =
new RouteExtractor[shapeless.::[HeaderInput, El], shapeless.::[K, KIn], shapeless.::[Option[V], VIn], M, EIn] {
type Out = next.Out
def apply(request: EndpointRequest, inAgg: EIn): Extract[Out] = checkEmptyPath(request) { req =>
val key = show.show(wit.value)
req.headers.get(key.toLowerCase).fold(next(request, None :: inAgg)) { raw =>
value(raw).fold(BadRequestE[Out](s"header '$key' has not type ${value.typeDesc}")) { v =>
next(request, Some(v) :: inAgg)
}
}
}
}
implicit def fixedHeaderExtractor[K, V, El <: HList, KIn <: HList, VIn <: HList, M <: MethodType, EIn <: HList]
(implicit kWit: Witness.Aux[K], kShow: WitnessToString[K], vWit: Witness.Aux[V], vShow: WitnessToString[V], next: RouteExtractor[El, KIn, VIn, M, EIn]) =
new RouteExtractor[shapeless.::[FixedHeader[K, V], El], KIn, VIn, M, EIn] {
type Out = next.Out
def apply(request: EndpointRequest, inAgg: EIn): Extract[Out] = checkEmptyPath(request) { req =>
val key = kShow.show(kWit.value)
val value = vShow.show(vWit.value)
req.headers.get(key.toLowerCase).fold(BadRequestE[Out](s"missing header '$key'")) { raw =>
if (raw != value)
BadRequestE[Out](s"header '$key' has unexpected value '${raw}' - expected '${value}'")
else
next(request, inAgg)
}
}
}
implicit def ignoreServerHeaderSendExtractor[K, V, El <: HList, KIn <: HList, VIn <: HList, M <: MethodType, EIn <: HList]
(implicit next: RouteExtractor[El, KIn, VIn, M, EIn]) =
new RouteExtractor[shapeless.::[ServerHeaderSend[K, V], El], KIn, VIn, M, EIn] {
type Out = next.Out
def apply(request: EndpointRequest, inAgg: EIn): Extract[Out] = checkEmptyPath(request) { req =>
next(request, inAgg)
}
}
implicit def serverHeaderMatchExtractor[El <: HList, K, V, KIn <: HList, VIn <: HList, M <: MethodType, EIn <: HList]
(implicit wit: Witness.Aux[K], show: WitnessToString[K], value: ValueExtractor[V], next: RouteExtractor[El, KIn, VIn, M, shapeless.::[Map[String, V], EIn]]) =
new RouteExtractor[shapeless.::[ServerHeaderMatchInput, El], shapeless.::[K, KIn], shapeless.::[Map[String, V], VIn], M, EIn] {
type Out = next.Out
def apply(request: EndpointRequest, inAgg: EIn): Extract[Out] = checkEmptyPath(request) { req =>
val key = show.show(wit.value).toLowerCase
val matchesE = req.headers.foldLeft[Either[ExtractionError, Map[String, V]]](Right(Map.empty)) {
case (Right(agg), (k, raw)) =>
if (k.contains(key)) {
value(raw).fold(BadRequestE[Map[String, V]](s"header '$k' has not type ${value.typeDesc}")) { v =>
Right(agg + (k -> v))
}
}
else
Right(agg)
case (error, _) => error
}
matchesE.fold(
error => Left(error),
matches => next(request, matches :: inAgg)
)
}
}
implicit def getExtractor[EIn <: HList, REIn <: HList](implicit rev: Reverse.Aux[EIn, REIn]) = new RouteExtractor[HNil, HNil, HNil, GetCall, EIn] {
type Out = REIn
def apply(request: EndpointRequest, inAgg: EIn): Extract[Out] = checkEmptyPath(request) { req =>
if (req.method == "GET" || req.method == "OPTIONS")
Right(inAgg.reverse)
else
NotFoundE
}
}
implicit def putExtractor[EIn <: HList, REIn <: HList](implicit rev: Reverse.Aux[EIn, REIn]) = new RouteExtractor[HNil, HNil, HNil, PutCall, EIn] {
type Out = REIn
def apply(request: EndpointRequest, inAgg: EIn): Extract[Out] = checkEmptyPath(request) { req =>
if (req.method == "PUT" || req.method == "OPTIONS")
Right(inAgg.reverse)
else
NotFoundE
}
}
implicit def putWithBodyExtractor[BMT <: MediaType, Bd, EIn <: HList] =
new RouteExtractor[HNil, shapeless.::[FieldType[BMT, BodyField.T], HNil], shapeless.::[Bd, HNil], PutWithBodyCall, EIn] {
type Out = (BodyType[Bd], EIn)
def apply(request: EndpointRequest, inAgg: EIn): Extract[Out] = checkEmptyPath(request) { req =>
if (req.method == "PUT" || req.method == "OPTIONS")
Right((BodyType[Bd], inAgg))
else
NotFoundE
}
}
implicit def postExtractor[EIn <: HList] = new RouteExtractor[HNil, HNil, HNil, PostCall, EIn] {
type Out = EIn
def apply(request: EndpointRequest, inAgg: EIn): Extract[Out] = checkEmptyPath(request) { req =>
if (req.method == "POST" || req.method == "OPTIONS")
Right(inAgg)
else
NotFoundE
}
}
implicit def postWithBodyExtractor[BMT <: MediaType, Bd, EIn <: HList, REIn <: HList] =
new RouteExtractor[HNil, shapeless.::[FieldType[BMT, BodyField.T], HNil], shapeless.::[Bd, HNil], PostWithBodyCall, EIn] {
type Out = (BodyType[Bd], EIn)
def apply(request: EndpointRequest, inAgg: EIn): Extract[Out] = checkEmptyPath(request) { req =>
if (req.method == "POST" || req.method == "OPTIONS")
Right((BodyType[Bd], inAgg))
else
NotFoundE
}
}
implicit def deleteExtractor[EIn <: HList] = new RouteExtractor[HNil, HNil, HNil, DeleteCall, EIn] {
type Out = EIn
def apply(request: EndpointRequest, inAgg: EIn): Extract[Out] = checkEmptyPath(request) { req =>
if (req.method == "DELETE" || req.method == "OPTIONS")
Right(inAgg)
else
NotFoundE
}
}
}
trait ValueExtractor[A] extends (String => Option[A]) {
def typeDesc: String
}
object ValueExtractor extends ValueExtractorInstances
trait ValueExtractorInstances {
def extract[A](f: String => A, _typeDesc: String) = new ValueExtractor[A] {
val typeDesc = _typeDesc
def apply(raw: String): Option[A] = Try(f(raw)).toOption
}
implicit val booleanExtractor = extract[Boolean](_.toBoolean, "Boolean")
implicit val byteExtractor = extract[Byte](_.toByte, "Byte")
implicit val shortExtractor = extract[Short](_.toShort, "Short")
implicit val intExtractor = extract[Int](_.toInt, "Int")
implicit val longExtractor = extract[Long](_.toLong, "Long")
implicit val floatExtractor = extract[Float](_.toFloat, "Float")
implicit val doubleExtractor = extract[Double](_.toDouble, "Double")
implicit val stringExtractor = extract[String](identity, "String")
}
final case class BodyType[Bd]()
================================================
FILE: server/src/main/scala/typedapi/server/Serve.scala
================================================
package typedapi.server
/** Reduces an Endpoint and its EndpointExecutor to a simple Request => Response function. */
trait Serve[Req, Resp] {
def options(eReq: EndpointRequest): Option[(String, Map[String, String])]
def apply(req: Req, eReq: EndpointRequest): Either[ExtractionError, Resp]
}
================================================
FILE: server/src/main/scala/typedapi/server/ServeToList.scala
================================================
package typedapi.server
import shapeless._
import scala.annotation.implicitNotFound
@implicitNotFound("""Cannot find ServeToList instance to map Serve HList to List. Maybe you have different Request, Response types defined?.
serves: ${H}
request: ${Req}
response: ${Resp}""")
sealed trait ServeToList[H <: HList, Req, Resp] {
def apply(h: H): List[Serve[Req, Resp]]
}
object ServeToList extends ServeToListLowPrio
trait ServeToListLowPrio {
implicit def hnilToList[Req, Resp] = new ServeToList[HNil, Req, Resp] {
def apply(h: HNil): List[Serve[Req, Resp]] = Nil
}
implicit def serveToList[Req, Resp, T <: HList](implicit next: ServeToList[T, Req, Resp]) = new ServeToList[Serve[Req, Resp] :: T, Req, Resp] {
def apply(h: Serve[Req, Resp] :: T): List[Serve[Req, Resp]] = h.head :: next(h.tail)
}
}
================================================
FILE: server/src/main/scala/typedapi/server/ServerHeaderExtractor.scala
================================================
package typedapi.server
import typedapi.shared._
import shapeless._
import scala.annotation.implicitNotFound
@implicitNotFound("""Whoops, you should not be here. This seems to be a bug.
elements: ${El}""")
sealed trait ServerHeaderExtractor[El <: HList] {
def apply(agg: Map[String, String]): Map[String, String]
}
sealed trait ServerHeaderExtractorLowPrio {
implicit val serverHeaderReturnCase = new ServerHeaderExtractor[HNil] {
def apply(agg: Map[String, String]): Map[String, String] = agg
}
implicit def serverHeaderIgnoreCase[H, T <: HList](implicit next: ServerHeaderExtractor[T]) = new ServerHeaderExtractor[H :: T] {
def apply(agg: Map[String, String]): Map[String, String] = next(agg)
}
}
object ServerHeaderExtractor extends ServerHeaderExtractorLowPrio {
implicit def serverHeaderExtractCase[K, V, T <: HList]
(implicit kWit: Witness.Aux[K], kShow: WitnessToString[K], vWit: Witness.Aux[V], vShow: WitnessToString[V], next: ServerHeaderExtractor[T]) =
new ServerHeaderExtractor[ServerHeaderSend[K, V] :: T] {
def apply(agg: Map[String, String]): Map[String, String] = {
val key = kShow.show(kWit.value)
val value = vShow.show(vWit.value)
next(agg + (key -> value))
}
}
}
================================================
FILE: server/src/main/scala/typedapi/server/ServerManager.scala
================================================
package typedapi.server
import scala.annotation.tailrec
final case class ServerManager[S](server: S, host: String, port: Int)
object ServerManager {
def mount[S, Req, Resp, Out](server: ServerManager[S], endpoints: List[Serve[Req, Resp]])(implicit mounting: MountEndpoints.Aux[S, Req, Resp, Out]): Out =
mounting(server, endpoints)
}
trait MountEndpoints[S, Req, Resp] {
type Out
final def optionsHeaders(eps: List[Serve[Req, Resp]], eReq: EndpointRequest): Map[String, String] = {
@tailrec
def collect(serve: List[Serve[Req, Resp]], methods: List[String], headers: Map[String, String]): (List[String], Map[String, String]) = serve match {
case collection.immutable.::(endpoint, tail) => endpoint.options(eReq) match {
case Some((method, hds)) => collect(tail, method :: methods, hds ++ headers)
case _ => collect(tail, methods, headers)
}
case Nil => (methods, headers)
}
val (methods, headers) = collect(eps, Nil, Map.empty)
headers + (("Access-Control-Allow-Methods", methods.mkString(",")))
}
def apply(server: ServerManager[S], endpoints: List[Serve[Req, Resp]]): Out
}
object MountEndpoints {
type Aux[S, Req, Resp, Out0] = MountEndpoints[S, Req, Resp] { type Out = Out0 }
}
================================================
FILE: server/src/main/scala/typedapi/server/StatusCodes.scala
================================================
package typedapi.server
final case class SuccessCode(statusCode: Int) extends AnyVal
final case class ErrorCode(statusCode: Int) extends AnyVal
final case class HttpError(code: ErrorCode, message: String)
object StatusCodes {
// success codes
final val Continue = SuccessCode(100)
final val SwitchingProtocols = SuccessCode(101)
final val Processing = SuccessCode(102)
final val Ok = SuccessCode(200)
final val Created = SuccessCode(201)
final val Accepted = SuccessCode(202)
final val NonAuthoritativeInformation = SuccessCode(203)
final val NoContent = SuccessCode(204)
final val ResetContent = SuccessCode(205)
final val PartialContent = SuccessCode(206)
final val MultiStatus = SuccessCode(207)
final val AlreadyReported = SuccessCode(208)
final val IMUsed = SuccessCode(226)
final val MultipleChoices = SuccessCode(300)
final val MovedPermanently = SuccessCode(301)
final val Found = SuccessCode(302)
final val SeeOther = SuccessCode(303)
final val NotModified = SuccessCode(304)
final val UseProxy = SuccessCode(305)
final val TemporaryRedirect = SuccessCode(307)
final val PermanentRedirect = SuccessCode(308)
// error codes
final val BadRequest = ErrorCode(400)
final val Unauthorized = ErrorCode(401)
final val PaymentRequired = ErrorCode(402)
final val Forbidden = ErrorCode(403)
final val NotFound = ErrorCode(404)
final val MethodNotAllowed = ErrorCode(405)
final val NotAcceptable = ErrorCode(406)
final val ProxyAuthenticationRequired = ErrorCode(407)
final val RequestTimeout = ErrorCode(408)
final val Conflict = ErrorCode(409)
final val Gone = ErrorCode(410)
final val LengthRequired = ErrorCode(411)
final val PreconditionFailed = ErrorCode(412)
final val PayloadTooLarge = ErrorCode(413)
final val RequestURITooLong = ErrorCode(414)
final val UnsupportedMediaType = ErrorCode(415)
final val RequestedRangeNotSatisfiable = ErrorCode(416)
final val ExpectationFailed = ErrorCode(417)
final val ImAteapot = ErrorCode(418)
final val MisdirectedRequest = ErrorCode(421)
final val UnprocessableEntity = ErrorCode(422)
final val Locked = ErrorCode(423)
final val FailedDependency = ErrorCode(424)
final val UpgradeRequired = ErrorCode(426)
final val PreconditionRequired = ErrorCode(428)
final val TooManyRequests = ErrorCode(429)
final val RequestHeaderFieldsTooLarge = ErrorCode(431)
final val ConnectionClosedWithoutResult = ErrorCode(444)
final val UnavailableForLegalReasons = ErrorCode(451)
final val ClientClosedRequest = ErrorCode(499)
final val InternalServerError = ErrorCode(500)
final val NotImplemented = ErrorCode(501)
final val BadGateway = ErrorCode(502)
final val ServiceUnavailable = ErrorCode(503)
final val GatewayTimeout = ErrorCode(504)
final val HTTPVersionNotSupported = ErrorCode(505)
final val VariantAlsoNegotiates = ErrorCode(506)
final val InsufficientStorage = ErrorCode(507)
final val LoopDetected = ErrorCode(508)
final val NotExtended = ErrorCode(510)
final val NetworkAuthenticationRequired = ErrorCode(511)
final val NetworkConnectTimeoutError = ErrorCode(599)
}
================================================
FILE: server/src/main/scala/typedapi/server/package.scala
================================================
package typedapi
import typedapi.shared._
import shapeless._
import shapeless.ops.hlist.Mapper
import scala.language.higherKinds
package object server extends TypeLevelFoldLeftLowPrio
with TypeLevelFoldLeftListLowPrio
with WitnessToStringLowPrio
with ApiTransformer {
val SC = StatusCodes
type Result[A] = Either[HttpError, (SuccessCode, A)]
def successWith[A](code: SuccessCode)(a: A): Result[A] = Right(code -> a)
def success[A](a: A): Result[A] = successWith(StatusCodes.Ok)(a)
def errorWith[A](code: ErrorCode, message: String): Result[A] = Left(HttpError(code, message))
def derive[F[_]]: ExecutableDerivation[F] = new ExecutableDerivation[F]
def mount[S, El <: HList, KIn <: HList, VIn <: HList, M <: MethodType, ROut, F[_], FOut, Req, Resp, Out]
(server: ServerManager[S], endpoint: Endpoint[El, KIn, VIn, M, ROut, F, FOut])
(implicit executor: EndpointExecutor.Aux[Req, El, KIn, VIn, M, ROut, F, FOut, Resp], mounting: MountEndpoints.Aux[S, Req, Resp, Out]): Out =
mounting(server, List(new Serve[executor.R, executor.Out] {
def options(eReq: EndpointRequest): Option[(String, Map[String, String])] = {
endpoint.extractor(eReq, HNil) match {
case Right(_) => Some((endpoint.method, endpoint.headers))
case _ => None
}
}
def apply(req: executor.R, eReq: EndpointRequest): Either[ExtractionError, executor.Out] = executor(req, eReq, endpoint)
}))
def deriveAll[F[_]]: ExecutableCompositionDerivation[F] = new ExecutableCompositionDerivation[F]
object endpointToServe extends Poly1 {
implicit def default[El <: HList, KIn <: HList, VIn <: HList, M <: MethodType, ROut, F[_], FOut](implicit executor: EndpointExecutor[El, KIn, VIn, M, ROut, F, FOut]) =
at[Endpoint[El, KIn, VIn, M, ROut, F, FOut]] { endpoint =>
new Serve[executor.R, executor.Out] {
def options(eReq: EndpointRequest): Option[(String, Map[String, String])] = {
endpoint.extractor(eReq, HNil) match {
case Right(_) => Some((endpoint.method, endpoint.headers))
case _ => None
}
}
def apply(req: executor.R, eReq: EndpointRequest): Either[ExtractionError, executor.Out] = executor(req, eReq, endpoint)
}
}
}
def mount[S, End <: HList, Serv <: HList, Req, Resp, Out](server: ServerManager[S], end: End)(implicit mapper: Mapper.Aux[endpointToServe.type, End, Serv], toList: ServeToList[Serv, Req, Resp], mounting: MountEndpoints.Aux[S, Req, Resp, Out]): Out =
mounting(server, toList(end.map(endpointToServe)))
}
================================================
FILE: server/src/test/scala/typedapi/server/ApiToEndpointLinkSpec.scala
================================================
package typedapi.server
import typedapi.dsl._
import shapeless._
import org.specs2.mutable.Specification
final class ApiToEndpointLinkSpec extends Specification {
import StatusCodes._
case class Foo(name: String)
"link api definitions to endpoint functions" >> {
val Api = := :> "find" :> typedapi.dsl.Segment[String]('name) :>
Query[Int]('limit) :>
Client.Header('hello, 'world) :>
Server.Send('foo, 'bar) :> Server.Match[String]("hi") :>
Get[Json, List[Foo]]
val endpoint0 = derive[Option](Api).from((name, limit, hi) => Some(successWith(Ok)(List(Foo(name)).take(limit))))
endpoint0("john" :: 10 :: Map("hi" -> "whats", "hi-ho" -> "up") :: HNil) === Some(Right(Ok -> List(Foo("john"))))
endpoint0.headers == Map("foo" -> "bar")
endpoint0.method == "GET"
}
}
================================================
FILE: server/src/test/scala/typedapi/server/RouteExtractorSpec.scala
================================================
package typedapi.server
import typedapi.dsl._
import typedapi.shared._
import shapeless.{HList, HNil, Lazy}
import org.specs2.mutable.Specification
final class RouteExtractorSpec extends Specification {
case class Foo(name: String)
def extract[H <: HList, El <: HList, KIn <: HList, VIn <: HList, M <: MethodType, ROut, Out]
(api: ApiTypeCarrier[H])
(implicit folder: Lazy[TypeLevelFoldLeft.Aux[H, Unit, (El, KIn, VIn, M, Out)]],
extractor: RouteExtractor.Aux[El, KIn, VIn, M, HNil, ROut]): RouteExtractor.Aux[El, KIn, VIn, M, HNil, ROut] = extractor
"determine routes defined by requests and extract included data (segments, queries, headers)" >> {
"no data" >> {
val ext = extract(:= :> "hello" :> "world" :> Get[Json, Foo])
ext(EndpointRequest("GET", List("hello", "world"), Map.empty, Map.empty), HNil) === Right(HNil)
ext(EndpointRequest("GET", List("hello", "wrong"), Map.empty, Map.empty), HNil) === RouteExtractor.NotFoundE
ext(EndpointRequest("GET", List("hello"), Map.empty, Map.empty), HNil) === RouteExtractor.NotFoundE
ext(EndpointRequest("GET", Nil, Map.empty, Map.empty), HNil) === RouteExtractor.NotFoundE
}
"segments" >> {
val ext = extract(:= :> "foo" :> Segment[Int]('age) :> Get[Json, Foo])
ext(EndpointRequest("GET", List("foo", "0"), Map.empty, Map.empty), HNil) === Right(0 :: HNil)
ext(EndpointRequest("GET", List("foo", "wrong"), Map.empty, Map.empty), HNil) === RouteExtractor.NotFoundE
ext(EndpointRequest("GET", List("foo"), Map.empty, Map.empty), HNil) === RouteExtractor.NotFoundE
ext(EndpointRequest("GET", Nil, Map.empty, Map.empty), HNil) === RouteExtractor.NotFoundE
}
"queries" >> {
val ext0 = extract(:= :> "foo" :> Query[Int]('age) :> Get[Json, Foo])
ext0(EndpointRequest("GET", List("foo"), Map("age" -> List("0")), Map.empty), HNil) === Right(0 :: HNil)
ext0(EndpointRequest("GET", List("foo"), Map("age" -> List("wrong")), Map.empty), HNil) === RouteExtractor.BadRequestE("query 'age' has not type Int")
ext0(EndpointRequest("GET", List("foo"), Map("wrong" -> List("0")), Map.empty), HNil) === RouteExtractor.BadRequestE("missing query 'age'")
ext0(EndpointRequest("GET", List("foo"), Map.empty, Map.empty), HNil) === RouteExtractor.BadRequestE("missing query 'age'")
ext0(EndpointRequest("GET", List("foo", "bar"), Map.empty, Map.empty), HNil) === RouteExtractor.NotFoundE
val ext1 = extract(:= :> "foo" :> Query[Option[Int]]('age) :> Get[Json, Foo])
ext1(EndpointRequest("GET", List("foo"), Map("age" -> List("0")), Map.empty), HNil) === Right(Some(0) :: HNil)
ext1(EndpointRequest("GET", List("foo"), Map("wrong" -> List("0")), Map.empty), HNil) === Right(None :: HNil)
val ext2 = extract(:= :> "foo" :> Query[List[Int]]('age) :> Get[Json, Foo])
ext2(EndpointRequest("GET", List("foo"), Map("age" -> List("0", "1")), Map.empty), HNil) === Right(List(0, 1) :: HNil)
ext2(EndpointRequest("GET", List("foo"), Map.empty, Map.empty), HNil) === Right(Nil :: HNil)
}
"headers" >> {
val ext0 = extract(:= :> "foo" :> Header[Int]('age) :> Get[Json, Foo])
ext0(EndpointRequest("GET", List("foo"), Map.empty, Map("age" -> "0")), HNil) === Right(0 :: HNil)
ext0(EndpointRequest("GET", List("foo"), Map.empty, Map("age" -> "wrong")), HNil) === RouteExtractor.BadRequestE("header 'age' has not type Int")
ext0(EndpointRequest("GET", List("foo"), Map.empty, Map("wrong" -> "0")), HNil) === RouteExtractor.BadRequestE("missing header 'age'")
ext0(EndpointRequest("GET", List("foo"), Map.empty, Map.empty), HNil) === RouteExtractor.BadRequestE("missing header 'age'")
ext0(EndpointRequest("GET", List("foo", "bar"), Map.empty, Map.empty), HNil) === RouteExtractor.NotFoundE
val ext2 = extract(:= :> "foo" :> Header[Option[Int]]('age) :> Get[Json, Foo])
ext2(EndpointRequest("GET", List("foo"), Map.empty, Map("age" -> "0")), HNil) === Right(Some(0) :: HNil)
ext2(EndpointRequest("GET", List("foo"), Map.empty, Map.empty), HNil) === Right(None :: HNil)
val ext3 = extract(:= :> "foo" :> Header("Accept", "*") :> Get[Json, Foo])
ext3(EndpointRequest("GET", List("foo"), Map.empty, Map("accept" -> "*")), HNil) === Right(HNil)
ext3(EndpointRequest("GET", List("foo"), Map.empty, Map("wrong" -> "*")), HNil) === RouteExtractor.BadRequestE("missing header 'Accept'")
ext3(EndpointRequest("GET", List("foo"), Map.empty, Map("accept" -> "wrong")), HNil) === RouteExtractor.BadRequestE("header 'Accept' has unexpected value 'wrong' - expected '*'")
val ext4 = extract(:= :> "foo" :> Server.Send("Accept", "*") :> Get[Json, Foo])
ext4(EndpointRequest("GET", List("foo"), Map.empty, Map.empty), HNil) === Right(HNil)
val ext6 = extract(:= :> "foo" :> Server.Match[Int]("age") :> Get[Json, Foo])
ext6(EndpointRequest("GET", List("foo"), Map.empty, Map("age" -> "0")), HNil) === Right(Map("age" ->0) :: HNil)
ext6(EndpointRequest("GET", List("foo"), Map.empty, Map("age-and-what-not" -> "0")), HNil) === Right(Map("age-and-what-not" -> 0) :: HNil)
ext6(EndpointRequest("GET", List("foo"), Map.empty, Map("nope" -> "0")), HNil) === Right(Map.empty :: HNil)
ext6(EndpointRequest("GET", List("foo"), Map.empty, Map("age-" -> "hello")), HNil) === RouteExtractor.BadRequestE("header 'age-' has not type Int")
}
"body type" >> {
val ext0 = extract(:= :> ReqBody[Json, Foo] :> Put[Json, Foo])
ext0(EndpointRequest("PUT", Nil, Map.empty, Map.empty), HNil) === Right((BodyType[Foo], HNil))
ext0(EndpointRequest("PUT", List("foo", "bar"), Map.empty, Map.empty), HNil) === RouteExtractor.NotFoundE
ext0(EndpointRequest("OPTIONS", List("foo", "bar"), Map.empty, Map.empty), HNil) === RouteExtractor.NotFoundE
val ext1 = extract(:= :> ReqBody[Json, Foo] :> Post[Json, Foo])
ext1(EndpointRequest("POST", Nil, Map.empty, Map.empty), HNil) === Right((BodyType[Foo], HNil))
ext1(EndpointRequest("OPTIONS", Nil, Map.empty, Map.empty), HNil) === Right((BodyType[Foo], HNil))
}
"methods" >> {
val ext0 = extract(:= :> Get[Json, Foo])
ext0(EndpointRequest("GET", Nil, Map.empty, Map.empty), HNil) === Right(HNil)
ext0(EndpointRequest("WRONG", Nil, Map.empty, Map.empty), HNil) === RouteExtractor.NotFoundE
ext0(EndpointRequest("GET", List("foo"), Map.empty, Map.empty), HNil) === RouteExtractor.NotFoundE
ext0(EndpointRequest("OPTIONS", Nil, Map.empty, Map.empty), HNil) === Right(HNil)
val ext1 = extract(:= :> Put[Json, Foo])
ext1(EndpointRequest("PUT", Nil, Map.empty, Map.empty), HNil) === Right(HNil)
ext1(EndpointRequest("OPTIONS", Nil, Map.empty, Map.empty), HNil) === Right(HNil)
val ext2 = extract(:= :> Post[Json, Foo])
ext2(EndpointRequest("POST", Nil, Map.empty, Map.empty), HNil) === Right(HNil)
ext2(EndpointRequest("OPTIONS", Nil, Map.empty, Map.empty), HNil) === Right(HNil)
val ext3 = extract(:= :> Delete[Json, Foo])
ext3(EndpointRequest("DELETE", Nil, Map.empty, Map.empty), HNil) === Right(HNil)
ext3(EndpointRequest("OPTIONS", Nil, Map.empty, Map.empty), HNil) === Right(HNil)
}
"combinations" >> {
val ext0 = extract(:= :> "foo" :> Query[Int]('age) :> Header[String]('id) :> Get[Json, Foo])
ext0(EndpointRequest("GET", List("foo"), Map("age" -> List("0")), Map("id" -> "john")), HNil) === Right(0 :: "john" :: HNil)
val ext1 = extract(:= :> Get[Json, Foo])
ext1(EndpointRequest("GET", Nil, Map.empty, Map.empty), HNil) === Right(HNil)
}
}
}
================================================
FILE: server/src/test/scala/typedapi/server/ServeAndMountSpec.scala
================================================
package typedapi.server
import typedapi.dsl._
import typedapi.shared.MethodType
import shapeless.{HList, HNil, ::}
import shapeless.ops.hlist.{Prepend, Mapper}
import org.specs2.mutable.Specification
import scala.language.higherKinds
final class ServeAndMountSpec extends Specification {
import StatusCodes._
case class Foo(name: String)
sealed trait Req
case class TestRequest(uri: List[String], queries: Map[String, List[String]], headers: Map[String, String]) extends Req
case class TestRequestWithBody[Bd](uri: List[String], queries: Map[String, List[String]], headers: Map[String, String], body: Bd) extends Req
case class TestResponse(raw: Any)
implicit def execNoBodyId[El <: HList, KIn <: HList, VIn <: HList, M <: MethodType, FOut] =
new NoReqBodyExecutor[El, KIn, VIn, M, Option, FOut] {
type R = Req
type Out = TestResponse
def apply(req: Req, eReq: EndpointRequest, endpoint: Endpoint[El, KIn, VIn, M, VIn, Option, FOut]): Either[ExtractionError, Out] =
extract(eReq, endpoint).right.map { extracted =>
TestResponse(execute(extracted, endpoint))
}
}
implicit def execWithBody[El <: HList, KIn <: HList, VIn <: HList, Bd, M <: MethodType, ROut <: HList, POut <: HList, FOut](implicit _prepend: Prepend.Aux[ROut, Bd :: HNil, POut], _eqProof: POut =:= VIn) =
new ReqBodyExecutor[El, KIn, VIn, Bd, M, ROut, POut, Option, FOut] {
type R = Req
type Out = TestResponse
implicit val prepend = _prepend
implicit def eqProof = _eqProof
def apply(req: Req, eReq: EndpointRequest, endpoint: Endpoint[El, KIn, VIn, M, (BodyType[Bd], ROut), Option, FOut]): Either[ExtractionError, Out] =
extract(eReq, endpoint).right.map { case (_, extracted) =>
TestResponse(execute(extracted, req.asInstanceOf[TestRequestWithBody[Bd]].body, endpoint))
}
}
def toList[El <: HList, KIn <: HList, VIn <: HList, M <: MethodType, ROut, F[_], FOut](endpoint: Endpoint[El, KIn, VIn, M, ROut, F, FOut])
(implicit executor: EndpointExecutor[El, KIn, VIn, M, ROut, F, FOut]): List[Serve[executor.R, executor.Out]] =
List(new Serve[executor.R, executor.Out] {
def options(eReq: EndpointRequest): Option[(String, Map[String, String])] = {
endpoint.extractor(eReq, HNil) match {
case Right(_) => Some((endpoint.method, endpoint.headers))
case _ => None
}
}
def apply(req: executor.R, eReq: EndpointRequest): Either[ExtractionError, executor.Out] = executor(req, eReq, endpoint)
})
def toList[End <: HList, Serv <: HList](end: End)(implicit mapper: Mapper.Aux[endpointToServe.type, End, Serv], s: ServeToList[Serv, Req, TestResponse]): List[Serve[Req, TestResponse]] =
s(end.map(endpointToServe))
"serve endpoints as simple Request -> Response functions and mount them into a server" >> {
"serve single endpoint and no body" >> {
val Api = := :> "find" :> "user" :> Segment[String]('name) :> Query[Int]('sortByAge) :> Get[Json, List[Foo]]
val endpoint = derive[Option](Api).from((name, sortByAge) => Some(successWith(Ok)(List(Foo(name)))))
val served = toList(endpoint)
val req = TestRequest(List("find", "user", "joe"), Map("sortByAge" -> List("1")), Map.empty)
val eReq = EndpointRequest("GET", req.uri, req.queries, req.headers)
served.head(req, eReq) === Right(TestResponse(Some(Right(Ok -> List(Foo("joe"))))))
}
"check if route exists and return method" >> {
val Api = := :> "find" :> "user" :> Segment[String]('name) :> Query[Int]('sortByAge) :> Server.Send("Hello", "*") :> Get[Json, List[Foo]]
val endpoint = derive[Option](Api).from((name, sortByAge) => Some(successWith(Ok)(List(Foo(name)))))
val served = toList(endpoint)
val req0 = TestRequest(List("find", "user", "joe"), Map("sortByAge" -> List("1")), Map.empty)
val eReq0 = EndpointRequest("GET", req0.uri, req0.queries, req0.headers)
served.head.options(eReq0) === Some(("GET", Map(("Hello", "*"))))
val eReq1 = EndpointRequest("POST", req0.uri, req0.queries, req0.headers)
served.head.options(eReq1) === None
}
"serve single endpoint and with body" >> {
val Api = := :> "find" :> "user" :> Segment[String]('name) :> ReqBody[Json, Foo] :> Post[Json, List[Foo]]
val endpoint = derive[Option](Api).from((name, body) => Some(successWith(Ok)(List(Foo(name), body))))
val served = toList(endpoint)
val req = TestRequestWithBody(List("find", "user", "joe"), Map.empty, Map.empty, Foo("jim"))
val eReq = EndpointRequest("POST", req.uri, req.queries, req.headers)
served.head(req, eReq) === Right(TestResponse(Some(Right(Ok -> List(Foo("joe"), Foo("jim"))))))
}
"serve multiple endpoints" >> {
val Api =
(:= :> "find" :> "user" :> Segment[String]('name) :> Query[Int]('sortByAge) :> Get[Json, List[Foo]]) :|:
(:= :> "create" :> "user" :> ReqBody[Json, Foo] :> Post[Json, Foo])
def find(name: String, age: Int): Option[Result[List[Foo]]] = Some(successWith(Ok)(List(Foo(name))))
def create(foo: Foo): Option[Result[Foo]] = Some(successWith(Ok)(foo))
val endpoints = deriveAll[Option](Api).from(find _, create _)
val served = toList(endpoints)
val req = TestRequest(List("find", "user", "joe"), Map("sortByAge" -> List("1")), Map.empty)
val eReq = EndpointRequest("GET", req.uri, req.queries, req.headers)
served.head(req, eReq) === Right(TestResponse(Some(Right(Ok -> List(Foo("joe"))))))
}
}
}
================================================
FILE: shared/src/main/scala/typedapi/dsl/ApiDsl.scala
================================================
package typedapi.dsl
import typedapi.shared._
import shapeless._
sealed trait ApiList[H <: HList]
/** Basic operations. */
sealed trait MethodOps[H <: HList] {
def :>[MT <: MediaType, A](body: TypeCarrier[ReqBodyElement[MT, A]]): WithBodyCons[MT, A, H] = WithBodyCons()
def :>[M <: MethodElement](method: TypeCarrier[M]): ApiTypeCarrier[M :: H] = ApiTypeCarrier()
}
sealed trait PathOps[H <: HList] {
def :>[S](path: Witness.Lt[S]): PathCons[PathElement[S] :: H] = PathCons()
def :>[K, V](segment: TypeCarrier[SegmentParam[K, V]]): SegmentCons[SegmentParam[K, V] :: H] = SegmentCons()
}
sealed trait HeaderOps[H <: HList] {
def :>[K, V](header: TypeCarrier[Header
gitextract_555nqngw/
├── .gitignore
├── .travis.yml
├── CHANGELOG.md
├── LICENSE
├── README.md
├── akka-http-client/
│ └── src/
│ └── main/
│ └── scala/
│ └── typedapi/
│ └── client/
│ └── akkahttp/
│ └── package.scala
├── akka-http-server/
│ └── src/
│ └── main/
│ └── scala/
│ └── typedapi/
│ └── server/
│ └── akkahttp/
│ └── package.scala
├── ammonite-client-support/
│ └── src/
│ └── main/
│ └── scala/
│ └── typedapi/
│ └── client/
│ └── package.scala
├── build.sbt
├── client/
│ └── src/
│ ├── main/
│ │ └── scala/
│ │ └── typedapi/
│ │ └── client/
│ │ ├── ApiRequest.scala
│ │ ├── ClientManager.scala
│ │ ├── ExecutableDerivation.scala
│ │ ├── ExecutablesFromHList.scala
│ │ ├── FilterServerElements.scala
│ │ ├── RequestDataBuilder.scala
│ │ ├── package.scala
│ │ └── test/
│ │ ├── RequestInput.scala
│ │ └── package.scala
│ └── test/
│ └── scala/
│ └── typedapi/
│ └── client/
│ ├── ClientManagerSpec.scala
│ └── RequestDataBuilderSpec.scala
├── docs/
│ ├── ApiDefinition.md
│ ├── ClientCreation.md
│ ├── ExtendIt.md
│ ├── ServerCreation.md
│ └── example/
│ ├── ammonite_client_example.sc
│ ├── build.sbt
│ ├── client-js/
│ │ ├── index.html
│ │ └── src/
│ │ └── main/
│ │ └── scala/
│ │ └── Client.scala
│ ├── client-jvm/
│ │ └── src/
│ │ └── main/
│ │ └── scala/
│ │ └── Client.scala
│ ├── project/
│ │ ├── build.properties
│ │ └── plugins.sbt
│ ├── server/
│ │ └── src/
│ │ └── main/
│ │ └── scala/
│ │ └── Server.scala
│ └── shared/
│ └── src/
│ └── main/
│ └── scala/
│ ├── Apis.scala
│ └── User.scala
├── http-support-tests/
│ └── src/
│ └── test/
│ └── scala/
│ └── http/
│ └── support/
│ └── tests/
│ ├── User.scala
│ ├── client/
│ │ ├── AkkaHttpClientSupportSpec.scala
│ │ ├── Http4sClientSupportSpec.scala
│ │ ├── ScalajHttpClientSupportSpec.scala
│ │ └── TestServer.scala
│ ├── package.scala
│ └── server/
│ ├── AkkaHttpServerSupportSpec.scala
│ ├── Http4sServerSupportSpec.scala
│ └── ServerSupportSpec.scala
├── http4s-client/
│ └── src/
│ └── main/
│ └── scala/
│ └── typedapi/
│ └── client/
│ └── http4s/
│ └── package.scala
├── http4s-server/
│ └── src/
│ └── main/
│ └── scala/
│ └── typedapi/
│ └── server/
│ └── htt4ps/
│ └── package.scala
├── js-client/
│ └── src/
│ └── main/
│ └── scala/
│ └── typedapi/
│ └── client/
│ └── js/
│ └── package.scala
├── project/
│ ├── build.properties
│ ├── build.scala
│ └── plugins.sbt
├── scalaj-http-client/
│ └── src/
│ └── main/
│ └── scala/
│ └── typedapi/
│ └── client/
│ └── scalajhttp/
│ └── package.scala
├── server/
│ └── src/
│ ├── main/
│ │ └── scala/
│ │ └── typedapi/
│ │ └── server/
│ │ ├── Endpoint.scala
│ │ ├── EndpointComposition.scala
│ │ ├── EndpointExecutor.scala
│ │ ├── FilterClientElements.scala
│ │ ├── RouteExtractor.scala
│ │ ├── Serve.scala
│ │ ├── ServeToList.scala
│ │ ├── ServerHeaderExtractor.scala
│ │ ├── ServerManager.scala
│ │ ├── StatusCodes.scala
│ │ └── package.scala
│ └── test/
│ └── scala/
│ └── typedapi/
│ └── server/
│ ├── ApiToEndpointLinkSpec.scala
│ ├── RouteExtractorSpec.scala
│ └── ServeAndMountSpec.scala
└── shared/
└── src/
├── main/
│ └── scala/
│ └── typedapi/
│ ├── dsl/
│ │ ├── ApiDsl.scala
│ │ └── package.scala
│ ├── package.scala
│ ├── shared/
│ │ ├── ApiElement.scala
│ │ ├── ApiList.scala
│ │ ├── ApiTransformer.scala
│ │ ├── MediaTypes.scala
│ │ ├── TypeCarrier.scala
│ │ ├── TypeLevelFoldLeft.scala
│ │ ├── WitnessToString.scala
│ │ └── package.scala
│ └── util/
│ ├── Decoder.scala
│ └── Encoder.scala
└── test/
└── scala/
└── typedapi/
├── ApiDefinitionSpec.scala
├── SpecUtil.scala
├── dsl/
│ └── ApiDslSpec.scala
└── shared/
└── ApiTransformerSpec.scala
Condensed preview — 81 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (269K chars).
[
{
"path": ".gitignore",
"chars": 109,
"preview": "# ensime\n*.*~\n.ensime\n.ensime_cache\nproject/ensime-plugin.sbt\n\n# sbt\ntarget/\nproject/target/\nproject/project/"
},
{
"path": ".travis.yml",
"chars": 199,
"preview": "language: scala\n\nscala:\n - 2.11.11\n - 2.12.3\n\njdk:\n - oraclejdk8\n\nscript:\n - sbt ++$TRAVIS_SCALA_VERSION clean test\n"
},
{
"path": "CHANGELOG.md",
"chars": 3767,
"preview": "### 0.2.0\n - `StatusCodes` and `MediaTypes` are in distinct `object`s, thus, have to be imported explicitly\n - improved "
},
{
"path": "LICENSE",
"chars": 1069,
"preview": "MIT License\n\nCopyright (c) 2018 Paul Heymann\n\nPermission is hereby granted, free of charge, to any person obtaining a co"
},
{
"path": "README.md",
"chars": 6055,
"preview": "[](https://travis-ci.org/pheymann/typedapi)\n[!"
},
{
"path": "akka-http-client/src/main/scala/typedapi/client/akkahttp/package.scala",
"chars": 13514,
"preview": "package typedapi.client\n\nimport akka.http.scaladsl.HttpExt\nimport akka.http.scaladsl.model.RequestEntity\nimport akka.htt"
},
{
"path": "akka-http-server/src/main/scala/typedapi/server/akkahttp/package.scala",
"chars": 5415,
"preview": "package typedapi.server\n\nimport typedapi.shared.MethodType\nimport shapeless._\nimport shapeless.ops.hlist.Prepend\nimport "
},
{
"path": "ammonite-client-support/src/main/scala/typedapi/client/package.scala",
"chars": 1490,
"preview": "package typedapi.client\n\nimport typedapi.util._\nimport scalaj.http._\n\npackage object amm {\n\n type Id[A] = scalajh"
},
{
"path": "build.sbt",
"chars": 5565,
"preview": "import sbt.Keys._\n\nval `compiler-2.12` = Seq(\n \"-deprecation\",\n \"-encoding\", \"utf-8\",\n \"-explaintypes\",\n \"-feature\","
},
{
"path": "client/src/main/scala/typedapi/client/ApiRequest.scala",
"chars": 3485,
"preview": "package typedapi.client\n\nimport RequestDataBuilder.{Data, DataWithBody}\nimport typedapi.shared._\nimport shapeless._\n\nimp"
},
{
"path": "client/src/main/scala/typedapi/client/ClientManager.scala",
"chars": 503,
"preview": "package typedapi.client\n\n/** Provides a supported client instance and some basic configuration. */\nfinal case class Clie"
},
{
"path": "client/src/main/scala/typedapi/client/ExecutableDerivation.scala",
"chars": 938,
"preview": "package typedapi.client\n\nimport typedapi.shared.{MethodType, MediaType}\nimport shapeless._\nimport shapeless.labelled.Fie"
},
{
"path": "client/src/main/scala/typedapi/client/ExecutablesFromHList.scala",
"chars": 1314,
"preview": "package typedapi.client\n\nimport typedapi.shared.{MethodType, MediaType}\nimport shapeless._\nimport shapeless.labelled.Fie"
},
{
"path": "client/src/main/scala/typedapi/client/FilterServerElements.scala",
"chars": 1558,
"preview": "package typedapi.client\n\nimport typedapi.shared._\nimport shapeless._\n\n//TODO replace with Typelevelfoldleft\nsealed trait"
},
{
"path": "client/src/main/scala/typedapi/client/RequestDataBuilder.scala",
"chars": 12679,
"preview": "package typedapi.client\n\nimport typedapi.shared._\nimport shapeless._\nimport shapeless.labelled.FieldType\n\nimport scala.c"
},
{
"path": "client/src/main/scala/typedapi/client/package.scala",
"chars": 1571,
"preview": "package typedapi\n\nimport typedapi.shared._\nimport shapeless._\nimport shapeless.labelled.FieldType\nimport shapeless.ops.h"
},
{
"path": "client/src/main/scala/typedapi/client/test/RequestInput.scala",
"chars": 540,
"preview": "package typedapi.client.test\n\nfinal case class ReqInput(method: String,\n uri: List[String],\n "
},
{
"path": "client/src/main/scala/typedapi/client/test/package.scala",
"chars": 3827,
"preview": "package typedapi.client\n\nimport scala.language.higherKinds\n\npackage object test {\n\n type TestClientM = ClientManager[Un"
},
{
"path": "client/src/test/scala/typedapi/client/ClientManagerSpec.scala",
"chars": 272,
"preview": "package typedapi.client\n\nimport org.specs2.mutable.Specification\n\nfinal class ClientManagerSpec extends Specification {\n"
},
{
"path": "client/src/test/scala/typedapi/client/RequestDataBuilderSpec.scala",
"chars": 6160,
"preview": "package typedapi.client\n\nimport typedapi.dsl._\nimport typedapi.client.test._\n\nimport shapeless.Id\nimport org.specs2.muta"
},
{
"path": "docs/ApiDefinition.md",
"chars": 6699,
"preview": "## How to define an API\nThe central idea behind Typedapi is to make client and server implementation as boilerplate-free"
},
{
"path": "docs/ClientCreation.md",
"chars": 4181,
"preview": "## Create a client from your API\nAfter we [defined](https://github.com/pheymann/typedapi/blob/master/docs/ApiDefinition."
},
{
"path": "docs/ExtendIt.md",
"chars": 2196,
"preview": "## Extend Typedapi to fit your needs\nYou ended up in this file if:\n - the default implements for a HTTP framework doesn'"
},
{
"path": "docs/ServerCreation.md",
"chars": 2024,
"preview": "## Create a server from your API\nAfter we [defined](https://github.com/pheymann/typedapi/blob/master/docs/ApiDefinition."
},
{
"path": "docs/example/ammonite_client_example.sc",
"chars": 379,
"preview": "import $ivy.`com.github.pheymann::typedapi-ammonite-client:0.2.0-M1`\n\nimport typedapi._\nimport client._\nimport amm._\n\nva"
},
{
"path": "docs/example/build.sbt",
"chars": 1823,
"preview": "\nval typedapiVersion = \"0.2.0\"\nval http4sVersion = \"0.18.0\"\n\nval commonSettings = Seq(\n scalaVersion := \"2.12.4\"\n)\n\nl"
},
{
"path": "docs/example/client-js/index.html",
"chars": 127,
"preview": "<!DOCTYPE html>\n<html lang=\"en\">\n <body>\n <script src=\"target/scala-2.12/client-js-fastopt.js\"></script>\n </body>\n<"
},
{
"path": "docs/example/client-js/src/main/scala/Client.scala",
"chars": 1175,
"preview": "\nimport typedapi.client._\nimport typedapi.client.js._\nimport org.scalajs.dom.ext.Ajax\nimport io.circe._\nimport io.circe."
},
{
"path": "docs/example/client-jvm/src/main/scala/Client.scala",
"chars": 772,
"preview": "\nimport typedapi.client._\nimport typedapi.client.http4s._\nimport cats.effect.IO\nimport org.http4s._\nimport org.http4s.ci"
},
{
"path": "docs/example/project/build.properties",
"chars": 17,
"preview": "sbt.version=1.0.4"
},
{
"path": "docs/example/project/plugins.sbt",
"chars": 56,
"preview": "addSbtPlugin(\"org.scala-js\" % \"sbt-scalajs\" % \"0.6.22\")\n"
},
{
"path": "docs/example/server/src/main/scala/Server.scala",
"chars": 1523,
"preview": "\nimport typedapi.server._\nimport typedapi.server.http4s._\nimport cats.effect.IO\nimport org.http4s._\nimport org.http4s.ci"
},
{
"path": "docs/example/shared/src/main/scala/Apis.scala",
"chars": 3832,
"preview": "\nobject FromDsl {\n\n import typedapi.dsl._\n\n /* NOTE: we have to add the 'Access-Control-Allow-Origin' header to the se"
},
{
"path": "docs/example/shared/src/main/scala/User.scala",
"chars": 259,
"preview": "\nimport io.circe.syntax._\nimport io.circe.generic.JsonCodec\n\nfinal case class User(name: String, age: Int)\n\nobject User "
},
{
"path": "http-support-tests/src/test/scala/http/support/tests/User.scala",
"chars": 387,
"preview": "package http.support.tests\n\nimport cats.effect.IO\nimport io.circe.generic.semiauto._\nimport org.http4s.circe._\nimport or"
},
{
"path": "http-support-tests/src/test/scala/http/support/tests/client/AkkaHttpClientSupportSpec.scala",
"chars": 2696,
"preview": "package http.support.tests.client\n\nimport http.support.tests.{User, UserCoding, Api}\nimport typedapi.client._\nimport typ"
},
{
"path": "http-support-tests/src/test/scala/http/support/tests/client/Http4sClientSupportSpec.scala",
"chars": 1895,
"preview": "package http.support.tests.client\n\nimport http.support.tests.{UserCoding, User, Api}\nimport typedapi.client._\nimport typ"
},
{
"path": "http-support-tests/src/test/scala/http/support/tests/client/ScalajHttpClientSupportSpec.scala",
"chars": 2252,
"preview": "package http.support.tests.client\n\nimport http.support.tests.{UserCoding, User, Api}\nimport typedapi.client._\nimport typ"
},
{
"path": "http-support-tests/src/test/scala/http/support/tests/client/TestServer.scala",
"chars": 2988,
"preview": "package http.support.tests.client\n\nimport http.support.tests.{User, UserCoding}\nimport cats.effect.IO\nimport io.circe.sy"
},
{
"path": "http-support-tests/src/test/scala/http/support/tests/package.scala",
"chars": 1334,
"preview": "package http.support\n\nimport typedapi.dsl._\n\npackage object tests {\n\n val Api =\n (:= :> \"path\" :> Get[Json, User]) :"
},
{
"path": "http-support-tests/src/test/scala/http/support/tests/server/AkkaHttpServerSupportSpec.scala",
"chars": 1418,
"preview": "package http.support.tests.server\n\nimport http.support.tests.{Api, UserCoding}\nimport typedapi.server._\nimport typedapi."
},
{
"path": "http-support-tests/src/test/scala/http/support/tests/server/Http4sServerSupportSpec.scala",
"chars": 833,
"preview": "package http.support.tests.server\n\nimport http.support.tests.{UserCoding, Api}\nimport typedapi.server._\nimport typedapi."
},
{
"path": "http-support-tests/src/test/scala/http/support/tests/server/ServerSupportSpec.scala",
"chars": 6101,
"preview": "package http.support.tests.server\n\nimport typedapi.server._\nimport http.support.tests.{User, UserCoding}\nimport org.http"
},
{
"path": "http4s-client/src/main/scala/typedapi/client/http4s/package.scala",
"chars": 7012,
"preview": "package typedapi.client\n\nimport cats.{Monad, MonadError, Applicative}\nimport org.http4s._\nimport org.http4s.client._\nimp"
},
{
"path": "http4s-server/src/main/scala/typedapi/server/htt4ps/package.scala",
"chars": 4504,
"preview": "package typedapi.server\n\nimport typedapi.shared.MethodType\nimport cats.Monad\nimport cats.implicits._\nimport cats.effect."
},
{
"path": "js-client/src/main/scala/typedapi/client/js/package.scala",
"chars": 6130,
"preview": "package typedapi.client\n\nimport typedapi.util._\nimport org.scalajs.dom.XMLHttpRequest\nimport org.scalajs.dom.ext.Ajax\n\ni"
},
{
"path": "project/build.properties",
"chars": 17,
"preview": "sbt.version=1.0.4"
},
{
"path": "project/build.scala",
"chars": 2018,
"preview": "import sbt._\n\nobject Dependencies {\n\n private val specs2V = \"3.9.4\"\n\n val shared = Seq(\n \"com.chuusai\" %% \"shapeles"
},
{
"path": "project/plugins.sbt",
"chars": 165,
"preview": "addSbtPlugin(\"org.xerial.sbt\" % \"sbt-sonatype\" % \"2.3\")\n\naddSbtPlugin(\"com.jsuereth\" % \"sbt-pgp\" % \"1.1.0\")\n\naddSbtPlugi"
},
{
"path": "scalaj-http-client/src/main/scala/typedapi/client/scalajhttp/package.scala",
"chars": 5086,
"preview": "package typedapi.client\n\nimport typedapi.util._\nimport scalaj.http._\n\npackage object scalajhttp {\n\n type Id[A] = "
},
{
"path": "server/src/main/scala/typedapi/server/Endpoint.scala",
"chars": 2520,
"preview": "package typedapi.server\n\nimport typedapi.shared._\nimport shapeless._\nimport shapeless.labelled.FieldType\nimport shapeles"
},
{
"path": "server/src/main/scala/typedapi/server/EndpointComposition.scala",
"chars": 5521,
"preview": "package typedapi.server\n\nimport typedapi.shared._\nimport shapeless._\nimport shapeless.labelled.FieldType\nimport shapeles"
},
{
"path": "server/src/main/scala/typedapi/server/EndpointExecutor.scala",
"chars": 1756,
"preview": "package typedapi.server\n\nimport typedapi.shared.MethodType\nimport shapeless._\nimport shapeless.ops.hlist.Prepend\n\nimport"
},
{
"path": "server/src/main/scala/typedapi/server/FilterClientElements.scala",
"chars": 1740,
"preview": "package typedapi.server\n\nimport typedapi.shared.{ClientHeaderElement, ClientHeaderParam, ClientHeaderCollParam}\nimport s"
},
{
"path": "server/src/main/scala/typedapi/server/RouteExtractor.scala",
"chars": 12984,
"preview": "package typedapi.server\n\nimport typedapi.shared._\nimport shapeless.{HList, HNil, Witness}\nimport shapeless.labelled.Fiel"
},
{
"path": "server/src/main/scala/typedapi/server/Serve.scala",
"chars": 299,
"preview": "package typedapi.server\n\n/** Reduces an Endpoint and its EndpointExecutor to a simple Request => Response function. */\nt"
},
{
"path": "server/src/main/scala/typedapi/server/ServeToList.scala",
"chars": 825,
"preview": "package typedapi.server\n\nimport shapeless._\n\nimport scala.annotation.implicitNotFound\n\n@implicitNotFound(\"\"\"Cannot find "
},
{
"path": "server/src/main/scala/typedapi/server/ServerHeaderExtractor.scala",
"chars": 1267,
"preview": "package typedapi.server\n\nimport typedapi.shared._\nimport shapeless._\n\nimport scala.annotation.implicitNotFound\n\n@implici"
},
{
"path": "server/src/main/scala/typedapi/server/ServerManager.scala",
"chars": 1286,
"preview": "package typedapi.server\n\nimport scala.annotation.tailrec\n\nfinal case class ServerManager[S](server: S, host: String, por"
},
{
"path": "server/src/main/scala/typedapi/server/StatusCodes.scala",
"chars": 3940,
"preview": "package typedapi.server\n\nfinal case class SuccessCode(statusCode: Int) extends AnyVal\nfinal case class ErrorCode(statusC"
},
{
"path": "server/src/main/scala/typedapi/server/package.scala",
"chars": 2691,
"preview": "package typedapi\n\nimport typedapi.shared._\nimport shapeless._\nimport shapeless.ops.hlist.Mapper\n\nimport scala.language.h"
},
{
"path": "server/src/test/scala/typedapi/server/ApiToEndpointLinkSpec.scala",
"chars": 879,
"preview": "package typedapi.server\n\nimport typedapi.dsl._\nimport shapeless._\n\nimport org.specs2.mutable.Specification\n\nfinal class "
},
{
"path": "server/src/test/scala/typedapi/server/RouteExtractorSpec.scala",
"chars": 7690,
"preview": "package typedapi.server\n\nimport typedapi.dsl._\nimport typedapi.shared._\nimport shapeless.{HList, HNil, Lazy}\nimport org."
},
{
"path": "server/src/test/scala/typedapi/server/ServeAndMountSpec.scala",
"chars": 5703,
"preview": "package typedapi.server\n\nimport typedapi.dsl._\nimport typedapi.shared.MethodType\nimport shapeless.{HList, HNil, ::}\nimpo"
},
{
"path": "shared/src/main/scala/typedapi/dsl/ApiDsl.scala",
"chars": 3586,
"preview": "package typedapi.dsl\n\nimport typedapi.shared._\nimport shapeless._\n\nsealed trait ApiList[H <: HList]\n\n/** Basic operation"
},
{
"path": "shared/src/main/scala/typedapi/dsl/package.scala",
"chars": 1304,
"preview": "package typedapi\n\nimport typedapi.shared._\n\npackage object dsl extends MethodToReqBodyLowPrio with MethodToStringLowPrio"
},
{
"path": "shared/src/main/scala/typedapi/package.scala",
"chars": 1793,
"preview": "\nimport typedapi.shared._\nimport shapeless._\nimport shapeless.ops.hlist.Prepend\n\npackage object typedapi extends MethodT"
},
{
"path": "shared/src/main/scala/typedapi/shared/ApiElement.scala",
"chars": 3538,
"preview": "package typedapi.shared\n\nimport scala.annotation.implicitNotFound\n\nsealed trait ApiElement\n\n/** Type-container providing"
},
{
"path": "shared/src/main/scala/typedapi/shared/ApiList.scala",
"chars": 2184,
"preview": "package typedapi.shared\n\nimport shapeless._\n\n/** Typecarrier to construct a complete path description from [[PathElement"
},
{
"path": "shared/src/main/scala/typedapi/shared/ApiTransformer.scala",
"chars": 5589,
"preview": "package typedapi.shared\n\nimport shapeless._\nimport shapeless.labelled.FieldType\n\nimport scala.annotation.implicitNotFoun"
},
{
"path": "shared/src/main/scala/typedapi/shared/MediaTypes.scala",
"chars": 32037,
"preview": "package typedapi.shared\n\ntrait MediaType { self =>\n def value: String\n}\n\nobject MediaTypes {\n\n case object NoMediaType"
},
{
"path": "shared/src/main/scala/typedapi/shared/TypeCarrier.scala",
"chars": 1031,
"preview": "package typedapi.shared\n\nimport shapeless._\n\nimport scala.language.higherKinds\n\n/** As the name says this case class is "
},
{
"path": "shared/src/main/scala/typedapi/shared/TypeLevelFoldLeft.scala",
"chars": 2340,
"preview": "package typedapi.shared\n\nimport shapeless._\n\nimport scala.annotation.implicitNotFound\n\n// INTERNAL API\n\n/** Reimplements"
},
{
"path": "shared/src/main/scala/typedapi/shared/WitnessToString.scala",
"chars": 458,
"preview": "package typedapi.shared\n\nimport scala.annotation.implicitNotFound\n\n@implicitNotFound(\"Couldn't find transformation for w"
},
{
"path": "shared/src/main/scala/typedapi/shared/package.scala",
"chars": 167,
"preview": "package typedapi\n\nimport shapeless.Witness\n\npackage object shared {\n\n final val BodyField = Witness('body)\n fina"
},
{
"path": "shared/src/main/scala/typedapi/util/Decoder.scala",
"chars": 319,
"preview": "package typedapi.util\n\nimport scala.language.higherKinds\n\ntrait Decoder[F[_], A] extends (String => F[Either[Exception, "
},
{
"path": "shared/src/main/scala/typedapi/util/Encoder.scala",
"chars": 258,
"preview": "package typedapi.util\n\nimport scala.language.higherKinds\n\ntrait Encoder[F[_], A] extends (A => F[String])\n\nobject Encode"
},
{
"path": "shared/src/test/scala/typedapi/ApiDefinitionSpec.scala",
"chars": 2908,
"preview": "package typedapi\n\nimport typedapi.shared._\nimport shapeless._\n\nimport SpecUtil._\n\n// compilation-only test\nobject ApiDef"
},
{
"path": "shared/src/test/scala/typedapi/SpecUtil.scala",
"chars": 283,
"preview": "package typedapi\n\nimport shapeless.HList\n\nimport scala.language.higherKinds\n\nobject SpecUtil {\n\n class TestHelper[Act <"
},
{
"path": "shared/src/test/scala/typedapi/dsl/ApiDslSpec.scala",
"chars": 3692,
"preview": "package typedapi.dsl\n\nimport typedapi.SpecUtil._\nimport typedapi.shared._\nimport shapeless._\n\n// compilation-only test\no"
},
{
"path": "shared/src/test/scala/typedapi/shared/ApiTransformerSpec.scala",
"chars": 3359,
"preview": "package typedapi.shared\n\nimport typedapi.Json\nimport shapeless._\nimport shapeless.labelled.FieldType\n\n// compilation-onl"
}
]
About this extraction
This page contains the full source code of the pheymann/typedapi GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 81 files (247.9 KB), approximately 71.5k tokens. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.
Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.