├── static
└── test.data
├── .npmignore
├── .gitignore
├── bsconfig.json
├── .travis.yml
├── LICENSE
├── README.md
├── package.json
├── tests
├── reference.data
└── test.sh
├── example
└── Index.res
└── src
├── Express.resi
└── Express.res
/static/test.data:
--------------------------------------------------------------------------------
1 | This is the content of test.data a static file
2 |
--------------------------------------------------------------------------------
/.npmignore:
--------------------------------------------------------------------------------
1 | .DS_Store
2 | /node_modules/
3 | .merlin
4 | *.log
5 | /tests/
6 | /static/
7 | /example/
8 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | .DS_Store
2 | /node_modules*
3 | .merlin
4 | /lib/
5 | *.log
6 | .bsb.lock
7 | tests/test.data
8 | package-lock.json
9 | yarn.lock
10 |
--------------------------------------------------------------------------------
/bsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "bs-express",
3 | "bsc-flags": [
4 | "-bs-super-errors"
5 | ],
6 | "sources": [
7 | "src",
8 | {
9 | "dir": "example",
10 | "type": "dev"
11 | }
12 | ],
13 | "bs-dev-dependencies": [
14 | "@glennsl/bs-json"
15 | ],
16 | "refmt": 3,
17 | "warnings": {
18 | "number": "-105"
19 | }
20 | }
21 |
--------------------------------------------------------------------------------
/.travis.yml:
--------------------------------------------------------------------------------
1 | language: node_js
2 | cache: yarn
3 | node_js:
4 | - "9"
5 | script:
6 | - yarn run build
7 | - npm test
8 | deploy:
9 | skip_cleanup: true
10 | provider: npm
11 | email: nick@cuthbert.co.za
12 | api_key:
13 | secure: f1vFPrmISx58292+OUy6j6AZi9yOGIR5RoIJsBQThPnxAVgQQabvRxG0eJixt/1Q9Pl1wbL13xvrcrfbGqFgPZnkDPeY5iesgG3xJ+4TKYzo3jCNfgMQb+VW+oMeGi/DoX8xe81pjaQjYCZJHhy5IQwbfmdnI9VhPdhAhfX61hZFnNsLfDCM1W6mWIDvXXdEIVETybgiw+C5g8TBcV2Lp1i7zJj6UAohxINUyjYfgercHJfePJQwmxe6hJBenVxPA3bCGAVFvMRPSllegopf1v5NtkqJmDENLQ48dvcwTULwc6vAaQIZw87MJ9H0SnXBA512INfdWsZoeG6VjkLx604AQVRlxViGAK0HhKN1ldFsm+H4As1sff/5g/OKKzgBVIkqNa/bpZsjPiZwnbaYa1UmOMkhjjjYSMIJ1lYpW4coQX0OJLhh9YBIoBMw6q6JdnBHq//z0DhfG3H3Jk5jmUNRRkCx1pyBwrXgChi+jnSkEoWLbQ28su4ICyZ138L9OolBoXymzOiU767fzR3xN+omHkhtm///tZFXH1QKW7VTtALWg6wUoVwlj1MI0Lkq8EeMX8VoJlg9OH0aEYgpuRPXWGKmG09nm+zDRSysTrdC46pwJCuxXHFh+t3LUWDgUrVBYPl9RWNB4AyJ3g1vO4RJQIloPNjiuce3q9t6ABo=
14 | on:
15 | tags: true
16 | branch: master
17 | repo: reasonml-community/bs-express
18 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2016 Ramana Venkata
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # bs-express
2 |
3 | Express bindings for [BuckleScript](https://github.com/bloomberg/bucklescript) in [Reason](https://github.com/facebook/reason).
4 |
5 | ## Installing
6 |
7 | 1. Install `bs-express` using npm:
8 |
9 | ```
10 | npm install --save bs-express
11 | ```
12 |
13 | 2. Add bs-express as a dependency to your `bsconfig.json`:
14 |
15 | ```json
16 | {
17 | "name": "your-project",
18 | "bs-dependencies": ["bs-express"]
19 | }
20 | ```
21 |
22 | ---
23 |
24 | Right now the library is somewhat underdocumented, so please view the interface file [`Express.rei`](./src/Express.rei) or the [example folder](./example/) to see library usage.
25 |
26 | ---
27 |
28 | ## Contributing
29 |
30 | If you'd like to contribute, you can follow the instructions below to get things working locally.
31 |
32 | ### Getting Started
33 |
34 | 1. After cloning the repo, install the dependencies
35 |
36 | ```shell
37 | npm install
38 | ```
39 |
40 | 2. Build and start the example server:
41 |
42 | ```shell
43 | npm start
44 | ```
45 |
46 | ### Running the tests
47 |
48 | To run tests, run the command:
49 |
50 | ```shell
51 | npm test
52 | ```
53 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "bs-express",
3 | "version": "1.0.2",
4 | "description": "Express bindings in Reason",
5 | "main": "index.js",
6 | "dependencies": {
7 | "body-parser": "^1.19.0",
8 | "express": "^4.17.1"
9 | },
10 | "devDependencies": {
11 | "@glennsl/bs-json": "^5.0.2",
12 | "bs-platform": "^8.4.2",
13 | "husky": "^1.0.0-rc.13"
14 | },
15 | "scripts": {
16 | "build": "bsb -make-world",
17 | "start": "npm run-script build && node lib/js/example/Index.js",
18 | "watch-run": "nodemon lib/js/example/",
19 | "watch": "bsb -make-world -w",
20 | "clean": "bsb -clean-world",
21 | "test": "cd tests && ./test.sh"
22 | },
23 | "repository": {
24 | "type": "git",
25 | "url": "git+https://github.com/reasonml-community/bs-express.git"
26 | },
27 | "keywords": [
28 | "reasonml",
29 | "bucklescript",
30 | "expressjs",
31 | "web",
32 | "server",
33 | "nodejs"
34 | ],
35 | "license": "MIT",
36 | "bugs": {
37 | "url": "https://github.com/reasonml-community/bs-express/issues"
38 | },
39 | "homepage": "https://github.com/reasonml-community/bs-express#readme",
40 | "rebel": {},
41 | "husky": {
42 | "hooks": {}
43 | }
44 | }
45 |
--------------------------------------------------------------------------------
/tests/reference.data:
--------------------------------------------------------------------------------
1 |
2 | -- Root path--
3 | {"success":true}
4 | status: 200
5 |
6 | -- Invalid path--
7 |
8 |
9 |
10 |
11 | Error
12 |
13 |
14 | Cannot GET /invalid-path
15 |
16 |
17 | status: 404
18 |
19 | -- Static middleware--
20 | This is the content of test.data a static file
21 | status: 200
22 |
23 | -- POST + Query param (valid)--
24 | {"success":true}
25 | status: 200
26 |
27 | -- POST + Query param (invalid)--
28 |
29 |
30 |
31 |
32 | Error
33 |
34 |
35 | Cannot POST /999/id
36 |
37 |
38 | status: 404
39 |
40 | -- PUT + Query param (valid)--
41 | {"success":true}
42 | status: 200
43 |
44 | -- PUT + Query param (invalid)--
45 |
46 |
47 |
48 |
49 | Error
50 |
51 |
52 | Cannot PUT /999/id
53 |
54 |
55 | status: 404
56 |
57 | -- PUT + Query param (valid)--
58 | {"success":true}
59 | status: 200
60 |
61 | -- PUT + Query param (invalid)--
62 |
63 |
64 |
65 |
66 | Error
67 |
68 |
69 | Cannot PATCH /999/id
70 |
71 |
72 | status: 404
73 |
74 | -- DELETE + Query param (valid)--
75 | {"success":true}
76 | status: 200
77 |
78 | -- DELETE + Query param (invalid)--
79 |
80 |
81 |
82 |
83 | Error
84 |
85 |
86 | Cannot DELETE /999/id
87 |
88 |
89 | status: 404
90 |
91 | -- baseUrl property--
92 | {"success":true}
93 | status: 200
94 |
95 | -- hostname property--
96 | {"success":true}
97 | status: 200
98 |
99 | -- method property--
100 | {"success":true}
101 | status: 200
102 |
103 | -- method originalUrl--
104 | {"success":true}
105 | status: 200
106 |
107 | -- method path--
108 | {"success":true}
109 | status: 200
110 |
111 | -- method path--
112 | {"success":true}
113 | status: 200
114 |
115 | -- Query parameters--
116 | {"success":true}
117 | status: 200
118 |
119 | -- Fresh--
120 | {"success":true}
121 | status: 200
122 |
123 | -- Stale--
124 | {"success":true}
125 | status: 200
126 |
127 | -- Secure--
128 | {"success":true}
129 | status: 200
130 |
131 | -- XHR--
132 | {"success":true}
133 | status: 200
134 |
135 | -- Redirect--
136 | Found. Redirecting to /redir/target
137 | status: 302
138 |
139 | -- Redirect with Code--
140 | Moved Permanently. Redirecting to /redir/target
141 | status: 301
142 |
143 | -- Non 200 Http status--
144 | Not Found
145 | status: 404
146 |
147 | -- Non 200 Http status--
148 | {"success":true}
149 | status: 500
150 |
151 | -- Promise Middleware--
152 | status: 204
153 |
154 | -- Failing Promise Middleware--
155 | Caught Failing Promise
156 | status: 500
157 |
158 | -- Can catch Ocaml Exception--
159 | Elvis has left the building!
160 | status: 402
161 |
162 | -- Can use express router--
163 | Created
164 | status: 201
165 |
166 | -- Can specify that a router behaves in a case sensitive manner--
167 |
168 |
169 |
170 |
171 | Error
172 |
173 |
174 | Cannot GET /router-options/case-sensitive
175 |
176 |
177 | status: 404
178 |
179 | -- Can specify that a router behaves in a case sensitive manner--
180 | OK
181 | status: 200
182 |
183 | -- Can specify that a router behaves in a strict manner--
184 |
185 |
186 |
187 |
188 | Error
189 |
190 |
191 | Cannot GET /router-options/strict
192 |
193 |
194 | status: 404
195 |
196 | -- Can specify that a router behaves in a strict manner--
197 | OK
198 | status: 200
199 |
200 | -- Can bind middleware to a particular param--
201 | Created
202 | status: 201
203 |
204 | -- Can set cookies--
205 | OK
206 | status: 200# Netscape HTTP Cookie File
207 | localhost FALSE / FALSE 0 test-cookie cool-cookie
208 |
209 | -- Can clear cookies--
210 | Set-Cookie: test-cookie2=; Path=/; Expires=Thu, 01 Jan 1970 00:00:00 GMT
211 |
212 | -- Can accept JSON using builtin middleware--
213 | {"number":8}
214 | status: 200
215 | -- Can accept UrlEncoded body using builtin middleware--
216 | {"number":8}
217 | status: 200
218 | -- Accepts--
219 | {"success":true}
220 | -- Accepts Charsets--
221 | {"success":true}
222 | -- Get--
223 | {"success":true}
224 | -- Can parse text using bodyparser middleware--
225 | This is a test body
226 | -- Can set response header via setHeader--
227 | X-Test-Header: Set
228 |
229 | -- Can the user user the javascipt http object directly--
230 | The server has been called 44 times.
--------------------------------------------------------------------------------
/tests/test.sh:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env bash
2 |
3 | # Unit test to run again an Express server defined in examples/Index.re
4 | #
5 | # This test uses 'curl' to query the web server and output the
6 | # HTTP response in 'test.data'.
7 | #
8 | # It then makes a diff with the expected data stored in 'reference.data'
9 |
10 | TEST_DATA=test.data
11 |
12 | clean_previous_test() {
13 | rm -f $TEST_DATA
14 | }
15 |
16 | function print_test_title() {
17 | echo >> $TEST_DATA
18 | echo "-- $1--" >> $TEST_DATA
19 | }
20 |
21 | print_test_url() {
22 | # --cookie-jar outputs a comment header that differs between curl versions,
23 | # so use grep to filter it out
24 | curl --cookie-jar - -X $1 -w "\nstatus: %{http_code}" http://localhost:3000$2 | grep "^[^#]" 2>&1 >> $TEST_DATA
25 | }
26 |
27 | run_test() {
28 | print_test_title "$1"
29 | print_test_url "$2" "$3" "$4"
30 | }
31 |
32 | # Run test server in background and save PID
33 | cd ..
34 | node lib/js/example/Index.js &
35 | TEST_SERVER_PID=$!
36 | cd tests
37 |
38 | # Ugly hack to wait for the server to start
39 | sleep 2s
40 |
41 | clean_previous_test;
42 |
43 | run_test 'Root path' 'GET' '/'
44 | run_test 'Invalid path' 'GET' '/invalid-path'
45 | run_test 'Static middleware' 'GET' '/static/test.data'
46 | run_test 'POST + Query param (valid)' 'POST' '/123/id'
47 | run_test 'POST + Query param (invalid)' 'POST' '/999/id'
48 | run_test 'PUT + Query param (valid)' 'PUT' '/123/id'
49 | run_test 'PUT + Query param (invalid)' 'PUT' '/999/id'
50 | run_test 'PUT + Query param (valid)' 'PATCH' '/123/id'
51 | run_test 'PUT + Query param (invalid)' 'PATCH' '/999/id'
52 | run_test 'DELETE + Query param (valid)' 'DELETE' '/123/id'
53 | run_test 'DELETE + Query param (invalid)' 'DELETE' '/999/id'
54 | run_test 'baseUrl property' 'GET' '/baseUrl'
55 | run_test 'hostname property' 'GET' '/hostname'
56 | # run_test 'ip property' 'GET' '/ip'
57 | run_test 'method property' 'GET' '/method'
58 | run_test 'method originalUrl' 'GET' '/originalUrl'
59 | run_test 'method path' 'GET' '/path'
60 | run_test 'method path' 'GET' '/protocol'
61 | run_test 'Query parameters' 'GET' '/query?key=value'
62 | run_test 'Fresh' 'GET' '/fresh'
63 | run_test 'Stale' 'GET' '/stale'
64 | run_test 'Secure' 'GET' '/secure'
65 | run_test 'XHR' 'GET' '/xhr'
66 | run_test 'Redirect' 'GET' '/redir'
67 | run_test 'Redirect with Code' 'GET' '/redircode'
68 | run_test 'Non 200 Http status' 'GET' '/not-found'
69 | run_test 'Non 200 Http status' 'GET' '/error'
70 | run_test 'Promise Middleware' 'GET' '/promise'
71 | run_test 'Failing Promise Middleware' 'GET' '/failing-promise'
72 | run_test 'Can catch Ocaml Exception' 'GET' '/ocaml-exception'
73 | run_test 'Can use express router' 'GET' '/testing/testing/123'
74 | run_test 'Can specify that a router behaves in a case sensitive manner' 'GET' '/router-options/case-sensitive'
75 | run_test 'Can specify that a router behaves in a case sensitive manner' 'GET' '/router-options/Case-sensitive'
76 | run_test 'Can specify that a router behaves in a strict manner' 'GET' '/router-options/strict'
77 | run_test 'Can specify that a router behaves in a strict manner' 'GET' '/router-options/strict/'
78 | run_test 'Can bind middleware to a particular param' 'GET' '/param-test/123'
79 | run_test 'Can set cookies' 'GET' '/cookie-set-test'
80 |
81 | run_cookie_clear_test() {
82 | print_test_title "$1"
83 | curl -i -X $2 -w "\nstatus: %{http_code}\n" http://localhost:3000$3 2>&1 | grep -Fi Set-Cookie >> $TEST_DATA
84 | # curl -X $2 -w "\nstatus: %{http_code}\n" http://localhost:3000$3 2>&1 >> $TEST_DATA
85 | }
86 |
87 | run_cookie_clear_test 'Can clear cookies' 'GET' '/cookie-clear-test'
88 |
89 |
90 | run_json_test() {
91 | print_test_title "$1"
92 | curl -X POST -H "Content-Type: application/json" -w "\nstatus: %{http_code}" -d "$3" http://localhost:3000$2 2>&1 >> $TEST_DATA
93 | }
94 |
95 | run_json_test 'Can accept JSON using builtin middleware' '/builtin-middleware/json-doubler' '{ "number": 4 }'
96 |
97 | run_urlencoded_test() {
98 | print_test_title "$1"
99 | curl -X POST -H "Content-Type: application/x-www-form-urlencoded" -w "\nstatus: %{http_code}" -d "$3" http://localhost:3000$2 2>&1 >> $TEST_DATA
100 | }
101 |
102 | run_urlencoded_test 'Can accept UrlEncoded body using builtin middleware' '/builtin-middleware/urlencoded-doubler' 'number=4'
103 |
104 | run_header_test() {
105 | print_test_title "$1"
106 | curl -X "$2" -H "$3" http://localhost:3000$4 2>&1 >> $TEST_DATA
107 | }
108 |
109 | run_header_test 'Accepts' 'GET' 'Accept: audio/*; q=0.2, audio/basic' \
110 | '/accepts'
111 |
112 | run_header_test 'Accepts Charsets' 'GET' 'Accept-Charset: UTF-8' \
113 | '/accepts-charsets'
114 |
115 | run_header_test 'Get' 'GET' 'key: value' \
116 | '/get'
117 |
118 | run_text_test() {
119 | print_test_title "$1"
120 | curl -X POST -H "Content-Type: text/plain" -d "$3" http://localhost:3000$2 2>&1 >> $TEST_DATA
121 | }
122 |
123 | run_text_test "Can parse text using bodyparser middleware" "/router4/text-body" 'This is a test body'
124 |
125 | run_response_header_test() {
126 | print_test_title "$1"
127 | curl -i -X $2 -w "\nstatus: %{http_code}\n" http://localhost:3000$3 2>&1 | grep -Fi X-Test-Header >> $TEST_DATA
128 | }
129 |
130 | run_response_header_test 'Can set response header via setHeader' 'GET' '/response-set-header'
131 |
132 | run_text_test "Can the user user the javascipt http object directly" "/get-request-count"
133 |
134 | # Stop server
135 | kill $TEST_SERVER_PID
136 |
137 | # compare test output to reference data
138 |
139 | REFERENCE_DATA=reference.data
140 | diff $TEST_DATA $REFERENCE_DATA
141 |
--------------------------------------------------------------------------------
/example/Index.res:
--------------------------------------------------------------------------------
1 | open Express
2 |
3 | /* The tests below relies upon the ability to store in the Request
4 | objects abritrary JSON properties.
5 |
6 | Each middleware will both check that previous middleware
7 | have been called by making properties exists in the Request object and
8 | upon success will themselves adds another property to the Request.
9 |
10 | */
11 | /* [checkProperty req next property k] makes sure [property] is
12 | present in [req]. If success then [k()] is invoked, otherwise
13 | [Next.route] is called with next */
14 | let checkProperty = (req, next, property, k, res) => {
15 | let reqData = Request.asJsonObject(req)
16 | switch Js.Dict.get(reqData, property) {
17 | | None => next(Next.route, res)
18 | | Some(x) =>
19 | switch Js.Json.decodeBoolean(x) {
20 | | Some(b) when b => k(res)
21 | | _ => next(Next.route, res)
22 | }
23 | }
24 | }
25 |
26 | /* same as [checkProperty] but with a list of properties */
27 | let checkProperties = (req, next, properties, k, res) => {
28 | let rec aux = properties =>
29 | switch properties {
30 | | list{} => k(res)
31 | | list{p, ...tl} => checkProperty(req, next, p, _ => aux(tl), res)
32 | }
33 | aux(properties)
34 | }
35 |
36 | /* [setProperty req property] sets the [property] in the [req] Request
37 | value */
38 | let setProperty = (req, property, res) => {
39 | let reqData = Request.asJsonObject(req)
40 | Js.Dict.set(reqData, property, Js.Json.boolean(true))
41 | res
42 | }
43 |
44 | /* return the string value for [key], None if the key is not in [dict]
45 | TODO once BOption.map is released */
46 | let getDictString = (dict, key) =>
47 | switch Js.Dict.get(dict, key) {
48 | | Some(json) => Js.Json.decodeString(json)
49 | | _ => None
50 | }
51 |
52 | /* make a common JSON object representing success */
53 | let makeSuccessJson = () => {
54 | let json = Js.Dict.empty()
55 | Js.Dict.set(json, "success", Js.Json.boolean(true))
56 | Js.Json.object_(json)
57 | }
58 |
59 | let app = express()
60 | /*
61 | If you would like to set view engine
62 | App.set(app, "view engine", "pug");
63 | */
64 |
65 | App.disable(app, ~name="x-powered-by")
66 |
67 | App.useOnPath(
68 | app,
69 | ~path="/",
70 | Middleware.from((next, req, res) =>
71 | res |> setProperty(req, "middleware0") |> next(Next.middleware)
72 | ),
73 | ) /* call the next middleware in the processing pipeline */
74 |
75 | App.useWithMany(
76 | app,
77 | [
78 | Middleware.from((next, req) =>
79 | checkProperty(req, next, "middleware0", res =>
80 | res |> setProperty(req, "middleware1") |> next(Next.middleware)
81 | )
82 | ),
83 | Middleware.from((next, req) =>
84 | checkProperties(req, next, list{"middleware0", "middleware1"}, res =>
85 | next(Next.middleware, setProperty(req, "middleware2", res))
86 | )
87 | ),
88 | ],
89 | )
90 |
91 | App.get(
92 | app,
93 | ~path="/",
94 | Middleware.from((next, req) => {
95 | let previousMiddlewares = list{"middleware0", "middleware1", "middleware2"}
96 | checkProperties(req, next, previousMiddlewares, Response.sendJson(_, makeSuccessJson()))
97 | }),
98 | )
99 |
100 | App.useOnPath(
101 | app,
102 | ~path="/static",
103 | {
104 | let options = Static.defaultOptions()
105 | Static.make("static", options) |> Static.asMiddleware
106 | },
107 | )
108 |
109 | App.postWithMany(
110 | app,
111 | ~path="/:id/id",
112 | [
113 | Middleware.from((next, req) => {
114 | let previousMiddlewares = list{"middleware0", "middleware1", "middleware2"}
115 | checkProperties(req, next, previousMiddlewares, res =>
116 | switch getDictString(Request.params(req), "id") {
117 | | Some("123") => Response.sendJson(res, makeSuccessJson())
118 | | _ => next(Next.route, res)
119 | }
120 | )
121 | }),
122 | ],
123 | )
124 |
125 | App.patchWithMany(
126 | app,
127 | ~path="/:id/id",
128 | [
129 | Middleware.from((next, req) => {
130 | let previousMiddlewares = list{"middleware0", "middleware1", "middleware2"}
131 | checkProperties(req, next, previousMiddlewares, res =>
132 | switch getDictString(Request.params(req), "id") {
133 | | Some("123") => Response.sendJson(res, makeSuccessJson())
134 | | _ => next(Next.route, res)
135 | }
136 | )
137 | }),
138 | ],
139 | )
140 |
141 | App.putWithMany(
142 | app,
143 | ~path="/:id/id",
144 | [
145 | Middleware.from((next, req) => {
146 | let previousMiddlewares = list{"middleware0", "middleware1", "middleware2"}
147 | checkProperties(req, next, previousMiddlewares, res =>
148 | switch getDictString(Request.params(req), "id") {
149 | | Some("123") => Response.sendJson(res, makeSuccessJson())
150 | | _ => next(Next.route, res)
151 | }
152 | )
153 | }),
154 | ],
155 | )
156 |
157 | App.deleteWithMany(
158 | app,
159 | ~path="/:id/id",
160 | [
161 | Middleware.from((next, req) => {
162 | let previousMiddlewares = list{"middleware0", "middleware1", "middleware2"}
163 | checkProperties(req, next, previousMiddlewares, res =>
164 | switch getDictString(Request.params(req), "id") {
165 | | Some("123") => Response.sendJson(res, makeSuccessJson())
166 | | _ => next(Next.route, res)
167 | }
168 | )
169 | }),
170 | ],
171 | )
172 |
173 | /* If you have set up view engine, you can uncomment this "get"
174 | Middleware.from((_, _) =>{
175 | let dict: Js.Dict.t(string) = Js.Dict.empty();
176 | Response.render("index", dict, ());
177 | });
178 | */
179 |
180 | App.get(
181 | app,
182 | ~path="/baseUrl",
183 | Middleware.from((next, req) =>
184 | switch Request.baseUrl(req) {
185 | | "" => Response.sendJson(_, makeSuccessJson())
186 | | _ => next(Next.route)
187 | }
188 | ),
189 | )
190 |
191 | App.get(
192 | app,
193 | ~path="/hostname",
194 | Middleware.from((next, req) =>
195 | switch Request.hostname(req) {
196 | | "localhost" => Response.sendJson(_, makeSuccessJson())
197 | | _ => next(Next.route)
198 | }
199 | ),
200 | )
201 |
202 | App.get(
203 | app,
204 | ~path="/ip",
205 | Middleware.from((next, req) =>
206 | switch Request.ip(req) {
207 | | "127.0.0.1" => Response.sendJson(_, makeSuccessJson())
208 | | s =>
209 | Js.log(s)
210 | next(Next.route)
211 | /* TODO why is it printing ::1 */
212 | }
213 | ),
214 | )
215 |
216 | App.get(
217 | app,
218 | ~path="/method",
219 | Middleware.from((next, req) =>
220 | switch Request.httpMethod(req) {
221 | | Request.Get => Response.sendJson(_, makeSuccessJson())
222 | | s =>
223 | Js.log(s)
224 | next(Next.route)
225 | }
226 | ),
227 | )
228 |
229 | App.get(
230 | app,
231 | ~path="/originalUrl",
232 | Middleware.from((next, req) =>
233 | switch Request.originalUrl(req) {
234 | | "/originalUrl" => Response.sendJson(_, makeSuccessJson())
235 | | s =>
236 | Js.log(s)
237 | next(Next.route)
238 | }
239 | ),
240 | )
241 |
242 | App.get(
243 | app,
244 | ~path="/path",
245 | Middleware.from((next, req) =>
246 | switch Request.path(req) {
247 | | "/path" => Response.sendJson(_, makeSuccessJson())
248 | | s =>
249 | Js.log(s)
250 | next(Next.route)
251 | }
252 | ),
253 | )
254 |
255 | App.get(
256 | app,
257 | ~path="/protocol",
258 | Middleware.from((next, req) =>
259 | switch Request.protocol(req) {
260 | | Request.Http => Response.sendJson(_, makeSuccessJson())
261 | | s =>
262 | Js.log(s)
263 | next(Next.route)
264 | }
265 | ),
266 | )
267 |
268 | App.get(
269 | app,
270 | ~path="/query",
271 | Middleware.from((next, req) =>
272 | switch getDictString(Request.query(req), "key") {
273 | | Some("value") => Response.sendJson(_, makeSuccessJson())
274 | | _ => next(Next.route)
275 | }
276 | ),
277 | )
278 |
279 | App.get(
280 | app,
281 | ~path="/not-found",
282 | Middleware.from((_, _) => Response.sendStatus(_, Response.StatusCode.NotFound)),
283 | )
284 |
285 | App.get(
286 | app,
287 | ~path="/error",
288 | Middleware.from((_, _, res) =>
289 | res
290 | ->Response.status(Response.StatusCode.InternalServerError)
291 | ->Response.sendJson(makeSuccessJson())
292 | ),
293 | )
294 |
295 | App.getWithMany(
296 | app,
297 | ~path="/accepts",
298 | [
299 | Middleware.from((next, req) =>
300 | switch Request.accepts(req, ["audio/whatever", "audio/basic"]) {
301 | | Some("audio/basic") => next(Next.middleware)
302 | | _ => next(Next.route)
303 | }
304 | ),
305 | Middleware.from((next, req) =>
306 | switch Request.accepts(req, ["text/css"]) {
307 | | None => Response.sendJson(_, makeSuccessJson())
308 | | _ => next(Next.route)
309 | }
310 | ),
311 | ],
312 | )
313 |
314 | let \">>" = (f, g, x) => x |> f |> g
315 |
316 | App.getWithMany(
317 | app,
318 | ~path="/accepts-charsets",
319 | [
320 | Middleware.from((next, req) =>
321 | switch Request.acceptsCharsets(req, ["UTF-8", "UTF-16"]) {
322 | | Some("UTF-8") => next(Next.middleware)
323 | | _ => next(Next.route)
324 | }
325 | ),
326 | Middleware.from((next, req) =>
327 | switch Request.acceptsCharsets(req, ["UTF-16"]) {
328 | | None => Response.sendJson(_, makeSuccessJson())
329 | | _ => next(Next.route)
330 | }
331 | ),
332 | ],
333 | )
334 |
335 | App.get(
336 | app,
337 | ~path="/get",
338 | Middleware.from((next, req) =>
339 | switch Request.get(req, "key") {
340 | | Some("value") => Response.sendJson(_, makeSuccessJson())
341 | | _ => next(Next.route)
342 | }
343 | ),
344 | )
345 |
346 | App.get(
347 | app,
348 | ~path="/fresh",
349 | Middleware.from((next, req) =>
350 | if !Request.fresh(req) {
351 | Response.sendJson(_, makeSuccessJson())
352 | } else {
353 | next(Next.route)
354 | }
355 | ),
356 | )
357 |
358 | App.get(
359 | app,
360 | ~path="/stale",
361 | Middleware.from((next, req) =>
362 | if Request.stale(req) {
363 | Response.sendJson(_, makeSuccessJson())
364 | } else {
365 | next(Next.route)
366 | }
367 | ),
368 | )
369 |
370 | App.get(
371 | app,
372 | ~path="/secure",
373 | Middleware.from((next, req) =>
374 | if !Request.secure(req) {
375 | Response.sendJson(_, makeSuccessJson())
376 | } else {
377 | next(Next.route)
378 | }
379 | ),
380 | )
381 |
382 | App.get(
383 | app,
384 | ~path="/xhr",
385 | Middleware.from((next, req) =>
386 | if !Request.xhr(req) {
387 | Response.sendJson(_, makeSuccessJson())
388 | } else {
389 | next(Next.route)
390 | }
391 | ),
392 | )
393 |
394 | App.get(app, ~path="/redir", Middleware.from((_, _) => Response.redirect(_, "/redir/target")))
395 |
396 | App.get(
397 | app,
398 | ~path="/redircode",
399 | Middleware.from((_, _) => Response.redirectCode(_, 301, "/redir/target")),
400 | )
401 |
402 | App.getWithMany(
403 | app,
404 | ~path="/ocaml-exception",
405 | [
406 | Middleware.from((_, _, _next) => raise(Failure("Elvis has left the building!"))),
407 | Middleware.fromError((_, err, _, res) =>
408 | switch err {
409 | | Failure(f) =>
410 | res->Response.status(Response.StatusCode.PaymentRequired)->Response.sendString(f)
411 | | _ => res->Response.sendStatus(Response.StatusCode.NotFound)
412 | }
413 | ),
414 | ],
415 | )
416 |
417 | App.get(
418 | app,
419 | ~path="/promise",
420 | PromiseMiddleware.from((_req, _next, res) =>
421 | res->Response.sendStatus(Response.StatusCode.NoContent)->Js.Promise.resolve
422 | ),
423 | )
424 |
425 | App.getWithMany(
426 | app,
427 | ~path="/failing-promise",
428 | [
429 | PromiseMiddleware.from((_, _, _next) => Js.Promise.reject(Not_found)),
430 | PromiseMiddleware.fromError((_, _req, _next, res) =>
431 | res
432 | ->Response.status(Response.StatusCode.InternalServerError)
433 | ->Response.sendString("Caught Failing Promise")
434 | ->Js.Promise.resolve
435 | ),
436 | ],
437 | )
438 |
439 | let router1 = router()
440 |
441 | Router.get(router1, ~path="/123", Middleware.from((_, _) => Response.sendStatus(_, Created)))
442 |
443 | App.useRouterOnPath(app, ~path="/testing/testing", router1)
444 |
445 | let router2 = router(~caseSensitive=true, ~strict=true, ())
446 |
447 | Router.get(router2, ~path="/Case-sensitive", Middleware.from((_, _) => Response.sendStatus(_, Ok)))
448 |
449 | Router.get(router2, ~path="/strict/", Middleware.from((_, _) => Response.sendStatus(_, Ok)))
450 |
451 | App.useRouterOnPath(app, ~path="/router-options", router2)
452 |
453 | App.param(
454 | app,
455 | ~name="identifier",
456 | Middleware.from((_next, _req) => Response.sendStatus(_, Created)),
457 | )
458 |
459 | App.get(
460 | app,
461 | ~path="/param-test/:identifier",
462 | Middleware.from((_next, _req) => Response.sendStatus(_, BadRequest)),
463 | )
464 |
465 | App.get(
466 | app,
467 | ~path="/cookie-set-test",
468 | Middleware.from((_next, _req, res) =>
469 | res
470 | ->Response.cookie(~name="test-cookie", Js.Json.string("cool-cookie"))
471 | ->Response.sendStatus(Ok)
472 | ),
473 | )
474 |
475 | App.get(
476 | app,
477 | ~path="/cookie-clear-test",
478 | Middleware.from((_next, _req, res) =>
479 | res->Response.clearCookie(~name="test-cookie2", ())->Response.sendStatus(Ok)
480 | ),
481 | )
482 |
483 | App.get(
484 | app,
485 | ~path="/response-set-header",
486 | Middleware.from((_, _, res) =>
487 | res->Response.setHeader("X-Test-Header", "Set")->Response.sendStatus(Response.StatusCode.Ok)
488 | ),
489 | )
490 |
491 | let router3 = router(~caseSensitive=true, ~strict=true, ())
492 |
493 | open ByteLimit
494 |
495 | Router.use(router3, Middleware.json(~limit=5.0 |> mb, ()))
496 |
497 | Router.use(router3, Middleware.urlencoded(~extended=true, ()))
498 |
499 | module Body = {
500 | type payload = {"number": int}
501 | let jsonDecoder = json => {
502 | open Json.Decode
503 | {"number": json |> field("number", int)}
504 | }
505 | let urlEncodedDecoder = dict =>
506 | {
507 | "number": Js.Dict.unsafeGet(dict, "number") |> int_of_string,
508 | }
509 | let encoder = body => {
510 | open Json.Encode
511 | object_(list{("number", body["number"] |> int)})
512 | }
513 | }
514 |
515 | let raiseIfNone = x =>
516 | switch x {
517 | | Some(value) => value
518 | | None => Js.Exn.raiseError("Body is none")
519 | }
520 |
521 | Router.post(
522 | router3,
523 | ~path="/json-doubler",
524 | Middleware.from((_next, req, res) => {
525 | let json = req->Request.bodyJSON->raiseIfNone->Body.jsonDecoder
526 | Response.sendJson(res, {"number": json["number"] * 2}->Body.encoder)
527 | }),
528 | )
529 |
530 | Router.post(
531 | router3,
532 | ~path="/urlencoded-doubler",
533 | Middleware.from((_next, req, res) => {
534 | let decoded = req->Request.bodyURLEncoded->raiseIfNone->Body.urlEncodedDecoder
535 | Response.sendJson(res, {"number": decoded["number"] * 2}->Body.encoder)
536 | }),
537 | )
538 |
539 | App.useRouterOnPath(app, ~path="/builtin-middleware", router3)
540 |
541 | let router4 = router(~caseSensitive=true, ~strict=true, ())
542 |
543 | Router.use(router4, Middleware.text())
544 |
545 | Router.post(
546 | router4,
547 | ~path="/text-body",
548 | Middleware.from((_next, req, res) =>
549 | Response.sendString(res, Request.bodyText(req)->raiseIfNone)
550 | ),
551 | )
552 |
553 | App.useRouterOnPath(app, ~path="/router4", router4)
554 |
555 | let onListen = e =>
556 | switch e {
557 | | exception Js.Exn.Error(e) => {
558 | Js.log(e)
559 | Node.Process.exit(1)
560 | }
561 | | _ => Js.log("Listening at http://127.0.0.1:3000")
562 | }
563 |
564 | let server = App.listen(app, ~port=3000, ~onListen, ())
565 |
566 | let countRequests = server => {
567 | let count = ref(0)
568 | HttpServer.on(server, #request((_, _) => count := count.contents + 1))
569 | () => {
570 | let result = count.contents
571 | count := -1
572 | result
573 | }
574 | }
575 |
576 | let getRequestsCount = countRequests(server)
577 |
578 | App.post(
579 | app,
580 | ~path="/get-request-count",
581 | Middleware.from((_, _, res) =>
582 | Response.sendString(
583 | res,
584 | "The server has been called " ++ (string_of_int(getRequestsCount()) ++ " times."),
585 | )
586 | ),
587 | )
588 |
589 | /* Other examples are
590 | App.listen app ();
591 | App.listen app port::1000 ();
592 | App.listen app port::1000 onListen::(fun e => Js.log e) ();
593 | */
594 | /* -- Test the server --
595 | npm run start && cd tests && ./test.sh
596 | */
597 |
--------------------------------------------------------------------------------
/src/Express.resi:
--------------------------------------------------------------------------------
1 | @ocaml.doc(
2 | "abstract type which ensure middleware function must either
3 | call the [next] function or one of the [send] function on the
4 | response object.
5 |
6 | This should be a great argument for OCaml, the type system
7 | prevents silly error which in this case would make the server hang"
8 | )
9 | type complete
10 |
11 | module Error: {
12 | @ocaml.doc("Error type")
13 | type t = exn
14 |
15 | @bs.send @bs.return(null_undefined_to_opt)
16 | external message: Js_exn.t => option = "message"
17 | @bs.send @bs.return(null_undefined_to_opt)
18 | external name: Js_exn.t => option = "name"
19 | }
20 |
21 | module Request: {
22 | type t
23 | type params = Js.Dict.t
24 |
25 | @ocaml.doc("[params request] return the JSON object filled with the
26 | request parameters")
27 | @bs.get
28 | external params: t => params = "params"
29 |
30 | @ocaml.doc(
31 | "[asJsonObject request] casts a [request] to a JSON object. It is
32 | common in Express application to use the Request object as a
33 | placeholder to maintain state through the various middleware which
34 | are executed."
35 | )
36 | external asJsonObject: t => Js.Dict.t = "%identity"
37 |
38 | @ocaml.doc("[baseUrl request] returns the 'baseUrl' property") @bs.get
39 | external baseUrl: t => string = "baseUrl"
40 |
41 | @ocaml.doc(
42 | "When using the json body-parser middleware and receiving a request with a
43 | content type of \"application/json\", this property is a Js.Json.t that
44 | contains the body sent by the request."
45 | )
46 | @bs.get
47 | @bs.return(null_undefined_to_opt)
48 | external bodyJSON: t => option = "body"
49 |
50 | @ocaml.doc(
51 | "When using the raw body-parser middleware and receiving a request with a
52 | content type of \"application/octet-stream\", this property is a
53 | Node_buffer.t that contains the body sent by the request."
54 | )
55 | @bs.get
56 | @bs.return(null_undefined_to_opt)
57 | external bodyRaw: t => option = "body"
58 |
59 | @ocaml.doc(
60 | "When using the text body-parser middleware and receiving a request with a
61 | content type of \"text/plain\", this property is a string that
62 | contains the body sent by the request."
63 | )
64 | let bodyText: t => option
65 |
66 | @ocaml.doc(
67 | "When using the urlencoded body-parser middleware and receiving a request
68 | with a content type of \"application/x-www-form-urlencoded\", this property
69 | is a Js.Dict.t string that contains the body sent by the request."
70 | )
71 | let bodyURLEncoded: t => option>
72 |
73 | @ocaml.doc(
74 | "When using cookie-parser middleware, this property is an object
75 | that contains cookies sent by the request. If the request contains
76 | no cookies, it defaults to {}."
77 | )
78 | @bs.get
79 | @bs.return(null_undefined_to_opt)
80 | external cookies: t => option> = "cookies"
81 |
82 | @ocaml.doc(
83 | "When using cookie-parser middleware, this property contains signed cookies
84 | sent by the request, unsigned and ready for use. Signed cookies reside in
85 | a different object to show developer intent; otherwise, a malicious attack
86 | could be placed on req.cookie values (which are easy to spoof).
87 | Note that signing a cookie does not make it “hidden” or encrypted;
88 | but simply prevents tampering (because the secret used to
89 | sign is private)."
90 | )
91 | @bs.get
92 | @bs.return(null_undefined_to_opt)
93 | external signedCookies: t => option> = "signedCookies"
94 |
95 | @ocaml.doc("[hostname request] Contains the hostname derived from the Host HTTP header.") @bs.get
96 | external hostname: t => string = "hostname"
97 |
98 | @ocaml.doc("[ip request] Contains the remote IP address of the request.") @bs.get
99 | external ip: t => string = "ip"
100 |
101 | @ocaml.doc("[fresh request] returns [true] whether the request is \"fresh\"") @bs.get
102 | external fresh: t => bool = "fresh"
103 |
104 | @ocaml.doc("[stale request] returns [true] whether the request is \"stale\"") @bs.get
105 | external stale: t => bool = "stale"
106 |
107 | @ocaml.doc(
108 | "[method_ request] return a string corresponding to the HTTP
109 | method of the request: GET, POST, PUT, and so on"
110 | )
111 | @bs.get
112 | external methodRaw: t => string = "method"
113 | type httpMethod =
114 | | Get
115 | | Post
116 | | Put
117 | | Delete
118 | | Head
119 | | Options
120 | | Trace
121 | | Connect
122 | | Patch
123 |
124 | @ocaml.doc(
125 | "[method_ request] return a variant corresponding to the HTTP
126 | method of the request: Get, Post, Put, and so on"
127 | )
128 | let httpMethod: t => httpMethod
129 |
130 | @ocaml.doc(
131 | "[originalUrl request] returns the original url. See
132 | https://expressjs.com/en/4x/api.html#req.originalUrl"
133 | )
134 | @bs.get
135 | external originalUrl: t => string = "originalUrl"
136 |
137 | @ocaml.doc("[path request] returns the path part of the request URL.") @bs.get
138 | external path: t => string = "path"
139 | type protocol =
140 | | Http
141 | | Https
142 |
143 | @ocaml.doc(
144 | "[protocol request] returns the request protocol string: either http
145 | or (for TLS requests) https."
146 | )
147 | let protocol: t => protocol
148 |
149 | @ocaml.doc("[secure request] returns [true] if a TLS connection is established") @bs.get
150 | external secure: t => bool = "secure"
151 |
152 | @ocaml.doc(
153 | "[query request] returns an object containing a property for each
154 | query string parameter in the route. If there is no query string,
155 | it returns the empty object, {}"
156 | )
157 | @bs.get
158 | external query: t => Js.Dict.t = "query"
159 |
160 | @ocaml.doc(
161 | "[acceptsRaw accepts types] checks if the specified content types
162 | are acceptable, based on the request's Accept HTTP header field.
163 | The method returns the best match, or if none of the specified
164 | content types is acceptable, returns [false]"
165 | )
166 | let accepts: (t, array) => option
167 | let acceptsCharsets: (t, array) => option
168 |
169 | @ocaml.doc(
170 | "[get return field] returns the specified HTTP request header
171 | field (case-insensitive match)"
172 | )
173 | @bs.send
174 | @bs.return(null_undefined_to_opt)
175 | external get: (t, string) => option = "get"
176 |
177 | @ocaml.doc(
178 | "[xhr request] returns [true] if the request’s X-Requested-With
179 | header field is \"XMLHttpRequest\", indicating that the request was
180 | issued by a client library such as jQuery"
181 | )
182 | @bs.get
183 | external xhr: t => bool = "xhr"
184 | }
185 |
186 | module Response: {
187 | type t
188 | module StatusCode: {
189 | @bs.deriving(jsConverter)
190 | type t =
191 | | Ok
192 | | Created
193 | | Accepted
194 | | NonAuthoritativeInformation
195 | | NoContent
196 | | ResetContent
197 | | PartialContent
198 | | MultiStatus
199 | | AleadyReported
200 | | IMUsed
201 | | MultipleChoices
202 | | MovedPermanently
203 | | Found
204 | | SeeOther
205 | | NotModified
206 | | UseProxy
207 | | SwitchProxy
208 | | TemporaryRedirect
209 | | PermanentRedirect
210 | | BadRequest
211 | | Unauthorized
212 | | PaymentRequired
213 | | Forbidden
214 | | NotFound
215 | | MethodNotAllowed
216 | | NotAcceptable
217 | | ProxyAuthenticationRequired
218 | | RequestTimeout
219 | | Conflict
220 | | Gone
221 | | LengthRequired
222 | | PreconditionFailed
223 | | PayloadTooLarge
224 | | UriTooLong
225 | | UnsupportedMediaType
226 | | RangeNotSatisfiable
227 | | ExpectationFailed
228 | | ImATeapot
229 | | MisdirectedRequest
230 | | UnprocessableEntity
231 | | Locked
232 | | FailedDependency
233 | | UpgradeRequired
234 | | PreconditionRequired
235 | | TooManyRequests
236 | | RequestHeaderFieldsTooLarge
237 | | UnavailableForLegalReasons
238 | | InternalServerError
239 | | NotImplemented
240 | | BadGateway
241 | | ServiceUnavailable
242 | | GatewayTimeout
243 | | HttpVersionNotSupported
244 | | VariantAlsoNegotiates
245 | | InsufficientStorage
246 | | LoopDetected
247 | | NotExtended
248 | | NetworkAuthenticationRequired
249 | let fromInt: int => option
250 | let toInt: t => int
251 | }
252 |
253 | let cookie: (
254 | t,
255 | ~name: string,
256 | ~maxAge: int=?,
257 | ~expiresGMT: Js.Date.t=?,
258 | ~httpOnly: bool=?,
259 | ~secure: bool=?,
260 | ~signed: bool=?,
261 | ~path: string=?,
262 | ~sameSite: [#Lax | #Strict | #None]=?,
263 | ~domain: string=?,
264 | Js.Json.t
265 | ) => t
266 |
267 | @ocaml.doc(
268 | "Web browsers and other compliant clients will only clear the cookie if the given options is identical to those given to res.cookie(), excluding expires and maxAge."
269 | )
270 | let clearCookie: (
271 | t,
272 | ~name: string,
273 | ~httpOnly: bool=?,
274 | ~secure: bool=?,
275 | ~signed: bool=?,
276 | ~path: string=?,
277 | ~sameSite: [#Lax | #Strict | #None]=?,
278 | ()
279 | ) => t
280 |
281 | @bs.send external sendFile: (t, string, 'a) => complete = "sendFile"
282 | @bs.send external sendString: (t, string) => complete = "send"
283 | @bs.send external sendJson: (t, Js.Json.t) => complete = "json"
284 | // not covered in tests
285 | @bs.send external sendBuffer: (t, Node.Buffer.t) => complete = "send"
286 | // not covered in tests
287 | @bs.send external sendArray: (t, array<'a>) => complete = "send"
288 | @bs.send external sendRawStatus: (t, int) => complete = "sendStatus"
289 | let sendStatus: (t, StatusCode.t) => complete
290 | @bs.send external rawStatus: (t, int) => t = "status"
291 | let status: (t, StatusCode.t) => t
292 |
293 | // not covered in tests
294 | @bs.send @ocaml.deprecated("Use sendJson instead`")
295 | external json: (t, Js.Json.t) => complete = "json"
296 | @bs.send
297 | external redirectCode: (t, int, string) => complete = "redirect"
298 | @bs.send external redirect: (t, string) => complete = "redirect"
299 | @bs.send external setHeader: (t, string, string) => t = "set"
300 | // not covered in tests
301 | @bs.send external setType: (t, string) => t = "type"
302 | // not covered in tests
303 | @bs.send external setLinks: (t, Js.Dict.t) => t = "links"
304 | // not covered in tests
305 | @bs.send external end_: t => complete = "end"
306 | // not covered in tests
307 | @bs.send external render: (t, string, 'v, 'a) => complete = "render"
308 | }
309 |
310 | module Next: {
311 | type content
312 | type t = (Js.undefined, Response.t) => complete
313 |
314 | @ocaml.doc("value to use as [next] callback argument to invoke the next middleware")
315 | let middleware: Js.undefined
316 |
317 | @ocaml.doc(
318 | "value to use as [next] callback argument to skip middleware processing for the current route."
319 | )
320 | let route: Js.undefined
321 |
322 | @ocaml.doc(
323 | "[error e] returns the argument for [next] callback to be propagate
324 | error [e] through the chain of middleware."
325 | )
326 | let error: Error.t => Js.undefined
327 | }
328 |
329 | module ByteLimit: {
330 | type t =
331 | | B(int)
332 | | Kb(float)
333 | | Mb(float)
334 | | Gb(float)
335 | let b: int => t
336 | let kb: float => t
337 | let mb: float => t
338 | let gb: float => t
339 | }
340 |
341 | module Middleware: {
342 | type t
343 | type next = Next.t
344 | let json: (~inflate: bool=?, ~strict: bool=?, ~limit: ByteLimit.t=?, unit) => t
345 | let text: (
346 | ~defaultCharset: string=?,
347 | ~fileType: string=?,
348 | ~inflate: bool=?,
349 | ~limit: ByteLimit.t=?,
350 | unit,
351 | ) => t
352 | let raw: (~inflate: bool=?, ~fileType: string=?, ~limit: ByteLimit.t=?, unit) => t
353 | let urlencoded: (
354 | ~extended: bool=?,
355 | ~inflate: bool=?,
356 | ~limit: ByteLimit.t=?,
357 | ~parameterLimit: int=?,
358 | unit,
359 | ) => t
360 | module type S = {
361 | type f
362 | let from: f => t
363 | type errorF
364 | let fromError: errorF => t
365 | }
366 | module type ApplyMiddleware = {
367 | type f
368 | let apply: (f, next, Request.t, Response.t) => unit
369 | type errorF
370 | let applyWithError: (errorF, next, Error.t, Request.t, Response.t) => unit
371 | }
372 | module Make: (A: ApplyMiddleware) => (S with type f = A.f and type errorF = A.errorF)
373 | include S
374 | with type f = (next, Request.t, Response.t) => complete
375 | and type errorF = (next, Error.t, Request.t, Response.t) => complete
376 | }
377 |
378 | module PromiseMiddleware: Middleware.S
379 | with type f = (Middleware.next, Request.t, Response.t) => Js.Promise.t
380 | and type errorF = (Middleware.next, Error.t, Request.t, Response.t) => Js.Promise.t
381 |
382 | module type Routable = {
383 | type t
384 | let use: (t, Middleware.t) => unit
385 | let useWithMany: (t, array) => unit
386 | let useOnPath: (t, ~path: string, Middleware.t) => unit
387 | let useOnPathWithMany: (t, ~path: string, array) => unit
388 | let get: (t, ~path: string, Middleware.t) => unit
389 | let getWithMany: (t, ~path: string, array) => unit
390 | let options: (t, ~path: string, Middleware.t) => unit
391 | let optionsWithMany: (t, ~path: string, array) => unit
392 | let param: (t, ~name: string, Middleware.t) => unit
393 | let post: (t, ~path: string, Middleware.t) => unit
394 | let postWithMany: (t, ~path: string, array) => unit
395 | let put: (t, ~path: string, Middleware.t) => unit
396 | let putWithMany: (t, ~path: string, array) => unit
397 | let patch: (t, ~path: string, Middleware.t) => unit
398 | let patchWithMany: (t, ~path: string, array) => unit
399 | let delete: (t, ~path: string, Middleware.t) => unit
400 | let deleteWithMany: (t, ~path: string, array) => unit
401 | }
402 |
403 | module MakeBindFunctions: (
404 | T: {
405 | type t
406 | },
407 | ) => (Routable with type t = T.t)
408 |
409 | module Router: {
410 | include Routable
411 | let make: (~caseSensitive: bool=?, ~mergeParams: bool=?, ~strict: bool=?, unit) => t
412 | external asMiddleware: t => Middleware.t = "%identity"
413 | }
414 |
415 | module HttpServer: {
416 | type t
417 | @bs.send
418 | external on: (
419 | t,
420 | @bs.string [#request((Request.t, Response.t) => unit) | #close(unit => unit)],
421 | ) => unit = "on"
422 | }
423 |
424 | let router: (~caseSensitive: bool=?, ~mergeParams: bool=?, ~strict: bool=?, unit) => Router.t
425 |
426 | module App: {
427 | include Routable
428 | let useRouter: (t, Router.t) => unit
429 | let useRouterOnPath: (t, ~path: string, Router.t) => unit
430 | @bs.module external make: unit => t = "express"
431 | external asMiddleware: t => Middleware.t = "%identity"
432 | let listen: (
433 | t,
434 | ~port: int=?,
435 | ~hostname: string=?,
436 | ~onListen: Js.null_undefined => unit=?,
437 | unit,
438 | ) => HttpServer.t
439 | @bs.send external disable: (t, ~name: string) => unit = "disable"
440 | @bs.send external set: (t, string, string) => unit = "set"
441 | @bs.send external engine : (t, string, 'engine) => unit = "engine"
442 | }
443 |
444 | @ocaml.doc("[express ()] creates an instance of the App class.
445 | Alias for [App.make ()]")
446 | let express: unit => App.t
447 |
448 | module Static: {
449 | type options
450 | type stat
451 | type t
452 |
453 | let defaultOptions: unit => options
454 | @bs.set external dotfiles: (options, string) => unit = "dotfiles"
455 | @bs.set external etag: (options, bool) => unit = "etag"
456 | @bs.set external extensions: (options, array) => unit = "extensions"
457 | @bs.set external fallthrough: (options, bool) => unit = "fallthrough"
458 | @bs.set external immutable: (options, bool) => unit = "immutable"
459 | @bs.set external indexBool: (options, bool) => unit = "index"
460 | @bs.set external indexString: (options, string) => unit = "index"
461 | @bs.set external lastModified: (options, bool) => unit = "lastModified"
462 | @bs.set external maxAge: (options, int) => unit = "maxAge"
463 | @bs.set external redirect: (options, bool) => unit = "redirect"
464 | @bs.set external setHeaders: (options, (Request.t, string, stat) => unit) => unit = "setHeaders"
465 |
466 | @ocaml.doc("[make directory] creates a static middleware for [directory]") @bs.module("express")
467 | external make: (string, options) => t = "static"
468 |
469 | @ocaml.doc("[asMiddleware static] casts [static] to a Middleware type")
470 | external asMiddleware: t => Middleware.t = "%identity"
471 | }
472 |
--------------------------------------------------------------------------------
/src/Express.res:
--------------------------------------------------------------------------------
1 | type complete
2 |
3 | module Error = {
4 | type t = exn
5 |
6 | @bs.send @bs.return(null_undefined_to_opt)
7 | external message: Js_exn.t => option = "message"
8 | @bs.send @bs.return(null_undefined_to_opt)
9 | external name: Js_exn.t => option = "name"
10 | }
11 |
12 | module Request = {
13 | type t
14 | type params = Js_dict.t
15 | @bs.get external params: t => params = "params"
16 |
17 | external asJsonObject: t => Js_dict.t = "%identity"
18 |
19 | @bs.get external baseUrl: t => string = "baseUrl"
20 | @bs.get external body_: t => 'a = "body"
21 |
22 | @bs.get @bs.return(null_undefined_to_opt)
23 | external bodyJSON: t => option = "body"
24 |
25 | @bs.get @bs.return(null_undefined_to_opt)
26 | external bodyRaw: t => option = "body"
27 |
28 | let bodyText: t => option = req => {
29 | let body: string = body_(req)
30 | if Js.Json.test(body, Js.Json.String) {
31 | Some(body)
32 | } else {
33 | None
34 | }
35 | }
36 | let decodeStringDict = json => Js.Json.decodeObject(json) |> Js.Option.andThen((. obj) => {
37 | let source: Js.Dict.t = Obj.magic(obj)
38 | let allStrings =
39 | Js.Dict.values(source) |> Array.fold_left(
40 | (prev, value) => prev && Js.Json.test(value, Js.Json.String),
41 | true,
42 | )
43 | if allStrings {
44 | Some(source)
45 | } else {
46 | None
47 | }
48 | })
49 | let bodyURLEncoded: t => option> = req => {
50 | let body: Js.Json.t = body_(req)
51 | decodeStringDict(body)
52 | }
53 |
54 | @bs.get @bs.return(null_undefined_to_opt)
55 | external cookies: t => option> = "cookies"
56 |
57 | @bs.get @bs.return(null_undefined_to_opt)
58 | external signedCookies: t => option> = "signedCookies"
59 |
60 | @bs.get external hostname: t => string = "hostname"
61 |
62 | @bs.get external ip: t => string = "ip"
63 |
64 | @bs.get external fresh: t => bool = "fresh"
65 |
66 | @bs.get external stale: t => bool = "stale"
67 |
68 | @bs.get external methodRaw: t => string = "method"
69 | type httpMethod =
70 | | Get
71 | | Post
72 | | Put
73 | | Delete
74 | | Head
75 | | Options
76 | | Trace
77 | | Connect
78 | | Patch
79 | let httpMethod: t => httpMethod = req =>
80 | switch methodRaw(req) {
81 | | "GET" => Get
82 | | "POST" => Post
83 | | "PUT" => Put
84 | | "PATCH" => Patch
85 | | "DELETE" => Delete
86 | | "HEAD" => Head
87 | | "OPTIONS" => Options
88 | | "TRACE" => Trace
89 | | "CONNECT" => Connect
90 | | s => failwith("Express.Request.method_ Unexpected method: " ++ s)
91 | }
92 |
93 | @bs.get external originalUrl: t => string = "originalUrl"
94 |
95 | @bs.get external path: t => string = "path"
96 |
97 | type protocol =
98 | | Http
99 | | Https
100 | let protocol: t => protocol = req => {
101 | module Raw = {
102 | @bs.get external protocol: t => string = "protocol"
103 | }
104 | switch Raw.protocol(req) {
105 | | "http" => Http
106 | | "https" => Https
107 | | s => failwith("Express.Request.protocol, Unexpected protocol: " ++ s)
108 | }
109 | }
110 |
111 | @bs.get
112 | external secure: t => bool = "secure"
113 |
114 | @bs.get external query: t => Js.Dict.t = "query"
115 |
116 | let accepts: (t, array) => option = (req, types) => {
117 | module Raw = {
118 | @bs.send
119 | external accepts: (t, array) => Js.Json.t = "accepts"
120 | }
121 | let ret = Raw.accepts(req, types)
122 | let tagged_t = Js_json.classify(ret)
123 | switch tagged_t {
124 | | JSONString(x) => Some(x)
125 | | _ => None
126 | }
127 | }
128 |
129 | let acceptsCharsets: (t, array) => option = (req, types) => {
130 | module Raw = {
131 | @bs.send
132 | external acceptsCharsets: (t, array) => Js.Json.t = "acceptsCharsets"
133 | }
134 | let ret = Raw.acceptsCharsets(req, types)
135 | let tagged_t = Js_json.classify(ret)
136 | switch tagged_t {
137 | | JSONString(x) => Some(x)
138 | | _ => None
139 | }
140 | }
141 | @bs.send @bs.return(null_undefined_to_opt)
142 | external get: (t, string) => option = "get"
143 |
144 | @bs.get
145 | external xhr: t => bool = "xhr"
146 | }
147 |
148 | module Response = {
149 | type t
150 | module StatusCode = {
151 | @bs.deriving(jsConverter)
152 | type t =
153 | | @bs.as(200) Ok
154 | | @bs.as(201) Created
155 | | @bs.as(202) Accepted
156 | | @bs.as(203) NonAuthoritativeInformation
157 | | @bs.as(204) NoContent
158 | | @bs.as(205) ResetContent
159 | | @bs.as(206) PartialContent
160 | | @bs.as(207) MultiStatus
161 | | @bs.as(208) AleadyReported
162 | | @bs.as(226) IMUsed
163 | | @bs.as(300) MultipleChoices
164 | | @bs.as(301) MovedPermanently
165 | | @bs.as(302) Found
166 | | @bs.as(303) SeeOther
167 | | @bs.as(304) NotModified
168 | | @bs.as(305) UseProxy
169 | | @bs.as(306) SwitchProxy
170 | | @bs.as(307) TemporaryRedirect
171 | | @bs.as(308) PermanentRedirect
172 | | @bs.as(400) BadRequest
173 | | @bs.as(401) Unauthorized
174 | | @bs.as(402) PaymentRequired
175 | | @bs.as(403) Forbidden
176 | | @bs.as(404) NotFound
177 | | @bs.as(405) MethodNotAllowed
178 | | @bs.as(406) NotAcceptable
179 | | @bs.as(407) ProxyAuthenticationRequired
180 | | @bs.as(408) RequestTimeout
181 | | @bs.as(409) Conflict
182 | | @bs.as(410) Gone
183 | | @bs.as(411) LengthRequired
184 | | @bs.as(412) PreconditionFailed
185 | | @bs.as(413) PayloadTooLarge
186 | | @bs.as(414) UriTooLong
187 | | @bs.as(415) UnsupportedMediaType
188 | | @bs.as(416) RangeNotSatisfiable
189 | | @bs.as(417) ExpectationFailed
190 | | @bs.as(418) ImATeapot
191 | | @bs.as(421) MisdirectedRequest
192 | | @bs.as(422) UnprocessableEntity
193 | | @bs.as(423) Locked
194 | | @bs.as(424) FailedDependency
195 | | @bs.as(426) UpgradeRequired
196 | | @bs.as(428) PreconditionRequired
197 | | @bs.as(429) TooManyRequests
198 | | @bs.as(431) RequestHeaderFieldsTooLarge
199 | | @bs.as(451) UnavailableForLegalReasons
200 | | @bs.as(500) InternalServerError
201 | | @bs.as(501) NotImplemented
202 | | @bs.as(502) BadGateway
203 | | @bs.as(503) ServiceUnavailable
204 | | @bs.as(504) GatewayTimeout
205 | | @bs.as(505) HttpVersionNotSupported
206 | | @bs.as(506) VariantAlsoNegotiates
207 | | @bs.as(507) InsufficientStorage
208 | | @bs.as(508) LoopDetected
209 | | @bs.as(510) NotExtended
210 | | @bs.as(511) NetworkAuthenticationRequired
211 | let fromInt = tFromJs
212 | let toInt = tToJs
213 | }
214 | @bs.send
215 | external cookie_: (t, string, Js.Json.t, 'a) => unit = "cookie"
216 | @bs.send
217 | external clearCookie_: (t, string, 'a) => unit = "clearCookie"
218 | @bs.deriving(jsConverter)
219 | type sameSite = [@bs.as("lax") #Lax | @bs.as("strict") #Strict | @bs.as("none") #None]
220 | external toDict: 'a => Js.Dict.t> = "%identity"
221 | let filterKeys = obj => {
222 | let result = toDict(obj)
223 | result
224 | |> Js.Dict.entries
225 | |> Js.Array.filter(((_key, value)) => !Js.Nullable.isNullable(value))
226 | |> Js.Dict.fromArray
227 | }
228 | let cookie = (
229 | response,
230 | ~name,
231 | ~maxAge=?,
232 | ~expiresGMT=?,
233 | ~httpOnly=?,
234 | ~secure=?,
235 | ~signed=?,
236 | ~path=?,
237 | ~sameSite: option=?,
238 | ~domain=?,
239 | value
240 | ) => {
241 | cookie_(
242 | response,
243 | name,
244 | value,
245 | {
246 | "maxAge": maxAge |> Js.Nullable.fromOption,
247 | "expires": expiresGMT |> Js.Nullable.fromOption,
248 | "path": path |> Js.Nullable.fromOption,
249 | "httpOnly": httpOnly |> Js.Nullable.fromOption,
250 | "secure": secure |> Js.Nullable.fromOption,
251 | "sameSite": sameSite |> Js.Option.map((. x) => sameSiteToJs(x)) |> Js.Nullable.fromOption,
252 | "signed": signed |> Js.Nullable.fromOption,
253 | "domain": domain |> Js.Nullable.fromOption,
254 | } |> filterKeys
255 | )
256 | response
257 | }
258 | let clearCookie = (
259 | response,
260 | ~name,
261 | ~httpOnly=?,
262 | ~secure=?,
263 | ~signed=?,
264 | ~path="/",
265 | ~sameSite: option=?,
266 | ()
267 | ) => {
268 | clearCookie_(
269 | response,
270 | name,
271 | {
272 | "maxAge": Js.Nullable.undefined,
273 | "expires": Js.Nullable.undefined,
274 | "path": path,
275 | "httpOnly": httpOnly |> Js.Nullable.fromOption,
276 | "secure": secure |> Js.Nullable.fromOption,
277 | "sameSite": sameSite |> Js.Option.map((. x) => sameSiteToJs(x)) |> Js.Nullable.fromOption,
278 | "signed": signed |> Js.Nullable.fromOption,
279 | } |> filterKeys
280 | )
281 | response
282 | }
283 | @bs.send external sendFile: (t, string, 'a) => complete = "sendFile"
284 | @bs.send external sendString: (t, string) => complete = "send"
285 | @bs.send external sendJson: (t, Js.Json.t) => complete = "json"
286 | @bs.send external sendBuffer: (t, Node.Buffer.t) => complete = "send"
287 | @bs.send external sendArray: (t, array<'a>) => complete = "send"
288 | @bs.send external sendRawStatus: (t, int) => complete = "sendStatus"
289 | let sendStatus = (inst, statusCode) => sendRawStatus(inst, StatusCode.toInt(statusCode))
290 | @bs.send external rawStatus: (t, int) => t = "status"
291 | let status = (inst, statusCode) => rawStatus(inst, StatusCode.toInt(statusCode))
292 | @bs.send @ocaml.deprecated("Use sendJson instead`")
293 | external json: (t, Js.Json.t) => complete = "json"
294 | @bs.send
295 | external redirectCode: (t, int, string) => complete = "redirect"
296 | @bs.send external redirect: (t, string) => complete = "redirect"
297 | @bs.send external setHeader: (t, string, string) => t = "set"
298 | @bs.send external setType: (t, string) => t = "type"
299 | @bs.send external setLinks: (t, Js.Dict.t) => t = "links"
300 | @bs.send external end_: t => complete = "end"
301 | @bs.send external render: (t, string, 'v, 'a) => complete = "render"
302 | }
303 |
304 | module Next: {
305 | type content
306 | type t = (Js.undefined, Response.t) => complete
307 | let middleware: Js.undefined
308 |
309 | let route: Js.undefined
310 |
311 | let error: Error.t => Js.undefined
312 | } = {
313 | type content
314 | type t = (Js.undefined, Response.t) => complete
315 | let middleware = Js.undefined
316 | external castToContent: 'a => content = "%identity"
317 | let route = Js.Undefined.return(castToContent("route"))
318 | let error = (e: Error.t) => Js.Undefined.return(castToContent(e))
319 | }
320 |
321 | module ByteLimit = {
322 | @bs.deriving(accessors)
323 | type t =
324 | | B(int)
325 | | Kb(float)
326 | | Mb(float)
327 | | Gb(float)
328 | let toBytes = x =>
329 | switch x {
330 | | Some(B(b)) => Js.Nullable.return(b)
331 | | Some(Kb(kb)) => Js.Nullable.return(int_of_float(1024.0 *. kb))
332 | | Some(Mb(mb)) => Js.Nullable.return(int_of_float(1024.0 *. 1024.0 *. mb))
333 | | Some(Gb(gb)) => Js.Nullable.return(int_of_float(1024.0 *. 1024.0 *. 1024.0 *. gb))
334 | | None => Js.Nullable.undefined
335 | }
336 | }
337 |
338 | module Middleware = {
339 | type next = Next.t
340 | type t
341 | type jsonOptions = {"inflate": bool, "strict": bool, "limit": Js.nullable}
342 | type urlEncodedOptions = {
343 | "extended": bool,
344 | "inflate": bool,
345 | "limit": Js.nullable,
346 | "parameterLimit": Js.nullable,
347 | }
348 | type textOptions = {
349 | "defaultCharset": string,
350 | "inflate": bool,
351 | "type": string,
352 | "limit": Js.Nullable.t,
353 | }
354 | type rawOptions = {"inflate": bool, "type": string, "limit": Js.Nullable.t}
355 | @bs.module("express") @bs.val external json_: jsonOptions => t = "json"
356 | @bs.module("express") @bs.val
357 | external urlencoded_: urlEncodedOptions => t = "urlencoded"
358 | let json = (~inflate=true, ~strict=true, ~limit=?, ()) =>
359 | json_({
360 | "inflate": inflate,
361 | "strict": strict,
362 | "limit": ByteLimit.toBytes(limit),
363 | })
364 | @bs.module("body-parser") @bs.val
365 | external text_: textOptions => t = "text"
366 | let text = (
367 | ~defaultCharset="utf-8",
368 | ~fileType="text/plain",
369 | ~inflate=true,
370 | ~limit: option=?,
371 | (),
372 | ) =>
373 | text_({
374 | "defaultCharset": defaultCharset,
375 | "type": fileType,
376 | "limit": ByteLimit.toBytes(limit),
377 | "inflate": inflate,
378 | })
379 | let urlencoded = (~extended=false, ~inflate=true, ~limit=?, ~parameterLimit=?, ()) =>
380 | urlencoded_({
381 | "inflate": inflate,
382 | "extended": extended,
383 | "parameterLimit": parameterLimit |> Js.Nullable.fromOption,
384 | "limit": ByteLimit.toBytes(limit),
385 | })
386 | @bs.module("body-parser") @bs.val external raw_: rawOptions => t = "raw"
387 | let raw = (
388 | ~inflate=true,
389 | ~fileType="application/octet-stream",
390 | ~limit: option=?,
391 | (),
392 | ) =>
393 | raw_({
394 | "type": fileType,
395 | "limit": ByteLimit.toBytes(limit),
396 | "inflate": inflate,
397 | })
398 | module type S = {
399 | type f
400 | type errorF
401 | let from: f => t
402 | /* Generate the common Middleware binding function for a given
403 | * type. This Functor is used for the Router and App classes. */
404 | let fromError: errorF => t
405 | }
406 | module type ApplyMiddleware = {
407 | type f
408 | let apply: (f, next, Request.t, Response.t) => unit
409 | type errorF
410 | let applyWithError: (errorF, next, Error.t, Request.t, Response.t) => unit
411 | }
412 | module Make = (A: ApplyMiddleware): (S with type f = A.f and type errorF = A.errorF) => {
413 | type f = A.f
414 | external unsafeFrom: 'a => t = "%identity"
415 | let from = middleware => {
416 | let aux = (next, content, _) => next(content)
417 | unsafeFrom((req, res, next) => A.apply(middleware, aux(next), req, res))
418 | }
419 | type errorF = A.errorF
420 | let fromError = middleware => {
421 | let aux = (next, content, _) => next(content)
422 | unsafeFrom((err, req, res, next) => A.applyWithError(middleware, aux(next), err, req, res))
423 | }
424 | }
425 | include Make({
426 | type f = (next, Request.t, Response.t) => complete
427 | type errorF = (next, Error.t, Request.t, Response.t) => complete
428 | let apply = (f, next, req, res) =>
429 | try f(next, req, res) catch {
430 | | e => next(Next.error(e), res)
431 | } |> ignore
432 | let applyWithError = (f, next, err, req, res) =>
433 | try f(next, err, req, res) catch {
434 | | e => next(Next.error(e), res)
435 | } |> ignore
436 | })
437 | }
438 |
439 | module PromiseMiddleware = Middleware.Make({
440 | type f = (Middleware.next, Request.t, Response.t) => Js.Promise.t
441 | type errorF = (Middleware.next, Error.t, Request.t, Response.t) => Js.Promise.t
442 | external castToErr: Js.Promise.error => Error.t = "%identity"
443 | let apply = (f, next, req, res) => {
444 | let promise: Js.Promise.t = try f(next, req, res) catch {
445 | | e => Js.Promise.resolve(next(Next.error(e), res))
446 | }
447 | promise |> Js.Promise.catch(err => {
448 | let err = castToErr(err)
449 | Js.Promise.resolve(next(Next.error(err), res))
450 | }) |> ignore
451 | }
452 | let applyWithError = (f, next, err, req, res) => {
453 | let promise: Js.Promise.t = try f(next, err, req, res) catch {
454 | | e => Js.Promise.resolve(next(Next.error(e), res))
455 | }
456 | promise |> Js.Promise.catch(err => {
457 | let err = castToErr(err)
458 | Js.Promise.resolve(next(Next.error(err), res))
459 | }) |> ignore
460 | }
461 | })
462 |
463 | module type Routable = {
464 | type t
465 | let use: (t, Middleware.t) => unit
466 | let useWithMany: (t, array) => unit
467 | let useOnPath: (t, ~path: string, Middleware.t) => unit
468 | let useOnPathWithMany: (t, ~path: string, array) => unit
469 | let get: (t, ~path: string, Middleware.t) => unit
470 | let getWithMany: (t, ~path: string, array) => unit
471 | let options: (t, ~path: string, Middleware.t) => unit
472 | let optionsWithMany: (t, ~path: string, array) => unit
473 | let param: (t, ~name: string, Middleware.t) => unit
474 | let post: (t, ~path: string, Middleware.t) => unit
475 | let postWithMany: (t, ~path: string, array) => unit
476 | let put: (t, ~path: string, Middleware.t) => unit
477 | let putWithMany: (t, ~path: string, array) => unit
478 | let patch: (t, ~path: string, Middleware.t) => unit
479 | let patchWithMany: (t, ~path: string, array) => unit
480 | let delete: (t, ~path: string, Middleware.t) => unit
481 | let deleteWithMany: (t, ~path: string, array) => unit
482 | }
483 |
484 | module MakeBindFunctions = (
485 | T: {
486 | type t
487 | },
488 | ): (Routable with type t = T.t) => {
489 | type t = T.t
490 | @bs.send external use: (T.t, Middleware.t) => unit = "use"
491 | @bs.send
492 | external useWithMany: (T.t, array) => unit = "use"
493 | @bs.send
494 | external useOnPath: (T.t, ~path: string, Middleware.t) => unit = "use"
495 | @bs.send
496 | external useOnPathWithMany: (T.t, ~path: string, array) => unit = "use"
497 | @bs.send external get: (T.t, ~path: string, Middleware.t) => unit = "get"
498 | @bs.send
499 | external getWithMany: (T.t, ~path: string, array) => unit = "get"
500 | @bs.send external options: (T.t, ~path: string, Middleware.t) => unit = "options"
501 | @bs.send external optionsWithMany: (T.t, ~path: string, array) => unit = "options"
502 | @bs.send
503 | external param: (T.t, ~name: string, Middleware.t) => unit = "param"
504 | @bs.send external post: (T.t, ~path: string, Middleware.t) => unit = "post"
505 | @bs.send
506 | external postWithMany: (T.t, ~path: string, array) => unit = "post"
507 | @bs.send external put: (T.t, ~path: string, Middleware.t) => unit = "put"
508 | @bs.send
509 | external putWithMany: (T.t, ~path: string, array) => unit = "put"
510 | @bs.send external patch: (T.t, ~path: string, Middleware.t) => unit = "patch"
511 | @bs.send
512 | external patchWithMany: (T.t, ~path: string, array) => unit = "patch"
513 | @bs.send external delete: (T.t, ~path: string, Middleware.t) => unit = "delete"
514 | @bs.send
515 | external deleteWithMany: (T.t, ~path: string, array) => unit = "delete"
516 | }
517 |
518 | module Router = {
519 | include MakeBindFunctions({
520 | type t
521 | })
522 | type routerArgs = {"caseSensitive": bool, "mergeParams": bool, "strict": bool}
523 | @bs.module("express") @bs.val external make_: routerArgs => t = "Router"
524 | let make = (~caseSensitive=false, ~mergeParams=false, ~strict=false, ()) =>
525 | make_({
526 | "caseSensitive": caseSensitive,
527 | "mergeParams": mergeParams,
528 | "strict": strict,
529 | })
530 | external asMiddleware: t => Middleware.t = "%identity"
531 | }
532 |
533 | let router = Router.make
534 |
535 | module HttpServer = {
536 | type t
537 | @bs.send
538 | external on: (
539 | t,
540 | @bs.string [#request((Request.t, Response.t) => unit) | #close(unit => unit)],
541 | ) => unit = "on"
542 | }
543 |
544 | module App = {
545 | include MakeBindFunctions({
546 | type t
547 | })
548 | let useRouter = (app, router) => Router.asMiddleware(router) |> use(app)
549 | let useRouterOnPath = (app, ~path, router) => Router.asMiddleware(router) |> useOnPath(app, ~path)
550 | @bs.module external make: unit => t = "express"
551 |
552 | external asMiddleware: t => Middleware.t = "%identity"
553 |
554 | @bs.send
555 | external listen_: (
556 | t,
557 | int,
558 | string,
559 | @bs.uncurry (Js.Null_undefined.t => unit),
560 | ) => HttpServer.t = "listen"
561 | let listen = (app, ~port=3000, ~hostname="0.0.0.0", ~onListen=_ => (), ()) =>
562 | listen_(app, port, hostname, onListen)
563 | @bs.send external disable: (t, ~name: string) => unit = "disable"
564 | @bs.send external set: (t, string, string) => unit = "set"
565 | @bs.send external engine : (t, string, 'engine) => unit = "engine"
566 | }
567 |
568 | let express = App.make
569 |
570 | module Static = {
571 | type options
572 | type stat
573 | type t
574 |
575 | let defaultOptions: unit => options = (): options => Obj.magic(Js_obj.empty())
576 | @bs.set external dotfiles: (options, string) => unit = "dotfiles"
577 | @bs.set external etag: (options, bool) => unit = "etag"
578 | @bs.set external extensions: (options, array) => unit = "extensions"
579 | @bs.set external fallthrough: (options, bool) => unit = "fallthrough"
580 | @bs.set external immutable: (options, bool) => unit = "immutable"
581 | @bs.set external indexBool: (options, bool) => unit = "index"
582 | @bs.set external indexString: (options, string) => unit = "index"
583 | @bs.set external lastModified: (options, bool) => unit = "lastModified"
584 | @bs.set external maxAge: (options, int) => unit = "maxAge"
585 | @bs.set external redirect: (options, bool) => unit = "redirect"
586 | @bs.set external setHeaders: (options, (Request.t, string, stat) => unit) => unit = "setHeaders"
587 |
588 | @bs.module("express") external make: (string, options) => t = "static"
589 |
590 | external asMiddleware: t => Middleware.t = "%identity"
591 | }
592 |
--------------------------------------------------------------------------------