From 306edb6bd7174f56e4fb5217650b77915352eef1 Mon Sep 17 00:00:00 2001 From: sigma-andex <77549848+sigma-andex@users.noreply.github.com> Date: Sat, 11 Jun 2022 17:46:03 +0100 Subject: [PATCH] Update changelog and docs --- CHANGELOG.md | 4 +- docs/Basics.md | 157 +++++++++++++++++++++++++++++--------------- docs/Differences.md | 56 +--------------- docs/Requests.md | 96 +++++++++++++++++++++++++++ 4 files changed, 205 insertions(+), 108 deletions(-) create mode 100644 docs/Requests.md diff --git a/CHANGELOG.md b/CHANGELOG.md index eed9bbf..15cce82 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,7 +2,9 @@ ## Unpublished -- Add json request parsing simplifications +## V1.2.0 + +- Add json request parsing - Add request validation ## v1.1.0 diff --git a/docs/Basics.md b/docs/Basics.md index b76dba8..763dcf6 100644 --- a/docs/Basics.md +++ b/docs/Basics.md @@ -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 -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` that will run once the server has booted. The signature of the router function is: ```purescript -HTTPure.Request -> HTTPure.ResponseM +HTTPurple.Request route -> HTTPurple.ResponseM ``` 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 guide](./Middleware.md). -## Non-SSL - -You can create an HTTPure server without SSL using `HTTPure.serve`: +You can create an HTTPurple server using `HTTPurple.serve`: ```purescript -main :: HTTPure.ServerM -main = HTTPure.serve 8080 router $ log "Server up" -``` +import Prelude hiding ((/)) -Most of the [examples](./Examples), besides [the SSL Example](./Examples/SSL), -use this method to create the server. +import HTTPurple -You can also create a server using a custom -[`HTTP.ListenOptions`](http://bit.ly/2G42rLd) value: +data Route = Hello String +derive instance Generic Route _ -```purescript -main :: HTTPure.ServerM -main = HTTPure.serve' customOptions router $ log "Server up" -``` - -## SSL - -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 +route :: RouteDuplex' Route +route = mkRoute + { "Hello": "hello" / segment + } + +main :: ServerM main = - HTTPure.serveSecure 8080 "./Certificate.cer" "./Key.key" router $ - log "Server up" + serve { port: 8080 } { route, router } + 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 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 -main :: HTTPure.ServerM -main = - HTTPure.serveSecure' customSSLOptions customOptions router $ - log "Server up" +{ closingHandler: NoClosingHandler } ``` +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 import Prelude @@ -82,14 +138,11 @@ import Prelude import Data.Posix.Signal (Signal(SIGINT, SIGTERM)) import Effect (Effect) import Effect.Console (log) -import HTTPure (serve, ok) +import HTTPurple (serve, ok) import Node.Process (onSignal) main :: Effect Unit main = do - closingHandler <- serve 8080 (const $ ok "hello world!") do - log $ "Server now up on port 8080" - - onSignal SIGINT $ closingHandler $ log "Received SIGINT, stopping service now." - onSignal SIGTERM $ closingHandler $ log "Received SIGTERM, stopping service now." + closingHandler <- serve 8080 { route, router } + -- do something with closingHandler ``` diff --git a/docs/Differences.md b/docs/Differences.md index a947c91..8a1a6a5 100644 --- a/docs/Differences.md +++ b/docs/Differences.md @@ -7,7 +7,6 @@ If you have used HTTPure before, you'll probably want to go through the followin * [startup options](#startup-options) * [request parsing and validation](#request-parsing-and-validation) * [other improvements](#other-improvmenets) -* [hot module reloading](#hot-module-reloading) ## Routing-duplex @@ -118,61 +117,8 @@ main = ## 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. +HTTPurple 🪁 has some helpers to make json parsing and validation very simple. See the [requests guide](./Requests.md) for more information. ## 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 -``` diff --git a/docs/Requests.md b/docs/Requests.md new file mode 100644 index 0000000..4ad106d --- /dev/null +++ b/docs/Requests.md @@ -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 +```