Skip to content
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

feat!: introduce declarative function signatures #347

Merged
merged 1 commit into from Oct 8, 2021
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.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
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",
matthewrobertson marked this conversation as resolved.
Show resolved Hide resolved
"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
anniefu marked this conversation as resolved.
Show resolved Hide resolved
* @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
matthewrobertson marked this conversation as resolved.
Show resolved Hide resolved
): 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<{
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Optional: Since we're adding a 3rd param, we might want to use an object rather than ordered list.

export async function getUserFunction({
  codeLocation,
  functionTarget,
  signatureType,
} ... )

That way, we could destructure when calling / using this function.

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
matthewrobertson marked this conversation as resolved.
Show resolved Hide resolved
//
// 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 () => {
matthewrobertson marked this conversation as resolved.
Show resolved Hide resolved
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');
});
});