Skip to content

Commit

Permalink
feat: introduce declarative function signatures
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 978b26c
Show file tree
Hide file tree
Showing 7 changed files with 253 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
52 changes: 8 additions & 44 deletions src/index.ts
Expand Up @@ -14,49 +14,13 @@
// 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();
import {registrationAPI} from './registration_container';

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 * from './functions';

export default registrationAPI;

// 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 './registration_container';

// 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
62 changes: 62 additions & 0 deletions src/main.ts
@@ -0,0 +1,62 @@
// 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';

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;
}
};
80 changes: 80 additions & 0 deletions src/registration_container.ts
@@ -0,0 +1,80 @@
// 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 regsitration 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 restered 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);
};

/**
* The declative function registration API exposed as a default export
* from the root of the npm package.
*/
export const registrationAPI = {
/**
* Register a function that repsponds to HTTP requests. See
* [Writing HTTP Functions]{@link https://cloud.google.com/functions/docs/http} for
* more details.
* @param functionName the name of the function
* @param handler the function to invoke when handling HTTP requests
*/
http(functionName: string, handler: HttpFunction): void {
register(functionName, 'http', handler);
},

/**
* Register a function that handles cloudevents. See
* [Writing CloudEvent Functions]{@link https://cloud.google.com/functions/docs/writing}
* for more details.
* @param functionName the name of the function
* @param handler the function to trigger when handling cloudevents
*/
cloudevent(functionName: string, handler: CloudEventFunction): void {
register(functionName, 'cloudevent', handler);
},
};
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 {registrationAPI} from '../src/registration_container';

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 () => {
registrationAPI.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 registed and non registered functions', async () => {
registrationAPI.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 () => {
registrationAPI.cloudevent('registeredFunction', () => {});
const loadedFunction = await loader.getUserFunction(
process.cwd() + '/test/data/with_main',
'registeredFunction',
'http'
);
assert.strictEqual(loadedFunction?.signatureType, 'cloudevent');
});
});
36 changes: 36 additions & 0 deletions test/registration_container.ts
@@ -0,0 +1,36 @@
// 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 {
registrationAPI,
getRegisteredFunction,
} from '../src/registration_container';

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

it('can register cloudevent functions', () => {
registrationAPI.cloudevent('ceFunction', () => 'CE_PASS');
const {userFunction, signatureType} = getRegisteredFunction('ceFunction')!;
assert.deepStrictEqual('cloudevent', signatureType);
assert.deepStrictEqual((userFunction as () => string)(), 'CE_PASS');
});
});

0 comments on commit 978b26c

Please sign in to comment.