Skip to content

Commit

Permalink
Merge pull request #1545 from softwaremill/simple-client
Browse files Browse the repository at this point in the history
Add a synchronous SimpleHttpClient
  • Loading branch information
adamw authored Sep 13, 2022
2 parents bc5f655 + e5dd191 commit da68e0b
Show file tree
Hide file tree
Showing 19 changed files with 326 additions and 14 deletions.
2 changes: 2 additions & 0 deletions build.sbt
Original file line number Diff line number Diff line change
Expand Up @@ -993,6 +993,7 @@ lazy val examples = (projectMatrix in file("examples"))
asyncHttpClientFs2Backend,
json4s,
circe,
upickle,
scribeBackend,
slf4jBackend
)
Expand Down Expand Up @@ -1044,6 +1045,7 @@ lazy val docs: ProjectMatrix = (projectMatrix in file("generated-docs")) // impo
sprayJson,
zioJson,
jsoniter,
upickle,
asyncHttpClientZioBackend,
// asyncHttpClientMonixBackend, // monix backends are commented out because they depend on cats-effect2
asyncHttpClientFs2Backend,
Expand Down
6 changes: 5 additions & 1 deletion core/src/main/scala/sttp/client3/RequestT.scala
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,11 @@ import scala.concurrent.duration.Duration

/** Describes a HTTP request, along with a description of how the response body should be handled.
*
* The request can be sent using a [[SttpBackend]], which provides a superset of the required capabilities.
* The request can be sent:
*
* - synchronously, using [[SimpleHttpClient.send()]]
* - using the [[send(SttpBackend)]] methods, which support any effect. The backend must provide a superset of the
* capabilities required by the request.
*
* @param response
* Description of how the response body should be handled. Needs to be specified upfront so that the response is
Expand Down
46 changes: 46 additions & 0 deletions core/src/main/scalajs/sttp/client3/SimpleHttpClient.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
package sttp.client3

import scala.concurrent.{ExecutionContext, Future}

/** A simple, synchronous http client. Usage example:
*
* {{{
* import sttp.client3.{SimpleHttpClient, UriContext, basicRequest}
*
* val client = SimpleHttpClient()
* val request = basicRequest.get(uri"https://httpbin.org/get")
* val response = client.send(request)
* response.map(r => println(r.body))
* }}}
*
* Wraps an [[SttpBackend]], which can be substituted or modified using [[wrapBackend]], adding e.g. logging.
*
* Creating a client allocates resources, hence when no longer needed, the client should be closed using [[close]].
*/
case class SimpleHttpClient(backend: SttpBackend[Future, Any]) {

def send[T](request: Request[T, Any]): Future[Response[T]] = backend.send(request)

def withBackend(newBackend: SttpBackend[Future, Any]): SimpleHttpClient = copy(backend = newBackend)
def wrapBackend(f: SttpBackend[Future, Any] => SttpBackend[Future, Any]): SimpleHttpClient =
copy(backend = f(backend))

def close(): Future[Unit] = backend.close()
}

object SimpleHttpClient {
def apply(): SimpleHttpClient = SimpleHttpClient(FetchBackend())

/** Runs the given function `f` with a new, default instance of [[SimpleHttpClient]] and closes the client after the
* function completes, cleaning up any resources.
*/
def apply[T](f: SimpleHttpClient => Future[T])(implicit ec: ExecutionContext): Future[T] = {
val client = SimpleHttpClient()
f(client)
.map(r => client.close().map(_ => r))
.recover { case t: Throwable =>
client.close().map(_ => Future.failed[T](t)).recover { case _ => Future.failed[T](t) }.flatMap(identity)
}
.flatMap(identity)
}
}
1 change: 1 addition & 0 deletions core/src/main/scalajs/sttp/client3/quick.scala
Original file line number Diff line number Diff line change
Expand Up @@ -4,4 +4,5 @@ import scala.concurrent.Future

object quick extends SttpApi {
lazy val backend: SttpBackend[Future, Any] = FetchBackend()
lazy val simpleHttpClient: SimpleHttpClient = SimpleHttpClient(backend)
}
40 changes: 40 additions & 0 deletions core/src/main/scalajvm/sttp/client3/SimpleHttpClient.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
package sttp.client3

