8000 Improve `validate()` function for plugin options (#1323) · patrickloeber/pyscript@dfa116e · GitHub
[go: up one dir, main page]

Skip to content

Commit dfa116e

Browse files
Improve validate() function for plugin options (pyscript#1323)
* Add `validateConfigParameter` and `validateConfigParameterFromArray` functions to validate user-provided parameters from py-config * Add units tests for `validateConfigParameter` and `validateConfigParameterFromArray` --------- Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
1 parent 3a9fd3c commit dfa116e

File tree

3 files changed

+204
-20
lines changed

3 files changed

+204
-20
lines changed

pyscriptjs/src/plugin.ts

Lines changed: 72 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import type { PyScriptApp } from './main';
22
import type { AppConfig } from './pyconfig';
3-
import type { UserError } from './exceptions';
3+
import { UserError, ErrorCode } from './exceptions';
44
import { getLogger } from './logger';
55
import { make_PyScript } from './components/pyscript';
66
import { InterpreterClient } from './interpreter_client';
@@ -266,3 +266,74 @@ export function define_custom_element(tag: string, pyElementClass: PyElementClas
266266

267267
customElements.define(tag, ProxyCustomElement);
268268
}
269+
270+
// Members of py-config in plug that we want to validate must be one of these types
271+
type BaseConfigObject = string | boolean | number | undefined;
272+
273+
/**
274+
* Validate that parameter the user provided to py-config conforms to the specified validation function;
275+
* if not, throw an error explaining the bad value. If no value is provided, set the parameter
276+
* to the provided default value
277+
* This is the most generic validation function; other validation functions for common situations follow
278+
* @param options.config - The (extended) AppConfig object from py-config
279+
* @param {string} options.name - The name of the key in py-config to be checked
280+
* @param {(b:BaseConfigObject) => boolean} options.validator - the validation function used to test the user-supplied value
281+
* @param {BaseConfigObject} options.defaultValue - The default value for this parameter, if none is provided
282+
* @param {string} [options.hintMessage] - The message to show in a user error if the supplied value isn't valid
283+
*/
284+
export function validateConfigParameter(options: {
285+
config: AppConfig;
286+
name: string;
287+
validator: (b: BaseConfigObject) => boolean;
288+
defaultValue: BaseConfigObject;
289+
hintMessage?: string;
290+
}) {
291+
//Validate that the default value is acceptable, at runtime
292+
if (!options.validator(options.defaultValue)) {
293+
throw Error(
294+
`Default value ${JSON.stringify(options.defaultValue)} for ${options.name} is not a valid argument, ` +
295+
`according to the provided validator function. ${options.hintMessage ? options.hintMessage : ''}`,
296+
);
297+
}
298+
299+
const value = options.config[options.name] as BaseConfigObject;
300+
if (value !== undefined && !options.validator(value)) {
301+
//Use default hint message if none is provided:
302+
const hintOutput = `Invalid value ${JSON.stringify(value)} for config.${options.name}. ${
303+
options.hintMessage ? options.hintMessage : ''
304+
}`;
305+
throw new UserError(ErrorCode.BAD_CONFIG, hintOutput);
306+
}
307+
if (value === undefined) {
308+
options.config[options.name] = options.defaultValue;
309+
}
310+
}
311+
312+
/**
313+
* Validate that parameter the user provided to py-config is one of the acceptable values in
314+
* the given Array; if not, throw an error explaining the bad value. If no value is provided,
315+
* set the parameter to the provided default value
316+
* @param options.config - The (extended) AppConfig object from py-config
317+
* @param {string} options.name - The name of the key in py-config to be checked
318+
* @param {Array<BaseConfigObject>} options.possibleValues: The acceptable values for this parameter
319+
* @param {BaseConfigObject} options.defaultValue: The default value for this parameter, if none is provided
320+
*/
321+
export function validateConfigParameterFromArray(options: {
322+
config: AppConfig;
323+
name: string;
324+
possibleValues: Array<BaseConfigObject>;
325+
defaultValue: BaseConfigObject;
326+
}) {
327+
const validator = (b: BaseConfigObject) => options.possibleValues.includes(b);
328+
const hint = `The only accepted values are: [${options.possibleValues
329+
.map(item => JSON.stringify(item))
330+
.join(', ')}]`;
331+
332+
validateConfigParameter({
333+
config: options.config,
334+
name: options.name,
335+
validator: validator,
336+
defaultValue: options.defaultValue,
337+
hintMessage: hint,
338+
});
339+
}

pyscriptjs/src/plugins/pyterminal.ts

Lines changed: 16 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1,28 +1,15 @@
11
import type { PyScriptApp } from '../main';
22
import type { AppConfig } from '../pyconfig';
3-
import { Plugin } from '../plugin';
4-
import { UserError, ErrorCode } from '../exceptions';
3+
import { Plugin, val 10000 idateConfigParameterFromArray } from '../plugin';
54
import { getLogger } from '../logger';
65
import { type Stdio } from '../stdio';
76
import { InterpreterClient } from '../interpreter_client';
87

9-
type AppConfigStyle = AppConfig & { terminal?: boolean | 'auto'; docked?: boolean | 'docked' };
10-
118
const logger = getLogger('py-terminal');
129

13-
const validate = (config: AppConfigStyle, name: string, default_: string) => {
14-
const value = config[name] as undefined | boolean | string;
15-
if (value !== undefined && value !== true && value !== false && value !== default_) {
16-
const got = JSON.stringify(value);
17-
throw new UserError(
18-
ErrorCode.BAD_CONFIG,
19-
`Invalid value for config.${name}: the only accepted` +
20-
`values are true, false and "${default_}", got "${got}".`,
21-
);
22-
}
23-
if (value === undefined) {
24-
config[name] = default_;
25-
}
10+
type AppConfigStyle = AppConfig & {
11+
terminal?: string | boolean;
12+
docked?: string | boolean;
2613
};
2714

2815
export class PyTerminalPlugin extends Plugin {
@@ -35,8 +22,18 @@ export class PyTerminalPlugin extends Plugin {
3522

3623
configure(config: AppConfigStyle) {
3724
// validate the terminal config and handle default values
38-
validate(config, 'terminal', 'auto');
39-
validate(config, 'docked', 'docked');
25+
validateConfigParameterFromArray({
26+
config: config,
27+
name: 'terminal',
28+
possibleValues: [true, false, 'auto'],
29+
defaultValue: 'auto',
30+
});
31+
validateConfigParameterFromArray({
32+
config: config,
33+
name: 'docked',
34+
8000 possibleValues: [true, false, 'docked'],
35+
defaultValue: 'docked',
36+
});
4037
}
4138

4239
beforeLaunch(config: AppConfigStyle) {

pyscriptjs/tests/unit/plugin.test.ts

Lines changed: 116 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,116 @@
1+
import { validateConfigParameter, validateConfigParameterFromArray } from '../../src/plugin';
2+
import { UserError } from '../../src/exceptions';
3+
4+
describe('validateConfigParameter', () => {
5+
const validator = a => a.charAt(0) === 'a';
6+
7+
it('should not change a matching config option', () => {
8+
const pyconfig = { a: 'a1', dummy: 'dummy' };
9+
validateConfigParameter({
10+
config: pyconfig,
11+
name: 'a',
12+
validator: validator,
13+
defaultValue: 'a_default',
14+
hintMessage: "Should start with 'a'",
15+
});
16+
expect(pyconfig).toStrictEqual({ a: 'a1', dummy: 'dummy' });
17+
});
18+
19+
it('should set the default value if no value is present', () => {
20+
const pyconfig = { dummy: 'dummy' };
21+
validateConfigParameter({
22+
config: pyconfig,
23+
name: 'a',
24+
validator: validator,
25+
defaultValue: 'a_default',
26+
hintMessage: "Should start with 'a'",
27+
});
28+
expect(pyconfig).toStrictEqual({ a: 'a_default', dummy: 'dummy' });
29+
});
30+
31+
it('should error if the provided value is not valid', () => {
32+
const pyconfig = { a: 'NotValidValue', dummy: 'dummy' };
33+
const func = () =>
34+
validateConfigParameter({
35+
config: pyconfig,
36+
name: 'a',
37+
validator: validator,
38+
defaultValue: 'a_default',
39+
hintMessage: "Should start with 'a'",
40+
});
41+
expect(func).toThrow(UserError);
42+
expect(func).toThrow('(PY1000): Invalid value "NotValidValue" for config.a. Should start with \'a\'');
43+
});
44+
45+
it('should error if the provided default is not valid', () => {
46+
const pyconfig = { a: 'a1', dummy: 'dummy' };
47+
const func = () =>
48+
validateConfigParameter({
49+
config: pyconfig,
50+
name: 'a',
51+
validator: validator,
52+
defaultValue: 'NotValidDefault',
53+
hintMessage: "Should start with 'a'",
54+
});
55+
expect(func).toThrow(Error);
56+
expect(func).toThrow(
57+
'Default value "NotValidDefault" for a is not a valid argument, according to the provided validator function. Should start with \'a\'',
58+
);
59+
});
60+
});
61+
62+
describe('validateConfigParameterFromArray', () => {
63+
const possibilities = ['a1', 'a2', true, 42, 'a_default'];
64+
65+
it('should not change a matching config option', () => {
66+
const pyconfig = { a: 'a1', dummy: 'dummy' };
67+
validateConfigParameterFromArray({
68+
config: pyconfig,
69+
name: 'a',
70+
possibleValues: possibilities,
71+
defaultValue: 'a_default',
72+
});
73+
expect(pyconfig).toStrictEqual({ a: 'a1', dummy: 'dummy' });
74+
});
75+
76+
it('should set the default value if no value is present', () => {
77+
const pyconfig = { dummy: 'dummy' };
78+
validateConfigParameterFromArray({
79+
config: pyconfig,
80+
name: 'a',
81+
possibleValues: possibilities,
82+
defaultValue: 'a_default',
83+
});
84+
expect(pyconfig).toStrictEqual({ a: 'a_default', dummy: 'dummy' });
85+
});
86+
87+
it('should error if the provided value is not in possible_values', () => {
88+
const pyconfig = { a: 'NotValidValue', dummy: 'dummy' };
89+
const func = () =>
90+
validateConfigParameterFromArray({
91+
config: pyconfig,
92+
name: 'a',
93+
possibleValues: possibilities,
94+
defaultValue: 'a_default',
95+
});
96+
expect(func).toThrow(Error);
97+
expect(func).toThrow(
98+
'(PY1000): Invalid value "NotValidValue" for config.a. The only accepted values are: ["a1", "a2", true, 42, "a_default"]',
99+
);
100+
});
101+
102+
it('should error if the provided default is not in possible_values', () => {
103+
const pyconfig = { a: 'a1', dummy: 'dummy' };
104+
const func = () =>
105+
validateConfigParameterFromArray({
106+
config: pyconfig,
107+
name: 'a',
108+
possibleValues: possibilities,
109+
defaultValue: 'NotValidDefault',
110+
});
111+
expect(func).toThrow(Error);
112+
expect(func).toThrow(
113+
'Default value "NotValidDefault" for a is not a valid argument, according to the provided validator function. The only accepted values are: ["a1", "a2", true, 42, "a_default"]',
114+
);
115+
});
116+
});

0 commit comments

Comments
 (0)
0