The commons library contains an RPC based REST framework for defining REST services using Scala traits. It may be used for implementing both client and server side and works in both JVM and JS, as long as appropriate network layer is implemented. For JVM, Jetty-based implementations for client and server are provided.
The core of REST framework is platform independent and network-implementation indepenedent and therefore
has no external dependencies. Because of that, it's a part of commons-core
module. This is enough to
be able to define REST interfaces. But if you want to expose your REST interface through an actual HTTP
server or have an actual HTTP client for that interface, you need separate implementations for that.
The commons-jetty
module provides Jetty-based implementations for JVM.
Table of Contents generated with DocToc
- Quickstart example
- REST API traits
- Serialization
- API evolution
- Implementing backends
First, make sure appropriate dependencies are configured for your project (assuming SBT):
val commonsVersion: String = ??? // appropriate version of scala-commons here
libraryDependencies ++= Seq(
"com.avsystem.commons" %% "commons-core" % commonsVersion,
"com.avsystem.commons" %% "commons-jetty" % commonsVersion
)
Then, define some trivial REST interface:
import com.avsystem.commons.rest._
trait UserApi {
/** Returns ID of newly created user */
def createUser(name: String, birthYear: Int): Future[String]
}
object UserApi extends DefaultRestApiCompanion[UserApi]
Then, implement it on server side and expose it on localhost port 9090 using Jetty:
import com.avsystem.commons.jetty.rest.RestHandler
import org.eclipse.jetty.server.Server
class UserApiImpl extends UserApi {
def createUser(name: String, birthYear: Int) = Future.successful(s"$name-ID")
}
object ServerMain {
def main(args: Array[String]): Unit = {
val server = new Server(9090)
server.setHandler(RestHandler[UserApi](new UserApiImpl))
server.start()
server.join()
}
}
Finally, obtain a client proxy for your API using Jetty HTTP client and make a call:
import com.avsystem.commons.jetty.rest.RestClient
import org.eclipse.jetty.client.HttpClient
import scala.concurrent.Await
import scala.concurrent.duration._
object ClientMain {
def main(args: Array[String]): Unit = {
val client = new HttpClient
client.start()
val proxy = RestClient[UserApi](client, "http://localhost:9090/")
// just for this example, normally it's not recommended
import scala.concurrent.ExecutionContext.Implicits.global
val result = proxy.createUser("Fred", 1990)
.andThen { case _ => client.stop() }
.andThen {
case Success(id) => println(s"User $id created")
case Failure(cause) => cause.printStackTrace()
}
// just wait until future is complete so that main thread doesn't finish prematurely
Await.result(result, 10.seconds)
}
}
If we look at HTTP traffic, that's what we'll see:
Request:
POST http://localhost:9090/createUser HTTP/1.1
Accept-Encoding: gzip
User-Agent: Jetty/9.3.23.v20180228
Host: localhost:9090
Content-Type: application/json;charset=utf-8
Content-Length: 32
{"name":"Fred","birthYear":1990}
Response:
HTTP/1.1 200 OK
Date: Wed, 18 Jul 2018 11:43:08 GMT
Content-Type: application/json;charset=utf-8
Content-Length: 9
Server: Jetty(9.3.23.v20180228)
"Fred-ID"
As we saw in the quickstart example, REST API is defined by a Scala trait adjusted with annotations. This approach is analogous to various well-established REST frameworks for other languages, e.g. JAX-RS for Java. However, such frameworks are usually based on runtime reflection while in Scala it can be done using compile-time reflection through macros which offers several advantages:
- platform independency - REST traits are understood by both ScalaJVM and ScalaJS
- full type information - compile-time reflection is not limited by type erasure
- type safety - compile-time reflection can perform thorough validation of REST traits and raise compilation errors in case anything is wrong
- pluggable typeclass based serialization - for serialization of REST parameters and results, typeclasses are used which also offers strong compile-time safety. If any of your parameters or method results cannot be serialized, a detailed compilation error will be raised.
In order for a trait to be understood as REST API, it must have a well defined companion object that contains appropriate implicits:
- in order to expose REST API on a server, implicit instances of
RawRest.AsRawRpc
andRestMetadata
for API trait are required. - in order to use REST API client, implicit instances of
RawRest.AsRealRpc
andRestMetadata
for API trait are required. - when API trait is used by both client and server,
RawRest.AsRawRpc
andRawRest.AsRealRpc
may be provided by a single combined instance ofRawRest.AsRawRealRpc
for API trait.
Usually there is no need to declare these implicit instances manually because you can use one of the convenience base classes for REST API companion objects, e.g.
import com.avsystem.commons.rest._
trait MyApi { ... }
object MyApi extends DefaultRestApiCompanion[MyApi]
DefaultRestApiCompanion
takes a magic implicit parameter generated by a macro which will effectively
materialize all the necessary typeclass instances mentioned earlier. The "Default
" in its name means that
DefaultRestImplicits
is used as a provider of serialization-related implicits. See serialization
for more details on customizing serialization.
DefaultRestApiCompanion
provides all the implicit instances necessary for both the client and server.
If you intend to use your API trait only on the server or only on the client, you may want to use more lightweight
DefaultRestClientApiCompanion
or DefaultRestServerApiCompanion
. This may help you reduce the amount of macro
generated code and make compilation faster.
On less frequent occasions you might be unable to use one of the companion base classes. In such situations you must declare all the implicit instances manually (however, they will still be implemented with a macro). For example:
import com.avsystem.commons.rest._
trait MyApi { ... }
object GenericApi {
import DefaultRestImplicits._
implicit val restAsRawReal: RawRest.AsRawRealRpc[MyApi] = RawRest.materializeAsRawReal
implicit val restMetadata: RestMetadata[MyApi] = RestMetadata.materializeForRpc
}
This is usually necessary when implicit instances need some additional implicit dependencies or when the API trait is generic (has type parameters).
REST macro engine inspects API trait and looks for all abstract methods. It then tries to translate every abstract method into a HTTP REST call.
- By default (if not annotated explicitly) each method is interpreted as HTTP
POST
. - Method name is appended to the URL path.
- Every parameter is interpreted as part of the body - all the body parameters will be
combined into a JSON object sent through HTTP body. However, for
GET
methods, every parameter is interpreted as URL query parameter while the body is empty. - Result type of each method is typically expected to be a
Future
wrapping some arbitrary response type. This response type will be serialized into HTTP response which by default translates it into JSON and creates a200 OK
response withapplication/json
content type. If response type isUnit
(method result type isFuture[Unit]
) then empty body is created when serializing and body is ignored when deseriarlizing. - Each method may also throw a
HttpErrorException
(or return failedFuture
). It will be automatically translated into appropriate HTTP error response with given status code and plaintext message.
For details on how exactly serialization works and how to customize it, see serialization.
Note that if you don't want to use Future
, this customization also allows you to use other wrappers for method result types.
As mentioned earlier, each trait method is by default translated into a POST
request.
You can specify which HTTP method you want by explicitly annotating trait method as
@GET
/@POST
/@PATCH
/@PUT
or @DELETE
(from com.avsystem.commons.rest
package).
@DELETE def deleteUser(id: String): Future[Unit]
Trait method annotated as @GET
is interpreted somewhat differently from other HTTP methods.
Its parameters are interpreted as query parameters rather than body parameters. For example:
@GET def getUsername(id: String): Future[String]
Calling getUsername("ID")
on the client will result in HTTP request:
GET http://localhost:9090/getUsername?userId=ID HTTP/1.1
Accept-Encoding: gzip
User-Agent: Jetty/9.3.23.v20180228
Host: localhost:9090
By default, method name is appended to URL path when translating method call to HTTP request.
This can be customized. Every annotation specifying HTTP method (e.g. GET
) takes an optional
path
argument that you can use to customize your path:
@GET("username") def getUsername(id: String): Future[String]
The specified path may be multipart (it may contain slashes) or it may even be empty. However, for server-side APIs all paths must be unambiguous, i.e. there must not be more than one method translating to the same path. This is validated in runtime, upon creating a server.
Empty paths may be especially useful for prefix methods.
If a parameter of REST API trait method is annotated as @Path
, its value is
appended to URL path rather than translated into query parameter or body part.
@GET("username") def getUsername(@Path id: String): Future[String]
Calling getUsername("ID")
will make a HTTP request on path username/ID
.
If there are multiple @Path
parameters, their values are appended to the path in the
order of declaration. Each path parameters may also optionally specify a path suffix that
will be appended to path after value of each parameter:
@GET("users") def getUsername(@Path(pathSuffix = "name") id: String): Future[String]
Calling getUsername("ID")
will make a HTTP request on path users/ID/name
.
This way you can model completely arbitrary path patterns.
Values of path parameters are serialized into PathValue
objects,
see serialization for more details.
You may explicitly request that some parameter is translated into URL query parameter
using @Query
annotation. As mentioned earlier, parameters of GET
methods are treated
as query parameters by default, so this is only strictly necessary for non-GET
methods.
@Query
annotation also takes optional name
parameter which may be specified to customize
URL parameter name. If not specified, trait method parameter name is used.
Values of query parameters are serialized into QueryValue
objects,
see serialization for more details.
You may also request that some parameter is translated into a HTTP header using @Header
annotation.
It takes an obligatory name
argument that specifies HTTP header name.
Values of header parameters are serialized into HeaderValue
objects,
see serialization for more details.
As mentioned earlier, every parameter of non-GET
API trait method is interpreted as a field
of a JSON object sent as HTTP body. Just like for path, query and header parameters, there is a
@JsonBodyParam
annotation which requests this explicitly. However, it only makes sense to use this
annotation when one wants to customize the field name because GET
methods do not accept body parameters.
A method annotated as @GET
having a parameter annotated as @JsonBodyParam
will be rejected by REST
macro engine.
JSON body parameters are serialized into JsonValue
objects,
see serialization for more details.
Every non-GET
API method may also take a single parameter annotated as @Body
in place of multiple
unannotated parameters (implicitly interpreted as @JsonBodyParam
s). This way the value of that parameter
will be serialized straight into HTTP body. This gives you full control over the contents sent in HTTP body
and their format (i.e. it no longer needs to be application/json
).
case class User(id: String, login: String)
object User extends HasGenCodec[User]
@PUT def updateUser(@Body user: User): Future[Unit]
Single body parameters are serialized into HttpBody
objects,
see serialization for more details.
If a method in REST API trait doesn't return Future[T]
(or other type that
properly translates to asynchronous HTTP response) then it may also be interpreted as prefix method.
Prefix methods are methods that return other REST API traits. They are useful for:
- capturing common path or path/query/header parameters in a single prefix call
- splitting your REST API into multiple traits in order to better organize it
Just like HTTP API methods (GET
, POST
, etc.), prefix methods have their own
annotation that can be used explicitly when you want your trait method to be treated as
a prefix method. This annotation is @Prefix
and just like HTTP method annotations, it
takes an optional path
parameter. If you don't need to specify path explicitly then
annotation is not necessary as long as your method returns a valid REST API trait
(where "valid" is determined by presence of appropriate implicits -
see companion objects).
Prefix methods may take parameters. They are interpreted as path parameters by default,
but they may also be annotated as @Query
or @Header
. Prefix methods must not take
body parameters.
Path and parameters collected by a prefix method will be prepended/added to the HTTP request generated by a HTTP method call on the API trait returned by this prefix method. This way prefix methods "contribute" to the final HTTP requests.
However, sometimes it may also be useful to create completely "transparent" prefix methods - prefix methods with empty path and no parameters. This is useful when you want to refactor your REST API trait by grouping methods into multiple, separate traits without changing the format of HTTP requests.
Example of prefix method that adds authentication header to the overall API:
trait UserApi { ... }
object UserApi extends DefaultRestApiCompanion[UserApi]
trait RootApi {
@Prefix("") def auth(@Header("Authorization") token: String): UserApi
}
object RootApi extends DefaultRestApiCompanion[RootApi]
REST macro engine must be able to generate code that serializes and deserializes every parameter value and every method result into appropriate raw values which can be easily sent through network. Serialization in REST framework is typeclass based, which is a typical, functional and typesafe approach to serialization in Scala.
Examples of typeclass based serialization libraries include GenCodec (which is the default serialization used by this REST framework), circe (one of the most popular JSON libraries for Scala) or µPickle. Any of these solutions can be plugged into REST framework.
Depending on the context where a type is used in a REST API trait, it will be serialized to a different raw value:
- path/query/header parameters are serialized as
PathValue
/QueryValue
/HeaderValue
- JSON body parameters are serialized as
JsonValue
- Single body parameters are serialized as
HttpBody
- Response types are serialized as
RestResponse
- Prefix result types (other REST API traits) are "serialized" as
RawRest
When a macro needs to serialize a value of some type (let's call it Real
) to one of these raw types
listed above (let's call it Raw
) then it looks for an implicit instance of AsRaw[Raw, Real]
.
In the same manner, an implicit instance of AsReal[Raw, Real]
is used for deserialization.
Additionally, if there is an implicit instance of AsRawReal[Raw, Real]
then it serves both purposes.
These implicit instances may come from multiple sources:
- implicit scope of the
Raw
type (e.g. its companion object) - implicit scope of the
Real
type (e.g. its companion object) - implicits plugged by REST API trait companion
(e.g.
DefaultRestApiCompanion
plugs inDefaultRestImplicits
) - imports
Of course, these implicits may also depend on other implicits which effectively means that
you can use whatever typeclass-based serialization library you want.
For example, you can define an instance of AsRaw[JsonValue, Real]
which actually uses
Encoder[Real]
from circe. See Customizing serialization
for more details.
Path, query and header parameter values are serialized into PathValue
/QueryValue
/HeaderValue
.
These three classes are all simple String
wrappers. Thanks to the fact that they are distinct types,
you can have completely different serialization defined for each one of them if you need.
There are no "global" implicits defined for these raw types. They must be either imported, defined by each
"real" type or plugged in by REST API trait companion. For example, the DefaultRestApiCompanion
and its
variations automatically provide serialization to PathValue
/QueryValue
/HeaderValue
based on GenKeyCodec
and additional instances for Float
and Double
. This effectively provides serialization for all the
primitive types, its Java boxed counterparts, String
, all NamedEnum
s, Java enums and Timestamp
.
It's also easy to provide path/query/header serialization for any type which has a natural, unambiguous textual
representation.
Serialized values of path & query parameters are automatically URL-encoded when being embedded into HTTP requests. This means that serialization should not worry about that.
JSON body parameters are serialized into JsonValue
which is also a simple wrapper class over String
,
but is importantly distinct from PathValue
/QueryValue
/HeaderValue
because it must always contain
a valid JSON string. This is required because JSON body parameters are ultimately composed into a single
JSON object sent as HTTP body.
There are no "global" implicits defined for JsonValue
- JSON serialization must be either imported,
defined by each "real" type manually or plugged in by REST API trait companion. For example,
DefaultRestApiCompanion
and its variations automatically provide serialization to JsonValue
based
on GenCodec
, which means that if DefaultRestApiCompanion
is used then every type used in REST API trait
that has a GenCodec
instance will be serializable to JsonValue
.
Single body parameters (annotated as @Body
) are serialized straight into HttpBody
, which encapsulates
not only raw content but also MIME type. This way you can define custom body serializations for your types and
you are not limited to application/json
.
By default (if there are no more specific implicit instances defined),
serialization to HttpBody
falls back to JsonValue
and simply wraps JSON string into HTTP body
with application/json
type. This means that all types serializable to JsonValue
are automatically
serializable to HttpBody
.
Result type of every REST API method is wrapped into Try
(in case the method throws an exception)
and "serialized" into RawRest.Async[RestResponse]
.
This means that macro engine looks for an implicit instance of AsRaw/AsReal[RawRest.Async[RestResponse], Try[R]]
for every HTTP method with result type R
.
RestResponse
itself is a simple class that aggregates HTTP status code and body.
RestResponse
companion object defines default implicit instances of AsRaw/Real[RawRest.Async[RestResponse], Try[Future[R]]]
which depends on implicit AsRaw/Real[RestResponse, R]
. This effectively means that if your method returns Future[R]
then
it's enough if R
is serializable as RestResponse
.
However, there are even more defaults provided: if R
is serializable as HttpBody
then it's automatically serializable
as RestResponse
. This default translation of HttpBody
into RestResponse
always uses 200 as a status code.
When translating RestResponse
into HttpBody
and response contains other status code than 200, HttpErrorException
is thrown
(which will be subsequently captured into failed Future
).
Going even further with defaults, all types serializable as JsonValue
are serializable as HttpBody
.
This effectively means that when your method returns Future[R]
then you can provide serialization
of R
into any of the following: JsonValue
, HttpBody
, RestResponse
- depending on how much control
you need.
Ultimately, if you don't want to use Future
s, you may replace it with some other asynchronous wrapper type,
e.g. Monix Task or some IO monad.
See supporting result containers other than Future
.
When Scala compiler needs to find an implicit, it searches two scopes: lexical scope and implicit scope.
Lexical scope is made of locally visible and imported implicits. It has priority over implicit scope - implicit scope is searched only when implicit could not be found in lexical scope.
Implicit scope is made of companion objects of all traits and classes associated with the
type of implicit being searched for. Consult Scala Language Specification
for precise definition of the word "associated". As an example, implicit scope of type AsRaw[JsonValue,MyClass]
is
made of companion objects of AsRaw
, JsonValue
, MyClass
+ companion objects of all supertraits, superclasses and
enclosing traits/classes of MyClass
.
Implicits defined in implicit scope are effectively global and don't need to be imported.
REST framework deliberately provides no default implicits for serialization and deserialization. Instead, it introduces a mechanism through which serialization implicits are injected by companion objects of REST API traits. Thanks to this mechanism REST framework is not bound to any specific serialization library. At the same time it provides a concise method to inject serialization implicits that does not require importing them explicitly.
An example usage of this mechanism is DefaultRestApiCompanion
which injects GenCodec
-based
serialization.
Let's say you want to use e.g. circe for serialization to JsonValue
.
First, define a trait that contains implicits which translate Circe's Encoder
and Decoder
into
appropriate instances of AsReal
/AsRaw
:
import com.avsystem.commons.rest._
import com.avsystem.commons.rpc._
import io.circe._
import io.circe.parser._
import io.circe.syntax._
trait CirceRestImplicits {
implicit def encoderBasedAsRawJson[T: Encoder]: Fallback[AsRaw[JsonValue, T]] =
Fallback(AsRaw.create(v => JsonValue(v.asJson.noSpaces)))
implicit def decoderBasedJsonAsReal[T: Decoder]: Fallback[AsReal[JsonValue, T]] =
Fallback(AsReal.create(json => decode(json.value).fold(throw _, identity)))
}
object CirceRestImplicits extends CirceRestImplicits
Note that implicits are wrapped into Fallback
. This is not strictly required, but it's recommended
because these implicits ultimately will have to be imported into lexical scope, even if a macro does it
for us. However, we don't want these implicits to have higher priority than implicits from companion objects
of some concrete classes which need custom serialization. Because of that, we wrap our implicits into
Fallback
which keeps them visible but without elevated priority.
Now, in order to define a REST API trait that uses Circe-based serialization, you must appropriately inject it into its companion object:
trait MyRestApi { ... }
object MyRestApi extends RestApiCompanion[CirceRestImplicits, MyRestApi](CirceRestImplicits)
If you happen to use this often (e.g. because you always want to use Circe) then it may be useful to create convenience companion base class just for Circe:
abstract class CirceRestApiCompanion[Real](
implicit inst: RpcMacroInstances[CirceRestImplicits, FullInstances, Real])
) extends RestApiCompanion[CirceRestImplicits, Real](CirceRestImplicits)
Now you can define your trait more concisely as:
trait MyRestApi { ... }
object MyRestApi extends CirceRestApiCompanion[MyRestApi]
If you need to write manual serialization for your own type, the easiest way to do this is to provide appropriate implicit in its companion object:
class MyClass { ... }
object MyClass {
implicit val jsonAsRawReal: AsRawReal[JsonValue, MyClass] = AsRawReal.create(...)
}
If you need to define serialization implicits for a third party type, you can't do it through implicit scope because you can't modify its companion object. Instead, you can adjust implicits injected into REST API trait companion object.
Assume that companion objects of your REST API traits normally extend DefaultRestApiCompanion
, i.e.
GenCodec
-based serialization is used. Now, you can extend DefaultRestImplicits
to add serialization for
third party types:
trait EnhancedRestImplicits extends DefaultRestImplicits {
implicit val thirdPartyJsonAsRawReal: AsRawReal[JsonValue, ThirdParty] =
AsRawReal.create(...)
}
object EnhancedRestImplicits extends EnhancedRestImplicits
Then, you need to define your REST API trait as:
trait MyRestApi { ... }
object MyRestApi extends RestApiCompanion[EnhancedRestImplicits, MyRestApi](EnhancedRestImplicits)
By default, every HTTP method in REST API trait must return its return wrapped into a Future
.
However, Future
is low level and limited in many ways (e.g. there is no way to control when the actual
asynchronous computation starts). It is possible to use other task-like containers, e.g.
Monix Task or Cats IO.
In order to do that, you must provide some additional "serialization" implicits which will translate your
wrapped results into RawRest.Async[RestResponse]
. Just like when
providing serialization for third party type,
you should put that implicit into a trait and inject it into REST API trait's companion object.
Also note that the macro engine re-wraps the already wrapped result into Try
in case some REST API method throws an
exception. Therefore, if you want your methods to return Task[T]
then you need serialization for Try[Task[T]]
.
Additionally, you should provide an implicit instance of HttpResponseType
, similar to the one defined
in its companion object for Future
s. This drives materialization of RestMetadata
in a similar way
AsRaw
and AsReal
drive materialization of real<->raw interface translation.
Here's an example that shows what exactly must be implemented to add Monix Task support:
import monix.eval.Task
import com.avsystem.commons.rest.RawRest
trait MonixTaskRestImplicits {
implicit def taskAsAsync[T](implicit asResp: AsRaw[RestResponse, T]): AsRaw[RawRest.Async[RestResponse], Try[Task[T]]] =
AsRaw.create(...)
implicit def asyncAsTask[T](implicit fromResp: AsReal[RestResponse, T]): AsReal[RawRest.Async[RestResponse], Try[Task[T]]] =
AsReal.create(...)
implicit def taskResponseType[T]: HttpResponseType[Task[T]] =
new HttpResponseType[Task[T]] {}
}
object MonixTaskRestImplicits
REST framework gives you a certain amount of guarantees about backwards compatibility of your API. Here's a list of changes that you may safely do to your REST API traits without breaking clients that still use the old version:
- Adding new REST methods, as long as paths are still the same and unambiguous.
- Renaming REST methods, as long as old
path
is configured for them explicitly (e.g.@GET("oldname") def newname(...)
) - Reordering parameters of your REST methods, except for
@Path
parameters which may be freely intermixed with other parameters but they must retain the same order relative to each other. - Splitting parameters into multiple parameter lists or making them
implicit
. - Renaming
@Path
parameters - their names are not used in REST requests - Renaming non-
@Path
parameters, as long as the previous name is explicitly configured by@Query
,@Header
or@JsonBodyParam
annotation. - Removing non-
@Path
parameters - even if the client sends them, the server will just ignore them. - Adding new non-
@Path
parameters, as long as default value is provided for them - either as Scala-level default parameter value or by using@whenAbsent
annotation. The server will simply use the default value if parameter is missing in incoming HTTP request. - Changing parameter or result types or their serialization - as long as serialized formats of new and old type
are compatible. This depends on on the serialization library you're using. If you're using
GenCodec
, consult its documentation on retaining backwards compatibility.
Core REST framework (commons-core
module only) does not contain any backends, i.e. there is no actual network
client or server implementation. However, it's fairly easy to build them as most of the heavy-lifting related to
REST is already done by the core framework (its macro engine, in particular).
Jetty-based RestClient
and RestHandler
are provided by commons-jetty
module. They may serve as simple
reference backend implementation.
RawRest
object defines following type aliases:
type Callback[T] = Try[T] => Unit
type Async[T] = Callback[T] => Unit
type HandleRequest = RestRequest => Async[RestResponse]
RestRequest
is a simple, immutable representation of HTTP request. It contains HTTP method, path, URL
parameters, HTTP header values and a body (HttpBody
). All data is already in serialized form so it can be
easily sent through network.
RestResponse
is, similarly, a simple representation of HTTP response. RestResponse
is made of HTTP status
code and HTTP body (HttpBody
, which also contains MIME type).
An existing implementation of REST API trait can be easily turned into a HandleRequest
function using RawRest.asHandleRequest
.
Therefore, the only thing you need to do to expose your REST API trait as an actual web service it to turn
HandleRequest
function into a server. This is usually just a matter of translating native HTTP request into RestRequest
,
passing them to HandleRequest
function and translating resulting RestResponse
to native HTTP response.
See RestServlet
for an example implementation.
If you already have a HandleRequest
function, you can easily turn it into an implementation of desired REST API trait
using RawRest.fromHandleRequest
. This implementation is a macro-generated proxy which translates actual
method calls into invocations of provided HandleRequest
function.
Therefore, the only thing you need to to in order to wrap a native HTTP client into a REST API trait instance is
to turn this native HTTP client into a HandleRequest
function.
See Jetty-based RestClient
for
an example implementation.