Skip to content

Commit

Permalink
feat: introduce declarative function signatures (#347)
Browse files Browse the repository at this point in the history
This commit introduces an API that allows developers to explictly register
their functions against the framework so that they can declaratively
configure thier signature type.
  • Loading branch information
matthewrobertson committed Oct 8, 2021
1 parent 51cb666 commit db1ba9e
Show file tree
Hide file tree
Showing 7 changed files with 249 additions and 58 deletions.
2 changes: 1 addition & 1 deletion package.json
Expand Up @@ -7,7 +7,7 @@
},
"repository": "GoogleCloudPlatform/functions-framework-nodejs",
"main": "build/src/index.js",
"types": "build/src/functions.d.ts",
"types": "build/src/index.d.ts",
"dependencies": {
"body-parser": "^1.18.3",
"express": "^4.16.4",
Expand Down
73 changes: 73 additions & 0 deletions src/function_registry.ts
@@ -0,0 +1,73 @@
// Copyright 2021 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

import {HttpFunction, CloudEventFunction, HandlerFunction} from './functions';
import {SignatureType} from './types';

interface RegisteredFunction {
signatureType: SignatureType;
userFunction: HandlerFunction;
}

/**
* Singleton map to hold the registered functions
*/
const registrationContainer = new Map<string, RegisteredFunction>();

/**
* Helper method to store a registered function in the registration container
*/
const register = (
functionName: string,
signatureType: SignatureType,
userFunction: HandlerFunction
): void => {
registrationContainer.set(functionName, {
signatureType,
userFunction,
});
};

/**
* Get a declaratively registered function
* @param functionName the name with which the function was registered
* @returns the registered function and signature type or undefined no function matching
* the provided name has been registered.
*/
export const getRegisteredFunction = (
functionName: string
): RegisteredFunction | undefined => {
return registrationContainer.get(functionName);
};

/**
* Register a function that responds to HTTP requests.
* @param functionName the name of the function
* @param handler the function to invoke when handling HTTP requests
*/
export const http = (functionName: string, handler: HttpFunction): void => {
register(functionName, 'http', handler);
};

/**
* Register a function that handles CloudEvents.
* @param functionName the name of the function
* @param handler the function to trigger when handling cloudevents
*/
export const cloudevent = (
functionName: string,
handler: CloudEventFunction
): void => {
register(functionName, 'cloudevent', handler);
};
50 changes: 6 additions & 44 deletions src/index.ts
Expand Up @@ -14,49 +14,11 @@
// See the License for the specific language governing permissions and
// limitations under the License.

// Functions framework entry point that configures and starts Node.js server
// that runs user's code on HTTP request.
import {getUserFunction} from './loader';
import {ErrorHandler} from './invoker';
import {getServer} from './server';
import {parseOptions, helpText, OptionsError} from './options';
import {main} from './main';

(async () => {
try {
const options = parseOptions();
export * from './functions';

if (options.printHelp) {
console.error(helpText);
return;
}
const userFunction = await getUserFunction(
options.sourceLocation,
options.target
);
if (!userFunction) {
console.error('Could not load the function, shutting down.');
// eslint-disable-next-line no-process-exit
process.exit(1);
}
const server = getServer(userFunction!, options.signatureType);
const errorHandler = new ErrorHandler(server);
server
.listen(options.port, () => {
errorHandler.register();
if (process.env.NODE_ENV !== 'production') {
console.log('Serving function...');
console.log(`Function: ${options.target}`);
console.log(`Signature type: ${options.signatureType}`);
console.log(`URL: http://localhost:${options.port}/`);
}
})
.setTimeout(0); // Disable automatic timeout on incoming connections.
} catch (e) {
if (e instanceof OptionsError) {
console.error(e.message);
// eslint-disable-next-line no-process-exit
process.exit(1);
}
throw e;
}
})();
export {http, cloudevent} from './function_registry';

// Call the main method to load the user code and start the http server.
main();
22 changes: 16 additions & 6 deletions src/loader.ts
Expand Up @@ -22,10 +22,9 @@ import * as path from 'path';
import * as semver from 'semver';
import * as readPkgUp from 'read-pkg-up';
import {pathToFileURL} from 'url';
/**
* Import function signature type's definition.
*/
import {HandlerFunction} from './functions';
import {SignatureType} from './types';
import {getRegisteredFunction} from './function_registry';

// Dynamic import function required to load user code packaged as an
// ES module is only available on Node.js v13.2.0 and up.
Expand Down Expand Up @@ -85,8 +84,12 @@ const dynamicImport = new Function(
*/
export async function getUserFunction(
codeLocation: string,
functionTarget: string
): Promise<HandlerFunction | null> {
functionTarget: string,
signatureType: SignatureType
): Promise<{
userFunction: HandlerFunction;
signatureType: SignatureType;
} | null> {
try {
const functionModulePath = getFunctionModulePath(codeLocation);
if (functionModulePath === null) {
Expand All @@ -111,6 +114,13 @@ export async function getUserFunction(
functionModule = require(functionModulePath);
}

// If the customer declaratively registered a function matching the target
// return that.
const registeredFunction = getRegisteredFunction(functionTarget);
if (registeredFunction) {
return registeredFunction;
}

let userFunction = functionTarget
.split('.')
.reduce((code, functionTargetPart) => {
Expand Down Expand Up @@ -143,7 +153,7 @@ export async function getUserFunction(
return null;
}

return userFunction as HandlerFunction;
return {userFunction: userFunction as HandlerFunction, signatureType};
} catch (ex) {
let additionalHint: string;
// TODO: this should be done based on ex.code rather than string matching.
Expand Down
66 changes: 66 additions & 0 deletions src/main.ts
@@ -0,0 +1,66 @@
// Copyright 2019 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

// Functions framework entry point that configures and starts Node.js server
// that runs user's code on HTTP request.
import {getUserFunction} from './loader';
import {ErrorHandler} from './invoker';
import {getServer} from './server';
import {parseOptions, helpText, OptionsError} from './options';

/**
* Main entrypoint for the functions framework that loads the user's function
* and starts the HTTP server.
*/
export const main = async () => {
try {
const options = parseOptions();

if (options.printHelp) {
console.error(helpText);
return;
}
const loadedFunction = await getUserFunction(
options.sourceLocation,
options.target,
options.signatureType
);
if (!loadedFunction) {
console.error('Could not load the function, shutting down.');
// eslint-disable-next-line no-process-exit
process.exit(1);
}
const {userFunction, signatureType} = loadedFunction;
const server = getServer(userFunction!, signatureType);
const errorHandler = new ErrorHandler(server);
server
.listen(options.port, () => {
errorHandler.register();
if (process.env.NODE_ENV !== 'production') {
console.log('Serving function...');
console.log(`Function: ${options.target}`);
console.log(`Signature type: ${signatureType}`);
console.log(`URL: http://localhost:${options.port}/`);
}
})
.setTimeout(0); // Disable automatic timeout on incoming connections.
} catch (e) {
if (e instanceof OptionsError) {
console.error(e.message);
// eslint-disable-next-line no-process-exit
process.exit(1);
}
throw e;
}
};
37 changes: 37 additions & 0 deletions test/function_registry.ts
@@ -0,0 +1,37 @@
// Copyright 2019 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
import * as assert from 'assert';
import * as FunctionRegistry from '../src/function_registry';

describe('function_registry', () => {
it('can register http functions', () => {
FunctionRegistry.http('httpFunction', () => 'HTTP_PASS');
const {
userFunction,
signatureType,
} = FunctionRegistry.getRegisteredFunction('httpFunction')!;
assert.deepStrictEqual('http', signatureType);
assert.deepStrictEqual((userFunction as () => string)(), 'HTTP_PASS');
});

it('can register cloudevent functions', () => {
FunctionRegistry.cloudevent('ceFunction', () => 'CE_PASS');
const {
userFunction,
signatureType,
} = FunctionRegistry.getRegisteredFunction('ceFunction')!;
assert.deepStrictEqual('cloudevent', signatureType);
assert.deepStrictEqual((userFunction as () => string)(), 'CE_PASS');
});
});
57 changes: 50 additions & 7 deletions test/loader.ts
Expand Up @@ -17,6 +17,7 @@ import * as express from 'express';
import * as semver from 'semver';
import * as functions from '../src/functions';
import * as loader from '../src/loader';
import * as FunctionRegistry from '../src/function_registry';

describe('loading function', () => {
interface TestData {
Expand All @@ -40,11 +41,13 @@ describe('loading function', () => {

for (const test of testData) {
it(`should load ${test.name}`, async () => {
const loadedFunction = (await loader.getUserFunction(
const loadedFunction = await loader.getUserFunction(
process.cwd() + test.codeLocation,
test.target
)) as functions.HttpFunction;
const returned = loadedFunction(express.request, express.response);
test.target,
'http'
);
const userFunction = loadedFunction?.userFunction as functions.HttpFunction;
const returned = userFunction(express.request, express.response);
assert.strictEqual(returned, 'PASS');
});
}
Expand All @@ -69,10 +72,12 @@ describe('loading function', () => {

for (const test of esmTestData) {
const loadFn: () => Promise<functions.HttpFunction> = async () => {
return loader.getUserFunction(
const loadedFunction = await loader.getUserFunction(
process.cwd() + test.codeLocation,
test.target
) as Promise<functions.HttpFunction>;
test.target,
'http'
);
return loadedFunction?.userFunction as functions.HttpFunction;
};
if (semver.lt(process.version, loader.MIN_NODE_VERSION_ESMODULES)) {
it(`should fail to load function in an ES module ${test.name}`, async () => {
Expand All @@ -86,4 +91,42 @@ describe('loading function', () => {
});
}
}

it('loads a declaratively registered function', async () => {
FunctionRegistry.http('registeredFunction', () => {
return 'PASS';
});
const loadedFunction = await loader.getUserFunction(
process.cwd() + '/test/data/with_main',
'registeredFunction',
'http'
);
const userFunction = loadedFunction?.userFunction as functions.HttpFunction;
const returned = userFunction(express.request, express.response);
assert.strictEqual(returned, 'PASS');
});

it('allows a mix of registered and non registered functions', async () => {
FunctionRegistry.http('registeredFunction', () => {
return 'FAIL';
});
const loadedFunction = await loader.getUserFunction(
process.cwd() + '/test/data/with_main',
'testFunction',
'http'
);
const userFunction = loadedFunction?.userFunction as functions.HttpFunction;
const returned = userFunction(express.request, express.response);
assert.strictEqual(returned, 'PASS');
});

it('respects the registered signature type', async () => {
FunctionRegistry.cloudevent('registeredFunction', () => {});
const loadedFunction = await loader.getUserFunction(
process.cwd() + '/test/data/with_main',
'registeredFunction',
'http'
);
assert.strictEqual(loadedFunction?.signatureType, 'cloudevent');
});
});

0 comments on commit db1ba9e

Please sign in to comment.