/** A simple, synchronous http client. Usage example:
*
* {{{
* import sttp.client3.{SimpleHttpClient, UriContext, basicRequest}
*
* val client = SimpleHttpClient()
* val request = basicRequest.get(uri"https://httpbin.org/get")
* val response = client.send(request)
* println(response.body)
* }}}
*
* Wraps an [[SttpBackend]], which can be substituted or modified using [[wrapBackend]], adding e.g. logging.
*
* Creating a client allocates resources, hence when no longer needed, the client should be closed using [[close]].
*/
case class SimpleHttpClient(backend: SttpBackend[Identity, Any]) {

def send[T](request: Request[T, Any]): Response[T] = backend.send(request)

def withBackend(newBackend: SttpBackend[Identity, Any]): SimpleHttpClient = copy(backend = newBackend)
def wrapBackend(f: SttpBackend[Identity, Any] => SttpBackend[Identity, Any]): SimpleHttpClient =
copy(backend = f(backend))

def close(): Unit = backend.close()
}

object SimpleHttpClient {
def apply(): SimpleHttpClient = SimpleHttpClient(HttpClientSyncBackend())

/** Runs the given function `f` with a new, default instance of [[SimpleHttpClient]] and closes the client after the
* function completes, cleaning up any resources.
*/
def apply[T](f: SimpleHttpClient => T): T = {
val client = SimpleHttpClient()
try f(client)
finally client.close()
}
}
1 change: 1 addition & 0 deletions core/src/main/scalajvm/sttp/client3/quick.scala
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,5 @@ package sttp.client3

object quick extends SttpApi {
lazy val backend: SttpBackend[Identity, Any] = HttpClientSyncBackend()
lazy val simpleHttpClient: SimpleHttpClient = SimpleHttpClient(backend)
}
40 changes: 40 additions & 0 deletions core/src/main/scalanative/sttp/client3/SimpleHttpClient.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
package sttp.client3

/** A simple, synchronous http client. Usage example:
*
* {{{
* import sttp.client3.{SimpleHttpClient, UriContext, basicRequest}
*
* val client = SimpleHttpClient()
* val request = basicRequest.get(uri"https://httpbin.org/get")
* val response = client.send(request)
* println(response.body)
* }}}
*
* Wraps an [[SttpBackend]], which can be substituted or modified using [[wrapBackend]], adding e.g. logging.
*
* Creating a client allocates resources, hence when no longer needed, the client should be closed using [[close]].
*/
case class SimpleHttpClient(backend: SttpBackend[Identity, Any]) {

def send[T](request: Request[T, Any]): Response[T] = backend.send(request)

def withBackend(newBackend: SttpBackend[Identity, Any]): SimpleHttpClient = copy(backend = newBackend)
def wrapBackend(f: SttpBackend[Identity, Any] => SttpBackend[Identity, Any]): SimpleHttpClient =
copy(backend = f(backend))

def close(): Unit = backend.close()
}

object SimpleHttpClient {
def apply(): SimpleHttpClient = SimpleHttpClient(CurlBackend())

/** Runs the given function `f` with a new, default instance of [[SimpleHttpClient]] and closes the client after the
* function completes, cleaning up any resources.
*/
def apply[T](f: SimpleHttpClient => T): T = {
val client = SimpleHttpClient()
try f(client)
finally client.close()
}
}
1 change: 1 addition & 0 deletions core/src/main/scalanative/sttp/client3/quick.scala
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,5 @@ package sttp.client3

object quick extends SttpApi {
lazy val backend: SttpBackend[Identity, Any] = CurlBackend()
lazy val simpleHttpClient: SimpleHttpClient = SimpleHttpClient(backend)
}
15 changes: 15 additions & 0 deletions docs/examples.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,21 @@

