Skip to content

Commit 4eb040b

Browse files
committed
rubico-http-server example
1 parent ff04c7b commit 4eb040b

File tree

3 files changed

+208
-0
lines changed

3 files changed

+208
-0
lines changed
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
{
2+
"name": "rubico-http-server",
3+
"version": "0.0.0",
4+
"description": "A rubico HTTP server",
5+
"author": "Richard Tong",
6+
"license": "MIT",
7+
"main": "index.js",
8+
"repository": {
9+
"type": "git",
10+
"url": "github:a-synchronous/rubico",
11+
"directory": "examples/rubico-http-server"
12+
},
13+
"scripts": {
14+
"test": "node runserver.test.js"
15+
},
16+
"dependencies": {
17+
"rubico": "^2.7.3"
18+
},
19+
"devDependencies": {
20+
"thunk-test": "^1.3.1"
21+
}
22+
}
Lines changed: 132 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,132 @@
1+
#!/usr/bin/env node
2+
3+
require('rubico/global')
4+
const http = require('http')
5+
6+
/**
7+
* @name runserver
8+
*
9+
* @synopsis
10+
* ```coffeescript [specscript]
11+
* module http
12+
*
13+
* runserver(options? {
14+
* port?: number,
15+
* }) -> { server http.Server }
16+
* ```
17+
*/
18+
function runserver(options = {}) {
19+
const { port = 8080 } = options
20+
21+
const userTable = {
22+
async getById(userId) {
23+
return {
24+
id: userId,
25+
name: `User ${userId}`,
26+
createTime: 1,
27+
birthdate: '2020-01-01',
28+
profilePictureUrl: 'https://rubico.land/assets/rubico-logo.png',
29+
}
30+
}
31+
}
32+
33+
function healthCheckHandler(request, response) {
34+
response.writeHead(200, {
35+
'Content-Type': 'text/plain',
36+
})
37+
response.end('ok')
38+
}
39+
40+
function optionsHandler(request, response) {
41+
response.writeHead(204, {
42+
'Access-Control-Allow-Origin': '*',
43+
'Access-Control-Allow-Methods': '*',
44+
'Access-Control-Allow-Headers': '*',
45+
'Access-Control-Max-Age': '86400',
46+
})
47+
response.end()
48+
}
49+
50+
async function getUserHandler(request, response) {
51+
const userId = request.url.match(/\d+/)[0]
52+
53+
// validate
54+
if (isNaN(Number(userId))) {
55+
const error = new Error('Bad Request')
56+
error.code = 400
57+
throw error
58+
}
59+
60+
// userTable is a theoretical client for a database
61+
const user = await userTable.getById(userId)
62+
63+
// handle not found
64+
if (user == null) {
65+
const error = new Error('Not Found')
66+
error.code = 404
67+
throw error
68+
}
69+
70+
// ensure no private user information is exposed
71+
const publicUser = {
72+
id: user.id,
73+
name: user.name,
74+
birthdate: user.birthdate,
75+
profilePictureUrl: user.profilePictureUrl,
76+
createTime: user.createTime,
77+
}
78+
79+
// send back the user resource in the response body
80+
response.writeHead(200, {
81+
'Access-Control-Allow-Origin': '*',
82+
'Content-Type': 'application/json',
83+
})
84+
response.end(JSON.stringify({
85+
user: publicUser,
86+
}))
87+
}
88+
89+
function notFoundHandler(request, response) {
90+
response.writeHead(404, {
91+
'Content-Type': 'text/plain',
92+
})
93+
response.end('Not Found')
94+
}
95+
96+
function errorHandler(request, response) {
97+
console.error(error)
98+
if (typeof error.code != 'number') {
99+
error.code = 500
100+
}
101+
response.writeHead(error.code, {
102+
'Access-Control-Allow-Origin': '*',
103+
'Content-Type': 'text/plain',
104+
})
105+
response.end(error.message)
106+
}
107+
108+
const combinedHandler = tryCatch(
109+
switchCase([
110+
request => request.url.startsWith('/health'),
111+
healthCheckHandler,
112+
113+
request => request.method == 'OPTIONS',
114+
optionsHandler,
115+
116+
request => request.method == 'GET' && /^\/user\/\d+$/.test(request.url),
117+
getUserHandler,
118+
119+
notFoundHandler,
120+
]),
121+
122+
errorHandler
123+
)
124+
125+
const server = http.createServer(combinedHandler)
126+
127+
server.listen(port)
128+
129+
return { server }
130+
}
131+
132+
module.exports = runserver
Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
const Test = require('thunk-test')
2+
const assert = require('assert')
3+
const Http = require('presidium/Http')
4+
const runserver = require('./runserver')
5+
6+
const test = new Test('runserver', async () => {
7+
const { server } = runserver({ port: 7357 })
8+
9+
const http = new Http('http://localhost:7357')
10+
11+
{ // /health
12+
const response = await http.get('/health')
13+
assert.equal(response.status, 200)
14+
assert.equal(await response.text(), 'ok')
15+
}
16+
17+
{ // OPTIONS
18+
const response = await http.options('/')
19+
assert.equal(response.headers.get('access-control-allow-origin'), '*')
20+
assert.equal(response.headers.get('access-control-allow-methods'), '*')
21+
assert.equal(response.headers.get('access-control-allow-headers'), '*')
22+
assert.equal(response.headers.get('access-control-max-age'), '86400')
23+
assert.equal(response.status, 204)
24+
assert.equal(await response.text(), '')
25+
}
26+
27+
{ // get user
28+
const response = await http.get('/user/100')
29+
assert.equal(response.status, 200)
30+
assert.deepEqual(await response.json(), {
31+
user: {
32+
id: '100',
33+
name: 'User 100',
34+
createTime: 1,
35+
birthdate: '2020-01-01',
36+
profilePictureUrl: 'https://rubico.land/assets/rubico-logo.png',
37+
},
38+
})
39+
}
40+
41+
{ // not found
42+
const response = await http.get('/not-found')
43+
assert.equal(response.status, 404)
44+
assert.equal(await response.text(), 'Not Found')
45+
}
46+
47+
server.close()
48+
}).case()
49+
50+
if (process.argv[1] == __filename) {
51+
test()
52+
}
53+
54+
module.exports = test

0 commit comments

Comments
 (0)