diff --git a/.github/document.sh b/.github/document.sh
new file mode 100755
index 0000000..8876325
--- /dev/null
+++ b/.github/document.sh
@@ -0,0 +1,16 @@
+export FL_TITLE="Functional HTTP Server"
+export FL_DESCRIPTION="A simple HTTP server inspired by Express and in tune with Functional Programming principles in \
+JavaScript for Deno."
+export FL_GITHUB_URL="https://github.com/sebastienfilion/functional-http-server"
+export FL_DENO_URL="https://deno.land/x/functional_http_server"
+export FL_VERSION="v0.3.1"
+
+deno run --allow-all --unstable ../@functional:generate-documentation/cli.js document \
+"$FL_TITLE" \
+"$FL_DESCRIPTION" \
+$FL_GITHUB_URL \
+$FL_DENO_URL \
+$FL_VERSION \
+./.github/readme-fragment-usage.md \
+./library/*.js \
+./.github/readme-fragment-license.md
diff --git a/.github/fl-word_art-http_server-reverse.svg b/.github/fl-logo.svg
similarity index 100%
rename from .github/fl-word_art-http_server-reverse.svg
rename to .github/fl-logo.svg
diff --git a/.github/readme-fragment-license.md b/.github/readme-fragment-license.md
new file mode 100644
index 0000000..2c1c8ad
--- /dev/null
+++ b/.github/readme-fragment-license.md
@@ -0,0 +1,20 @@
+## Contributing
+
+We appreciate your help! Please, [read the guidelines](./CONTRIBUTING.md).
+
+## License
+
+Copyright © 2020 - Sebastien Filion
+
+Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated
+documentation files (the "Software"), to deal in the Software without restriction, including without limitation the
+rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit
+persons to whom the Software is furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all copies or substantial portions of the
+Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE
+WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
+COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
+OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
\ No newline at end of file
diff --git a/.github/readme-fragment-usage.md b/.github/readme-fragment-usage.md
new file mode 100644
index 0000000..c3e2588
--- /dev/null
+++ b/.github/readme-fragment-usage.md
@@ -0,0 +1,100 @@
+## Usage
+
+Functional HTTP Server is optimized to write elegant and powerful point-free functions. This example uses the Ramda
+library - for simplification - but you should be able to use any library that implements the Fantasy-land
+specifications.
+
+This example showcase how to create an endpoint handler for `POST /hoge` that writes to a local file and to Redis
+simultaneously the content of the request's body and, replies with `201`.
+
+```js
+import Task from "https://deno.land/x/functional@v1.3.2/library/Task.js";
+import {
+ decodeRaw,
+ encodeText,
+ evert,
+ safeExtract
+} from "https://deno.land/x/functional@v1.3.2/library/utilities.js";
+import File from "https://deno.land/x/functional_io@v1.1.0/library/File.js";
+import Request from "https://deno.land/x/functional_io@v1.1.0/library/Request.js";
+import Response from "https://deno.land/x/functional_io@v1.1.0/library/Response.js";
+import { fetch } from "https://deno.land/x/functional_io@v1.1.0/library/browser_safe.js";
+import { writeFile } from "https://deno.land/x/functional_io@v1.1.0/library/fs.js";
+import RedisRequest from "https://deno.land/x/functional_redis@v0.2.0/library/RedisRequest.js";
+import { $$rawPlaceholder } from "https://deno.land/x/functional_redis@v0.2.0/library/Symbol.js";
+import { executeRedisCommandWithSession } from "https://deno.land/x/functional_redis@v0.2.0/library/client.js";
+
+import { handlers, route } from "https://deno.land/x/functional_http_server@v0.3.1/library/route.js";
+import startHTTPServer from "https://deno.land/x/functional_http_server@v0.3.1/library/server.js";
+
+startHTTPServer(
+ { port: 8080 },
+ route(
+ handlers.post(
+ "/hoge",
+ compose(
+ map(_ => Response.Created({ 'content-type': "text/plain" }, encodeText("Created!"))),
+ converge(
+ (...tasks) => evert(Task, tasks),
+ [
+ compose(
+ executeRedisCommandWithSession({ port: 6379 }),
+ concat(RedisRequest("SET", new Uint8Array([]), [ "hoge", $$rawPlaceholder ]))
+ ),
+ compose(
+ writeFile({}),
+ concat(File.fromPath(`${Deno.cwd()}/hoge`))
+ )
+ ]
+ )
+ )
+ )
+ )
+);
+
+const container = await fetch(
+ Request(
+ {
+ headers: {
+ 'accept': 'text/plain',
+ 'content-type': 'text/plain'
+ },
+ method: 'POST',
+ url: 'http://localhost:8080/hoge'
+ },
+ encodeText("Hello, Hoge!")
+ )
+).run()
+
+const response = safeExtract("Failed to unpack the response", container);
+
+assert(Response.Success.is(response));
+assertEquals(response.headers.status, 201);
+
+server.close();
+```
+
+## Simple HTTP server
+
+The fastest way to start a HTTP server is to use the `startHTTPServer` function.
+The function takes two arguments; the first argument is the options, and the second is a unary
+function that takes a `Request` and return a `Task` of a `Response`.
+
+```js
+import Task from "https://deno.land/x/functional@v1.3.2/library/Task.js";
+import Response from "https://deno.land/x/functional_io@v1.1.0/library/Response.js";
+import startHTTPServer from "https://deno.land/x/functional_http_server@v0.3.1/library/server.js";
+
+startHTTPServer({ port: 8080 }, request => Task.of(Response.OK({}, request.raw)));
+```
+
+You can test this simple server by executing it your file
+
+```bash
+$ deno run --allow-net server.js
+```
+
+```bash
+$ curl localhost:8080 -d "Hello, Hoge!"
+> Hello, Hoge!
+```
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..8b33dc4
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,24 @@
+.DS_Store
+.data/
+.docs/
+.dump/
+.idea/
+.nyc_output/
+.sass-cache/
+coverage/
+journal/
+node_modules/
+out/
+scratch/
+
+*.db
+*.iml
+*.log
+*.rdb
+*.zip
+
+.todo.md
+
+dmfx
+.dmfx
+*.dmfx.js
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
new file mode 100644
index 0000000..324240c
--- /dev/null
+++ b/CONTRIBUTING.md
@@ -0,0 +1,42 @@
+# Contributor guidelines
+
+## What do I need to know to help?
+
+If you are looking to help to with a code contribution our project uses JavaScript to run on Deno and modern browsers.
+If you don't feel ready to make a code contribution yet, no problem! You can also check out
+[the issues](https://github.com/sebastienfilion/functional/issues).
+
+Never made an open source contribution before? Wondering how contributions work in the in our project? Here's a quick
+rundown!
+
+ 1. Find an issue that you are interested in addressing, or a feature that you would like to add;
+ 2. Fork the repository associated with the issue to your local GitHub organization. This means that you will have a
+ copy of the repository under `github-username/repository-name`;
+ 3. Clone the repository to your local machine using git clone https://github.com/github-username/repository-name.git;
+ 4. Create a new branch for your fix using `git checkout -b branch-name-here`. The preferred pattern is to prefix the
+ branch name, i.e.: `fix/[issue-number|*]`, `document/*` or, `implement/[issue-number|*]`;
+ 5. Make the appropriate changes for the issue you are trying to address, or the feature that you want to implement;
+ 6. Use git to commit your changes with a descriptive message, you can refer to
+ [this article](https://dev.to/jacobherrington/how-to-write-useful-commit-messages-my-commit-message-template-20n9)
+ to learn how to write a good commit message;
+ 7. Push the changes to the remote repository using git push origin branch-name-here;
+ 8. Submit a pull request to the upstream repository;
+ 9. Title the pull request with a short description of the changes made and the issue or bug number associated with
+ your change. For example, you can title an issue like so "Add log messages #4352";
+ 10. In the description of the pull request, explain the changes that you made, any issues you think exist with the
+ pull request you made, and any questions you have for the maintainer. It's OK if your pull request is not perfect
+ (no pull request is), the reviewer will be able to help you fix any problems and improve it!
+ 11. Wait for the pull request to be reviewed by a maintainer;
+ 12. Make changes to the pull request if the reviewing maintainer recommends them.
+ 13. Celebrate your success after your pull request is merged!
+
+## Where can I go for help?
+
+If you need help, you can ask questions [on Discord](https://discord.gg/gp83e8dr).
+
+## What does the Code of Conduct mean for me?
+
+Our Code of Conduct means that you are responsible for treating everyone on the project with respect and courtesy
+regardless of their identity. If you are the victim of any inappropriate behavior or comments as described in our
+Code of Conduct, we are here for you and will do the best to ensure that the abuser is reprimanded appropriately,
+per our code.
\ No newline at end of file
diff --git a/README.md b/README.md
index 7389801..0a8c00a 100644
--- a/README.md
+++ b/README.md
@@ -1,14 +1,93 @@
-
+
-A simple HTTP server inspired by Express and in tune with Functional Programming principles in JavaScript for Deno.
+A simple HTTP server inspired by Express and in tune with Functional Programming principles in JavaScript for Deno.
-[](https://github.com/sebastienfilion/functional-http-server@v0.3.0)
-[](https://github.com/denoland/deno)
-[](https://github.com/sebastienfilion/functional-http-server/releases)
-[](https://github.com/sebastienfilion/functional-http-server/blob/v0.1.1/LICENSE)
+[](https://deno.land/x/functional_http_server@v0.3.1)
+[](https://github.com/denoland/deno)
+[](https://github.com/sebastienfilion/functional-http-server/releases)
+[](https://github.com/sebastienfilion/functional-http-server/blob/v0.3.1/LICENSE)
+[](https://discord.gg/)
* [Simple HTTP server](#simple-http-server)
* [Routing](#routing)
+ * [Server](#server)
+ * [Utilities](#utilities)
+
+## Usage
+
+Functional HTTP Server is optimized to write elegant and powerful point-free functions. This example uses the Ramda
+library - for simplification - but you should be able to use any library that implements the Fantasy-land
+specifications.
+
+This example showcase how to create an endpoint handler for `POST /hoge` that writes to a local file and to Redis
+simultaneously the content of the request's body and, replies with `201`.
+
+```js
+import Task from "https://deno.land/x/functional@v1.3.2/library/Task.js";
+import {
+ decodeRaw,
+ encodeText,
+ evert,
+ safeExtract
+} from "https://deno.land/x/functional@v1.3.2/library/utilities.js";
+import File from "https://deno.land/x/functional_io@v1.1.0/library/File.js";
+import Request from "https://deno.land/x/functional_io@v1.1.0/library/Request.js";
+import Response from "https://deno.land/x/functional_io@v1.1.0/library/Response.js";
+import { fetch } from "https://deno.land/x/functional_io@v1.1.0/library/browser_safe.js";
+import { writeFile } from "https://deno.land/x/functional_io@v1.1.0/library/fs.js";
+import RedisRequest from "https://deno.land/x/functional_redis@v0.2.0/library/RedisRequest.js";
+import { $$rawPlaceholder } from "https://deno.land/x/functional_redis@v0.2.0/library/Symbol.js";
+import { executeRedisCommandWithSession } from "https://deno.land/x/functional_redis@v0.2.0/library/client.js";
+
+import { handlers, route } from "https://deno.land/x/functional_http_server@v0.3.1/library/route.js";
+import startHTTPServer from "https://deno.land/x/functional_http_server@v0.3.1/library/server.js";
+
+startHTTPServer(
+ { port: 8080 },
+ route(
+ handlers.post(
+ "/hoge",
+ compose(
+ map(_ => Response.Created({ 'content-type': "text/plain" }, encodeText("Created!"))),
+ converge(
+ (...tasks) => evert(Task, tasks),
+ [
+ compose(
+ executeRedisCommandWithSession({ port: 6379 }),
+ concat(RedisRequest("SET", new Uint8Array([]), [ "hoge", $$rawPlaceholder ]))
+ ),
+ compose(
+ writeFile({}),
+ concat(File.fromPath(`${Deno.cwd()}/hoge`))
+ )
+ ]
+ )
+ )
+ )
+ )
+);
+
+const container = await fetch(
+ Request(
+ {
+ headers: {
+ 'accept': 'text/plain',
+ 'content-type': 'text/plain'
+ },
+ method: 'POST',
+ url: 'http://localhost:8080/hoge'
+ },
+ encodeText("Hello, Hoge!")
+ )
+).run()
+
+const response = safeExtract("Failed to unpack the response", container);
+
+assert(Response.Success.is(response));
+assertEquals(response.headers.status, 201);
+
+server.close();
+```
## Simple HTTP server
@@ -17,9 +96,9 @@ The function takes two arguments; the first argument is the options, and the sec
function that takes a `Request` and return a `Task` of a `Response`.
```js
-import Task from "https://deno.land/x/functional@v1.2.1/library/Task.js";
-import Response from "https://deno.land/x/functional_io@v1.0.0/library/Response.js";
-import startHTTPServer from "https://deno.land/x/functional_http_server@v0.3.0/library/server.js";
+import Task from "https://deno.land/x/functional@v1.3.2/library/Task.js";
+import Response from "https://deno.land/x/functional_io@v1.1.0/library/Response.js";
+import startHTTPServer from "https://deno.land/x/functional_http_server@v0.3.1/library/server.js";
startHTTPServer({ port: 8080 }, request => Task.of(Response.OK({}, request.raw)));
```
@@ -35,6 +114,8 @@ $ curl localhost:8080 -d "Hello, Hoge!"
> Hello, Hoge!
```
+---
+
## Routing
The main routing tool that comes bundled with this library is conveniently called `route`.
@@ -44,10 +125,10 @@ The assertion function takes a `Request` and return a `Boolean`, the handling fu
must return a `Task` of a `Response`.
```js
-import Task from "https://deno.land/x/functional@v1.2.1/library/Task.js";
-import { encodeText } from "https://deno.land/x/functional@v1.2.1/library/utilities.js";
-import Response from "https://deno.land/x/functional_io@v1.0.0/library/Response.js";
-import { route } from "https://deno.land/x/functional_http_server@v0.3.0/library/route.js";
+import Task from "https://deno.land/x/functional@v1.3.2/library/Task.js";
+import { encodeText } from "https://deno.land/x/functional@v1.3.2/library/utilities.js";
+import Response from "https://deno.land/x/functional_io@v1.1.0/library/Response.js";
+import { route } from "https://deno.land/x/functional_http_server@v0.3.1/library/route.js";
startHTTPServer(
{ port: 8080 },
@@ -66,10 +147,10 @@ Because the pattern is common, this library also offers a collection of handler
the assertion function. Each handler takes a `String` or a `RegExp` and a unary function.
```js
-import Task from "https://deno.land/x/functional@v1.2.1/library/Task.js";
-import { encodeText } from "https://deno.land/x/functional@v1.2.1/library/utilities.js";
-import Response from "https://deno.land/x/functional_io@v1.0.0/library/Response.js";
-import { handlers, route } from "https://deno.land/x/functional_http_server@v0.3.0/library/route.js";
+import Task from "https://deno.land/x/functional@v1.3.2/library/Task.js";
+import { encodeText } from "https://deno.land/x/functional@v1.3.2/library/utilities.js";
+import Response from "https://deno.land/x/functional_io@v1.1.0/library/Response.js";
+import { handlers, route } from "https://deno.land/x/functional_http_server@v0.3.1/library/route.js";
startHTTPServer(
{ port: 8080 },
@@ -79,65 +160,114 @@ startHTTPServer(
);
```
-#### Routing with the `explodeRequest` utility
+#### handlers`.delete`
+`String|RegExp → (Request → Task Response) → Task Response`
+
+This function takes a string or a regex and a unary function that takes a `Request` and return a task of a
+`Response`. The handler will apply the unary function to a HTTP requests that uses the `DELETE` method if the path
+equals or match the first argument.
+
+```js
+import { handlers } from "https://deno.land/x/functional_http_server@v0.3.1/library/route.js";
+
+startHTTPServer({ port: 8080 }, handlers.delete(/\/hoge\/(?[a-z]+)$/, handleDestroyHoge));
+```
-The function `explodeRequest` is a utility that will parse the headers and serialize the body of a `Request`, for
-convenience. The function takes two arguments; a binary function that returns a `Task` of `Response` and a `Request`.
+#### handlers`.get`
+`String|RegExp → (Request → Task Response) → Task Response`
-The binary function handler will be called with an object containing the original headers, the parsed query string
-and other parameters; the second argument is the body of request serialized based on the content type.
+This function takes a string or a regex and a unary function that takes a `Request` and return a task of a
+`Response`. The handler will apply the unary function to a HTTP requests that uses the `GET` method if the path
+equals or match the first argument.
```js
-import { explodeRequest } from "https://deno.land/x/functional_http_server@v0.3.0/library/utilities.js";
+import { handlers } from "https://deno.land/x/functional_http_server@v0.3.1/library/route.js";
-startHTTPServer(
- { port: 8080 },
- route(
- handlers.get('/users', explodeRequest(({ status }) => retrieveUsers({ filters: { status } }))),
- handlers.post(/\/users\/(?.+)$/, explodeRequest(({ userID }, { data: user }) => updateUser(userID, user)))
- )
-);
+startHTTPServer({ port: 8080 }, handlers.get(/\/hoge\/(?[a-z]+)$/, handleRetrieveHoge));
```
-For this sample, a `GET` request made with a query string will be parsed as an object.
+#### handlers`.patch`
+`String|RegExp → (Request → Task Response) → Task Response`
-```bash
-$ curl localhost:8080/users?status=active
+This function takes a string or a regex and a unary function that takes a `Request` and return a task of a
+`Response`. The handler will apply the unary function to a HTTP requests that uses the `PATCH` method if the path
+equals or match the first argument.
+
+```js
+import { handlers } from "https://deno.land/x/functional_http_server@v0.3.1/library/route.js";
+
+startHTTPServer({ port: 8080 }, handlers.patch(/\/hoge\/(?[a-z]+)$/, handleUpdateHoge));
```
-And, a `POST` request with a body as JSON will be parsed as well.
+#### handlers`.post`
+`String|RegExp → (Request → Task Response) → Task Response`
-```bash
-$ curl localhost:8080/users/hoge -X POST -H "Content-Type: application/json" -d "{\"data\":{\"fullName\":\"Hoge\"}}"
+This function takes a string or a regex and a unary function that takes a `Request` and return a task of a
+`Response`. The handler will apply the unary function to a HTTP requests that uses the `POST` method if the path
+equals or match the first argument.
+
+```js
+import { handlers } from "https://deno.land/x/functional_http_server@v0.3.1/library/route.js";
+
+startHTTPServer({ port: 8080 }, handlers.post('/hoge', handleCreateHoge));
```
- The function `explodeRequest` should cover most use-cases but if you need to create your own parser, check out the
- [`parseRequest`](#parsing-requests) function.
+#### handlers`.put`
+`String|RegExp → (Request → Task Response) → Task Response`
+
+This function takes a string or a regex and a unary function that takes a `Request` and return a task of a
+`Response`. The handler will apply the unary function to a HTTP requests that uses the `PUT` method if the path
+equals or match the first argument.
-#### Composing routes
+```js
+import { handlers } from "https://deno.land/x/functional_http_server@v0.3.1/library/route.js";
+
+startHTTPServer({ port: 8080 }, handlers.put(/\/hoge\/(?[a-z]+)$/, handleUpdateHoge));
+```
-Finally, you can compose your routes for increased readability.
+#### `route`
+`([ (Request → Boolean), (Request → Task Response) ],...) → Task Response`
+
+This functions takes an arbitrary amount of array pairs of functions and return a task of a `Response`. The first
+function of the pair is a predicate, it takes a `Request` and returns a `Boolean`. The second function of the pair
+is a unary function that takes a `Request` and return a task of a `Response`; it will be executed only if the first
+function returns `true`.
```js
-const userRoutes = [ handlers.get('/', handleRetrieveUsers), ... ];
-const sensorRoutes = [ handlers.get('/', handleRetrieveSensors), ... ];
+import { route } from "https://deno.land/x/functional_http_server@v0.3.1/library/route.js";
-startHTTPServer({ port: 8080 }, route(...userRoutes, ...sensorRoutes));
+startHTTPServer(
+ { port: 8080 },
+ route(
+ [
+ request => request.headers.method === "POST" && request.headers.url === "/hoge",
+ request => Task.of(Response.Created({}, encodeText("Created")))
+ ]
+ )
+);
```
-### Middleware
+The handler can be easily composed using the spread operator.
-Before talking about middlewares, I think it is important to talk about the power of function composition and couple of
-things special about `startHTTPServer` and `route`:
+```js
+startHTTPServer(
+ { port: 8080 },
+ route(
+ ...hogeRouteHandlers,
+ ...piyoRouteHandlers,
+ ...fugaRouteHandlers
+ )
+);
+```
- 1. The function `startHTTPServer` takes a unary function that must return a `Task` of `Response`.
- 2. The function `route`, will always return early if the argument is not a `Request`.
+The handler will be short-circuited if it's not passed a `Request`. This makes it easy to write a
+function to preflight a request.
-So for example, if you needed to discard any request with a content type that is not `application/json`, you could
+For example, if you needed to discard any request that doesn't accept `application/json`, you could
do the following.
```js
-import { compose } from "https://x.nest.land/ramda@0.27.0/source/index.js";
+import { compose } from "https://deno.land/x/ramda@v0.27.2/mod.ts";
startHTTPServer(
{ port: 8080 },
@@ -150,29 +280,89 @@ startHTTPServer(
);
```
+---
+
+## Server
+
+### `stream`
+`(Request → Task Response) → AsyncIterator → _|_`
+
+This function takes a unaryFunction -- which itself takes a
+[`Request`](https://github.com/sebastienfilion/functional-io#request) and, returns a Task of a
+[`Response`](https://github.com/sebastienfilion/functional-io#Response) -- and, an Async Iterator of a
+[Deno HTTP request](https://deno.land/std@0.82.0/http). The function doesn't resolve to a value.
+
+### `startHTTPServer`
+`Object → (Request → Response) → Listener`
+
+This function takes an object of options and, a unary function -- which itself takes a
+[`Request`](https://github.com/sebastienfilion/functional-io#request) and, returns a Task of a
+[`Response`](https://github.com/sebastienfilion/functional-io#Response). The function will return a server instance
+that can be closed (`server.close()`). [See the Deno server library](https://deno.land/std@0.82.0/http) for reference.
+
+```js
+import Task from "https://deno.land/x/functional@v1.3.2/library/Task.js";
+import Response from "https://deno.land/x/functional_io@v1.1.0/library/Response.js";
+import startHTTPServer from "https://deno.land/x/functional_http_server@v0.3.1/library/server.js";
+
+startHTTPServer({ port: 8080 }, request => Task.of(Response.OK({}, request.raw)));
+```
+
+---
+
+## Utilities
+
+### `parseBody`
+`Request → a`
+
+This function takes a `Request` and return the most appropriate parsing of the body;
+an object if the content-type of the request is `application/json` or, a string if the content type of the request is
+`text/*`.
+
+```js
+import { parseBody } from "https://deno.land/x/functional_http_server@v0.3.1/library/utilities.js";
+
+assertEquals(
+ parseBody(
+ Request({ 'content-type': 'application/json' }, encodeText(JSON.stringify({ piyo: 'piyo' })))
+ ),
+ { piyo: 'piyo' }
+);
+```
+
+### `parseQueryString`
+`Request → Record`
+
+This function takes a `Request` and return an record of the query string.
+
+```js
+import { parseQueryString } from "https://deno.land/x/functional_http_server@v0.3.1/library/utilities.js";
+
+assertEquals(
+ parseQueryString(Request({ url: '/?hoge=hoge' }, new Uint8Array([]))),
+ { hoge: 'hoge' }
+);
+```
+
+---
-## Deno
+## Contributing
-This codebase uses [Deno](https://deno.land/#installation).
+We appreciate your help! Please, [read the guidelines](./CONTRIBUTING.md).
-### MIT License
+## License
Copyright © 2020 - Sebastien Filion
-Permission is hereby granted, free of charge, to any person obtaining a copy
-of this software and associated documentation files (the "Software"), to deal
-in the Software without restriction, including without limitation the rights
-to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
-copies of the Software, and to permit persons to whom the Software is
-furnished to do so, subject to the following conditions:
-
-The above copyright notice and this permission notice shall be included in all
-copies or substantial portions of the Software.
-
-THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
-IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
-FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
-AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
-LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
-OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
-SOFTWARE.
\ No newline at end of file
+Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated
+documentation files (the "Software"), to deal in the Software without restriction, including without limitation the
+rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit
+persons to whom the Software is furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all copies or substantial portions of the
+Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE
+WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
+COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
+OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
\ No newline at end of file
diff --git a/library/route.js b/library/route.js
index d870855..8aff2c6 100644
--- a/library/route.js
+++ b/library/route.js
@@ -10,13 +10,30 @@ import {
path,
prop,
test
-} from "https://x.nest.land/ramda@0.27.0/source/index.js";
-import Task from "https://deno.land/x/functional@v1.2.1/library/Task.js";
-import Request from "https://deno.land/x/functional_io@v1.0.0/library/Request.js";
-import Response from "https://deno.land/x/functional_io@v1.0.0/library/Response.js";
+} from "https://deno.land/x/ramda@v0.27.2/mod.ts";
+import Task from "https://deno.land/x/functional@v1.3.2/library/Task.js";
+import Request from "https://deno.land/x/functional_io@v1.1.0/library/Request.js";
+import Response from "https://deno.land/x/functional_io@v1.1.0/library/Response.js";
-import { assertIsRegex } from "https://deno.land/x/functional@v1.2.1/library/utilities.js";
+import { assertIsRegex } from "https://deno.land/x/functional@v1.3.2/library/utilities.js";
+const factorizeHandler = method => (pattern, naryFunction) =>
+ [
+ both(
+ compose(equals(method), path([ 'headers', 'method' ])),
+ compose(
+ ifElse(_ => assertIsRegex(pattern), test(pattern), equals(pattern)),
+ prop(1),
+ match(/(^.*?)(?:\?.*){0,1}$/),
+ path([ 'headers', 'url' ])
+ )
+ ),
+ (request, options = {}) =>
+ naryFunction.length === 2 ? naryFunction({ pattern, ...options }, request) : naryFunction(request)
+ ]
+
+// * :: a -> (Request -> Response) -> [ (Request -> boolean), (Request -> Response) ]
+// * :: a -> ((Request, RegExp) -> Response) -> [ (Request -> boolean), (Request -> Response) ]
/**
* ## Routing
*
@@ -27,10 +44,10 @@ import { assertIsRegex } from "https://deno.land/x/functional@v1.2.1/library/uti
* must return a `Task` of a `Response`.
*
* ```js
- * import Task from "https://deno.land/x/functional@v1.2.1/library/Task.js";
- * import Response from "https://deno.land/x/functional_io@v1.0.0/library/Response.js";
- * import { route } from "./library/route.js";
- * import { encodeText } from "./library/utilities.js";
+ * import Task from "https://deno.land/x/functional@v1.3.2/library/Task.js";
+ * import { encodeText } from "https://deno.land/x/functional@v1.3.2/library/utilities.js";
+ * import Response from "https://deno.land/x/functional_io@v1.1.0/library/Response.js";
+ * import { route } from "https://deno.land/x/functional_http_server@v0.3.1/library/route.js";
*
* startHTTPServer(
* { port: 8080 },
@@ -49,78 +66,137 @@ import { assertIsRegex } from "https://deno.land/x/functional@v1.2.1/library/uti
* the assertion function. Each handler takes a `String` or a `RegExp` and a unary function.
*
* ```js
- * import Task from "https://deno.land/x/functional@v1.2.1/library/Task.js";
- * import Response from "https://deno.land/x/functional_io@v1.0.0/library/Response.js";
- * import { handlers, route } from "./library/route.js";
- * import { encodeText } from "./library/utilities.js";
+ * import Task from "https://deno.land/x/functional@v1.3.2/library/Task.js";
+ * import { encodeText } from "https://deno.land/x/functional@v1.3.2/library/utilities.js";
+ * import Response from "https://deno.land/x/functional_io@v1.1.0/library/Response.js";
+ * import { handlers, route } from "https://deno.land/x/functional_http_server@v0.3.1/library/route.js";
*
* startHTTPServer(
* { port: 8080 },
* route(
* handlers.get('/', _ => Task.of(Response.OK({ 'content-type': 'text/plain' }, encodeText("Hello, Hoge!"))))
- * );
+ * )
* );
* ```
*
- * #### Routing with the `explodeRequest` utility
+ * #### handlers`.delete`
+ * `String|RegExp -> (Request -> Task Response) -> Task Response`
+ *
+ * This function takes a string or a regex and a unary function that takes a `Request` and return a task of a
+ * `Response`. The handler will apply the unary function to a HTTP requests that uses the `DELETE` method if the path
+ * equals or match the first argument.
*
- * The function `explodeRequest` is a utility that will parse the headers and serialize the body of a `Request`, for
- * convenience. The function takes two arguments; a binary function that returns a `Task` of `Response` and a `Request`.
+ * ```js
+ * import { handlers } from "https://deno.land/x/functional_http_server@v0.3.1/library/route.js";
+ *
+ * startHTTPServer({ port: 8080 }, handlers.delete(/\/hoge\/(?[a-z]+)$/, handleDestroyHoge));
+ * ```
*
- * The binary function handler will be called with an object containing the original headers, the parsed query string
- * and other parameters; the second argument is the body of request serialized based on the content type.
+ * #### handlers`.get`
+ * `String|RegExp -> (Request -> Task Response) -> Task Response`
+ *
+ * This function takes a string or a regex and a unary function that takes a `Request` and return a task of a
+ * `Response`. The handler will apply the unary function to a HTTP requests that uses the `GET` method if the path
+ * equals or match the first argument.
*
* ```js
- * import { explodeRequest } from "./library/utilities.js";
+ * import { handlers } from "https://deno.land/x/functional_http_server@v0.3.1/library/route.js";
*
- * startHTTPServer(
- * { port: 8080 },
- * route(
- * handlers.get('/users', explodeRequest(({ status }) => retrieveUsers({ filters: { status } }))),
- * handlers.post(/\/users\/(?.+)$/, explodeRequest(({ userID }, { data: user }) => updateUser(userID, user)))
- * )
- * );
+ * startHTTPServer({ port: 8080 }, handlers.get(/\/hoge\/(?[a-z]+)$/, handleRetrieveHoge));
* ```
*
- * For this sample, a `GET` request made with a query string will be parsed as an object.
+ * #### handlers`.patch`
+ * `String|RegExp -> (Request -> Task Response) -> Task Response`
*
- * ```bash
- * $ curl localhost:8080/users?status=active
+ * This function takes a string or a regex and a unary function that takes a `Request` and return a task of a
+ * `Response`. The handler will apply the unary function to a HTTP requests that uses the `PATCH` method if the path
+ * equals or match the first argument.
+ *
+ * ```js
+ * import { handlers } from "https://deno.land/x/functional_http_server@v0.3.1/library/route.js";
+ *
+ * startHTTPServer({ port: 8080 }, handlers.patch(/\/hoge\/(?[a-z]+)$/, handleUpdateHoge));
* ```
*
- * And, a `POST` request with a body as JSON will be parsed as well.
+ * #### handlers`.post`
+ * `String|RegExp -> (Request -> Task Response) -> Task Response`
+ *
+ * This function takes a string or a regex and a unary function that takes a `Request` and return a task of a
+ * `Response`. The handler will apply the unary function to a HTTP requests that uses the `POST` method if the path
+ * equals or match the first argument.
+ *
+ * ```js
+ * import { handlers } from "https://deno.land/x/functional_http_server@v0.3.1/library/route.js";
*
- * ```bash
- * $ curl localhost:8080/users/hoge -X POST -H "Content-Type: application/json" -d "{\"data\":{\"fullName\":\"Hoge\"}}"
+ * startHTTPServer({ port: 8080 }, handlers.post('/hoge', handleCreateHoge));
* ```
*
- * The function `explodeRequest` should cover most use-cases but if you need to create your own parser, check out the
- * [`parseRequest`](#parsing-requests) function.
+ * #### handlers`.put`
+ * `String|RegExp -> (Request -> Task Response) -> Task Response`
*
- * #### Composing routes
+ * This function takes a string or a regex and a unary function that takes a `Request` and return a task of a
+ * `Response`. The handler will apply the unary function to a HTTP requests that uses the `PUT` method if the path
+ * equals or match the first argument.
*
- * Finally, you can compose your routes for increased readability.
+ * ```js
+ * import { handlers } from "https://deno.land/x/functional_http_server@v0.3.1/library/route.js";
+ *
+ * startHTTPServer({ port: 8080 }, handlers.put(/\/hoge\/(?[a-z]+)$/, handleUpdateHoge));
+ * ```
+ */
+export const handlers = {
+ delete: factorizeHandler('DELETE'),
+ get: factorizeHandler('GET'),
+ patch: factorizeHandler('PATCH'),
+ post: factorizeHandler('POST'),
+ put: factorizeHandler('PUT')
+};
+
+
+/**
+ * #### `route`
+ * `([ (Request -> Boolean), (Request -> Task Response) ],...) -> Task Response`
+ *
+ * This functions takes an arbitrary amount of array pairs of functions and return a task of a `Response`. The first
+ * function of the pair is a predicate, it takes a `Request` and returns a `Boolean`. The second function of the pair
+ * is a unary function that takes a `Request` and return a task of a `Response`; it will be executed only if the first
+ * function returns `true`.
*
* ```js
- * const userRoutes = [ handlers.get('/', handleRetrieveUsers), ... ];
- * const sensorRoutes = [ handlers.get('/', handleRetrieveSensors), ... ];
+ * import { route } from "https://deno.land/x/functional_http_server@v0.3.1/library/route.js";
*
- * startHTTPServer({ port: 8080 }, route(...userRoutes, ...sensorRoutes));
+ * startHTTPServer(
+ * { port: 8080 },
+ * route(
+ * [
+ * request => request.headers.method === "POST" && request.headers.url === "/hoge",
+ * request => Task.of(Response.Created({}, encodeText("Created")))
+ * ]
+ * )
+ * );
* ```
*
- * ### Middleware
+ * The handler can be easily composed using the spread operator.
*
- * Before talking about middlewares, I think it is important to talk about the power of function composition and couple of
- * things special about `startHTTPServer` and `route`:
+ * ```js
+ * startHTTPServer(
+ * { port: 8080 },
+ * route(
+ * ...hogeRouteHandlers,
+ * ...piyoRouteHandlers,
+ * ...fugaRouteHandlers
+ * )
+ * );
+ * ```
*
- * 1. The function `startHTTPServer` takes a unary function that must return a `Task` of `Response`.
- * 2. The function `route`, will always return early if the argument is not a `Request`.
+ * The handler will be short-circuited if it's not passed a `Request`. This makes it easy to write a
+ * function to preflight a request.
*
- * So for example, if you needed to discard any request with a content type that is not `application/json`, you could
+ * For example, if you needed to discard any request that doesn't accept `application/json`, you could
* do the following.
*
* ```js
- * import { compose } from "https://x.nest.land/ramda@0.27.0/source/index.js";
+ * import { compose } from "https://deno.land/x/ramda@v0.27.2/mod.ts";
*
* startHTTPServer(
* { port: 8080 },
@@ -133,33 +209,6 @@ import { assertIsRegex } from "https://deno.land/x/functional@v1.2.1/library/uti
* );
* ```
*/
-
-const factorizeHandler = method => (pattern, naryFunction) =>
- [
- both(
- compose(equals(method), path([ 'headers', 'method' ])),
- compose(
- ifElse(_ => assertIsRegex(pattern), test(pattern), equals(pattern)),
- prop(1),
- match(/(^.*?)(?:\?.*){0,1}$/),
- path([ 'headers', 'url' ])
- )
- ),
- (request, options = {}) =>
- naryFunction.length === 2 ? naryFunction({ pattern, ...options }, request) : naryFunction(request)
- ]
-
-// * :: a -> (Request -> Response) -> [ (Request -> boolean), (Request -> Response) ]
-// * :: a -> ((Request, RegExp) -> Response) -> [ (Request -> boolean), (Request -> Response) ]
-export const handlers = {
- delete: factorizeHandler('DELETE'),
- get: factorizeHandler('GET'),
- post: factorizeHandler('POST'),
- put: factorizeHandler('PUT')
-};
-
-
-// route :: ([ (Request -> Boolean), (Request -> Task Response) ]...) -> Task Response
export const route = (...routeList) => cond(
[
[
@@ -172,4 +221,4 @@ export const route = (...routeList) => cond(
_ => Task.of(Response.NotFound({}, new Uint8Array([])))
]
]
-);
\ No newline at end of file
+);
diff --git a/library/route_test.js b/library/route_test.js
index ab70dbd..fb74f48 100644
--- a/library/route_test.js
+++ b/library/route_test.js
@@ -1,7 +1,7 @@
import { assert, assertEquals } from "https://deno.land/std@0.79.0/testing/asserts.ts"
-import Task from "https://deno.land/x/functional@v1.2.1/library/Task.js";
-import Request from "https://deno.land/x/functional_io@v1.0.0/library/Request.js";
-import Response from "https://deno.land/x/functional_io@v1.0.0/library/Response.js";
+import Task from "https://deno.land/x/functional@v1.3.2/library/Task.js";
+import Request from "https://deno.land/x/functional_io@v1.1.0/library/Request.js";
+import Response from "https://deno.land/x/functional_io@v1.1.0/library/Response.js";
import { handlers, route } from "./route.js";
diff --git a/library/server.js b/library/server.js
index 90b7747..5ab9d72 100644
--- a/library/server.js
+++ b/library/server.js
@@ -1,36 +1,9 @@
import { gray, red } from "https://deno.land/std@0.79.0/fmt/colors.ts";
import { serve, serveTLS } from "https://deno.land/std@0.79.0/http/server.ts";
-import { cond, curry, reduce } from "https://x.nest.land/ramda@0.27.0/source/index.js";
-import { encodeText } from "https://deno.land/x/functional@v1.2.1/library/utilities.js";
-import Request from "https://deno.land/x/functional_io@v1.0.0/library/Request.js";
-import Response from "https://deno.land/x/functional_io@v1.0.0/library/Response.js";
-
-/**
- * ## Simple HTTP server
- *
- * The fastest way to start a HTTP server is to use the `startHTTPServer` function.
- * The function takes two arguments; the first argument is the options, and the second is a unary
- * function that takes a `Request` and return a `Task` of a `Response`.
- *
- * ```js
- * import Task from "https://deno.land/x/functional@v1.2.1/library/Task.js";
- * import Response from "https://deno.land/x/functional_io@v1.0.0/library/Response.js";
- * import startHTTPServer from "./library/server.js";
- *
- * startHTTPServer({ port: 8080 }, request => Task.of(Response.OK({}, request.raw)));
- * ```
- *
- * You can test this simple server by executing it your file
- *
- * ```bash
- * $ deno run --allow-net server.js
- * ```
- *
- * ```bash
- * $ curl localhost:8080 -d "Hello, Hoge!"
- * > Hello, Hoge!
- * ```
- */
+import { cond, curry, reduce, toPairs } from "https://deno.land/x/ramda@v0.27.2/mod.ts";
+import { encodeText } from "https://deno.land/x/functional@v1.3.2/library/utilities.js";
+import Request from "https://deno.land/x/functional_io@v1.1.0/library/Request.js";
+import Response from "https://deno.land/x/functional_io@v1.1.0/library/Response.js";
const destructureHeaders = headers => reduce(
(accumulator, [ key, value ]) => Object.defineProperty(accumulator, key, { enumerable: true, value }),
@@ -38,13 +11,31 @@ const destructureHeaders = headers => reduce(
headers.entries()
);
-// stream :: (Request -> Task Response) -> AsyncIterator -> _|_
+/**
+ * ## Server
+ */
+
+/**
+ * ### `stream`
+ * `(Request -> Task Response) -> AsyncIterator -> _|_`
+ *
+ * This function takes a unaryFunction -- which itself takes a
+ * [`Request`](https://github.com/sebastienfilion/functional-io#request) and, returns a Task of a
+ * [`Response`](https://github.com/sebastienfilion/functional-io#Response) -- and, an Async Iterator of a
+ * [Deno HTTP request](https://deno.land/std@0.82.0/http). The function doesn't resolve to a value.
+ */
export const stream = curry(
async (unaryFunction, iterator) => {
for await (const _request of iterator) {
const { body = new Uint8Array([]), headers, method, url } = _request;
- const handleResponse = response => _request.respond({ ...response.headers, body: response.raw });
+ const handleResponse = response => _request.respond(
+ {
+ body: response.raw,
+ headers: response.headers instanceof Headers ? response.headers : new Headers(toPairs(response.headers)),
+ status: response.headers.status
+ }
+ );
const handleError = error =>
console.error(red(`An error occurred in an handler: ${error.message}\n${gray(error.stack)}`))
|| _request.respond({ status: 500, body: encodeText(error.message) })
@@ -69,7 +60,23 @@ export const stream = curry(
}
);
-// startServer :: Options -> (Request -> Response) -> _|_
+/**
+ * ### `startHTTPServer`
+ * `Object -> (Request -> Response) -> Listener`
+ *
+ * This function takes an object of options and, a unary function -- which itself takes a
+ * [`Request`](https://github.com/sebastienfilion/functional-io#request) and, returns a Task of a
+ * [`Response`](https://github.com/sebastienfilion/functional-io#Response). The function will return a server instance
+ * that can be closed (`server.close()`). [See the Deno server library](https://deno.land/std@0.82.0/http) for reference.
+ *
+ * ```js
+ * import Task from "https://deno.land/x/functional@v1.3.2/library/Task.js";
+ * import Response from "https://deno.land/x/functional_io@v1.1.0/library/Response.js";
+ * import startHTTPServer from "https://deno.land/x/functional_http_server@v0.3.1/library/server.js";
+ *
+ * startHTTPServer({ port: 8080 }, request => Task.of(Response.OK({}, request.raw)));
+ * ```
+ */
export const startHTTPServer = (options, unaryFunction) => {
const server = options.certificatePath && options.keyPath
? serveTLS(options)
diff --git a/library/server_test.js b/library/server_test.js
index 0dfa12f..05a40f8 100644
--- a/library/server_test.js
+++ b/library/server_test.js
@@ -1,12 +1,22 @@
import { assert, assertEquals } from "https://deno.land/std@0.79.0/testing/asserts.ts";
-import { compose, converge, mergeRight } from "https://x.nest.land/ramda@0.27.0/source/index.js";
-
-import Either from "https://deno.land/x/functional@v1.2.1/library/Either.js";
-import Task from "https://deno.land/x/functional@v1.2.1/library/Task.js";
-import { decodeRaw, encodeText, safeExtract } from "https://deno.land/x/functional@v1.2.1/library/utilities.js";
-import { fetch } from "https://deno.land/x/functional_io@v1.0.0/library/browser_safe.js";
-import Request from "https://deno.land/x/functional_io@v1.0.0/library/Request.js";
-import Response from "https://deno.land/x/functional_io@v1.0.0/library/Response.js";
+import { applyTo, concat, compose, converge, map } from "https://deno.land/x/ramda@v0.27.2/mod.ts";
+
+import Either from "https://deno.land/x/functional@v1.3.2/library/Either.js";
+import Task from "https://deno.land/x/functional@v1.3.2/library/Task.js";
+import {
+ decodeRaw,
+ encodeText,
+ evert,
+ safeExtract
+} from "https://deno.land/x/functional@v1.3.2/library/utilities.js";
+import File from "https://deno.land/x/functional_io@v1.1.0/library/File.js";
+import Request from "https://deno.land/x/functional_io@v1.1.0/library/Request.js";
+import Response from "https://deno.land/x/functional_io@v1.1.0/library/Response.js";
+import { fetch } from "https://deno.land/x/functional_io@v1.1.0/library/browser_safe.js";
+import { writeFile } from "https://deno.land/x/functional_io@v1.1.0/library/fs.js";
+import RedisRequest from "https://deno.land/x/functional_redis@v0.2.0/library/RedisRequest.js";
+import { $$rawPlaceholder } from "https://deno.land/x/functional_redis@v0.2.0/library/Symbol.js";
+import { executeRedisCommandWithSession } from "https://deno.land/x/functional_redis@v0.2.0/library/client.js";
import { handlers, route } from "./route.js";
import { startHTTPServer } from "./server.js";
@@ -199,3 +209,58 @@ Deno.test(
server.close();
}
);
+
+Deno.test(
+ "Scenario 1",
+ async () => {
+ const server = startHTTPServer(
+ { port: 8080 },
+ route(
+ handlers.post(
+ "/hoge",
+ compose(
+ map(_ => Response.Created({ 'content-type': "text/plain" }, encodeText("Created!"))),
+ converge(
+ (...tasks) => evert(Task, tasks),
+ [
+ compose(
+ executeRedisCommandWithSession({ port: 6379 }),
+ concat(RedisRequest("SET", new Uint8Array([]), [ "hoge", $$rawPlaceholder ]))
+ ),
+ compose(
+ writeFile({}),
+ concat(File.fromPath(`${Deno.cwd()}/hoge`))
+ )
+ ]
+ )
+ )
+ )
+ )
+ );
+
+ const container = await fetch(
+ Request(
+ {
+ headers: {
+ 'accept': 'text/plain',
+ 'content-type': 'text/plain'
+ },
+ method: 'POST',
+ url: 'http://localhost:8080/hoge'
+ },
+ encodeText("Hello, Hoge!")
+ )
+ ).run()
+
+ const response = safeExtract("Failed to unpack the response", container);
+
+ assert(Response.Success.is(response), `Failed with HTTP error (${response.headers.status}): ${decodeRaw(response.raw)}`);
+ assertEquals(response.headers.status, 201);
+
+ server.close();
+
+ await createRedisSession(executeRedisCommand(RedisRequest.flushall()))(({ port: 6379 })).run();
+
+ await Deno.remove(`${Deno.cwd()}/hoge`);
+ }
+);
diff --git a/library/utilities.js b/library/utilities.js
index a1dd23d..82f3719 100644
--- a/library/utilities.js
+++ b/library/utilities.js
@@ -14,15 +14,33 @@ import {
prop,
split,
test
-} from "https://x.nest.land/ramda@0.27.0/source/index.js";
-import { assertIsRegex, decodeRaw } from "https://deno.land/x/functional@v1.2.1/library/utilities.js";
+} from "https://deno.land/x/ramda@v0.27.2/mod.ts";
+import { assertIsRegex, decodeRaw } from "https://deno.land/x/functional@v1.3.2/library/utilities.js";
/**
- * ### Parsing Requests
+ * ## Utilities
*/
-// parseBody :: Options -> Request -> a
-export const parseBody = _ => ap(
+/**
+ * ### `parseBody`
+ * `Request -> a`
+ *
+ * This function takes a `Request` and return the most appropriate parsing of the body;
+ * an object if the content-type of the request is `application/json` or, a string if the content type of the request is
+ * `text/*`.
+ *
+ * ```js
+ * import { parseBody } from "https://deno.land/x/functional_http_server@v0.3.1/library/utilities.js";
+ *
+ * assertEquals(
+ * parseBody(
+ * Request({ 'content-type': 'application/json' }, encodeText(JSON.stringify({ piyo: 'piyo' })))
+ * ),
+ * { piyo: 'piyo' }
+ * );
+ * ```
+ */
+export const parseBody = ap(
curry((request, parse) =>
request.raw.length > 0 ? parse(request.raw) : {}),
cond([
@@ -35,7 +53,7 @@ export const parseBody = _ => ap(
],
[
compose(
- test(/text\/plain/),
+ test(/text\/.*/),
path([ 'headers', 'content-type' ])
),
always(decodeRaw)
@@ -47,7 +65,21 @@ export const parseBody = _ => ap(
])
);
-// parseQueryString :: Request -> { k: string }
+/**
+ * ### `parseQueryString`
+ * `Request -> Record`
+ *
+ * This function takes a `Request` and return an record of the query string.
+ *
+ * ```js
+ * import { parseQueryString } from "https://deno.land/x/functional_http_server@v0.3.1/library/utilities.js";
+ *
+ * assertEquals(
+ * parseQueryString(Request({ url: '/?hoge=hoge' }, new Uint8Array([]))),
+ * { hoge: 'hoge' }
+ * );
+ * ```
+ */
export const parseQueryString = ifElse(
test(/\?/),
compose(
@@ -88,7 +120,7 @@ export const parseRequest = parsers => curry(
);
// explodeRequest :: ({ k: string }, a) -> Task Response) -> Options -> Request -> Task Response
-export const explodeRequest = parseRequest([ parseMeta, parseBody ]);
+export const explodeRequest = parseRequest([ parseMeta, _ => parseBody ]);
// factorizeMiddleware :: (Request -> Task a) -> (Request -> a -> Task Response) -> Request -> Task Response
export const factorizeMiddleware = middlewareFunction => handlerFunction =>
diff --git a/library/utilities_test.js b/library/utilities_test.js
index 44d0a27..15f43da 100644
--- a/library/utilities_test.js
+++ b/library/utilities_test.js
@@ -1,10 +1,10 @@
import { assert, assertEquals } from "https://deno.land/std@0.79.0/testing/asserts.ts"
-import { curry } from "https://x.nest.land/ramda@0.27.0/source/index.js";
+import { curry } from "https://deno.land/x/ramda@v0.27.2/mod.ts";
-import Request from "https://deno.land/x/functional_io@v1.0.0/library/Request.js";
-import Response from "https://deno.land/x/functional_io@v1.0.0/library/Response.js";
-import Task from "https://deno.land/x/functional@v1.2.1/library/Task.js";
-import { decodeRaw, encodeText, safeExtract } from "https://deno.land/x/functional@v1.2.1/library/utilities.js";
+import Request from "https://deno.land/x/functional_io@v1.1.0/library/Request.js";
+import Response from "https://deno.land/x/functional_io@v1.1.0/library/Response.js";
+import Task from "https://deno.land/x/functional@v1.3.2/library/Task.js";
+import { decodeRaw, encodeText, safeExtract } from "https://deno.land/x/functional@v1.3.2/library/utilities.js";
import {
explodeRequest,
@@ -108,12 +108,12 @@ Deno.test(
"parseBody",
() => {
assertEquals(
- parseBody({})(Request({}, new Uint8Array([]))),
+ parseBody(Request({}, new Uint8Array([]))),
{}
);
assertEquals(
- parseBody({})(
+ parseBody(
Request({ 'content-type': 'application/json' }, encodeText(JSON.stringify({ piyo: 'piyo' })))
),
{ piyo: 'piyo' }