Update changelog and docs

This commit is contained in:
sigma-andex 2022-06-11 17:46:03 +01:00
parent d6df08a726
commit 306edb6bd7
4 changed files with 205 additions and 108 deletions

View File

@ -2,7 +2,9 @@
## Unpublished ## Unpublished
- Add json request parsing simplifications ## V1.2.0
- Add json request parsing
- Add request validation - Add request validation
## v1.1.0 ## v1.1.0

View File

@ -1,16 +1,22 @@
# HTTPure Basics # HTTPurple Basics
This guide is a brief overview of the basics of creating a HTTPure server. This guide is a brief overview of the basics of creating a HTTPurple server.
## TOC
1. [Creating a Server](#creating-a-server)
1. [Hot module reloading](#hot-module-reloading)
1. [Further server settings](#further-server-settings)
## Creating a Server ## Creating a Server
To create a server, use `HTTPure.serve` (no SSL) or `HTTPure.serveSecure` (SSL). To create a server, use `HTTPurple.serve`.
Both of these functions take a port number, a router function, and an `Effect` Both of these functions take a port number, a router function, and an `Effect`
that will run once the server has booted. The signature of the router function that will run once the server has booted. The signature of the router function
is: is:
```purescript ```purescript
HTTPure.Request -> HTTPure.ResponseM HTTPurple.Request route -> HTTPurple.ResponseM
``` ```
For more details on routing, see the [Routing guide](./Routing.md). For more For more details on routing, see the [Routing guide](./Routing.md). For more
@ -18,63 +24,113 @@ details on responses, see the [Responses guide](./Responses.md). The router can
be composed with middleware; for more details, see the [Middleware be composed with middleware; for more details, see the [Middleware
guide](./Middleware.md). guide](./Middleware.md).
## Non-SSL You can create an HTTPurple server using `HTTPurple.serve`:
You can create an HTTPure server without SSL using `HTTPure.serve`:
```purescript ```purescript
main :: HTTPure.ServerM import Prelude hiding ((/))
main = HTTPure.serve 8080 router $ log "Server up"
```
Most of the [examples](./Examples), besides [the SSL Example](./Examples/SSL), import HTTPurple
use this method to create the server.
You can also create a server using a custom data Route = Hello String
[`HTTP.ListenOptions`](http://bit.ly/2G42rLd) value: derive instance Generic Route _
```purescript route :: RouteDuplex' Route
main :: HTTPure.ServerM route = mkRoute
main = HTTPure.serve' customOptions router $ log "Server up" { "Hello": "hello" / segment
``` }
## SSL main :: ServerM
You can create an SSL-enabled HTTPure server using `HTTPure.serveSecure`, which
has the same signature as `HTTPure.serve` except that it additionally takes a
path to a cert file and a path to a key file after the port number:
```purescript
main :: HTTPure.ServerM
main = main =
HTTPure.serveSecure 8080 "./Certificate.cer" "./Key.key" router $ serve { port: 8080 } { route, router }
log "Server up" where
router { route: Hello name } = ok $ "hello " <> name
```
`HTTPurple.serve` takes as arguments two records:
1. Server configuration - A record containing all additional settings that you want to pass. See [further server settings](#further-server-settings) for a list of all settings.
1. A record containing your route and a router for these routes. See the [routing guide](./Routing.md) for more information.
## 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
```
## Further server settings
HTTPurple 🪁 defines a series of settings that you can override.
Here is an example of the full list of server settings:
```
{
hostname: "localhost"
, port: 9000
, certFile: "./Certificate.cer"
, keyFile: "./Key.key"
, notFoundHandler: custom404Handler
, onStarted: log "Server started 🚀"
, closingHandler: NoClosingHandler
}
```
### SSL
**Note**: SSL is usually something that you want to handle at the infrastructure level and not within the application's http server. The SSL support is mainly here because HTTPure had it, but I might remove it in the near future if it hinders development.
You can create an SSL-enabled HTTPurple server using `HTTPurple.serve` by passing a certFile, a keyFile and an optionally a different port:
```purescript
main :: HTTPurple.ServerM
main =
HTTPurple.serve { port: 443, certFile : "./Certificate.cer", keyFile: "./Key.key" } { route, router }
...
``` ```
You can look at [the SSL Example](./Examples/SSL/Main.purs), which uses this You can look at [the SSL Example](./Examples/SSL/Main.purs), which uses this
method to create the server. method to create the server.
You can also create a server using a
[`HTTP.ListenOptions`](http://bit.ly/2G42rLd) and a
[`HTTPS.SSLOptions`](http://bit.ly/2G3Aljr):
### Closing handler
HTTPurple 🪁 comes with a default closing handler, so `Ctrl+x` just stops the server.
you can switch off this behaviour by passing
```purescript ```purescript
main :: HTTPure.ServerM { closingHandler: NoClosingHandler }
main =
HTTPure.serveSecure' customSSLOptions customOptions router $
log "Server up"
``` ```
to `serve` and define your own closing handler:
## Shutdown hook
To gracefully shut down a server you can add a shutdown hook. For this you will need to add the following dependencies:
```
posix-types
node-process
```
Then take the closing handler returned by `serve` and create a `SIGINT` and `SIGTERM` hook:
```purescript ```purescript
import Prelude import Prelude
@ -82,14 +138,11 @@ import Prelude
import Data.Posix.Signal (Signal(SIGINT, SIGTERM)) import Data.Posix.Signal (Signal(SIGINT, SIGTERM))
import Effect (Effect) import Effect (Effect)
import Effect.Console (log) import Effect.Console (log)
import HTTPure (serve, ok) import HTTPurple (serve, ok)
import Node.Process (onSignal) import Node.Process (onSignal)
main :: Effect Unit main :: Effect Unit
main = do main = do
closingHandler <- serve 8080 (const $ ok "hello world!") do closingHandler <- serve 8080 { route, router }
log $ "Server now up on port 8080" -- do something with closingHandler
onSignal SIGINT $ closingHandler $ log "Received SIGINT, stopping service now."
onSignal SIGTERM $ closingHandler $ log "Received SIGTERM, stopping service now."
``` ```

View File

@ -7,7 +7,6 @@ If you have used HTTPure before, you'll probably want to go through the followin
* [startup options](#startup-options) * [startup options](#startup-options)
* [request parsing and validation](#request-parsing-and-validation) * [request parsing and validation](#request-parsing-and-validation)
* [other improvements](#other-improvmenets) * [other improvements](#other-improvmenets)
* [hot module reloading](#hot-module-reloading)
## Routing-duplex ## Routing-duplex
@ -118,61 +117,8 @@ main =
## Request parsing and validation ## Request parsing and validation
HTTPurple 🪁 makes request parsing and validation super simple. My typical http service scenario looks like this: HTTPurple 🪁 has some helpers to make json parsing and validation very simple. See the [requests guide](./Requests.md) for more information.
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 ## 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. * 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
```

96
docs/Requests.md Normal file
View File

@ -0,0 +1,96 @@
# Requests
This section describes how to work with requests bodys. For information about routing see the [routing docs](Routing.md).
## TOC
1. [tl;dr](#tldr)
1. [introduction](#introduction)
1. [json request parsing](#1-json-request-parsing)
1. [data validation](#2-data-validation)
1. [business logic](#3-your-business-logic)
1. [output json](#4-output-json)
## tl;dr
```purescript
router { route: Home, method: Post, body } = usingCont do
jsonRequest :: MyRequest <- fromJson Argonaut.jsonDecoder body
input :: MyValidatedInput <- fromValidated validateMyRequest jsonRequest
output :: MyOutput <- lift $ doSomethingAndReturnAff input
ok' jsonHeaders $ toJson Argonaut.jsonEncoder output
```
## Introduction
A typical micro-service scenario looks like this:
1. You get a json request, so the first thing you need to do is to parse your json request format and return a bad request if it doesn't match your format.
2. Then you do some kind of data/business validation of the input. Is the field `email` really an email? So you want to do this semantic validation and return a bad request (with some error code) in case it is invalid.
3. Now since you have the validated input data, you can do your actual logic and produce some output data.
4. Finally you want to dump this output data as json
HTTPurple 🪁 provides a very simple way of handling this scenario based on continuations. Let's go through each step:
## 1. Json request parsing
HTTPurple 🪁 provides a `fromJson` method that makes json parsing of your body super simple. `fromJson` is library agnostic, 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.
If you don't know which one to use, I would recommend to go with the `argonaut` driver.
```bash
# for argonaut install the argonaut driver:
spago install httpurple-argonaut
# for yoga-json install the yoga-json driver:
spago install httpurple-yoga-json
```
`fromJson` returns a continuation, so just start your handler implementation with `usingCont` and then use `fromJson` with `Argonaut.jsonDecoder` or `Yoga.jsonDecoder`:
```purescript
router { route: Home, method: Post, body } = usingCont do
jsonRequest :: MyRequest <- fromJson Argonaut.jsonDecoder body
```
As you can see, really simple. This parses the body into your format and returns a bad request if it fails. If you need to customise the bad request, there is `fromJsonE` which allows you to pass a custom bad request handler.
## 2. Data validation
Next, we will want do validate our json data. Or put in other words, we want to transform the weakly typed json model into a strongly-typed internal data model.
`fromValidated` takes a validation function of the shape `input -> Either error validated`:
```purescript
router { route: Home, method: Post, body } = usingCont do
jsonRequest :: MyRequest <- fromJson Argonaut.jsonDecoder body
input :: MyValidatedInput <- fromValidated validateMyRequest jsonRequest
```
If you need a custom bad request handler, you can use `fromValidatedE` which takes an bad request handler as second parameter.
## 3. Your business logic
Now that you got your validated input, you can pass it to your business logic.
There is one caveat though, we are currently in a continuation, so you will need to `lift` your `Aff` returning function into the continuation monad:
```purescript
router { route: Home, method: Post, body } = usingCont do
jsonRequest :: MyRequest <- fromJson Argonaut.jsonDecoder body
input :: MyValidatedInput <- fromValidated validateMyRequest jsonRequest
output :: MyOutput <- lift $ myAffReturningFunction input
```
## 4. Output json
Finally you can return your json. Use the `toJson` function which takes the json driver and your output your data as json:
```purescript
router { route: Home, method: Post, body } = usingCont do
jsonRequest :: MyRequest <- fromJson Argonaut.jsonDecoder body
input :: MyValidatedInput <- fromValidated validateMyRequest jsonRequest
output :: MyOutput <- lift $ doSomethingAndReturnAff input
ok' jsonHeaders $ toJson Argonaut.jsonEncoder output
```
If your business logic output is strongly typed, I would write a transformer function to a weakly typed json model instead of writing custom json encoders:
```purescript
ok' jsonHeaders $ toJson Argonaut.jsonEncoder $ transformToApi output
```