An open API service indexing awesome lists of open source software.

https://github.com/tek/polysemy-http

polysemy effect for http-client
https://github.com/tek/polysemy-http

Last synced: about 1 year ago
JSON representation

polysemy effect for http-client

Awesome Lists containing this project

README

          

# About

This Haskell library provides a [Polysemy] effect for running HTTP requests
with [http-client].

# Example

```haskell
import Polysemy (runM)
import Polysemy.Resource (resourceToIO)
import qualified Polysemy.Http as Http
import Polysemy.Http (interpretHttpNative)
import Polysemy.Log (interpretLogStdout')

main :: IO ()
main = do
result <- runM $
resourceToIO $
interpretLogStdout' $
interpretHttpNative $
Http.request (Http.get "hackage.haskell.org" "package/polysemy-http")
print result
```

# API

## Request

The effect constructor `Http.request` takes an argument of type
`Polysemy.Http.Data.Request.Request`:

```haskell
data Request =
Request {
_method :: Method,
_host :: Host,
_port :: Maybe Port,
_tls :: Tls,
_path :: Path,
_headers :: [(HeaderName, HeaderValue)],
_cookies :: CookieJar,
_query :: [(QueryKey, Maybe QueryValue)],
_body :: Body
}
```

Most of these fields are just newtypes, except for `Method`, which is an enum:

```haskell
data Method =
Get | Post | ... | Custom Text
```

It has an `IsString` instance, so you can just write `"GET"` or `"delete"`.

All `Text` newtypes have `IsString` as well, and they will be converted to
`CI` and `ByteString` if needed when they are passed to [http-client].
`Body` is an `LByteString` newtype since that is what [aeson] typically
produces.
The port field is intended for nonstandard ports – if it is `Nothing`, the port
will be determined from `tls`.

## Response

`Http.request` returns `Either HttpError (Response LByteString)`, with
`Polysemy.Http.Data.Response.Response` looking like this:

```haskell
data Response b =
Response {
_status :: Status,
_body :: b,
_headers :: [Header],
_cookies :: CookieJar
}

data Header =
Header {
name :: HeaderName,
value :: HeaderValue
}
```

`Status` is from `http-types`, because it has some helpful combinators. Its
`Header` is just an alias, so this newtype is provided.
The parameter `b` is intended to allow you to write interpreters that produce
`Text` or something else, for example for [#testing].

# Streaming

The higher-order constructor `Http.stream` opens and closes the request
manually and passes the response to a handler function.
The function `streamResponse` provides a simpler interface for this mechanism
that runs a loop that passes individual chunks produced by [http-client] to
a callback handler of type `∀ x . StreamEvent r c h x -> Sem r x` that should
look like this:

```haskell
handle ::
StreamEvent Double (IO ByteString) Int a ->
Sem r a
handle = \case
StreamEvent.Acquire (Response status body headers) ->
pure 1
StreamEvent.Chunk handle (StreamChunk c) ->
pure ()
StreamEvent.Result (Response status body headers) handle ->
pure 5.5
StreamEvent.Release handle ->
pure ()

run :: Sem r Double
run =
Http.streamResponse (Request.get "host.com" "path/to/file") handle
```

If you were e.g. to write the data to disk, you would open the file in the
`Acquire` block, write the `ByteString` `c` in `Chunk`, and close the file in
`Release`.
The parameter `h` could then be `Handle`.
The callbacks are wrapped in `Resource.bracket`, so `Release` is guaranteed to
be called (as much as `Resource` is reliable).
The `Result` block is called when the transfer is complete; its returned value
is finally returned from `streamHandler.`
The `handle` is an arbitrary identifier that the user can return from
`Acquire`; it is not needed for the machinery and may be `()`.

# Entity

The library also provides effects for request and response entity de/encoding,
`EntityDecode d m a` and `EntityEncode d m a`, making it possible to abstract
over json implementations or content types using interpreters.
Since the effects are parameterized by the codec data type, one interpreter per
type must be used.

Implementations for [aeson] are available as `interpretEntityDecodeAeson` and
`interpretEntityEncodeAeson`:

```haskell
import Polysemy (run)
import qualified Polysemy.Http as Http
import Polysemy.Http (interpretEntityDecodeAeson)

data Dat { a :: Maybe Int, b :: Text }
deriving (Show, FromJSON)

main :: IO
main =
print $ run $ interpretEntityDecodeAeson $ Http.decode "{ \"b\": \"hello\" }"
```

There is not integration with the `Http` effect for this.

# Testing

Polysemy makes it very easy to switch the native interpreter for a mock, and
there is a convenience interpreter named `interpretHttpPure` that allows you
to specify a list of responses and chunks that should be produced:

```haskell
main :: IO ()
main = do
result <- runM $
resourceToIO $
interpretLogStdout' $
interpretHttpPure [Response (toEnum 200) "foo" []] [] $
Http.request (Http.get "hackage.haskell.org" "package/polysemy-http")
print result
```

[Polysemy]: https://hackage.haskell.org/package/polysemy
[http-client]: https://hackage.haskell.org/package/http-client
[http-types]: https://hackage.haskell.org/package/http-types
[aeson]: https://hackage.haskell.org/package/aeson