-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathhttpserver.coffee
134 lines (125 loc) · 4.17 KB
/
httpserver.coffee
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
## Case study and example of implementing a HTTP server with JSON-based REST API, using Agree
try
agree = require '..' # when running in ./examples of git
catch e
agree = require 'agree' # when running as standalone example
agreeExpress = agree.express
conditions = agreeExpress.conditions
Promise = agree.Promise # polyfill for node.js 0.10 compat
## Contracts
# In production, Contracts for public APIs should be kept in a separate file from implementation
contracts = {}
# Shared contract setup
jsonApiFunction = (method, path) ->
agree.function "#{method.toUpperCase()} #{path}"
.attr 'http_method', method
.attr 'http_path', path
.error agreeExpress.requestFail
.requires conditions.requestContentType 'application/json'
.ensures conditions.responseEnded
contracts.getSomeData = jsonApiFunction 'GET', '/somedata'
.ensures conditions.responseStatus 200
.ensures conditions.responseContentType 'application/json'
.ensures conditions.responseSchema
id: 'somedata.json'
type: 'object'
required: ['initial']
properties:
initial: { type: 'string' }
nonexist: { type: 'number' }
.successExample 'All headers correct',
_type: 'http-request-response'
headers:
'Content-Type': 'application/json'
responseCode: 200
.failExample 'Wrong Content-Type',
_type: 'http-request-response'
headers:
'Content-Type': 'text/html'
responseCode: 422
contracts.createResource = jsonApiFunction 'POST', '/newresource'
.requires conditions.requestSchema
id: 'newresource.json'
type: 'object'
required: ['name', 'tags']
properties:
name: { type: 'string' }
tags: { type: 'array', uniqueItems: true, items: { type: 'string' } }
.ensures conditions.responseStatus 201
.ensures conditions.responseContentType 'application/json' # even if we don't have body
.ensures conditions.responseHeaderMatches 'Location', /\/newresource\/[\d]+/
.successExample 'Valid data in body',
_type: 'http-request-response'
headers:
'Content-Type': 'application/json'
body:
name: 'myname'
tags: ['first', 'second']
responseCode: 201
.failExample 'Invalid data',
_type: 'http-request-response'
headers:
'Content-Type': 'application/json'
body:
name: 'valid'
tags: [1, 2, 3]
responseCode: 422
## Database access
# Simulated example of DB or key-value store, for keeping state. SQL or no-SQL in real-life
db =
state:
somekey: { initial: 'Foo' }
get: (key) ->
return new Promise (resolve, reject) ->
data = db.state[key]
return resolve data
set: (key, data) ->
return new Promise (resolve, reject) ->
db.state[key] = data
return resolve key
add: (key, data) ->
return new Promise (resolve, reject) ->
db.state[key] = [] if not db.state[key]?
db.state[key].push data
sub = db.state[key].length
return resolve sub
## Implementation
routes = {}
# Using regular Promises
routes.createResource = contracts.createResource.implement (req, res) ->
db.add 'newresource', req.body
.then (key) ->
res.set 'Location', "/newresource/#{key}"
res.set 'Content-Type', 'application/json' # we promised..
res.status(201).end()
Promise.resolve res
# Using static Promise chain
routes.getSomeData = contracts.getSomeData.implement( agree.Chain()
.describe 'respond with "somekey" data from DB as JSON'
.start (req, res) ->
@res = res
return 'somekey'
.then 'db.get', db.get
.then 'respond with JSON', (data) ->
@res.json data
Promise.resolve @res
.toFunction() # function with signature like start.. (res, req) ->
)
## Setup
express = require 'express'
bodyparser = require 'body-parser'
app = express()
app.use bodyparser.json()
app.use agreeExpress.mockingMiddleware
agreeExpress.selfDocument routes, '/'
agreeExpress.installExpressRoutes app, routes
module.exports = routes # for introspection by Agree tools
agree.testing.registerTester 'http-request-response', new agreeExpress.Tester app
## Run
main = () ->
port = process.env.PORT
port = 3333 if not port
app.listen port, (err) ->
throw err if err
console.log "#{process.argv[1]}: running on port #{port}"
main() if not module.parent