Add validator continuation
Update routing readme
This commit is contained in:
parent
13af9e70f5
commit
12726379bf
@ -3,6 +3,7 @@
|
|||||||
## Unpublished
|
## Unpublished
|
||||||
|
|
||||||
- Add json request parsing simplifications
|
- Add json request parsing simplifications
|
||||||
|
- Add request validation
|
||||||
|
|
||||||
## v1.1.0
|
## v1.1.0
|
||||||
|
|
||||||
|
184
Readme.md
184
Readme.md
@ -2,9 +2,9 @@
|
|||||||
|
|
||||||
[![License](https://img.shields.io/badge/license-MIT-blue.svg)](https://raw.githubusercontent.com/sigma-andex/purescript-httpurple/main/License)
|
[![License](https://img.shields.io/badge/license-MIT-blue.svg)](https://raw.githubusercontent.com/sigma-andex/purescript-httpurple/main/License)
|
||||||
|
|
||||||
A 🎨 colourful fork of the amazing [HTTPure](https://github.com/citizennet/purescript-httpure) http server framework.
|
A functional http framework with a focus on type-safety and simplicity.
|
||||||
|
|
||||||
Coming from HTTPure? You might want to have a look at the [differences to HTTPure](#differences-to-httpure).
|
This project was originally forked from the amazing [HTTPure](https://github.com/citizennet/purescript-httpure) http server framework. If you are coming from HTTPure you might want to have a look at the [differences to HTTPure](./docs/Differences.md).
|
||||||
|
|
||||||
## ToC
|
## ToC
|
||||||
1. [Installation](#installation)
|
1. [Installation](#installation)
|
||||||
@ -12,7 +12,6 @@ Coming from HTTPure? You might want to have a look at the [differences to HTTPur
|
|||||||
1. [Documenation](#documentation)
|
1. [Documenation](#documentation)
|
||||||
1. [Examples](#examples)
|
1. [Examples](#examples)
|
||||||
1. [Testing](#testing)
|
1. [Testing](#testing)
|
||||||
1. [Differences to HTTPure](#differences-to-httpure)
|
|
||||||
1. [License](#license)
|
1. [License](#license)
|
||||||
|
|
||||||
## Installation
|
## Installation
|
||||||
@ -93,185 +92,6 @@ To run the test suite, in the project root run:
|
|||||||
spago -x test.dhall test
|
spago -x test.dhall test
|
||||||
```
|
```
|
||||||
|
|
||||||
## Differences to HTTPure
|
|
||||||
|
|
||||||
HTTPurple 🪁 is a fork of [HTTPure](https://github.com/citizennet/purescript-httpure) that I started to freely experiment with some ideas I have on improving the usage experience. Currently I have no intentions on back-porting any of it to HTTPure, as I don't have the time for it and also don't want to restrict myself.
|
|
||||||
|
|
||||||
If you have used HTTPure before, you'll probably want to go through the following changes to get started using HTTPurple 🪁:
|
|
||||||
* [routing-duplex](#routing-duplex)
|
|
||||||
* [startup options](#startup-options)
|
|
||||||
* [request parsing and validation](#request-parsing-and-validation)
|
|
||||||
* [other improvements](#other-improvmenets)
|
|
||||||
* [hot module reloading](#hot-module-reloading)
|
|
||||||
|
|
||||||
### Routing-duplex
|
|
||||||
|
|
||||||
The most notable difference to HTTPure is that HTTPurple 🪁 uses the amazing [`routing-duplex`](https://github.com/natefaubion/purescript-routing-duplex) library for routing. I found the previous lookup-based routing tedious to work with, especially when having more complex routes, and quite error-prone, especially if you need reverse-routing for redirects.
|
|
||||||
|
|
||||||
[`routing-duplex`](https://github.com/natefaubion/purescript-routing-duplex) offers an elegant bidirectional routing which was initially designed for SPAs. Have a look at the really extensive [`documentation`](https://github.com/natefaubion/purescript-routing-duplex). The benefits of using routing-duplex are
|
|
||||||
* Much simpler and less tedious definition of routes
|
|
||||||
* Roundtrip printing/parsing of routes, so no more invalid redirects
|
|
||||||
* Exhaustive pattern matching so you are sure to match all defined routes
|
|
||||||
* Option to separate routes into logical groups
|
|
||||||
|
|
||||||
Here is a bit more elaborated examples:
|
|
||||||
|
|
||||||
```purescript
|
|
||||||
module Main where
|
|
||||||
|
|
||||||
import Prelude hiding ((/))
|
|
||||||
|
|
||||||
import Data.Either (Either(..))
|
|
||||||
import Data.Generic.Rep (class Generic)
|
|
||||||
import Data.Maybe (Maybe(..))
|
|
||||||
import Data.Tuple (Tuple(..))
|
|
||||||
import HTTPurple
|
|
||||||
|
|
||||||
data Route
|
|
||||||
= Home
|
|
||||||
| Profile String
|
|
||||||
| Account String
|
|
||||||
| Search { q :: String, sorting :: Maybe Sort }
|
|
||||||
derive instance Generic Route _
|
|
||||||
|
|
||||||
data Sort = Asc | Desc
|
|
||||||
derive instance Generic Sort _
|
|
||||||
|
|
||||||
sortToString :: Sort -> String
|
|
||||||
sortToString = case _ of
|
|
||||||
Asc -> "asc"
|
|
||||||
Desc -> "desc"
|
|
||||||
|
|
||||||
sortFromString :: String -> Either String Sort
|
|
||||||
sortFromString = case _ of
|
|
||||||
"asc" -> Right Asc
|
|
||||||
"desc" -> Right Desc
|
|
||||||
val -> Left $ "Not a sort: " <> val
|
|
||||||
|
|
||||||
sort :: RouteDuplex' String -> RouteDuplex' Sort
|
|
||||||
sort = as sortToString sortFromString
|
|
||||||
|
|
||||||
api :: RouteDuplex' Route
|
|
||||||
api = root $ sum
|
|
||||||
{ "Home": noArgs
|
|
||||||
, "Profile": "profile" / string segment
|
|
||||||
, "Account": "account" / string segment
|
|
||||||
, "Search": "search" ? { q: string, sorting: optional <<< sort }
|
|
||||||
}
|
|
||||||
|
|
||||||
main :: ServerM
|
|
||||||
main = serve { port: 8080 } { route: api, router: apiRouter }
|
|
||||||
where
|
|
||||||
|
|
||||||
apiRouter { route: Home } = ok "hello world!"
|
|
||||||
apiRouter { route: Profile profile } = ok $ "hello " <> profile <> "!"
|
|
||||||
apiRouter { route: Account account } = found' redirect ""
|
|
||||||
where
|
|
||||||
reverseRoute = print api $ Profile account
|
|
||||||
redirect = headers [ Tuple "Location" reverseRoute ]
|
|
||||||
apiRouter { route: Search { q, sorting } } = ok $ "searching for query " <> q <> " " <> case sorting of
|
|
||||||
Just Asc -> "ascending"
|
|
||||||
Just Desc -> "descending"
|
|
||||||
Nothing -> "defaulting to ascending"
|
|
||||||
```
|
|
||||||
|
|
||||||
### Startup options
|
|
||||||
|
|
||||||
HTTPurple 🪁 greatly simplifies the startup options and functions. The `serve`, `serve'`, `serveSecure` and `serveSecure'` have been merged into a single function `serve` that accepts listen options as the first parameter and uses sane defaults if you don't provide any.
|
|
||||||
|
|
||||||
The easiest way to start a server is to provide just the route and a router:
|
|
||||||
|
|
||||||
```purescript
|
|
||||||
main :: ServerM
|
|
||||||
main =
|
|
||||||
serve {} { route, router }
|
|
||||||
```
|
|
||||||
|
|
||||||
This will spin up the http server with sane defaults.
|
|
||||||
```bash
|
|
||||||
HTTPurple 🪁 up and running on http://0.0.0.0:8080
|
|
||||||
```
|
|
||||||
|
|
||||||
But you can overwrite any of the optional properties like this
|
|
||||||
|
|
||||||
```purescript
|
|
||||||
main :: ServerM
|
|
||||||
main =
|
|
||||||
serve {
|
|
||||||
hostname: "localhost"
|
|
||||||
, port: 9000
|
|
||||||
, certFile: "./Certificate.cer"
|
|
||||||
, keyFile: "./Key.key"
|
|
||||||
, notFoundHandler
|
|
||||||
, onStarted: log "Server started 🚀"
|
|
||||||
, closingHandler: NoClosingHandler
|
|
||||||
} { route, router }
|
|
||||||
where
|
|
||||||
notFoundHandler :: Request Unit -> ResponseM
|
|
||||||
notFoundHandler = const $ ok "Nothing to see here"
|
|
||||||
```
|
|
||||||
|
|
||||||
### Request parsing and validation
|
|
||||||
|
|
||||||
HTTPurple 🪁 makes request parsing and validation super simple. My typical http service scenario looks like this:
|
|
||||||
1. Parse the request json and return a bad request if the request body doesn't contain the valid json format
|
|
||||||
2. Validate the json input semanticall and transform it into some kind of internal model. Return bad request (with some error code) in case it is invalid.
|
|
||||||
3. Do something with the request
|
|
||||||
4. Return the output as a json
|
|
||||||
|
|
||||||
HTTPurple 🪁 uses continuations to make this standard scenario straight-forward (see example below).
|
|
||||||
|
|
||||||
Furthermore, HTTPurple 🪁 doesn't mandate a json parsing library. So you can use [`argonaut`](https://github.com/purescript-contrib/purescript-argonaut) using the [`argonaut-driver`](https://github.com/sigma-andex/purescript-httpurple-argonaut), use [`yoga-json`](https://github.com/rowtype-yoga/purescript-yoga-json) using the [`yoga-json-driver`](https://github.com/sigma-andex/purescript-httpurple-yoga-json) or write your own json driver.
|
|
||||||
|
|
||||||
Here is an example how that looks like:
|
|
||||||
```purescript
|
|
||||||
apiRouter { route: Home, method: Post, body } = usingCont do
|
|
||||||
req@{ name } :: HelloWorldRequest <- fromJson Argonaut.jsonDecoder body
|
|
||||||
ok $ "hello " <> name <> "!"
|
|
||||||
```
|
|
||||||
In case `fromJson` succeeds, the next step will be executed, otherwise a 400 bad request is returned.
|
|
||||||
|
|
||||||
### Other improvmenets
|
|
||||||
|
|
||||||
* Default closing handler - A default closing handler is provided so you can just stop your server using `ctrl+x` without having to worry about anything. You can deactivate it by setting `closingHandler: NoClosingHandler` in the listen options.
|
|
||||||
|
|
||||||
### Hot module reloading
|
|
||||||
|
|
||||||
With HTTPurple 🪁 you can easily set up a hot module reloading workflow:
|
|
||||||
|
|
||||||
Create an `index.js` with the content:
|
|
||||||
```javascript
|
|
||||||
import * as Main from './output/Main/index.js'
|
|
||||||
Main.main()
|
|
||||||
```
|
|
||||||
|
|
||||||
Add to `package.json`:
|
|
||||||
```json
|
|
||||||
...
|
|
||||||
"scripts": {
|
|
||||||
"hot": "spago build -w & nodemon \"node index.js\""
|
|
||||||
},
|
|
||||||
"type": "module",
|
|
||||||
...
|
|
||||||
```
|
|
||||||
|
|
||||||
Spin up:
|
|
||||||
```bash
|
|
||||||
npm run hot
|
|
||||||
```
|
|
||||||
Develop:
|
|
||||||
```bash
|
|
||||||
HTTPurple 🪁 up and running on http://0.0.0.0:8080
|
|
||||||
[nodemon] restarting due to changes...
|
|
||||||
[nodemon] restarting due to changes...
|
|
||||||
[nodemon] starting `node "node index.js" index.js`
|
|
||||||
HTTPurple 🪁 up and running on http://0.0.0.0:8080
|
|
||||||
[nodemon] restarting due to changes...
|
|
||||||
[nodemon] restarting due to changes...
|
|
||||||
[nodemon] starting `node "node index.js" index.js`
|
|
||||||
HTTPurple 🪁 up and running on http://0.0.0.0:8080
|
|
||||||
```
|
|
||||||
|
|
||||||
## License
|
## License
|
||||||
|
|
||||||
This is a fork of [HTTPure](https://github.com/citizennet/purescript-httpure), which is licensed under MIT. See the [original license](./LICENSES/httpure.LICENSE). This work is similarly licensed under [MIT](./License).
|
This is a fork of [HTTPure](https://github.com/citizennet/purescript-httpure), which is licensed under MIT. See the [original license](./LICENSES/httpure.LICENSE). This work is similarly licensed under [MIT](./License).
|
||||||
|
178
docs/Differences.md
Normal file
178
docs/Differences.md
Normal file
@ -0,0 +1,178 @@
|
|||||||
|
# Differences to HTTPure
|
||||||
|
|
||||||
|
HTTPurple 🪁 is a fork of [HTTPure](https://github.com/citizennet/purescript-httpure) that I started to freely experiment with some ideas I have on improving the usage experience. Currently I have no intentions on back-porting any of it to HTTPure, as I don't have the time for it and also don't want to restrict myself.
|
||||||
|
|
||||||
|
If you have used HTTPure before, you'll probably want to go through the following changes to get started using HTTPurple 🪁:
|
||||||
|
* [routing-duplex](#routing-duplex)
|
||||||
|
* [startup options](#startup-options)
|
||||||
|
* [request parsing and validation](#request-parsing-and-validation)
|
||||||
|
* [other improvements](#other-improvmenets)
|
||||||
|
* [hot module reloading](#hot-module-reloading)
|
||||||
|
|
||||||
|
## Routing-duplex
|
||||||
|
|
||||||
|
The most notable difference to HTTPure is that HTTPurple 🪁 uses the amazing [`routing-duplex`](https://github.com/natefaubion/purescript-routing-duplex) library for routing. I found the previous lookup-based routing tedious to work with, especially when having more complex routes, and quite error-prone, especially if you need reverse-routing for redirects.
|
||||||
|
|
||||||
|
[`routing-duplex`](https://github.com/natefaubion/purescript-routing-duplex) offers an elegant bidirectional routing which was initially designed for SPAs. Have a look at the really extensive [`documentation`](https://github.com/natefaubion/purescript-routing-duplex). The benefits of using routing-duplex are
|
||||||
|
* Much simpler and less tedious definition of routes
|
||||||
|
* Roundtrip printing/parsing of routes, so no more invalid redirects
|
||||||
|
* Exhaustive pattern matching so you are sure to match all defined routes
|
||||||
|
* Option to separate routes into logical groups
|
||||||
|
|
||||||
|
Here is a bit more elaborated examples:
|
||||||
|
|
||||||
|
```purescript
|
||||||
|
module Main where
|
||||||
|
|
||||||
|
import Prelude hiding ((/))
|
||||||
|
|
||||||
|
import Data.Either (Either(..))
|
||||||
|
import Data.Generic.Rep (class Generic)
|
||||||
|
import Data.Maybe (Maybe(..))
|
||||||
|
import Data.Tuple (Tuple(..))
|
||||||
|
import HTTPurple
|
||||||
|
|
||||||
|
data Route
|
||||||
|
= Home
|
||||||
|
| Profile String
|
||||||
|
| Account String
|
||||||
|
| Search { q :: String, sorting :: Maybe Sort }
|
||||||
|
derive instance Generic Route _
|
||||||
|
|
||||||
|
data Sort = Asc | Desc
|
||||||
|
derive instance Generic Sort _
|
||||||
|
|
||||||
|
sortToString :: Sort -> String
|
||||||
|
sortToString = case _ of
|
||||||
|
Asc -> "asc"
|
||||||
|
Desc -> "desc"
|
||||||
|
|
||||||
|
sortFromString :: String -> Either String Sort
|
||||||
|
sortFromString = case _ of
|
||||||
|
"asc" -> Right Asc
|
||||||
|
"desc" -> Right Desc
|
||||||
|
val -> Left $ "Not a sort: " <> val
|
||||||
|
|
||||||
|
sort :: RouteDuplex' String -> RouteDuplex' Sort
|
||||||
|
sort = as sortToString sortFromString
|
||||||
|
|
||||||
|
api :: RouteDuplex' Route
|
||||||
|
api = mkRoute
|
||||||
|
{ "Home": noArgs
|
||||||
|
, "Profile": "profile" / string segment
|
||||||
|
, "Account": "account" / string segment
|
||||||
|
, "Search": "search" ? { q: string, sorting: optional <<< sort }
|
||||||
|
}
|
||||||
|
|
||||||
|
main :: ServerM
|
||||||
|
main = serve { port: 8080 } { route: api, router: apiRouter }
|
||||||
|
where
|
||||||
|
|
||||||
|
apiRouter { route: Home } = ok "hello world!"
|
||||||
|
apiRouter { route: Profile profile } = ok $ "hello " <> profile <> "!"
|
||||||
|
apiRouter { route: Account account } = found' redirect ""
|
||||||
|
where
|
||||||
|
reverseRoute = print api $ Profile account
|
||||||
|
redirect = headers [ Tuple "Location" reverseRoute ]
|
||||||
|
apiRouter { route: Search { q, sorting } } = ok $ "searching for query " <> q <> " " <> case sorting of
|
||||||
|
Just Asc -> "ascending"
|
||||||
|
Just Desc -> "descending"
|
||||||
|
Nothing -> "defaulting to ascending"
|
||||||
|
```
|
||||||
|
|
||||||
|
## Startup options
|
||||||
|
|
||||||
|
HTTPurple 🪁 greatly simplifies the startup options and functions. The `serve`, `serve'`, `serveSecure` and `serveSecure'` have been merged into a single function `serve` that accepts listen options as the first parameter and uses sane defaults if you don't provide any.
|
||||||
|
|
||||||
|
The easiest way to start a server is to provide just the route and a router:
|
||||||
|
|
||||||
|
```purescript
|
||||||
|
main :: ServerM
|
||||||
|
main =
|
||||||
|
serve {} { route, router }
|
||||||
|
```
|
||||||
|
|
||||||
|
This will spin up the http server with sane defaults.
|
||||||
|
```bash
|
||||||
|
HTTPurple 🪁 up and running on http://0.0.0.0:8080
|
||||||
|
```
|
||||||
|
|
||||||
|
But you can overwrite any of the optional properties like this
|
||||||
|
|
||||||
|
```purescript
|
||||||
|
main :: ServerM
|
||||||
|
main =
|
||||||
|
serve {
|
||||||
|
hostname: "localhost"
|
||||||
|
, port: 9000
|
||||||
|
, certFile: "./Certificate.cer"
|
||||||
|
, keyFile: "./Key.key"
|
||||||
|
, notFoundHandler
|
||||||
|
, onStarted: log "Server started 🚀"
|
||||||
|
, closingHandler: NoClosingHandler
|
||||||
|
} { route, router }
|
||||||
|
where
|
||||||
|
notFoundHandler :: Request Unit -> ResponseM
|
||||||
|
notFoundHandler = const $ ok "Nothing to see here"
|
||||||
|
```
|
||||||
|
|
||||||
|
## Request parsing and validation
|
||||||
|
|
||||||
|
HTTPurple 🪁 makes request parsing and validation super simple. My typical http service scenario looks like this:
|
||||||
|
1. Parse the request json and return a bad request if the request body doesn't contain the valid json format
|
||||||
|
2. Validate the json input semanticall and transform it into some kind of internal model. Return bad request (with some error code) in case it is invalid.
|
||||||
|
3. Do something with the request
|
||||||
|
4. Return the output as a json
|
||||||
|
|
||||||
|
HTTPurple 🪁 uses continuations to make this standard scenario straight-forward (see example below).
|
||||||
|
|
||||||
|
Furthermore, HTTPurple 🪁 doesn't mandate a json parsing library. So you can use [`argonaut`](https://github.com/purescript-contrib/purescript-argonaut) using the [`argonaut-driver`](https://github.com/sigma-andex/purescript-httpurple-argonaut), use [`yoga-json`](https://github.com/rowtype-yoga/purescript-yoga-json) using the [`yoga-json-driver`](https://github.com/sigma-andex/purescript-httpurple-yoga-json) or write your own json driver.
|
||||||
|
|
||||||
|
Here is an example how that looks like:
|
||||||
|
```purescript
|
||||||
|
apiRouter { route: Home, method: Post, body } = usingCont do
|
||||||
|
req@{ name } :: HelloWorldRequest <- fromJson Argonaut.jsonDecoder body
|
||||||
|
ok $ "hello " <> name <> "!"
|
||||||
|
```
|
||||||
|
In case `fromJson` succeeds, the next step will be executed, otherwise a 400 bad request is returned.
|
||||||
|
|
||||||
|
## Other improvmenets
|
||||||
|
|
||||||
|
* Default closing handler - A default closing handler is provided so you can just stop your server using `ctrl+x` without having to worry about anything. You can deactivate it by setting `closingHandler: NoClosingHandler` in the listen options.
|
||||||
|
|
||||||
|
## Hot module reloading
|
||||||
|
|
||||||
|
With HTTPurple 🪁 you can easily set up a hot module reloading workflow:
|
||||||
|
|
||||||
|
Create an `index.js` with the content:
|
||||||
|
```javascript
|
||||||
|
import * as Main from './output/Main/index.js'
|
||||||
|
Main.main()
|
||||||
|
```
|
||||||
|
|
||||||
|
Add to `package.json`:
|
||||||
|
```json
|
||||||
|
...
|
||||||
|
"scripts": {
|
||||||
|
"hot": "spago build -w & nodemon \"node index.js\""
|
||||||
|
},
|
||||||
|
"type": "module",
|
||||||
|
...
|
||||||
|
```
|
||||||
|
|
||||||
|
Spin up:
|
||||||
|
```bash
|
||||||
|
npm run hot
|
||||||
|
```
|
||||||
|
Develop:
|
||||||
|
```bash
|
||||||
|
HTTPurple 🪁 up and running on http://0.0.0.0:8080
|
||||||
|
[nodemon] restarting due to changes...
|
||||||
|
[nodemon] restarting due to changes...
|
||||||
|
[nodemon] starting `node "node index.js" index.js`
|
||||||
|
HTTPurple 🪁 up and running on http://0.0.0.0:8080
|
||||||
|
[nodemon] restarting due to changes...
|
||||||
|
[nodemon] restarting due to changes...
|
||||||
|
[nodemon] starting `node "node index.js" index.js`
|
||||||
|
HTTPurple 🪁 up and running on http://0.0.0.0:8080
|
||||||
|
```
|
@ -26,7 +26,6 @@ route = RD.root $ RG.sum
|
|||||||
type HelloWorldRequest = { name :: String }
|
type HelloWorldRequest = { name :: String }
|
||||||
type HelloWorldResponse = { hello :: String }
|
type HelloWorldResponse = { hello :: String }
|
||||||
|
|
||||||
|
|
||||||
-- the following test decoder/encoder code is just for testing. in your project you will want to use
|
-- the following test decoder/encoder code is just for testing. in your project you will want to use
|
||||||
-- jsonEncoder and jsonDecoder from httpurple-argonaut or httpurple-yoga-json
|
-- jsonEncoder and jsonDecoder from httpurple-argonaut or httpurple-yoga-json
|
||||||
foreign import data Json :: Type
|
foreign import data Json :: Type
|
||||||
|
319
docs/Routing.md
319
docs/Routing.md
@ -1,21 +1,54 @@
|
|||||||
# Routing in HTTPure
|
# Routing in HTTPurple 🪁
|
||||||
|
|
||||||
Routing in HTTPure is designed on the simple principle of allowing PureScript to
|
## Table of contents
|
||||||
do what PureScript does best. When you create an HTTPure server, you pass it a
|
* [Routing introduction](#introduction)
|
||||||
router function:
|
* [The request record](#the-request-record)
|
||||||
|
* [Matching HTTP methods](#matching-http-methods)
|
||||||
|
* [Matching paths and query parameters](#matching-paths-and-query-parameters)
|
||||||
|
* [Adding further types](#adding-further-types)
|
||||||
|
* [Composing routes](#composing-routes)
|
||||||
|
* [Reverse routing](#reverse-routing)
|
||||||
|
* [Working with request headers](#working-with-request-headers)
|
||||||
|
|
||||||
|
## Routing introduction
|
||||||
|
|
||||||
|
HTTPurple 🪁 uses [`routing-duplex`](https://github.com/natefaubion/purescript-routing-duplex) for routing.
|
||||||
|
|
||||||
|
You'll need two things:
|
||||||
|
1. A data type representing your routes
|
||||||
|
2. A mapping between the data constructors of your data type and the paths
|
||||||
|
|
||||||
|
Here an example:
|
||||||
```purescript
|
```purescript
|
||||||
main = HTTPure.serve 8080 router $ Console.log "Server up"
|
import HTTPurple
|
||||||
|
|
||||||
|
-- We define a data type representing our route
|
||||||
|
-- In this case, a single route `SayHello` that accepts one parameter
|
||||||
|
data Route = SayHello String
|
||||||
|
derive instance Generic Route _ -- a generic instance is needed
|
||||||
|
|
||||||
|
-- The mapping between our data constructor `SayHello` and
|
||||||
|
-- the route /hello/<argument>
|
||||||
|
route :: RouteDuplex' Route
|
||||||
|
route = mkRoute
|
||||||
|
{ "SayHello": "hello" / string segment
|
||||||
|
}
|
||||||
|
|
||||||
|
-- We then start the http server passing the route and a handler function (router)
|
||||||
|
main :: ServerM
|
||||||
|
main = serve {} { route, router }
|
||||||
|
where
|
||||||
|
router { route: SayHello name } = ok $ "hello " <> name <> "!"
|
||||||
```
|
```
|
||||||
|
|
||||||
The router function is called for each inbound request to the HTTPure server.
|
The router function is called for each inbound request to the HTTPure server.
|
||||||
Its signature is:
|
Its signature is:
|
||||||
|
|
||||||
```purescript
|
```purescript
|
||||||
HTTPure.Request -> HTTPure.ResponseM
|
forall route. HTTPurple.Request route -> HTTPurple.ResponseM
|
||||||
```
|
```
|
||||||
|
|
||||||
So in HTTPure, routing is handled simply by the router being a pure function
|
So in HTTPurple, routing is handled simply by the router being a pure function
|
||||||
which is passed a value that contains all information about the current request,
|
which is passed a value that contains all information about the current request,
|
||||||
and which returns a response monad. There's no fancy path parsing and matching
|
and which returns a response monad. There's no fancy path parsing and matching
|
||||||
algorithm to learn, and everything is pure--you don't get anything or set
|
algorithm to learn, and everything is pure--you don't get anything or set
|
||||||
@ -34,18 +67,12 @@ guide](./Responses.md).
|
|||||||
|
|
||||||
## The Request Record
|
## The Request Record
|
||||||
|
|
||||||
The `HTTPure.Request` type is the input parameter for the router function. It is
|
The `HTTPurple.Request route` type is the input parameter for the router function. It is
|
||||||
a `Record` type that contains the following fields:
|
a `Record` type that contains the following fields:
|
||||||
|
|
||||||
- `method` - A member of `HTTPure.Method`.
|
- `method` - A member of `HTTPurple.Method`.
|
||||||
- `path` - An `Array` of `String` path segments. A path segment is a nonempty
|
- `route` - A data type representing your route with paths and query parameters.
|
||||||
string separated by a `"/"`. Empty segments are stripped out when HTTPure
|
- `headers` - A `HTTPurple.Headers` object. The `HTTPurple.Headers` newtype wraps
|
||||||
creates the `HTTPure.Request` record.
|
|
||||||
- `query` - An `Object` of `String` values. Note that if you have any query
|
|
||||||
parameters without values (for instance, a URL like `/foo?bar`), then the
|
|
||||||
value in the `Object` for that query parameter will be the empty `String`
|
|
||||||
(`""`).
|
|
||||||
- `headers` - A `HTTPure.Headers` object. The `HTTPure.Headers` newtype wraps
|
|
||||||
the `Object String` type and provides some typeclass instances that make more
|
the `Object String` type and provides some typeclass instances that make more
|
||||||
sense when working with HTTP headers.
|
sense when working with HTTP headers.
|
||||||
- `body` - A `String` containing the contents of the request body, or an empty
|
- `body` - A `String` containing the contents of the request body, or an empty
|
||||||
@ -55,49 +82,177 @@ Following are some more details on working with specific fields, but remember,
|
|||||||
you can combine guards and pattern matching for any or all of these fields
|
you can combine guards and pattern matching for any or all of these fields
|
||||||
however it makes sense for your use case.
|
however it makes sense for your use case.
|
||||||
|
|
||||||
## The Lookup Typeclass
|
## Matching paths and query parameters
|
||||||
|
|
||||||
You will find that much of HTTPure routing takes advantage of implementations of
|
Let's have a look at a bit more complex routing scenario.
|
||||||
the [HTTPure.Lookup](../src/HTTPure/Lookup.purs) typeclass. This typeclass
|
Imagine we are developing the backend service for a simple web shop.We want two define three routes:
|
||||||
defines the function `HTTPure.lookup` (or the infix version `!!`), along with a
|
- `/` which returns the data of the start page
|
||||||
few auxiliary helpers, for looking up a field out of an object with some key.
|
- `/categories/<category>/products/<product>` which takes two path parameters category name and product name and returns a certain product
|
||||||
There are three instances defined in HTTPure:
|
- `/search?q=<query>&sorting=<asc|desc>` which takes two query parameters, a search string and an optional sorting argument
|
||||||
|
|
||||||
1. `Lookup (Array t) Int t` - In this instance, `HTTPure.lookup` is the same as
|
```purescript
|
||||||
`Array.index`. Because the path is represented as an `Array` of `Strings`,
|
-- We define three data types representing the three routes
|
||||||
this can be used to retrieve the nth path segment by doing something like
|
data Route
|
||||||
`request.path !! n`.
|
= Home
|
||||||
2. `Lookup (Object t) String t` - In this instance, `HTTPure.lookup` is a
|
| Products String String -- the product route with two path parameters
|
||||||
flipped version of `Object.lookup`. Because the query is a `Object String`,
|
| Search { q :: String, sorting :: Maybe String } -- the search route with two query parameters, whereby sorting is optional
|
||||||
this instance can be used to retrieve the value of a query parameter by name,
|
derive instance Generic Route _
|
||||||
by doing something like `request.query !! "someparam"`.
|
|
||||||
3. `Lookup Headers String String` - This is similar to the example in #2, except
|
|
||||||
that it works with the `HTTPure.Headers` newtype, and the key is
|
|
||||||
case-insensitive (so `request.headers !! "X-Test" == request.headers !!
|
|
||||||
"x-test"`).
|
|
||||||
|
|
||||||
There are three infix operators defined on the `HTTPure.Lookup` typeclass that
|
|
||||||
are extremely useful for routing:
|
|
||||||
|
|
||||||
1. `!!` - This is an alias to `HTTPure.lookup` itself, and returns a `Maybe`
|
-- Next we define the route (mapping)
|
||||||
containing some type.
|
route :: RouteDuplex' Route
|
||||||
2. `!@` - This is the same as `HTTPure.lookup`, but it returns the actual value
|
route = mkRoute
|
||||||
instead of a `Maybe` containing the value. It only operates on instances of
|
{ "Home": noArgs -- the root route /
|
||||||
`HTTPure.Lookup` where the return type is a `Monoid`, and returns `mempty` if
|
, "Products": "categories" / string segment / "products" / string segment
|
||||||
`HTTPure.lookup` returns `Nothing`. It's especially useful when routing based
|
, "Search": "search" ? { q: string, sorting: optional <<< string }
|
||||||
on specific values in query parameters, path segments, or header fields.
|
}
|
||||||
3. `!?` - This returns `true` if the key on the right hand side is in the data
|
|
||||||
set on the left hand side. In other words, if `HTTPure.lookup` matches
|
-- Finally, we pass the route (mapping) to the server and also define a route handler
|
||||||
something, this is `true`, otherwise, this is `false`.
|
main :: ServerM
|
||||||
|
main = serve { port: 8080 } { route: route, router }
|
||||||
|
where
|
||||||
|
router { route: Home } = ok "home"
|
||||||
|
router { route: Products category product } = do
|
||||||
|
ok $ "category=" <> category <> ", product=" <> product
|
||||||
|
router { route: Search { q, sorting } } = ok $ "searching=" <> q <> "," <> case sorting of
|
||||||
|
Just "asc" -> "ascending"
|
||||||
|
_ -> "descending"
|
||||||
|
```
|
||||||
|
|
||||||
|
As you can see, in the route handler you can directly pattern match your data type. Pattern matching of the route is exhaustive so that you will get an error if you miss a route.
|
||||||
|
|
||||||
|
### Adding further types
|
||||||
|
|
||||||
|
We can add further type information to our route data type.
|
||||||
|
Instead of treating the path arguments of our product route as `String`, we can define newtypes for the arguments:
|
||||||
|
```purescript
|
||||||
|
newtype Category = Category String
|
||||||
|
instance Newtype Category String
|
||||||
|
|
||||||
|
newtype Product = Product String
|
||||||
|
instance Newtype Product String
|
||||||
|
```
|
||||||
|
|
||||||
|
We'll change our data constructor to accept the two newtypes:
|
||||||
|
```purescript
|
||||||
|
| Products Category Product
|
||||||
|
```
|
||||||
|
|
||||||
|
We can then define new segment types to match the different arguments:
|
||||||
|
|
||||||
|
```purescript
|
||||||
|
category :: RouteDuplex' Category
|
||||||
|
category = _Newtype R.segment
|
||||||
|
|
||||||
|
product :: RouteDuplex' Product
|
||||||
|
product = _Newtype segment
|
||||||
|
```
|
||||||
|
|
||||||
|
Now we can update our route mapping:
|
||||||
|
```purescript
|
||||||
|
"Products": "categories" / category / "products" / product
|
||||||
|
```
|
||||||
|
|
||||||
|
We can furthe add a more expressive type for our sorting query parameter
|
||||||
|
|
||||||
|
```purescript
|
||||||
|
data Sort = Asc | Desc
|
||||||
|
derive instance Generic Sort _
|
||||||
|
```
|
||||||
|
|
||||||
|
We can then define the conversion to and from string and define a new `RouteDuplex`:
|
||||||
|
```purescript
|
||||||
|
sortToString :: Sort -> String
|
||||||
|
sortToString = case _ of
|
||||||
|
Asc -> "asc"
|
||||||
|
Desc -> "desc"
|
||||||
|
|
||||||
|
sortFromString :: String -> Either String Sort
|
||||||
|
sortFromString = case _ of
|
||||||
|
"asc" -> Right Asc
|
||||||
|
"desc" -> Right Desc
|
||||||
|
val -> Left $ "Not a sort: " <> val
|
||||||
|
|
||||||
|
sort :: RouteDuplex' String -> RouteDuplex' Sort
|
||||||
|
sort = as sortToString sortFromString
|
||||||
|
```
|
||||||
|
|
||||||
|
We can then update our `Route` data type to use the new `Sort` data type
|
||||||
|
```purescript
|
||||||
|
| Search { q :: String, sorting :: Maybe Sort }
|
||||||
|
```
|
||||||
|
and use the new `sort` function in our route mapping:
|
||||||
|
```purescript
|
||||||
|
"Search": "search" ? { q: string, sorting: optional <<< sort }
|
||||||
|
```
|
||||||
|
|
||||||
|
As you can see, the `RoutingDuplex` approach is quite powerful and once you got a grip on it, it is also rather straight-forward to use.
|
||||||
|
I recommend you to also check out the [`routing-duplex` documentation](https://github.com/natefaubion/purescript-routing-duplex) which contains many more examples.
|
||||||
|
|
||||||
|
### Composing routes
|
||||||
|
|
||||||
|
Sometimes you might want to define two route data types to structure your routing logically. Composing routes is straight-forward with HTTPurple.
|
||||||
|
E.g. you could have an `ApiRoute` representing your business api and a `MetaRoute` for technical routes, such as a health check:
|
||||||
|
|
||||||
|
```purescript
|
||||||
|
data ApiRoute
|
||||||
|
= Home
|
||||||
|
derive instance Generic Route _
|
||||||
|
api :: RouteDuplex' ApiRoute
|
||||||
|
api = mkRoute
|
||||||
|
{ "Home": G.noArgs }
|
||||||
|
|
||||||
|
data MetaRoute = Health
|
||||||
|
|
||||||
|
derive instance Generic MetaRoute _
|
||||||
|
|
||||||
|
meta :: RouteDuplex' MetaRoute
|
||||||
|
meta = mkRoute { "Health": "health" / G.noArgs }
|
||||||
|
```
|
||||||
|
|
||||||
|
You can compose these two routes using the `<+>` operator, and compose the routing handlers with `orElse`:
|
||||||
|
|
||||||
|
```purescript
|
||||||
|
main :: ServerM
|
||||||
|
main = serve { port: 8080, notFoundHandler } { route: api <+> meta, router: apiRouter `orElse` metaRouter }
|
||||||
|
where
|
||||||
|
|
||||||
|
apiRouter { route: Home } = ok "hello world!"
|
||||||
|
metaRouter { route: Health } = ok """{"status":"ok"}"""
|
||||||
|
```
|
||||||
|
|
||||||
|
### Reverse routing
|
||||||
|
|
||||||
|
Reverse routing, e.g. for redirects or HATEOAS, is straight-forward using the `print` function from `routing-duplex`:
|
||||||
|
|
||||||
|
```purescript
|
||||||
|
data Route
|
||||||
|
= Old
|
||||||
|
| New
|
||||||
|
|
||||||
|
route :: RouteDuplex' Route
|
||||||
|
route = mkRoute
|
||||||
|
{ "Old": "old" / RG.noArgs
|
||||||
|
, "New": "new" / RG.noArgs
|
||||||
|
}
|
||||||
|
|
||||||
|
main :: ServerM
|
||||||
|
main = serve { port: 8080 } { route, router }
|
||||||
|
where
|
||||||
|
router { route: Old } = found' redirect ""
|
||||||
|
where
|
||||||
|
redirect = headers [ Tuple "Location" $ print route $ New account ]
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
## Matching HTTP Methods
|
## Matching HTTP Methods
|
||||||
|
|
||||||
You can use normal pattern matching to route based on the HTTP method:
|
You can use normal pattern matching to route based on the HTTP method:
|
||||||
|
|
||||||
```purescript
|
```purescript
|
||||||
router { method: HTTPure.Post } = HTTPure.ok "received a post"
|
router { method: HTTPurple.Post } = HTTPurple.ok "received a post"
|
||||||
router { method: HTTPure.Get } = HTTPure.ok "received a get"
|
router { method: HTTPurple.Get } = HTTPurple.ok "received a get"
|
||||||
router { method } = HTTPure.ok $ "received a " <> show method
|
router { method } = HTTPurple.ok $ "received a " <> show method
|
||||||
```
|
```
|
||||||
|
|
||||||
To see the list of methods that HTTPure understands, see the
|
To see the list of methods that HTTPure understands, see the
|
||||||
@ -105,62 +260,6 @@ To see the list of methods that HTTPure understands, see the
|
|||||||
routes based on the HTTP method, see [the Post
|
routes based on the HTTP method, see [the Post
|
||||||
example](./Examples/Post/Main.purs).
|
example](./Examples/Post/Main.purs).
|
||||||
|
|
||||||
## Working With Path Segments
|
|
||||||
|
|
||||||
Generally, there are two use cases for working with path segments: routing on
|
|
||||||
them, and using them as variables. When routing on path segments, you can route
|
|
||||||
on exact path matches:
|
|
||||||
|
|
||||||
```purescript
|
|
||||||
router { path: [ "exact" ] } = HTTPure.ok "matched /exact"
|
|
||||||
```
|
|
||||||
|
|
||||||
You can also route on partial path matches. It's cleanest to use PureScript
|
|
||||||
guards for this. For instance:
|
|
||||||
|
|
||||||
```purescript
|
|
||||||
router { path }
|
|
||||||
| path !@ 0 == "foo" = HTTPure.ok "matched something starting with /foo"
|
|
||||||
| path !@ 1 == "bar" = HTTPure.ok "matched something starting with /*/bar"
|
|
||||||
```
|
|
||||||
|
|
||||||
When using a path segment as a variable, simply extract the path segment using
|
|
||||||
the `HTTPure.Lookup` typeclass:
|
|
||||||
|
|
||||||
```purescript
|
|
||||||
router { path } = HTTPure.ok $ "Path segment 0: " <> path !@ 0
|
|
||||||
```
|
|
||||||
|
|
||||||
To see an example server that works with path segments, see [the Path Segments
|
|
||||||
example](./Examples/PathSegments/Main.purs).
|
|
||||||
|
|
||||||
## Working With Query Parameters
|
|
||||||
|
|
||||||
Working with query parameters is very similar to working with path segments. You
|
|
||||||
can route based on the _existence_ of a query parameter:
|
|
||||||
|
|
||||||
```purescript
|
|
||||||
router { query }
|
|
||||||
| query !? "foo" = HTTPure.ok "matched a request containing the 'foo' param"
|
|
||||||
```
|
|
||||||
|
|
||||||
Or you can route based on the _value_ of a query parameter:
|
|
||||||
|
|
||||||
```purescript
|
|
||||||
router { query }
|
|
||||||
| query !@ "foo" == "bar" = HTTPure.ok "matched a request with 'foo=bar'"
|
|
||||||
```
|
|
||||||
|
|
||||||
You can of course also use the value of a query parameter to calculate your
|
|
||||||
response:
|
|
||||||
|
|
||||||
```purescript
|
|
||||||
router { query } = HTTPure.ok $ "The value of 'foo' is " <> query !@ "foo"
|
|
||||||
```
|
|
||||||
|
|
||||||
To see an example server that works with query parameters, see [the Query
|
|
||||||
Parameters example](./Examples/QueryParameters/Main.purs).
|
|
||||||
|
|
||||||
## Working With Request Headers
|
## Working With Request Headers
|
||||||
|
|
||||||
Headers are again very similar to working with path segments or query
|
Headers are again very similar to working with path segments or query
|
||||||
@ -168,12 +267,12 @@ parameters:
|
|||||||
|
|
||||||
```purescript
|
```purescript
|
||||||
router { headers }
|
router { headers }
|
||||||
| headers !? "X-Foo" = HTTPure.ok "There is an 'X-Foo' header"
|
| headers !? "X-Foo" = HTTPurple.ok "There is an 'X-Foo' header"
|
||||||
| headers !@ "X-Foo" == "bar" = HTTPure.ok "The header 'X-Foo' is 'bar'"
|
| headers !@ "X-Foo" == "bar" = HTTPurple.ok "The header 'X-Foo' is 'bar'"
|
||||||
| otherwise = HTTPure.ok $ "The value of 'X-Foo' is " <> headers !@ "x-foo"
|
| otherwise = HTTPurple.ok $ "The value of 'X-Foo' is " <> headers !@ "x-foo"
|
||||||
```
|
```
|
||||||
|
|
||||||
Note that using the `HTTPure.Lookup` typeclass on headers is case-insensitive.
|
Note that using the `HTTPurple.Lookup` typeclass on headers is case-insensitive.
|
||||||
|
|
||||||
To see an example server that works with headers, see [the Headers
|
To see an example server that works with headers, see [the Headers
|
||||||
example](./Examples/Headers/Main.purs).
|
example](./Examples/Headers/Main.purs).
|
||||||
|
@ -12,6 +12,7 @@ module HTTPurple
|
|||||||
, module HTTPurple.Response
|
, module HTTPurple.Response
|
||||||
, module HTTPurple.Server
|
, module HTTPurple.Server
|
||||||
, module HTTPurple.Status
|
, module HTTPurple.Status
|
||||||
|
, module HTTPurple.Validation
|
||||||
, module Routing.Duplex
|
, module Routing.Duplex
|
||||||
, module Routing.Duplex.Generic
|
, module Routing.Duplex.Generic
|
||||||
, module Routing.Duplex.Generic.Syntax
|
, module Routing.Duplex.Generic.Syntax
|
||||||
@ -32,6 +33,7 @@ import HTTPurple.Response (Response, ResponseM, accepted, accepted', alreadyRepo
|
|||||||
import HTTPurple.Routes (type (<+>), combineRoutes, mkRoute, orElse, (<+>))
|
import HTTPurple.Routes (type (<+>), combineRoutes, mkRoute, orElse, (<+>))
|
||||||
import HTTPurple.Server (ServerM, serve)
|
import HTTPurple.Server (ServerM, serve)
|
||||||
import HTTPurple.Status (Status)
|
import HTTPurple.Status (Status)
|
||||||
|
import HTTPurple.Validation (fromValidated, fromValidatedE)
|
||||||
import Routing.Duplex (class RouteDuplexBuildParams, class RouteDuplexParams, RouteDuplex(..), RouteDuplex', as, boolean, buildParams, default, end, flag, int, many, many1, optional, param, params, parse, path, prefix, print, prop, record, rest, root, segment, string, suffix, (:=))
|
import Routing.Duplex (class RouteDuplexBuildParams, class RouteDuplexParams, RouteDuplex(..), RouteDuplex', as, boolean, buildParams, default, end, flag, int, many, many1, optional, param, params, parse, path, prefix, print, prop, record, rest, root, segment, string, suffix, (:=))
|
||||||
import Routing.Duplex.Generic (class GRouteDuplex, class GRouteDuplexCtr, noArgs, product, sum, (~))
|
import Routing.Duplex.Generic (class GRouteDuplex, class GRouteDuplexCtr, noArgs, product, sum, (~))
|
||||||
import Routing.Duplex.Generic.Syntax (gparams, gsep, (/), (?))
|
import Routing.Duplex.Generic.Syntax (gparams, gsep, (/), (?))
|
||||||
|
@ -18,7 +18,7 @@ import Data.Tuple (Tuple(..))
|
|||||||
import Effect.Aff.Class (class MonadAff)
|
import Effect.Aff.Class (class MonadAff)
|
||||||
import HTTPurple.Body (RequestBody, toString)
|
import HTTPurple.Body (RequestBody, toString)
|
||||||
import HTTPurple.Headers (Headers, headers)
|
import HTTPurple.Headers (Headers, headers)
|
||||||
import HTTPurple.Response (Response, badRequest')
|
import HTTPurple.Response (Response, badRequest)
|
||||||
|
|
||||||
newtype JsonDecoder err json = JsonDecoder (String -> Either err json)
|
newtype JsonDecoder err json = JsonDecoder (String -> Either err json)
|
||||||
|
|
||||||
@ -51,7 +51,7 @@ fromJsonContinuation (JsonDecoder decode) errorHandler body handler = do
|
|||||||
either errorHandler handler parseJson
|
either errorHandler handler parseJson
|
||||||
|
|
||||||
defaultErrorHandler :: forall (err :: Type) (m :: Type -> Type). MonadAff m => err -> m Response
|
defaultErrorHandler :: forall (err :: Type) (m :: Type -> Type). MonadAff m => err -> m Response
|
||||||
defaultErrorHandler = const $ badRequest' jsonHeaders ""
|
defaultErrorHandler = const $ badRequest ""
|
||||||
|
|
||||||
-- | Parse the `RequestBody` as json using the provided `JsonDecoder`.
|
-- | Parse the `RequestBody` as json using the provided `JsonDecoder`.
|
||||||
-- | If it fails, the error handler is called.
|
-- | If it fails, the error handler is called.
|
||||||
|
49
src/HTTPurple/Validation.purs
Normal file
49
src/HTTPurple/Validation.purs
Normal file
@ -0,0 +1,49 @@
|
|||||||
|
module HTTPurple.Validation
|
||||||
|
( fromValidated
|
||||||
|
, fromValidatedE
|
||||||
|
)
|
||||||
|
where
|
||||||
|
|
||||||
|
import Prelude
|
||||||
|
|
||||||
|
import Control.Monad.Cont (ContT(..))
|
||||||
|
import Data.Either (Either, either)
|
||||||
|
import Effect.Aff.Class (class MonadAff)
|
||||||
|
import HTTPurple.Response (Response, badRequest)
|
||||||
|
|
||||||
|
fromValidatedContinuation ::
|
||||||
|
forall input err validated m.
|
||||||
|
MonadAff m =>
|
||||||
|
(input -> Either err validated) ->
|
||||||
|
(err -> m Response) ->
|
||||||
|
input ->
|
||||||
|
(validated -> m Response) ->
|
||||||
|
m Response
|
||||||
|
fromValidatedContinuation validate errorHandler input handler =
|
||||||
|
either errorHandler handler $ validate $ input
|
||||||
|
|
||||||
|
defaultErrorHandler :: forall (err :: Type) (m :: Type -> Type). MonadAff m => err -> m Response
|
||||||
|
defaultErrorHandler = const $ badRequest ""
|
||||||
|
|
||||||
|
-- | Validate an input using a validate function.
|
||||||
|
-- | If validation fails, the error handler is called.
|
||||||
|
-- | Returns a continuation
|
||||||
|
fromValidatedE ::
|
||||||
|
forall input err validated m.
|
||||||
|
MonadAff m =>
|
||||||
|
(input -> Either err validated) ->
|
||||||
|
(err -> m Response) ->
|
||||||
|
input ->
|
||||||
|
ContT Response m validated
|
||||||
|
fromValidatedE validate errorHandler input = ContT $ fromValidatedContinuation validate errorHandler input
|
||||||
|
|
||||||
|
-- | Validate an input using a validate function.
|
||||||
|
-- | If validation fails, a bad request is resturned.
|
||||||
|
-- | Returns a continuation
|
||||||
|
fromValidated ::
|
||||||
|
forall input err validated m.
|
||||||
|
MonadAff m =>
|
||||||
|
(input -> Either err validated) ->
|
||||||
|
input ->
|
||||||
|
ContT Response m validated
|
||||||
|
fromValidated validator = fromValidatedE validator defaultErrorHandler
|
Loading…
Reference in New Issue
Block a user