Skip to content

Commit 5856009

Browse files
refactor: options parsing (#333)
This commit refactors the logic for parsing CLI flags and env vars into a separate module and adds unit tests.
1 parent 9cf46ed commit 5856009

File tree

3 files changed

+318
-87
lines changed

3 files changed

+318
-87
lines changed

src/index.ts

Lines changed: 38 additions & 87 deletions
Original file line numberDiff line numberDiff line change
@@ -16,96 +16,47 @@
1616

1717
// Functions framework entry point that configures and starts Node.js server
1818
// that runs user's code on HTTP request.
19-
// The following environment variables can be set to configure the framework:
20-
// - PORT - defines the port on which this server listens to all HTTP
21-
// requests.
22-
// - FUNCTION_TARGET - defines the name of the function within user's
23-
// node module to execute. If such a function is not defined,
24-
// then falls back to 'function' name.
25-
// - FUNCTION_SIGNATURE_TYPE - defines the type of the client function
26-
// signature:
27-
// - 'http' for function signature with HTTP request and HTTP
28-
// response arguments,
29-
// - 'event' for function signature with arguments
30-
// unmarshalled from an incoming request,
31-
// - 'cloudevent' for function signature with arguments
32-
// unmarshalled as CloudEvents from an incoming request.
33-
34-
import * as minimist from 'minimist';
35-
import {resolve} from 'path';
3619
import {getUserFunction} from './loader';
3720
import {ErrorHandler} from './invoker';
3821
import {getServer} from './server';
39-
import {SignatureType, isValidSignatureType} from './types';
40-
41-
// Supported command-line flags
42-
const FLAG = {
43-
PORT: 'port',
44-
TARGET: 'target',
45-
SIGNATURE_TYPE: 'signature-type', // dash
46-
SOURCE: 'source',
47-
};
48-
49-
// Supported environment variables
50-
const ENV = {
51-
PORT: 'PORT',
52-
TARGET: 'FUNCTION_TARGET',
53-
SIGNATURE_TYPE: 'FUNCTION_SIGNATURE_TYPE', // underscore
54-
SOURCE: 'FUNCTION_SOURCE',
55-
};
56-
57-
const argv = minimist(process.argv, {
58-
string: [FLAG.PORT, FLAG.TARGET, FLAG.SIGNATURE_TYPE],
59-
});
60-
61-
const CODE_LOCATION = resolve(
62-
argv[FLAG.SOURCE] || process.env[ENV.SOURCE] || ''
63-
);
64-
const PORT = argv[FLAG.PORT] || process.env[ENV.PORT] || '8080';
65-
const TARGET = argv[FLAG.TARGET] || process.env[ENV.TARGET] || 'function';
66-
67-
const SIGNATURE_TYPE = (
68-
argv[FLAG.SIGNATURE_TYPE] ||
69-
process.env[ENV.SIGNATURE_TYPE] ||
70-
'http'
71-
).toLowerCase();
72-
if (!isValidSignatureType(SIGNATURE_TYPE)) {
73-
console.error(
74-
`Function signature type must be one of: ${SignatureType.join(', ')}.`
75-
);
76-
// eslint-disable-next-line no-process-exit
77-
process.exit(1);
78-
}
22+
import {parseOptions, helpText, OptionsError} from './options';
7923

80-
// CLI Help Flag
81-
if (process.argv[2] === '-h' || process.argv[2] === '--help') {
82-
console.error(
83-
`Example usage:
84-
functions-framework --target=helloWorld --port=8080
85-
Documentation:
86-
https://github.com/GoogleCloudPlatform/functions-framework-nodejs`
87-
);
88-
// eslint-disable-next-line no-process-exit
89-
process.exit(0);
90-
}
24+
(async () => {
25+
try {
26+
const options = parseOptions();
9127

92-
getUserFunction(CODE_LOCATION, TARGET).then(userFunction => {
93-
if (!userFunction) {
94-
console.error('Could not load the function, shutting down.');
95-
// eslint-disable-next-line no-process-exit
96-
process.exit(1);
97-
}
98-
99-
const SERVER = getServer(userFunction!, SIGNATURE_TYPE!);
100-
const ERROR_HANDLER = new ErrorHandler(SERVER);
101-
102-
SERVER.listen(PORT, () => {
103-
ERROR_HANDLER.register();
104-
if (process.env.NODE_ENV !== 'production') {
105-
console.log('Serving function...');
106-
console.log(`Function: ${TARGET}`);
107-
console.log(`Signature type: ${SIGNATURE_TYPE}`);
108-
console.log(`URL: http://localhost:${PORT}/`);
28+
if (options.printHelp) {
29+
console.error(helpText);
30+
return;
10931
}
110-
}).setTimeout(0); // Disable automatic timeout on incoming connections.
111-
});
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+
})();

src/options.ts

Lines changed: 142 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,142 @@
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 * as minimist from 'minimist';
16+
import {resolve} from 'path';
17+
import {SignatureType, isValidSignatureType} from './types';
18+
19+
/**
20+
* Error thrown when an invalid option is provided.
21+
*/
22+
export class OptionsError extends Error {}
23+
24+
/**
25+
* The set of all options that can be used to configure the behaviour of
26+
* the framework.
27+
*/
28+
export interface FrameworkOptions {
29+
/**
30+
* The port on which this server listens to all HTTP requests.
31+
*/
32+
port: string;
33+
/**
34+
* The name of the function within user's node module to execute. If such a
35+
* function is not defined, then falls back to 'function' name.
36+
*/
37+
target: string;
38+
/**
39+
* The path to the source code file containing the client function.
40+
*/
41+
sourceLocation: string;
42+
/**
43+
* The signature type of the client function.
44+
*/
45+
signatureType: SignatureType;
46+
/**
47+
* Whether or not the --help CLI flag was provided.
48+
*/
49+
printHelp: boolean;
50+
}
51+
52+
/**
53+
* Helper class for parsing an configurable option from provided CLI flags
54+
* or environment variables.
55+
*/
56+
class ConfigurableOption<T> {
57+
constructor(
58+
/**
59+
* The CLI flag that can be use to configure this option.
60+
*/
61+
public readonly cliOption: string,
62+
/**
63+
* The name of the environment variable used to configure this option.
64+
*/
65+
private envVar: string,
66+
/**
67+
* The default value used when this option is not configured via a CLI flag
68+
* or environment variable.
69+
*/
70+
private defaultValue: T,
71+
/**
72+
* A function used to valid the user provided value.
73+
*/
74+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
75+
private validator: (x: any) => T = x => x as T
76+
) {}
77+
78+
parse(cliArgs: minimist.ParsedArgs, envVars: NodeJS.ProcessEnv): T {
79+
return this.validator(
80+
cliArgs[this.cliOption] || envVars[this.envVar] || this.defaultValue
81+
);
82+
}
83+
}
84+
85+
const PortOption = new ConfigurableOption('port', 'PORT', '8080');
86+
const FunctionTargetOption = new ConfigurableOption(
87+
'target',
88+
'FUNCTION_TARGET',
89+
'function'
90+
);
91+
const SourceLocationOption = new ConfigurableOption(
92+
'source',
93+
'FUNCTION_SOURCE',
94+
'',
95+
resolve
96+
);
97+
const SignatureOption = new ConfigurableOption(
98+
'signature-type',
99+
'FUNCTION_SIGNATURE_TYPE',
100+
'http' as SignatureType,
101+
x => {
102+
if (isValidSignatureType(x)) {
103+
return x;
104+
}
105+
throw new OptionsError(
106+
`Function signature type must be one of: ${SignatureType.join(', ')}.`
107+
);
108+
}
109+
);
110+
111+
export const helpText = `Example usage:
112+
functions-framework --target=helloWorld --port=8080
113+
Documentation:
114+
https://github.com/GoogleCloudPlatform/functions-framework-nodejs`;
115+
116+
/**
117+
* Parse the configurable framework options from the provided CLI arguments and
118+
* environment variables.
119+
* @param cliArgs the raw command line arguments
120+
* @param envVars the environment variables to parse options from
121+
* @returns the parsed options that should be used to configure the framework.
122+
*/
123+
export const parseOptions = (
124+
cliArgs: string[] = process.argv,
125+
envVars: NodeJS.ProcessEnv = process.env
126+
): FrameworkOptions => {
127+
const argv = minimist(cliArgs, {
128+
string: [
129+
PortOption.cliOption,
130+
FunctionTargetOption.cliOption,
131+
SignatureOption.cliOption,
132+
SourceLocationOption.cliOption,
133+
],
134+
});
135+
return {
136+
port: PortOption.parse(argv, envVars),
137+
target: FunctionTargetOption.parse(argv, envVars),
138+
sourceLocation: SourceLocationOption.parse(argv, envVars),
139+
signatureType: SignatureOption.parse(argv, envVars),
140+
printHelp: cliArgs[2] === '-h' || cliArgs[2] === '--help',
141+
};
142+
};

0 commit comments

Comments
 (0)