Add guides (#85)

This commit is contained in:
Connor Prussin 2017-10-25 21:03:24 -04:00 committed by GitHub
parent 133b98c9c6
commit c79e9e4346
4 changed files with 422 additions and 0 deletions

44
docs/Basics.md Normal file
View File

@ -0,0 +1,44 @@
# HTTPure Basics
This guide is a brief overview of the basics of creating a HTTPure server.
## Creating a Server
To create a server, use `HTTPure.serve` (no SSL) or `HTTPure.serve'` (SSL). Both
of these functions take a port number, a router function, and an `Eff` that will
run once the server has booted. The signature of the router function is:
```purescript
forall e. HTTPure.Request -> HTTPure.ResponseM e
```
For more details on routing, see the [Routing guide](./Routing.md). For more
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`:
```purescript
main = HTTPure.serve 8080 router $ Console.log "Server up"
```
Most of the [examples](./Examples), besides [the SSL Example](./Examples/SSL),
use this method to create the server.
## SSL
You can create an SSL-enabled HTTPure server using `HTTPure.serve'`, 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.serve 8080 "./Certificate.cer" "./Key.key" router $
Console.log "Server up"
```
You can look at [the SSL Example](./Examples/SSL/Main.purs), which uses this
method to create the server.

110
docs/Middleware.md Normal file
View File

@ -0,0 +1,110 @@
# Writing and Using Middleware in HTTPure
Since HTTPure routers are just pure functions, you can write a middleware by
simply creating a function that takes a router and an `HTTPure.Request`, and
returns an `HTTPure.ResponseM`. You can then simply use function composition to
combine middlewares, and pass your router to your composed middleware to
generate the decorated router!
See [the Middleware example](./Examples/Middleware/Main.purs) to see how you can
build, compose, and consume different types of middleware.
## Writing Middleware
A middleware is a function with the signature:
```purescript
forall e. (HTTPure.Request -> HTTPure.ResponseM e) ->
HTTPure.Request ->
HTTPure.ResponseM e
```
Note that the first argument is just the signature for a router function. So
essentially, your middleware should take a router and return a new router.
That's it! You can do pretty much anything with middlewares. Here are a few
examples of common middleware patterns:
You can write a middleware that wraps all future work in some behavior, like
logging or timing:
```purescript
myMiddleware router request = do
doSomethingBefore
response <- router request
doSomethingAfter
pure response
```
Or perhaps a middleware that injects something into the response:
```purescript
myMiddleware router request = do
response <- router request
HTTPure.response' response.status response.headers $
response.body <> "\n\nGenerated using my super duper middleware!"
```
You could even write a middleware that handles routing for some specific cases:
```purescript
myMiddleware _ { path: [ "somepath" ] } = HTTPure.ok "Handled by my middleware!"
myMiddleware router request = router request
```
Or even a middleware that conditionally includes another middleware:
```purescript
myMiddleware router = if something then someOtherMiddleware router else router
```
Just make sure your middlewares follow the correct signature, and users will be
able to compose them at will!
Note that because there is nothing fancy happening here, you could always write
higher order functions that don't follow this signature, if it makes sense. For
instance, you could write a function that takes two routers, and selects which
one to use based on some criteria. There is nothing wrong with this, but you
should try to use the middleware signature mentioned above as much as possible
as it will make your middleware easier to consume and compose.
## Consuming Middleware
Consuming middleware easy: simply compose all the middleware you want, and then
pass your router to the composed middleware. For instance:
```purescript
main = HTTPure.serve port composedRouter $ Console.log "Server is up!"
where
composedRouter = middlewareA <<< middlewareB <<< middlewareC $ router
```
Be aware of the ordering of the middleware that you compose--since we used
`<<<`, the middlewares will compose right-to-left. But because middlewares
choose when to apply the router to the request, this is a bit like wrapping the
router in each successive middleware from right to left. So when the router
executes on a request, those middlewares will actually _execute_
left-to-right--or from the outermost wrapper inwards.
In other words, say you have the following HTTPure server:
```purescript
middleware letter router request = do
EffClass.liftEff $ Console.log $ "Starting Middleware " <> letter
response <- router request
EffClass.liftEff $ Console.log $ "Ending Middleware " <> letter
pure response
main = HTTPure.serve port composedRouter $ Console.log "Server is up!"
where
composedRouter = middleware "A" <<< middleware "B" $ router
```
When this HTTPure server receives a request, the logs will include:
```
Starting Middleware A
Starting Middleware B
...
Ending Middleware B
Ending Middleware A
```

89
docs/Responses.md Normal file
View File

@ -0,0 +1,89 @@
# Creating HTTPure Responses
## The Response Monad
The `HTTPure.ResponseM` monad is the return type of the router function. It is
an `Aff` type that contains an `HTTPure.Response`. Because it is an `Aff`, it
transparent to add asynchronous behavior when you need.
To see an example server taking advantage of asynchronous responses, see [the
Async Response example](./Examples/AsyncResponse/Main.purs).
## Response Helpers
HTTPure defines a number of helpers for creating response monads for all
different HTTP response status codes. Some of these helpers take a body, for
instance, `HTTPure.ok` and `HTTPure.partialContent`, and some do not, for
instance `HTTPure.created` or `HTTPure.noContent`. There are prime functions
corresponding to each helper--for instance, `HTTPure.ok'` and
`HTTPure.created'`. The prime versions are the same as the base versions except
they also return response headers--see the [Setting Response
Headers](#setting-response-headers) section below for more details.
For a full list of helpers, see the [Response](../src/HTTPure/Response.purs)
module.
In some cases, the defined helpers don't cover your needs--for instance, if
HTTPure doesn't have a helper defined for some HTTP response code you care about
(and you don't want to [contribute it](../Contributing.md)--hint hint, you
should contribute it!), or if you need to specify a body where normally one
wouldn't be sent. For these cases, you can use `HTTPure.response`, which takes a
status code and a body. If you need to specify headers, use `HTTPure.response'`.
If you don't need to specify a body, you can use `HTTPure.emptyResponse` or
`HTTPure.emptyResponse'`.
## Raw Responses
The `HTTPure.ResponseM` monad contains a `HTTPure.Response` value, which is a
`Record` type containing the following fields:
- `status` - An `Int` HTTP response status code.
- `headers` - A `HTTPure.Headers` containing the HTTP response headers.
- `body` - A `String` containing the HTTP response body.
You can manually construct a response if you want to:
```purescript
router _ = pure $ { status: 200, headers: HTTPure.headers [], body: "foo" }
```
This can be useful in some circumstances, but in the vast majority of cases it's
recommended that you use the response helpers described above -- they are more
explicit and allow you to avoid using magic numbers in your code for HTTP status
codes.
## Setting Response Headers
If you need to return response headers, you can do so using the prime versions
of the response helpers. These functions take an `HTTPure.Headers` object. You
can construct an `HTTPure.Headers` in a few ways:
- `HTTPure.empty` - Construct an empty `HTTPure.Headers`
- `HTTPure.header` - Given a string with a header name and a string with a
value, construct a singleton `HTTPure.Headers`. For instance:
```purescript
headers = HTTPure.header "X-My-Header" "value"
```
- `HTTPure.headers` - Construct a `HTTPure.Headers` from an `Array` of `Tuples`
of two `Strings`, where the first `String` is the header name and the second
`String` is the header value. For instance:
```purescript
headers = HTTPure.headers
[ Tuple "X-Header-A" "valueA"
, Tuple "X-Header-B" "valueB"
]
```
Because `HTTPure.Headers` has an instance of `Semigroup`, you can also append
`HTTPure.Headers` objects:
```purescript
headers =
HTTPure.header "X-Header-A" "valueA" <> HTTPure.header "X-Header-B" "valueB"
```
To see an example server that sets response headers, see [the Headers
example](./Examples/Headers/Main.purs).

179
docs/Routing.md Normal file
View File

@ -0,0 +1,179 @@
# Routing in HTTPure
Routing in HTTPure is designed on the simple principle of allowing PureScript to
do what PureScript does best. When you create an HTTPure server, you pass it a
router function:
```purescript
main = HTTPure.serve 8080 router $ Console.log "Server up"
```
The router function is called for each inbound request to the HTTPure server.
Its signature is:
```purescript
forall e. HTTPure.Request -> HTTPure.ResponseM e
```
So in HTTPure, routing is handled simply by the router being a pure function
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
algorithm to learn, and everything is pure--you don't get anything or set
anything, you simply define the return value given the input parameters, like
any other pure function.
This is quite powerful, as all routing can be defined using the same PureScript
pattern matching and guard syntax you use everywhere else. It allows you to
break up your router to sub-routers easily, using whatever router grouping makes
sense for your app. It also leads to some powerful patterns for defining and
using middleware. For more details about defining and using middleware, see the
[Middleware guide](./Middleware.md).
For more details about the response monad, see the [Responses
guide](./Responses.md).
## The Request Record
The `HTTPure.Request` type is the input parameter for the router function. It is
a `Record` type that contains the following fields:
- `method` - A member of `HTTPure.Method`.
- `path` - An `Array` of `String` path segments. A path segment is a nonempty
string separated by a `"/"`. Empty segments are stripped out when HTTPure
creates the `HTTPure.Request` record.
- `query` - A `StrMap` 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 `StrMap` for that query parameter will be the empty `String`
(`""`).
- `headers` - A `HTTPure.Headers` object. The `HTTPure.Headers` newtype wraps
the `StrMap String` type and provides some typeclass instances that make more
sense when working with HTTP headers.
- `body` - A `String` containing the contents of the request body, or an empty
`String` if none was provided.
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
however it makes sense for your use case.
## The Lookup Typeclass
You will find that much of HTTPure routing takes advantage of implementations of
the [HTTPure.Lookup](../src/HTTPure/Lookup.purs) typeclass. This typeclass
defines the function `HTTPure.lookup` (or the infix version `!!`), along with a
few auxiliary helpers, for looking up a field out of an object with some key.
There are three instances defined in HTTPure:
1. `Lookup (Array t) Int t` - In this instance, `HTTPure.lookup` is the same as
`Array.index`. Because the path is represented as an `Array` of `Strings`,
this can be used to retrieve the nth path segment by doing something like
`request.path !! n`.
2. `Lookup (StrMap t) String t` - In this instance, `HTTPure.lookup` is a
flipped version of `StrMap.lookup`. Because the query is a `StrMap String`,
this instance can be used to retrieve the value of a query parameter by name,
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`
containing some type.
2. `!@` - This is the same as `HTTPure.lookup`, but it returns the actual value
instead of a `Maybe` containing the value. It only operates on instances of
`HTTPure.Lookup` where the return type is a `Monoid`, and returns `mempty` if
`HTTPure.lookup` returns `Nothing`. It's especially useful when routing based
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
something, this is `true`, otherwise, this is `false`.
## Matching HTTP Methods
You can use normal pattern matching to route based on the HTTP method:
```purescript
router { method: HTTPure.Post } = HTTPure.ok "received a post"
router { method: HTTPure.Get } = HTTPure.ok "received a get"
router { method } = HTTPure.ok $ "received a " <> show method
```
To see the list of methods that HTTPure understands, see the
[Method](../src/HTTPure/Method.purs) module. To see an example server that
routes based on the HTTP method, see [the Post
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
Headers are again very similar to working with path segments or query
parameters:
```purescript
router { headers }
| headers !? "X-Foo" = HTTPure.ok "There is an 'X-Foo' header"
| headers !@ "X-Foo" == "bar" = HTTPure.ok "The header 'X-Foo' is 'bar'"
| otherwise = HTTPure.ok $ "The value of 'X-Foo' is " <> headers !@ "x-foo"
```
Note that using the `HTTPure.Lookup` typeclass on headers is case-insensitive.
To see an example server that works with headers, see [the Headers
example](./Examples/Headers/Main.purs).