All of the examples are available [in the sources](https://github.com/softwaremill/sttp/blob/master/examples/src/main/scala/sttp/client3/examples) in runnable form.

## Use the simple synchronous client

Required dependencies:

```scala
libraryDependencies ++= List("com.softwaremill.sttp.client3" %% "core" % "@VERSION@")
```

Example code:

```eval_rst
.. literalinclude:: ../../examples/src/main/scala/sttp/client3/examples/SimpleClientGetAndPost.scala
:language: scala
```

## POST a form using the synchronous backend

Required dependencies:
Expand Down
1 change: 1 addition & 0 deletions docs/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,7 @@ Development and maintenance of sttp client is sponsored by [SoftwareMill](https:
:caption: Getting started
quickstart
simple_sync
how
goals
community
Expand Down
37 changes: 37 additions & 0 deletions docs/json.md
Original file line number Diff line number Diff line change
Expand Up @@ -210,6 +210,43 @@ implicit val payloadJsonCodec: JsonValueCodec[RequestPayload] = JsonCodecMaker.m
implicit val jsonEitherDecoder: JsonValueCodec[ResponsePayload] = JsonCodecMaker.make
val requestPayload = RequestPayload("some data")

val response: Identity[Response[Either[ResponseException[String, Exception], ResponsePayload]]] =
basicRequest
.post(uri"...")
.body(requestPayload)
.response(asJson[ResponsePayload])
.send(backend)
```

## uPickle

To encode and decode JSON using the [uPickle](https://github.com/com-lihaoyi/upickle) library, add the following dependency to your project:

```scala
"com.softwaremill.sttp.client3" %% "upickle" % "@VERSION@"
```

or for ScalaJS (cross build) projects:

```scala
"com.softwaremill.sttp.client3" %%% "upickle" % "@VERSION@"
```

To use, add an import: `import sttp.client3.upicklejson._` (or extend `SttpUpickleApi`) and define an implicit `ReadWriter` (or separately `Reader` and `Writer`) for your datatype.
Usage example:

```scala mdoc:compile-only
import sttp.client3._
import sttp.client3.upicklejson._
import upickle.default._

val backend: SttpBackend[Identity, Any] = HttpClientSyncBackend()

implicit val requestPayloadRW: ReadWriter[RequestPayload] = macroRW[RequestPayload]
implicit val responsePayloadRW: ReadWriter[ResponsePayload] = macroRW[ResponsePayload]

val requestPayload = RequestPayload("some data")

val response: Identity[Response[Either[ResponseException[String, Exception], ResponsePayload]]] =
basicRequest
.post(uri"...")
Expand Down
19 changes: 12 additions & 7 deletions docs/quickstart.md
Original file line number Diff line number Diff line change
@@ -1,8 +1,13 @@
# Quickstart

The core sttp client API comes in a single jar, with a transitive dependency on [sttp model](https://github.com/softwaremill/sttp-model). This also includes a default, [synchronous](backends/synchronous.md) backend, which is based on Java's `HttpClient`.
The core sttp client API comes in a single jar, with a transitive dependency on [sttp model](https://github.com/softwaremill/sttp-model). This also includes a default [synchronous simple client](simple_sync.md) and [synchronous](backends/synchronous.md) and [`Future`-based] backends, based on Java's `HttpClient`.

To integrate with other parts of your application, you'll often need to use an alternate backend (but what's important is that the API remains the same!). See the section on [backends](backends/summary.md) for a short guide on which backend to choose, and a list of all implementations.
To integrate with other parts of your application and various effect systems, you'll often need to use an alternate backend (but what's important is that the API remains the same!). See the section on [backends](backends/summary.md) for a short guide on which backend to choose, and a list of all implementations.

`sttp client` is available for Scala 2.11, 2.12 and 2.13, as well as for Scala 3 and requires Java 11 or higher.

`sttp client` is also available for Scala.js 1.0 and Scala Native. Note that not all modules are compatible with these
platforms, and that each has its own dedicated set of backends.

## Using sbt

Expand All @@ -12,9 +17,9 @@ The basic dependency which provides the API, together with a synchronous and `Fu
"com.softwaremill.sttp.client3" %% "core" % "@VERSION@"
```

`sttp client` is available for Scala 2.11, 2.12 and 2.13, as well as for Scala 3 and requires Java 11.
## Simple synchronous client

`sttp client` is also available for Scala.js 1.0. Note that not all modules are compatible and there are no backends that can be used on both. The last version compatible with Scala.js 0.6 was 2.2.1. Scala Native is supported as well.
If you'd like to send some requests synchronously, take a look at the [simple synchronous client](simple_sync.md).

## Using Ammonite

Expand All @@ -23,12 +28,12 @@ If you are an [Ammonite](https://ammonite.io) user, you can quickly start experi
```scala
import $ivy.`com.softwaremill.sttp.client3::core:@VERSION@`
import sttp.client3.quick._
quickRequest.get(uri"http://httpbin.org/ip").send(backend)
simpleSttpClient.send(quickRequest.get(uri"http://httpbin.org/ip"))
```

Importing the `quick` object has the same effect as importing `sttp.client3._`, plus defining a synchronous backend (`val backend = HttpClientSyncBackend()`), so that sttp can be used right away.

If the default `HttpURLConnectionBackend` for some reason is insufficient, you can also use one based on OkHttp or HttpClient:
If the default backend is for some reason insufficient, you can also use one based on OkHttp:

```scala
import $ivy.`com.softwaremill.sttp.client3::okhttp-backend:@VERSION@`
Expand Down Expand Up @@ -59,4 +64,4 @@ val response = basicRequest
println(response.body)
```

Next, read on about the [how sttp client works](how.md) or see some [examples](examples.md).
Next, read on [how sttp client works](how.md) or see some [examples](examples.md).
87 changes: 87 additions & 0 deletions docs/simple_sync.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
# Simple synchronous client

The core module of sttp client includes a simple, synchronous client, which can be used to send requests without
the need to choose or explicitly create a backend.

A simple request can be sent as follows:

```scala mdoc:compile-only
import sttp.client3.{SimpleHttpClient, UriContext, basicRequest}

val client = SimpleHttpClient()
val response = client.send(basicRequest.get(uri"https://httpbin.org/get"))
println(response.body)
```

Creating a client allocates resources (such as selector threads / connection pools), so when it's no longer needed, it
should be closed using `.close()`. Typically, you should have one client instance for your entire application.

## Serialising and parsing JSON

To serialize a custom type to a JSON body, or to deserialize the response body that is in the JSON format, you'll need
to add an integration with a JSON library. See [json](json.md) for a list of available libraries.

As an example, to integrate with the [uPickle](https://github.com/com-lihaoyi/upickle) library, add the following
dependency:

```scala
"com.softwaremill.sttp.client3" %% "upickle" % "@VERSION@"
```

Your code might then look as follows:

```scala mdoc:compile-only
import sttp.client3.{SimpleHttpClient, UriContext, basicRequest}
import sttp.client3.upicklejson._
import upickle.default._

val client = SimpleHttpClient()

case class MyRequest(field1: String, field2: Int)
// selected fields from the JSON that is being returned by httpbin
case class HttpBinResponse(origin: String, headers: Map[String, String])

implicit val myRequestRW: ReadWriter[MyRequest] = macroRW[MyRequest]
implicit val responseRW: ReadWriter[HttpBinResponse] = macroRW[HttpBinResponse]

val request = basicRequest
.post(uri"https://httpbin.org/post")
.body(MyRequest("test", 42))
.response(asJson[HttpBinResponse])
val response = client.send(request)

response.body match {
case Left(e) => println(s"Got response exception:\n$e")
case Right(r) => println(s"Origin's ip: ${r.origin}, header count: ${r.headers.size}")
}
```

## Adding logging

Logging can be added using the [logging backend wrapper](backends/wrappers/logging.md). For example, if you'd like to
use slf4j, you'll need the following dependency:

```
"com.softwaremill.sttp.client3" %% "slf4j-backend" % "@VERSION@"
```

Then, you'll need to configure your client:

```scala mdoc:compile-only
import sttp.client3.{SimpleHttpClient, UriContext, basicRequest}
import sttp.client3.logging.slf4j.Slf4jLoggingBackend

val client = SimpleHttpClient().wrapBackend(Slf4jLoggingBackend(_))
```

## Relationship with backends

The `SimpleHttpClient` serves as a simple starting point for sending requests in a synchronous way. For more advanced
use-cases, you should use an [sttp backend](backends/summary.md) directly. For example, if you'd like to send requests
asynchronously, getting a `Future` as the result. Or, if you manage side effects using an `IO` or `Task`.

In fact, an instance of `SimpleHttpClient` is a thin wrapper on top of a backend.

## Next steps

Read on [how sttp client works](how.md) or see some more [examples](examples.md).
Loading

0 comments on commit da68e0b

Please sign in to comment.