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 @@ -Functional Core +Functional HTTP Server -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. -[![deno land](http://img.shields.io/badge/available%20on-deno.land/x-lightgrey.svg?logo=deno&labelColor=black)](https://github.com/sebastienfilion/functional-http-server@v0.3.0) -[![deno version](https://img.shields.io/badge/deno-^1.4.6-lightgrey?logo=deno)](https://github.com/denoland/deno) -[![GitHub release](https://img.shields.io/github/v/release/sebastienfilion/functional)](https://github.com/sebastienfilion/functional-http-server/releases) -[![GitHub licence](https://img.shields.io/github/license/sebastienfilion/functional)](https://github.com/sebastienfilion/functional-http-server/blob/v0.1.1/LICENSE) +[![deno land](http://img.shields.io/badge/available%20on-deno.land/x-lightgrey.svg?logo=deno&labelColor=black)](https://deno.land/x/functional_http_server@v0.3.1) +[![deno version](https://img.shields.io/badge/deno-^1.6.1-lightgrey?logo=deno)](https://github.com/denoland/deno) +[![GitHub release](https://img.shields.io/github/v/release/sebastienfilion/functional-http-server)](https://github.com/sebastienfilion/functional-http-server/releases) +[![GitHub licence](https://img.shields.io/github/license/sebastienfilion/functional-http-server)](https://github.com/sebastienfilion/functional-http-server/blob/v0.3.1/LICENSE) +[![Discord Chat](https://img.shields.io/discord/790708610023555093.svg)](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' }