Skip to content

Commit a224c13

Browse files
authored
feat: prototype cloudevent function signature type (#147)
* feat: prototype cloudevent function signature type * fix: use SignatureType in helper message * fix: inline checks for signature type The type guards like `isHttpFunction` is only used one so it's better to just inline them. * fix: Populate cloudevent.data from the incoming request * fix: Remove unused test functions * docs: Update comments and docs re. cloudevent signature type * docs: minor README edits * fix: Simplify test for cloudevent functions * docs: use relative link to `cloudevents.md`
1 parent a574422 commit a224c13

File tree

9 files changed

+281
-106
lines changed

9 files changed

+281
-106
lines changed

README.md

+38-16
Original file line numberDiff line numberDiff line change
@@ -33,11 +33,11 @@ handling logic.
3333
3434
# Features
3535

36-
* Spin up a local development server for quick testing
37-
* Invoke a function in response to a request
38-
* Automatically unmarshal events conforming to the
39-
[CloudEvents](https://cloudevents.io/) spec
40-
* Portable between serverless platforms
36+
- Spin up a local development server for quick testing
37+
- Invoke a function in response to a request
38+
- Automatically unmarshal events conforming to the
39+
[CloudEvents](https://cloudevents.io/) spec
40+
- Portable between serverless platforms
4141

4242
# Installation
4343

@@ -63,8 +63,7 @@ Run the following command:
6363
npx @google-cloud/functions-framework --target=helloWorld
6464
```
6565

66-
Open http://localhost:8080/ in your browser and see *Hello, World*.
67-
66+
Open http://localhost:8080/ in your browser and see _Hello, World_.
6867

6968
# Quickstart: Set up a new project
7069

@@ -144,12 +143,12 @@ You can configure the Functions Framework using command-line flags or
144143
environment variables. If you specify both, the environment variable will be
145144
ignored.
146145

147-
Command-line flag | Environment variable | Description
148-
------------------------- | ------------------------- | -----------
149-
`--port` | `PORT` | The port on which the Functions Framework listens for requests. Default: `8080`
150-
`--target` | `FUNCTION_TARGET` | The name of the exported function to be invoked in response to requests. Default: `function`
151-
`--signature-type` | `FUNCTION_SIGNATURE_TYPE` | The signature used when writing your function. Controls unmarshalling rules and determines which arguments are used to invoke your function. Default: `http`; accepted values: `http` or `event`
152-
`--source` | `FUNCTION_SOURCE` | The path to the directory of your function. Default: `cwd` (the current working directory)
146+
| Command-line flag | Environment variable | Description |
147+
| ------------------ | ------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
148+
| `--port` | `PORT` | The port on which the Functions Framework listens for requests. Default: `8080` |
149+
| `--target` | `FUNCTION_TARGET` | The name of the exported function to be invoked in response to requests. Default: `function` |
150+
| `--signature-type` | `FUNCTION_SIGNATURE_TYPE` | The signature used when writing your function. Controls unmarshalling rules and determines which arguments are used to invoke your function. Default: `http`; accepted values: `http` or `event` or `cloudevent` |
151+
| `--source` | `FUNCTION_SOURCE` | The path to the directory of your function. Default: `cwd` (the current working directory) |
153152

154153
You can set command-line flags in your `package.json` via the `start` script.
155154
For example:
@@ -160,12 +159,12 @@ For example:
160159
}
161160
```
162161

163-
# Enable CloudEvents
162+
# Enable Google Cloud Functions Events
164163

165164
The Functions Framework can unmarshall incoming
166-
[CloudEvents](http://cloudevents.io) payloads to `data` and `context` objects.
165+
Google Cloud Functions [event](https://cloud.google.com/functions/docs/concepts/events-triggers#events) payloads to `data` and `context` objects.
167166
These will be passed as arguments to your function when it receives a request.
168-
Note that your function must use the event-style function signature:
167+
Note that your function must use the `event`-style function signature:
169168

170169
```js
171170
exports.helloEvents = (data, context) => {
@@ -182,6 +181,29 @@ For more details on this signature type, check out the Google Cloud Functions
182181
documentation on
183182
[background functions](https://cloud.google.com/functions/docs/writing/background#cloud_pubsub_example).
184183

184+
# Enable CloudEvents
185+
186+
The Functions Framework can unmarshall incoming
187+
[CloudEvents](http://cloudevents.io) payloads to a `cloudevent` object.
188+
It will be passed as an argument to your function when it receives a request.
189+
Note that your function must use the `cloudevent`-style function signature:
190+
191+
```js
192+
exports.helloCloudEvents = (cloudevent) => {
193+
console.log(cloudevent.specversion);
194+
console.log(cloudevent.type);
195+
console.log(cloudevent.source);
196+
console.log(cloudevent.subject);
197+
console.log(cloudevent.id);
198+
console.log(cloudevent.time);
199+
console.log(cloudevent.datacontenttype);
200+
};
201+
```
202+
203+
To enable CloudEvents, set the signature type to `cloudevent`. By default, the HTTP signature will be used and automatic event unmarshalling will be disabled.
204+
205+
Learn how to use CloudEvents in this [guide](docs/cloudevents.md).
206+
185207
# Advanced Docs
186208

187209
More advanced guides and docs can be found in the [`docs/` folder](docs/).

docs/cloudevents.md

+46
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
# CloudEvents
2+
3+
This guide shows you how to use the Functions Framework for local testing with:
4+
5+
- CloudEvents
6+
- [CloudEvents Conformance Testing](https://github.com/cloudevents/conformance)
7+
8+
## Local Testing of CloudEvents
9+
10+
In your `package.json`, specify `--signature-type=cloudevent"` for the `functions-framework`:
11+
12+
```sh
13+
{
14+
"scripts": {
15+
"start": "functions-framework --target=helloCloudEvents --signature-type=cloudevent"
16+
}
17+
}
18+
```
19+
20+
Create an `index.js` file:
21+
22+
```js
23+
exports.helloCloudEvents = (cloudevent) => {
24+
console.log(cloudevent.specversion);
25+
console.log(cloudevent.type);
26+
console.log(cloudevent.source);
27+
console.log(cloudevent.subject);
28+
console.log(cloudevent.id);
29+
console.log(cloudevent.time);
30+
console.log(cloudevent.datacontenttype);
31+
}
32+
```
33+
34+
Start the Functions Framework:
35+
36+
```sh
37+
npm start
38+
```
39+
40+
Your function will be serving at `http://localhost:8080/`, however,
41+
it is no longer accessible via `HTTP GET` requests from the browser.
42+
43+
### Create and send a cloudevent to the function
44+
```
45+
cloudevents send http://localhost:8080 --specver--id abc-123 --source cloudevents.conformance.tool --type foo.bar
46+
```

src/functions.ts

+11-1
Original file line numberDiff line numberDiff line change
@@ -26,10 +26,20 @@ export interface EventFunctionWithCallback {
2626
// tslint:disable-next-line:no-any
2727
(data: {}, context: Context, callback: Function): any;
2828
}
29+
export interface CloudEventFunction {
30+
// tslint:disable-next-line:no-any
31+
(cloudevent: CloudEventsContext): any;
32+
}
33+
export interface CloudEventFunctionWithCallback {
34+
// tslint:disable-next-line:no-any
35+
(cloudevent: CloudEventsContext, callback: Function): any;
36+
}
2937
export type HandlerFunction =
3038
| HttpFunction
3139
| EventFunction
32-
| EventFunctionWithCallback;
40+
| EventFunctionWithCallback
41+
| CloudEventFunction
42+
| CloudEventFunctionWithCallback;
3343

3444
/**
3545
* The Cloud Functions context object for the event.

src/index.ts

+12-4
Original file line numberDiff line numberDiff line change
@@ -23,9 +23,13 @@
2323
// node module to execute. If such a function is not defined,
2424
// then falls back to 'function' name.
2525
// - FUNCTION_SIGNATURE_TYPE - defines the type of the client function
26-
// signature, 'http' for function signature with HTTP request and HTTP
27-
// response arguments, or 'event' for function signature with arguments
28-
// unmarshalled from an incoming request.
26+
// signature:
27+
// - 'http' for function signature with HTTP request and HTTP
28+
// response arguments,
29+
// - 'event' for function signature with arguments
30+
// unmarshalled from an incoming request,
31+
// - 'cloudevent' for function signature with arguments
32+
// unmarshalled as CloudEvents from an incoming request.
2933

3034
import * as minimist from 'minimist';
3135
import { resolve } from 'path';
@@ -71,7 +75,11 @@ const SIGNATURE_TYPE =
7175
SIGNATURE_TYPE_STRING.toUpperCase() as keyof typeof SignatureType
7276
];
7377
if (SIGNATURE_TYPE === undefined) {
74-
console.error(`Function signature type must be one of 'http' or 'event'.`);
78+
console.error(
79+
`Function signature type must be one of: ${Object.values(
80+
SignatureType
81+
).join(', ')}.`
82+
);
7583
process.exit(1);
7684
}
7785

src/invoker.ts

+70-18
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,8 @@ import {
3333
HttpFunction,
3434
EventFunction,
3535
EventFunctionWithCallback,
36+
CloudEventFunction,
37+
CloudEventFunctionWithCallback,
3638
HandlerFunction,
3739
} from './functions';
3840

@@ -47,21 +49,9 @@ declare global {
4749
}
4850

4951
export enum SignatureType {
50-
HTTP,
51-
EVENT,
52-
}
53-
54-
/**
55-
* Checks whether the given user's function is an HTTP function.
56-
* @param fn User's function.
57-
* @param functionSignatureType Type of user's function signature.
58-
* @return True if user's function is an HTTP function, false otherwise.
59-
*/
60-
function isHttpFunction(
61-
fn: HandlerFunction,
62-
functionSignatureType: SignatureType
63-
): fn is HttpFunction {
64-
return functionSignatureType === SignatureType.HTTP;
52+
HTTP = 'http',
53+
EVENT = 'event',
54+
CLOUDEVENT = 'cloudevent',
6555
}
6656

6757
// Response object for the most recent request.
@@ -130,6 +120,58 @@ function makeHttpHandler(execute: HttpFunction): express.RequestHandler {
130120
};
131121
}
132122

123+
/**
124+
* Wraps cloudevent function (or cloudevent function with callback) in HTTP function
125+
* signature.
126+
* @param userFunction User's function.
127+
* @return HTTP function which wraps the provided event function.
128+
*/
129+
function wrapCloudEventFunction(
130+
userFunction: CloudEventFunction | CloudEventFunctionWithCallback
131+
): HttpFunction {
132+
return (req: express.Request, res: express.Response) => {
133+
const callback = process.domain.bind(
134+
// tslint:disable-next-line:no-any
135+
(err: Error | null, result: any) => {
136+
if (res.locals.functionExecutionFinished) {
137+
console.log('Ignoring extra callback call');
138+
} else {
139+
res.locals.functionExecutionFinished = true;
140+
if (err) {
141+
console.error(err.stack);
142+
}
143+
sendResponse(result, err, res);
144+
}
145+
}
146+
);
147+
let cloudevent = req.body;
148+
if (isBinaryCloudEvent(req)) {
149+
cloudevent = getBinaryCloudEventContext(req);
150+
cloudevent.data = req.body;
151+
}
152+
// Callback style if user function has more than 2 arguments.
153+
if (userFunction!.length > 2) {
154+
const fn = userFunction as CloudEventFunctionWithCallback;
155+
return fn(cloudevent, callback);
156+
}
157+
158+
const fn = userFunction as CloudEventFunction;
159+
Promise.resolve()
160+
.then(() => {
161+
const result = fn(cloudevent);
162+
return result;
163+
})
164+
.then(
165+
result => {
166+
callback(null, result);
167+
},
168+
err => {
169+
callback(err, undefined);
170+
}
171+
);
172+
};
173+
}
174+
133175
/**
134176
* Wraps event function (or event function with callback) in HTTP function
135177
* signature.
@@ -206,7 +248,7 @@ function registerFunctionRoutes(
206248
userFunction: HandlerFunction,
207249
functionSignatureType: SignatureType
208250
) {
209-
if (isHttpFunction(userFunction!, functionSignatureType)) {
251+
if (functionSignatureType === SignatureType.HTTP) {
210252
app.use('/favicon.ico|/robots.txt', (req, res, next) => {
211253
res.sendStatus(404);
212254
});
@@ -219,12 +261,22 @@ function registerFunctionRoutes(
219261
});
220262

221263
app.all('/*', (req, res, next) => {
222-
const handler = makeHttpHandler(userFunction);
264+
const handler = makeHttpHandler(userFunction as HttpFunction);
265+
handler(req, res, next);
266+
});
267+
} else if (functionSignatureType === SignatureType.EVENT) {
268+
app.post('/*', (req, res, next) => {
269+
const wrappedUserFunction = wrapEventFunction(userFunction as
270+
| EventFunction
271+
| EventFunctionWithCallback);
272+
const handler = makeHttpHandler(wrappedUserFunction);
223273
handler(req, res, next);
224274
});
225275
} else {
226276
app.post('/*', (req, res, next) => {
227-
const wrappedUserFunction = wrapEventFunction(userFunction);
277+
const wrappedUserFunction = wrapCloudEventFunction(userFunction as
278+
| CloudEventFunction
279+
| CloudEventFunctionWithCallback);
228280
const handler = makeHttpHandler(wrappedUserFunction);
229281
handler(req, res, next);
230282
});

test/data/with_main/foo.js

+2-13
Original file line numberDiff line numberDiff line change
@@ -4,21 +4,10 @@
44
* @param {!Object} req request context.
55
* @param {!Object} res response context.
66
*/
7-
function testHttpFunction (res, req) {
7+
function testFunction (req, res) {
88
return 'PASS'
99
};
1010

11-
/**
12-
* Test event function to test function loading.
13-
*
14-
* @param {!Object} data event payload.
15-
* @param {!Object} context event metadata.
16-
*/
17-
function testEventFunction (data, context) {
18-
return 'PASS';
19-
};
20-
2111
module.exports = {
22-
testHttpFunction,
23-
testEventFunction,
12+
testFunction,
2413
}

test/data/without_main/function.js

+2-13
Original file line numberDiff line numberDiff line change
@@ -4,21 +4,10 @@
44
* @param {!Object} req request context.
55
* @param {!Object} res response context.
66
*/
7-
function testHttpFunction (res, req) {
7+
function testFunction (req, res) {
88
return 'PASS'
99
};
1010

11-
/**
12-
* Test event function to test function loading.
13-
*
14-
* @param {!Object} data event payload.
15-
* @param {!Object} context event metadata.
16-
*/
17-
function testEventFunction (data, context) {
18-
return 'PASS';
19-
};
20-
2111
module.exports = {
22-
testHttpFunction,
23-
testEventFunction,
12+
testFunction,
2413
}

0 commit comments

Comments
 (0)