Skip to content

Commit 50f7de2

Browse files
author
Matt Mason
committed
Added expressjs style api for http responses
1 parent 7c10aca commit 50f7de2

File tree

11 files changed

+277
-9
lines changed

11 files changed

+277
-9
lines changed

src/WebJobs.Script/Binding/HttpBinding.cs

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -75,10 +75,11 @@ public override async Task BindAsync(BindingContext context)
7575
headers = (JObject)value;
7676
}
7777

78-
if (jo.TryGetValue("status", StringComparison.OrdinalIgnoreCase, out value) && value is JValue)
78+
if ((jo.TryGetValue("status", StringComparison.OrdinalIgnoreCase, out value) && value is JValue) ||
79+
(jo.TryGetValue("statusCode", StringComparison.OrdinalIgnoreCase, out value) && value is JValue))
7980
{
8081
statusCode = (HttpStatusCode)(int)value;
81-
}
82+
}
8283
}
8384
}
8485
catch (JsonException)

src/WebJobs.Script/Content/Script/functions.js

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@
33

44
var util = require('util');
55
var process = require('process');
6+
var request = require('./http/request');
7+
var response = require('./http/response');
68

79
module.exports = {
810
globalInitialization: globalInitialization,
@@ -69,6 +71,15 @@ function createFunction(f) {
6971
inputs.unshift(context);
7072
delete context._inputs;
7173

74+
var lowercaseTrigger = context._triggerType && context._triggerType.toLowerCase();
75+
switch (lowercaseTrigger) {
76+
case "httptrigger":
77+
context.req = request(context);
78+
context.res = response(context);
79+
break;
80+
}
81+
delete context._triggerType;
82+
7283
var result = f.apply(null, inputs);
7384
if (result && util.isFunction(result.then)) {
7485
context._promise = true;
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
// Copyright (c) .NET Foundation. All rights reserved.
2+
// Licensed under the MIT License. See License.txt in the project root for license information.
3+
4+
module.exports = (context) => {
5+
var req = context.req;
6+
req.get = (field) => req.headers[field];
7+
return req;
8+
};
Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
// Copyright (c) .NET Foundation. All rights reserved.
2+
// Licensed under the MIT License. See License.txt in the project root for license information.
3+
4+
module.exports = (context) => {
5+
var res = {
6+
headers: {},
7+
8+
end: (body) => {
9+
if (body !== undefined) {
10+
res.body = body;
11+
}
12+
context.done();
13+
return res;
14+
},
15+
16+
status: (statusCode) => {
17+
res.statusCode = statusCode;
18+
return res;
19+
},
20+
21+
set: (field, val) => {
22+
res.headers[field] = val;
23+
return res;
24+
},
25+
26+
sendStatus: (statusCode) => {
27+
return res.status(statusCode)
28+
.end();
29+
},
30+
31+
type: (type) => {
32+
return res.set('Content-Type', type);
33+
},
34+
35+
json: (body) => {
36+
return res.type('application/json')
37+
.send(body);
38+
},
39+
40+
get: (field) => {
41+
return res.headers[field]
42+
}
43+
};
44+
45+
res.send = res.end;
46+
res.header = res.set;
47+
48+
return res;
49+
};

src/WebJobs.Script/Description/Node/NodeFunctionInvoker.cs

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -332,11 +332,11 @@ private Dictionary<string, object> CreateScriptExecutionContext(object input, Da
332332
!string.IsNullOrEmpty(httpBinding.WebHookType))
333333
{
334334
input = requestObject["body"];
335-
336-
// make the entire request object available as well
337-
// this is symmetric with context.res which we also support
338-
context["req"] = requestObject;
339335
}
336+
337+
// make the entire request object available as well
338+
// this is symmetric with context.res which we also support
339+
context["req"] = requestObject;
340340
}
341341
else if (input is TimerInfo)
342342
{
@@ -373,6 +373,8 @@ private Dictionary<string, object> CreateScriptExecutionContext(object input, Da
373373

374374
bindings.Add(_trigger.Name, input);
375375

376+
context.Add("_triggerType", _trigger.Type);
377+
376378
return context;
377379
}
378380

src/WebJobs.Script/WebJobs.Script.csproj

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -460,6 +460,12 @@
460460
<Link>edge\edge.js</Link>
461461
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
462462
</Content>
463+
<Content Include="Content\Script\http\response.js">
464+
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
465+
</Content>
466+
<Content Include="Content\Script\http\request.js">
467+
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
468+
</Content>
463469
<EmbeddedResource Include="Properties\Resources.resx">
464470
<Generator>ResXFileCodeGenerator</Generator>
465471
<LastGenOutput>Resources.Designer.cs</LastGenOutput>

test/WebJobs.Script.Tests/NodeEndToEndTests.cs

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -349,6 +349,45 @@ public async Task HttpTrigger_Get()
349349
Assert.Equal("Test Request Header", reqHeaders["test-header"]);
350350
}
351351

352+
[Fact]
353+
public async Task HttpTriggerExpressApi_Get()
354+
{
355+
HttpRequestMessage request = new HttpRequestMessage
356+
{
357+
RequestUri = new Uri(string.Format("http://localhost/api/httptrigger?name=Mathew%20Charles&location=Seattle")),
358+
Method = HttpMethod.Get,
359+
};
360+
request.SetConfiguration(new HttpConfiguration());
361+
request.Headers.Add("test-header", "Test Request Header");
362+
363+
Dictionary<string, object> arguments = new Dictionary<string, object>
364+
{
365+
{ "request", request }
366+
};
367+
await Fixture.Host.CallAsync("HttpTriggerExpressApi", arguments);
368+
369+
HttpResponseMessage response = (HttpResponseMessage)request.Properties[ScriptConstants.AzureFunctionsHttpResponseKey];
370+
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
371+
372+
Assert.Equal("Test Response Header", response.Headers.GetValues("test-header").SingleOrDefault());
373+
Assert.Equal(MediaTypeHeaderValue.Parse("application/json; charset=utf-8"), response.Content.Headers.ContentType);
374+
375+
string body = await response.Content.ReadAsStringAsync();
376+
JObject resultObject = JObject.Parse(body);
377+
Assert.Equal("undefined", (string)resultObject["reqBodyType"]);
378+
Assert.Null((string)resultObject["reqBody"]);
379+
Assert.Equal("undefined", (string)resultObject["reqRawBodyType"]);
380+
Assert.Null((string)resultObject["reqRawBody"]);
381+
382+
// verify binding data was populated from query parameters
383+
Assert.Equal("Mathew Charles", (string)resultObject["bindingData"]["name"]);
384+
Assert.Equal("Seattle", (string)resultObject["bindingData"]["location"]);
385+
386+
// validate input headers
387+
JObject reqHeaders = (JObject)resultObject["reqHeaders"];
388+
Assert.Equal("Test Request Header", reqHeaders["test-header"]);
389+
}
390+
352391
[Fact]
353392
public async Task HttpTriggerPromise_TestBinding()
354393
{
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
{
2+
"bindings": [
3+
{
4+
"type": "httpTrigger",
5+
"name": "request",
6+
"direction": "in",
7+
"methods": [ "get", "post" ]
8+
},
9+
{
10+
"type": "http",
11+
"name": "response",
12+
"direction": "out"
13+
}
14+
]
15+
}
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
var util = require('util');
2+
3+
module.exports = function (context, req) {
4+
context.log('Node.js HttpTrigger function invoked.');
5+
6+
context.res.status(200)
7+
.set('test-req-header', context.req.get('test-header'))
8+
.set('test-header', 'Test Response Header')
9+
.type('application/json; charset=utf-8')
10+
.send({
11+
reqBodyType: typeof req.body,
12+
reqBodyIsArray: util.isArray(req.body),
13+
reqBody: req.body,
14+
reqRawBodyType: typeof req.rawBody,
15+
reqRawBody: req.rawBody,
16+
reqHeaders: req.headers,
17+
bindingData: context.bindingData
18+
});
19+
}

test/WebJobs.Script.Tests/TestScripts/Node/functions.tests.js

Lines changed: 115 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,16 +3,108 @@
33
var config = process.argv[process.argv.length - 1]
44
config = (config.indexOf('--config=') !== -1)? config.substr(9): 'Debug';
55

6-
var functions = require('../../bin/' + config + '/Content/Script/functions.js');
6+
function testRequire(script) {
7+
return require('../../bin/' + config + '/Content/Script/' + script);
8+
}
79

8-
var context = {};
9-
var logs = [];
10+
var functions = testRequire('functions.js');
11+
var response = testRequire('http/response.js');
12+
var request = testRequire('http/request.js');
13+
14+
describe('http', () => {
15+
describe('response', () => {
16+
var res, context;
17+
beforeEach(() => {
18+
context = {
19+
isDone: false,
20+
done: () => context.isDone = true
21+
};
22+
res = response(context);
23+
});
24+
25+
it('status', () => {
26+
res.status(200);
27+
expect(res.statusCode).to.equal(200);
28+
expect(context.isDone).to.be.false;
29+
});
30+
31+
it('sendStatus', () => {
32+
res.sendStatus(204);
33+
expect(res.statusCode).to.equal(204);
34+
expect(context.isDone).to.be.true;
35+
});
36+
37+
it('end', () => {
38+
res.end('test');
39+
expect(res.body).to.equal('test');
40+
expect(context.isDone).to.be.true;
41+
});
42+
43+
it('send', () => {
44+
res.send('test');
45+
expect(res.body).to.equal('test');
46+
expect(context.isDone).to.be.true;
47+
});
48+
49+
it('json', () => {
50+
res.json('test');
51+
expect(res.body).to.equal('test');
52+
expect(res.get('Content-Type')).to.equal('application/json');
53+
expect(context.isDone).to.be.true;
54+
});
55+
56+
it('set', () => {
57+
res.set('header', 'val');
58+
expect(res.headers.header).to.equal('val');
59+
expect(context.isDone).to.be.false;
60+
});
61+
62+
it('header', () => {
63+
res.header('header', 'val');
64+
expect(res.headers.header).to.equal('val');
65+
expect(context.isDone).to.be.false;
66+
});
67+
68+
it('type', () => {
69+
res.type('text/html');
70+
expect(res.get('Content-Type')).to.equal('text/html');
71+
expect(context.isDone).to.be.false;
72+
});
73+
74+
it('get', () => {
75+
res.set('header', 'val');
76+
expect(res.get('header')).to.equal('val');
77+
expect(context.isDone).to.be.false;
78+
});
79+
});
80+
81+
describe('request', () => {
82+
var req, context;
83+
84+
beforeEach(() => {
85+
context = {
86+
req: {
87+
headers: { test: 'val' }
88+
}
89+
};
90+
91+
req = request(context);
92+
});
93+
94+
it('get', () => {
95+
expect(req.get('test')).to.equal('val');
96+
})
97+
});
98+
});
1099

11100
describe('functions', () => {
101+
var context = {};
102+
var logs = [];
12103
beforeEach(() => {
13104
logs = [];
14105
context = {
15106
_inputs: [],
107+
bindings: {},
16108
log: (message) => logs.push(message),
17109
bind: (val, cb) => cb && cb(val)
18110
};
@@ -144,5 +236,25 @@ describe('functions', () => {
144236
done();
145237
});
146238
});
239+
240+
it('attaches context.res and context.req', () => {
241+
var func = functions.createFunction((context) => {
242+
context.res.status(200)
243+
.header('header', context.req.get('field'))
244+
.send('test');
245+
});
246+
247+
context._triggerType = "httpTrigger";
248+
context.req = {
249+
headers: { 'field': 'val' }
250+
};
251+
func(context, (results) => {
252+
expect(context.res.statusCode).to.equal(200);
253+
expect(context.res.body).to.equal('test');
254+
expect(context.res.headers.header).to.equal('val');
255+
expect(context._http).to.be.undefined;
256+
expect(context._done).to.be.true;
257+
});
258+
});
147259
});
148260
});

0 commit comments

Comments
 (0)