Skip to content

Commit db1ba9e

Browse files
feat: introduce declarative function signatures (#347)
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.
1 parent 51cb666 commit db1ba9e

File tree

7 files changed

+249
-58
lines changed

7 files changed

+249
-58
lines changed

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77
},
88
"repository": "GoogleCloudPlatform/functions-framework-nodejs",
99
"main": "build/src/index.js",
10-
"types": "build/src/functions.d.ts",
10+
"types": "build/src/index.d.ts",
1111
"dependencies": {
1212
"body-parser": "^1.18.3",
1313
"express": "^4.16.4",

src/function_registry.ts

Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
// Copyright 2021 Google LLC
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// http://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
import {HttpFunction, CloudEventFunction, HandlerFunction} from './functions';
16+
import {SignatureType} from './types';
17+
18+
interface RegisteredFunction {
19+
signatureType: SignatureType;
20+
userFunction: HandlerFunction;
21+
}
22+
23+
/**
24+
* Singleton map to hold the registered functions
25+
*/
26+
const registrationContainer = new Map<string, RegisteredFunction>();
27+
28+
/**
29+
* Helper method to store a registered function in the registration container
30+
*/
31+
const register = (
32+
functionName: string,
33+
signatureType: SignatureType,
34+
userFunction: HandlerFunction
35+
): void => {
36+
registrationContainer.set(functionName, {
37+
signatureType,
38+
userFunction,
39+
});
40+
};
41+
42+
/**
43+
* Get a declaratively registered function
44+
* @param functionName the name with which the function was registered
45+
* @returns the registered function and signature type or undefined no function matching
46+
* the provided name has been registered.
47+
*/
48+
export const getRegisteredFunction = (
49+
functionName: string
50+
): RegisteredFunction | undefined => {
51+
return registrationContainer.get(functionName);
52+
};
53+
54+
/**
55+
* Register a function that responds to HTTP requests.
56+
* @param functionName the name of the function
57+
* @param handler the function to invoke when handling HTTP requests
58+
*/
59+
export const http = (functionName: string, handler: HttpFunction): void => {
60+
register(functionName, 'http', handler);
61+
};
62+
63+
/**
64+
* Register a function that handles CloudEvents.
65+
* @param functionName the name of the function
66+
* @param handler the function to trigger when handling cloudevents
67+
*/
68+
export const cloudevent = (
69+
functionName: string,
70+
handler: CloudEventFunction
71+
): void => {
72+
register(functionName, 'cloudevent', handler);
73+
};

src/index.ts

Lines changed: 6 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -14,49 +14,11 @@
1414
// See the License for the specific language governing permissions and
1515
// limitations under the License.
1616

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

24-
(async () => {
25-
try {
26-
const options = parseOptions();
19+
export * from './functions';
2720

28-
if (options.printHelp) {
29-
console.error(helpText);
30-
return;
31-
}
32-
const userFunction = await getUserFunction(
33-
options.sourceLocation,
34-
options.target
35-
);
36-
if (!userFunction) {
37-
console.error('Could not load the function, shutting down.');
38-
// eslint-disable-next-line no-process-exit
39-
process.exit(1);
40-
}
41-
const server = getServer(userFunction!, options.signatureType);
42-
const errorHandler = new ErrorHandler(server);
43-
server
44-
.listen(options.port, () => {
45-
errorHandler.register();
46-
if (process.env.NODE_ENV !== 'production') {
47-
console.log('Serving function...');
48-
console.log(`Function: ${options.target}`);
49-
console.log(`Signature type: ${options.signatureType}`);
50-
console.log(`URL: http://localhost:${options.port}/`);
51-
}
52-
})
53-
.setTimeout(0); // Disable automatic timeout on incoming connections.
54-
} catch (e) {
55-
if (e instanceof OptionsError) {
56-
console.error(e.message);
57-
// eslint-disable-next-line no-process-exit
58-
process.exit(1);
59-
}
60-
throw e;
61-
}
62-
})();
21+
export {http, cloudevent} from './function_registry';
22+
23+
// Call the main method to load the user code and start the http server.
24+
main();

src/loader.ts

Lines changed: 16 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -22,10 +22,9 @@ import * as path from 'path';
2222
import * as semver from 'semver';
2323
import * as readPkgUp from 'read-pkg-up';
2424
import {pathToFileURL} from 'url';
25-
/**
26-
* Import function signature type's definition.
27-
*/
2825
import {HandlerFunction} from './functions';
26+
import {SignatureType} from './types';
27+
import {getRegisteredFunction} from './function_registry';
2928

3029
// Dynamic import function required to load user code packaged as an
3130
// ES module is only available on Node.js v13.2.0 and up.
@@ -85,8 +84,12 @@ const dynamicImport = new Function(
8584
*/
8685
export async function getUserFunction(
8786
codeLocation: string,
88-
functionTarget: string
89-
): Promise<HandlerFunction | null> {
87+
functionTarget: string,
88+
signatureType: SignatureType
89+
): Promise<{
90+
userFunction: HandlerFunction;
91+
signatureType: SignatureType;
92+
} | null> {
9093
try {
9194
const functionModulePath = getFunctionModulePath(codeLocation);
9295
if (functionModulePath === null) {
@@ -111,6 +114,13 @@ export async function getUserFunction(
111114
functionModule = require(functionModulePath);
112115
}
113116

117+
// If the customer declaratively registered a function matching the target
118+
// return that.
119+
const registeredFunction = getRegisteredFunction(functionTarget);
120+
if (registeredFunction) {
121+
return registeredFunction;
122+
}
123+
114124
let userFunction = functionTarget
115125
.split('.')
116126
.reduce((code, functionTargetPart) => {
@@ -143,7 +153,7 @@ export async function getUserFunction(
143153
return null;
144154
}
145155

146-
return userFunction as HandlerFunction;
156+
return {userFunction: userFunction as HandlerFunction, signatureType};
147157
} catch (ex) {
148158
let additionalHint: string;
149159
// TODO: this should be done based on ex.code rather than string matching.

src/main.ts

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
// Copyright 2019 Google LLC
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// http://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
// Functions framework entry point that configures and starts Node.js server
16+
// that runs user's code on HTTP request.
17+
import {getUserFunction} from './loader';
18+
import {ErrorHandler} from './invoker';
19+
import {getServer} from './server';
20+
import {parseOptions, helpText, OptionsError} from './options';
21+
22+
/**
23+
* Main entrypoint for the functions framework that loads the user's function
24+
* and starts the HTTP server.
25+
*/
26+
export const main = async () => {
27+
try {
28+
const options = parseOptions();
29+
30+
if (options.printHelp) {
31+
console.error(helpText);
32+
return;
33+
}
34+
const loadedFunction = await getUserFunction(
35+
options.sourceLocation,
36+
options.target,
37+
options.signatureType
38+
);
39+
if (!loadedFunction) {
40+
console.error('Could not load the function, shutting down.');
41+
// eslint-disable-next-line no-process-exit
42+
process.exit(1);
43+
}
44+
const {userFunction, signatureType} = loadedFunction;
45+
const server = getServer(userFunction!, signatureType);
46+
const errorHandler = new ErrorHandler(server);
47+
server
48+
.listen(options.port, () => {
49+
errorHandler.register();
50+
if (process.env.NODE_ENV !== 'production') {
51+
console.log('Serving function...');
52+
console.log(`Function: ${options.target}`);
53+
console.log(`Signature type: ${signatureType}`);
54+
console.log(`URL: http://localhost:${options.port}/`);
55+
}
56+
})
57+
.setTimeout(0); // Disable automatic timeout on incoming connections.
58+
} catch (e) {
59+
if (e instanceof OptionsError) {
60+
console.error(e.message);
61+
// eslint-disable-next-line no-process-exit
62+
process.exit(1);
63+
}
64+
throw e;
65+
}
66+
};

test/function_registry.ts

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
// Copyright 2019 Google LLC
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// http://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
import * as assert from 'assert';
15+
import * as FunctionRegistry from '../src/function_registry';
16+
17+
describe('function_registry', () => {
18+
it('can register http functions', () => {
19+
FunctionRegistry.http('httpFunction', () => 'HTTP_PASS');
20+
const {
21+
userFunction,
22+
signatureType,
23+
} = FunctionRegistry.getRegisteredFunction('httpFunction')!;
24+
assert.deepStrictEqual('http', signatureType);
25+
assert.deepStrictEqual((userFunction as () => string)(), 'HTTP_PASS');
26+
});
27+
28+
it('can register cloudevent functions', () => {
29+
FunctionRegistry.cloudevent('ceFunction', () => 'CE_PASS');
30+
const {
31+
userFunction,
32+
signatureType,
33+
} = FunctionRegistry.getRegisteredFunction('ceFunction')!;
34+
assert.deepStrictEqual('cloudevent', signatureType);
35+
assert.deepStrictEqual((userFunction as () => string)(), 'CE_PASS');
36+
});
37+
});

test/loader.ts

Lines changed: 50 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ import * as express from 'express';
1717
import * as semver from 'semver';
1818
import * as functions from '../src/functions';
1919
import * as loader from '../src/loader';
20+
import * as FunctionRegistry from '../src/function_registry';
2021

2122
describe('loading function', () => {
2223
interface TestData {
@@ -40,11 +41,13 @@ describe('loading function', () => {
4041

4142
for (const test of testData) {
4243
it(`should load ${test.name}`, async () => {
43-
const loadedFunction = (await loader.getUserFunction(
44+
const loadedFunction = await loader.getUserFunction(
4445
process.cwd() + test.codeLocation,
45-
test.target
46-
)) as functions.HttpFunction;
47-
const returned = loadedFunction(express.request, express.response);
46+
test.target,
47+
'http'
48+
);
49+
const userFunction = loadedFunction?.userFunction as functions.HttpFunction;
50+
const returned = userFunction(express.request, express.response);
4851
assert.strictEqual(returned, 'PASS');
4952
});
5053
}
@@ -69,10 +72,12 @@ describe('loading function', () => {
6972

7073
for (const test of esmTestData) {
7174
const loadFn: () => Promise<functions.HttpFunction> = async () => {
72-
return loader.getUserFunction(
75+
const loadedFunction = await loader.getUserFunction(
7376
process.cwd() + test.codeLocation,
74-
test.target
75-
) as Promise<functions.HttpFunction>;
77+
test.target,
78+
'http'
79+
);
80+
return loadedFunction?.userFunction as functions.HttpFunction;
7681
};
7782
if (semver.lt(process.version, loader.MIN_NODE_VERSION_ESMODULES)) {
7883
it(`should fail to load function in an ES module ${test.name}`, async () => {
@@ -86,4 +91,42 @@ describe('loading function', () => {
8691
});
8792
}
8893
}
94+
95+
it('loads a declaratively registered function', async () => {
96+
FunctionRegistry.http('registeredFunction', () => {
97+
return 'PASS';
98+
});
99+
const loadedFunction = await loader.getUserFunction(
100+
process.cwd() + '/test/data/with_main',
101+
'registeredFunction',
102+
'http'
103+
);
104+
const userFunction = loadedFunction?.userFunction as functions.HttpFunction;
105+
const returned = userFunction(express.request, express.response);
106+
assert.strictEqual(returned, 'PASS');
107+
});
108+
109+
it('allows a mix of registered and non registered functions', async () => {
110+
FunctionRegistry.http('registeredFunction', () => {
111+
return 'FAIL';
112+
});
113+
const loadedFunction = await loader.getUserFunction(
114+
process.cwd() + '/test/data/with_main',
115+
'testFunction',
116+
'http'
117+
);
118+
const userFunction = loadedFunction?.userFunction as functions.HttpFunction;
119+
const returned = userFunction(express.request, express.response);
120+
assert.strictEqual(returned, 'PASS');
121+
});
122+
123+
it('respects the registered signature type', async () => {
124+
FunctionRegistry.cloudevent('registeredFunction', () => {});
125+
const loadedFunction = await loader.getUserFunction(
126+
process.cwd() + '/test/data/with_main',
127+
'registeredFunction',
128+
'http'
129+
);
130+
assert.strictEqual(loadedFunction?.signatureType, 'cloudevent');
131+
});
89132
});

0 commit comments

Comments
 (0)