Skip to content

feat: prototype cloudevent function signature type #147

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 9 commits into from
Jun 5, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
54 changes: 38 additions & 16 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -33,11 +33,11 @@ handling logic.

# Features

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

# Installation

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

Open http://localhost:8080/ in your browser and see *Hello, World*.

Open http://localhost:8080/ in your browser and see _Hello, World_.

# Quickstart: Set up a new project

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

Command-line flag | Environment variable | Description
------------------------- | ------------------------- | -----------
`--port` | `PORT` | The port on which the Functions Framework listens for requests. Default: `8080`
`--target` | `FUNCTION_TARGET` | The name of the exported function to be invoked in response to requests. Default: `function`
`--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`
`--source` | `FUNCTION_SOURCE` | The path to the directory of your function. Default: `cwd` (the current working directory)
| Command-line flag | Environment variable | Description |
| ------------------ | ------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `--port` | `PORT` | The port on which the Functions Framework listens for requests. Default: `8080` |
| `--target` | `FUNCTION_TARGET` | The name of the exported function to be invoked in response to requests. Default: `function` |
| `--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` |
| `--source` | `FUNCTION_SOURCE` | The path to the directory of your function. Default: `cwd` (the current working directory) |

You can set command-line flags in your `package.json` via the `start` script.
For example:
Expand All @@ -160,12 +159,12 @@ For example:
}
```

# Enable CloudEvents
# Enable Google Cloud Functions Events

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

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

# Enable CloudEvents

The Functions Framework can unmarshall incoming
[CloudEvents](http://cloudevents.io) payloads to a `cloudevent` object.
It will be passed as an argument to your function when it receives a request.
Note that your function must use the `cloudevent`-style function signature:

```js
exports.helloCloudEvents = (cloudevent) => {
console.log(cloudevent.specversion);
console.log(cloudevent.type);
console.log(cloudevent.source);
console.log(cloudevent.subject);
console.log(cloudevent.id);
console.log(cloudevent.time);
console.log(cloudevent.datacontenttype);
};
```

To enable CloudEvents, set the signature type to `cloudevent`. By default, the HTTP signature will be used and automatic event unmarshalling will be disabled.

Learn how to use CloudEvents in this [guide](docs/cloudevents.md).

# Advanced Docs

More advanced guides and docs can be found in the [`docs/` folder](docs/).
Expand Down
46 changes: 46 additions & 0 deletions docs/cloudevents.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
# CloudEvents

This guide shows you how to use the Functions Framework for local testing with:

- CloudEvents
- [CloudEvents Conformance Testing](https://github.com/cloudevents/conformance)

## Local Testing of CloudEvents

In your `package.json`, specify `--signature-type=cloudevent"` for the `functions-framework`:

```sh
{
"scripts": {
"start": "functions-framework --target=helloCloudEvents --signature-type=cloudevent"
}
}
```

Create an `index.js` file:

```js
exports.helloCloudEvents = (cloudevent) => {
console.log(cloudevent.specversion);
console.log(cloudevent.type);
console.log(cloudevent.source);
console.log(cloudevent.subject);
console.log(cloudevent.id);
console.log(cloudevent.time);
console.log(cloudevent.datacontenttype);
}
```

Start the Functions Framework:

```sh
npm start
```

Your function will be serving at `http://localhost:8080/`, however,
it is no longer accessible via `HTTP GET` requests from the browser.

### Create and send a cloudevent to the function
```
cloudevents send http://localhost:8080 --specver--id abc-123 --source cloudevents.conformance.tool --type foo.bar
```
12 changes: 11 additions & 1 deletion src/functions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,10 +26,20 @@ export interface EventFunctionWithCallback {
// tslint:disable-next-line:no-any
(data: {}, context: Context, callback: Function): any;
}
export interface CloudEventFunction {
// tslint:disable-next-line:no-any
(cloudevent: CloudEventsContext): any;
}
export interface CloudEventFunctionWithCallback {
// tslint:disable-next-line:no-any
(cloudevent: CloudEventsContext, callback: Function): any;
}
export type HandlerFunction =
| HttpFunction
| EventFunction
| EventFunctionWithCallback;
| EventFunctionWithCallback
| CloudEventFunction
| CloudEventFunctionWithCallback;

