├── .gitignore ├── .npmignore ├── .travis.yml ├── LICENSE ├── README.md ├── bsconfig.json ├── example └── Index.res ├── package.json ├── src ├── Express.res └── Express.resi ├── static └── test.data └── tests ├── reference.data └── test.sh /.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 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | /node_modules/ 3 | .merlin 4 | *.log 5 | /tests/ 6 | /static/ 7 | /example/ 8 | -------------------------------------------------------------------------------- /.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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /static/test.data: -------------------------------------------------------------------------------- 1 | This is the content of test.data a static file 2 | -------------------------------------------------------------------------------- /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 | --------------------------------------------------------------------------------