Skip to content

Commit 6b0e88c

Browse files
Implement first version of the HTTP server
0 parents  commit 6b0e88c

File tree

8 files changed

+926
-0
lines changed

8 files changed

+926
-0
lines changed

LICENSE

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
MIT License
2+
3+
Copyright © 2020 - Sebastien Filion
4+
5+
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated
6+
documentation files (the "Software"), to deal in the Software without restriction, including without limitation the
7+
rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit
8+
persons to whom the Software is furnished to do so, subject to the following conditions:
9+
10+
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the
11+
Software.
12+
13+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE
14+
WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
15+
COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
16+
OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

README.md

Lines changed: 168 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,168 @@
1+
# Function HTTP server
2+
3+
## Simple HTTP server
4+
5+
The fastest way to start a HTTP server is to use the `startHTTPServer` function.
6+
The function takes two arguments; the first argument is the options, and the second is a unary
7+
function that takes a `Request` and return a `Task` of a `Response`.
8+
9+
```js
10+
import Task from "https://deno.land/x/[email protected]/library/Task.js";
11+
import Response from "https://deno.land/x/[email protected]/library/Response.js";
12+
import startHTTPServer from "./library/server.js";
13+
14+
startHTTPServer({ port: 8080 }, request => Task.of(Response.OK({}, request.raw)));
15+
```
16+
17+
You can test this simple server by executing it your file
18+
19+
```bash
20+
$ deno run --allow-net server.js
21+
```
22+
23+
```bash
24+
$ curl localhost:8080 -d "Hello, Hoge!"
25+
> Hello, Hoge!
26+
```
27+
28+
## Routing
29+
30+
The main routing tool that comes bundled with this library is conveniently called `route`.
31+
It takes a non-zero number of arguments which are defined by a pair of functions.
32+
The first function of the pair is used to assert whether or not to execute the second function.
33+
The assertion function takes a `Request` and return a `Boolean`, the handling function takes a `Request` and
34+
must return a `Task` of a `Response`.
35+
36+
```js
37+
import Task from "https://deno.land/x/[email protected]/library/Task.js";
38+
import Response from "https://deno.land/x/[email protected]/library/Response.js";
39+
import { route } from "./library/route.js";
40+
import { encodeText } from "./library/utilities.js";
41+
42+
startHTTPServer(
43+
{ port: 8080 },
44+
route(
45+
[
46+
request => request.headers.method === 'GET',
47+
_ => Task.of(Response.OK({ 'content-type': 'text/plain' }, encodeText("Hello, Hoge!")))
48+
]
49+
);
50+
);
51+
```
52+
53+
### Routing handlers
54+
55+
Because the pattern is common, this library also offers a collection of handler that automatically creates
56+
the assertion function. Each handler takes a `String` or a `RegExp` and a unary function.
57+
58+
```js
59+
import Task from "https://deno.land/x/[email protected]/library/Task.js";
60+
import Response from "https://deno.land/x/[email protected]/library/Response.js";
61+
import { handlers, route } from "./library/route.js";
62+
import { encodeText } from "./library/utilities.js";
63+
64+
startHTTPServer(
65+
{ port: 8080 },
66+
route(
67+
handlers.get('/', _ => Task.of(Response.OK({ 'content-type': 'text/plain' }, encodeText("Hello, Hoge!"))))
68+
);
69+
);
70+
```
71+
72+
#### Routing with the `explodeRequest` utility
73+
74+
The function `explodeRequest` is a utility that will parse the headers and serialize the body of a `Request`, for
75+
convenience. The function takes two arguments; a binary function that returns a `Task` of `Response` and a `Request`.
76+
77+
The binary function handler will be called with an object containing the original headers, the parsed query string
78+
and other parameters; the second argument is the body of request serialized based on the content type.
79+
80+
```js
81+
import { explodeRequest } from "./library/utilities.js";
82+
83+
startHTTPServer(
84+
{ port: 8080 },
85+
route(
86+
handlers.get('/users', explodeRequest(({ filters }) => retrieveUsers(filters))),
87+
handlers.post(/\/users\/(?<userID>.+)$/, explodeRequest(({ userID }, { data: user }) => updateUser(userID, user)))
88+
)
89+
);
90+
```
91+
92+
For this sample, a `GET` request made with a query string will be parsed as an object.
93+
94+
```bash
95+
$ curl localhost:8080/users?filters[status]=active
96+
```
97+
98+
And, a `POST` request with a body as JSON will be parsed as well.
99+
100+
```bash
101+
$ curl localhost:8080/users/hoge -X POST -H "Content-Type: application/json" -d "{\"data\":{\"fullName\":\"Hoge\"}}"
102+
```
103+
104+
The function `explodeRequest` should cover most use-cases but if you need to create your own parser, check out the
105+
[`parseRequest`](#parsing-requests) function.
106+
107+
#### Composing routes
108+
109+
Finally, you can compose your routes for increased readability.
110+
111+
```js
112+
const userRoutes = [ handlers.get('/', handleRetrieveUsers), ... ];
113+
const sensorRoutes = [ handlers.get('/', handleRetrieveSensors), ... ];
114+
115+
startHTTPServer({ port: 8080 }, route(...userRoutes, ...sensorRoutes));
116+
```
117+
118+
### Middleware
119+
120+
Before talking about middlewares, I think it is important to talk about the power of function composition and couple of
121+
things special about `startHTTPServer` and `route`:
122+
123+
1. The function `startHTTPServer` takes a unary function that must return a `Task` of `Response`.
124+
2. The function `route`, will always return early if the argument is not a `Request`.
125+
126+
So for example, if you needed to discard any request with a content type that is not `application/json`, you could
127+
do the following.
128+
129+
```js
130+
import { compose } from "https://x.nest.land/[email protected]/source/index.js";
131+
132+
startHTTPServer(
133+
{ port: 8080 },
134+
compose(
135+
route(...routes),
136+
request => request.headers.accept !== 'application/json'
137+
? Task.of(Response.BadRequest({}, new Uint8Array([])))
138+
: request
139+
)
140+
);
141+
```
142+
143+
144+
## Deno
145+
146+
This codebase uses [Deno](https://deno.land/#installation).
147+
148+
### MIT License
149+
150+
Copyright © 2020 - Sebastien Filion
151+
152+
Permission is hereby granted, free of charge, to any person obtaining a copy
153+
of this software and associated documentation files (the "Software"), to deal
154+
in the Software without restriction, including without limitation the rights
155+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
156+
copies of the Software, and to permit persons to whom the Software is
157+
furnished to do so, subject to the following conditions:
158+
159+
The above copyright notice and this permission notice shall be included in all
160+
copies or substantial portions of the Software.
161+
162+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
163+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
164+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
165+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
166+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
167+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
168+
SOFTWARE.

library/route.js

Lines changed: 177 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,177 @@
1+
import {
2+
both,
3+
complement,
4+
compose,
5+
cond,
6+
equals,
7+
identity,
8+
ifElse,
9+
match,
10+
path,
11+
prop,
12+
test
13+
} from "https://x.nest.land/[email protected]/source/index.js";
14+
import Task from "https://deno.land/x/[email protected]/library/Task.js";
15+
import Request from "https://deno.land/x/[email protected]/library/Request.js";
16+
import Response from "https://deno.land/x/[email protected]/library/Response.js";
17+
18+
import { assertIsRegex } from "https://deno.land/x/[email protected]/library/utilities.js";
19+
20+
/**
21+
* ## Routing
22+
*
23+
* The main routing tool that comes bundled with this library is conveniently called `route`.
24+
* It takes a non-zero number of arguments which are defined by a pair of functions.
25+
* The first function of the pair is used to assert whether or not to execute the second function.
26+
* The assertion function takes a `Request` and return a `Boolean`, the handling function takes a `Request` and
27+
* must return a `Task` of a `Response`.
28+
*
29+
* ```js
30+
* import Task from "https://deno.land/x/[email protected]/library/Task.js";
31+
* import Response from "https://deno.land/x/[email protected]/library/Response.js";
32+
* import { route } from "./library/route.js";
33+
* import { encodeText } from "./library/utilities.js";
34+
*
35+
* startHTTPServer(
36+
* { port: 8080 },
37+
* route(
38+
* [
39+
* request => request.headers.method === 'GET',
40+
* _ => Task.of(Response.OK({ 'content-type': 'text/plain' }, encodeText("Hello, Hoge!")))
41+
* ]
42+
* );
43+
* );
44+
* ```
45+
*
46+
* ### Routing handlers
47+
*
48+
* Because the pattern is common, this library also offers a collection of handler that automatically creates
49+
* the assertion function. Each handler takes a `String` or a `RegExp` and a unary function.
50+
*
51+
* ```js
52+
* import Task from "https://deno.land/x/[email protected]/library/Task.js";
53+
* import Response from "https://deno.land/x/[email protected]/library/Response.js";
54+
* import { handlers, route } from "./library/route.js";
55+
* import { encodeText } from "./library/utilities.js";
56+
*
57+
* startHTTPServer(
58+
* { port: 8080 },
59+
* route(
60+
* handlers.get('/', _ => Task.of(Response.OK({ 'content-type': 'text/plain' }, encodeText("Hello, Hoge!"))))
61+
* );
62+
* );
63+
* ```
64+
*
65+
* #### Routing with the `explodeRequest` utility
66+
*
67+
* The function `explodeRequest` is a utility that will parse the headers and serialize the body of a `Request`, for
68+
* convenience. The function takes two arguments; a binary function that returns a `Task` of `Response` and a `Request`.
69+
*
70+
* The binary function handler will be called with an object containing the original headers, the parsed query string
71+
* and other parameters; the second argument is the body of request serialized based on the content type.
72+
*
73+
* ```js
74+
* import { explodeRequest } from "./library/utilities.js";
75+
*
76+
* startHTTPServer(
77+
* { port: 8080 },
78+
* route(
79+
* handlers.get('/users', explodeRequest(({ filters }) => retrieveUsers(filters))),
80+
* handlers.post(/\/users\/(?<userID>.+)$/, explodeRequest(({ userID }, { data: user }) => updateUser(userID, user)))
81+
* )
82+
* );
83+
* ```
84+
*
85+
* For this sample, a `GET` request made with a query string will be parsed as an object.
86+
*
87+
* ```bash
88+
* $ curl localhost:8080/users?filters[status]=active
89+
* ```
90+
*
91+
* And, a `POST` request with a body as JSON will be parsed as well.
92+
*
93+
* ```bash
94+
* $ curl localhost:8080/users/hoge -X POST -H "Content-Type: application/json" -d "{\"data\":{\"fullName\":\"Hoge\"}}"
95+
* ```
96+
*
97+
* The function `explodeRequest` should cover most use-cases but if you need to create your own parser, check out the
98+
* [`parseRequest`](#parsing-requests) function.
99+
*
100+
* #### Composing routes
101+
*
102+
* Finally, you can compose your routes for increased readability.
103+
*
104+
* ```js
105+
* const userRoutes = [ handlers.get('/', handleRetrieveUsers), ... ];
106+
* const sensorRoutes = [ handlers.get('/', handleRetrieveSensors), ... ];
107+
*
108+
* startHTTPServer({ port: 8080 }, route(...userRoutes, ...sensorRoutes));
109+
* ```
110+
*
111+
* ### middleware
112+
*
113+
* This library doesn't come with a special function to create middleware or anything; but that doesn't mean you can't
114+
* do it with some function composition.
115+
*
116+
* Before jumping into an example, you need to keep in mind two concepts:
117+
*
118+
* 1. The function `startHTTPServer` takes a unary function that must return a `Task` of `Response`.
119+
* 2. The function `route`, will always return early if the argument is not a `Request`.
120+
*
121+
* So for example, if you needed to discard any request with a content type that is not `application/json`, you could
122+
* do the following.
123+
*
124+
* ```js
125+
* import { compose } from "https://x.nest.land/[email protected]/source/index.js";
126+
*
127+
* startHTTPServer(
128+
* { port: 8080 },
129+
* compose(
130+
* route(...routes),
131+
* request => request.headers.accept !== 'application/json'
132+
* ? Task.of(Response.BadRequest({}, new Uint8Array([])))
133+
* : request
134+
* )
135+
* );
136+
* ```
137+
*/
138+
139+
const factorizeHandler = method => (pattern, naryFunction) =>
140+
[
141+
both(
142+
compose(equals(method), path([ 'headers', 'method' ])),
143+
compose(
144+
ifElse(_ => assertIsRegex(pattern), test(pattern), equals(pattern)),
145+
prop(1),
146+
match(/(^.*?)(?:\?.*){0,1}$/),
147+
path([ 'headers', 'url' ])
148+
)
149+
),
150+
(request, options = {}) =>
151+
naryFunction.length === 2 ? naryFunction({ pattern, ...options }, request) : naryFunction(request)
152+
]
153+
154+
// * :: a -> (Request -> Response) -> [ (Request -> boolean), (Request -> Response) ]
155+
// * :: a -> ((Request, RegExp) -> Response) -> [ (Request -> boolean), (Request -> Response) ]
156+
export const handlers = {
157+
delete: factorizeHandler('DELETE'),
158+
get: factorizeHandler('GET'),
159+
post: factorizeHandler('POST'),
160+
put: factorizeHandler('PUT')
161+
};
162+
163+
164+
// route :: ([ (Request -> Boolean), (Request -> Task Response) ]...) -> Task Response
165+
export const route = (...routeList) => cond(
166+
[
167+
[
168+
complement(Request.is),
169+
identity
170+
],
171+
...routeList.map(([ assert, unaryFunction ]) => [ both(Request.is, assert), unaryFunction ]),
172+
[
173+
_ => true,
174+
_ => Task.of(Response.NotFound({}, new Uint8Array([])))
175+
]
176+
]
177+
);

0 commit comments

Comments
 (0)