/**
* The Cloud Functions context object for the event.
Expand Down
16 changes: 12 additions & 4 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,9 +23,13 @@
// node module to execute. If such a function is not defined,
// then falls back to 'function' name.
// - FUNCTION_SIGNATURE_TYPE - defines the type of the client function
// signature, 'http' for function signature with HTTP request and HTTP
// response arguments, or 'event' for function signature with arguments
// unmarshalled from an incoming request.
// signature:
// - 'http' for function signature with HTTP request and HTTP
// response arguments,
// - 'event' for function signature with arguments
// unmarshalled from an incoming request,
// - 'cloudevent' for function signature with arguments
// unmarshalled as CloudEvents from an incoming request.

import * as minimist from 'minimist';
import { resolve } from 'path';
Expand Down Expand Up @@ -71,7 +75,11 @@ const SIGNATURE_TYPE =
SIGNATURE_TYPE_STRING.toUpperCase() as keyof typeof SignatureType
];
if (SIGNATURE_TYPE === undefined) {
console.error(`Function signature type must be one of 'http' or 'event'.`);
console.error(
`Function signature type must be one of: ${Object.values(
SignatureType
).join(', ')}.`
);
process.exit(1);
}

Expand Down
88 changes: 70 additions & 18 deletions src/invoker.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,8 @@ import {
HttpFunction,
EventFunction,
EventFunctionWithCallback,
CloudEventFunction,
CloudEventFunctionWithCallback,
HandlerFunction,
} from './functions';

Expand All @@ -47,21 +49,9 @@ declare global {
}

export enum SignatureType {
HTTP,
EVENT,
}

/**
* Checks whether the given user's function is an HTTP function.
* @param fn User's function.
* @param functionSignatureType Type of user's function signature.
* @return True if user's function is an HTTP function, false otherwise.
*/
function isHttpFunction(
fn: HandlerFunction,
functionSignatureType: SignatureType
): fn is HttpFunction {
return functionSignatureType === SignatureType.HTTP;
HTTP = 'http',
EVENT = 'event',
CLOUDEVENT = 'cloudevent',
}

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

/**
* Wraps cloudevent function (or cloudevent function with callback) in HTTP function
* signature.
* @param userFunction User's function.
* @return HTTP function which wraps the provided event function.
*/
function wrapCloudEventFunction(
userFunction: CloudEventFunction | CloudEventFunctionWithCallback
): HttpFunction {
return (req: express.Request, res: express.Response) => {
const callback = process.domain.bind(
// tslint:disable-next-line:no-any
(err: Error | null, result: any) => {
if (res.locals.functionExecutionFinished) {
console.log('Ignoring extra callback call');
} else {
res.locals.functionExecutionFinished = true;
if (err) {
console.error(err.stack);
}
sendResponse(result, err, res);
}
}
);
let cloudevent = req.body;
if (isBinaryCloudEvent(req)) {
cloudevent = getBinaryCloudEventContext(req);
cloudevent.data = req.body;
}
// Callback style if user function has more than 2 arguments.
if (userFunction!.length > 2) {
const fn = userFunction as CloudEventFunctionWithCallback;
return fn(cloudevent, callback);
}

const fn = userFunction as CloudEventFunction;
Promise.resolve()
.then(() => {
const result = fn(cloudevent);
return result;
})
.then(
result => {
callback(null, result);
},
err => {
callback(err, undefined);
}
);
};
}

/**
* Wraps event function (or event function with callback) in HTTP function
* signature.
Expand Down Expand Up @@ -206,7 +248,7 @@ function registerFunctionRoutes(
userFunction: HandlerFunction,
functionSignatureType: SignatureType
) {
if (isHttpFunction(userFunction!, functionSignatureType)) {
if (functionSignatureType === SignatureType.HTTP) {
app.use('/favicon.ico|/robots.txt', (req, res, next) => {
res.sendStatus(404);
});
Expand All @@ -219,12 +261,22 @@ function registerFunctionRoutes(
});

app.all('/*', (req, res, next) => {
const handler = makeHttpHandler(userFunction);
const handler = makeHttpHandler(userFunction as HttpFunction);
handler(req, res, next);
});
} else if (functionSignatureType === SignatureType.EVENT) {
app.post('/*', (req, res, next) => {
const wrappedUserFunction = wrapEventFunction(userFunction as
| EventFunction
| EventFunctionWithCallback);
const handler = makeHttpHandler(wrappedUserFunction);
handler(req, res, next);
});
} else {
app.post('/*', (req, res, next) => {
const wrappedUserFunction = wrapEventFunction(userFunction);
const wrappedUserFunction = wrapCloudEventFunction(userFunction as
| CloudEventFunction
| CloudEventFunctionWithCallback);
const handler = makeHttpHandler(wrappedUserFunction);
handler(req, res, next);
});
Expand Down
15 changes: 2 additions & 13 deletions test/data/with_main/foo.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,21 +4,10 @@
* @param {!Object} req request context.
* @param {!Object} res response context.
*/
function testHttpFunction (res, req) {
function testFunction (req, res) {
return 'PASS'
};

/**
* Test event function to test function loading.
*
* @param {!Object} data event payload.
* @param {!Object} context event metadata.
*/
function testEventFunction (data, context) {
return 'PASS';
};

module.exports = {
testHttpFunction,
testEventFunction,
testFunction,
}
15 changes: 2 additions & 13 deletions test/data/without_main/function.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,21 +4,10 @@
* @param {!Object} req request context.
* @param {!Object} res response context.
*/
function testHttpFunction (res, req) {
function testFunction (req, res) {
return 'PASS'
};

/**
* Test event function to test function loading.
*
* @param {!Object} data event payload.
* @param {!Object} context event metadata.
*/
function testEventFunction (data, context) {
return 'PASS';
};

module.exports = {
testHttpFunction,
testEventFunction,
testFunction,
}
Loading