Add validator continuation

Update routing readme
This commit is contained in:
sigma-andex 2022-06-06 21:15:01 +01:00
parent 13af9e70f5
commit 12726379bf
8 changed files with 443 additions and 295 deletions

View File

@ -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
View File

@ -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
View 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
```

View File

@ -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

View File

@ -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).

View File

@ -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, (/), (?))

View File

@ -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.

View 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