Skip to content

Commit 914e0a5

Browse files
authored
Merge pull request #7194 from sproutleaf/dev-2.0
Update to param validator + test file
2 parents 7997372 + 659b17c commit 914e0a5

File tree

5 files changed

+309
-52
lines changed

5 files changed

+309
-52
lines changed

package-lock.json

Lines changed: 24 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,8 @@
2727
"gifenc": "^1.0.3",
2828
"libtess": "^1.2.2",
2929
"omggif": "^1.0.10",
30-
"opentype.js": "^1.3.1"
30+
"opentype.js": "^1.3.1",
31+
"zod-validation-error": "^3.3.1"
3132
},
3233
"devDependencies": {
3334
"@rollup/plugin-commonjs": "^25.0.7",
@@ -47,11 +48,13 @@
4748
"rollup": "^4.9.6",
4849
"rollup-plugin-string": "^3.0.0",
4950
"rollup-plugin-visualizer": "^5.12.0",
51+
"typescript": "^5.5.4",
5052
"unplugin-swc": "^1.4.2",
5153
"vite": "^5.0.2",
5254
"vite-plugin-string": "^1.2.2",
5355
"vitest": "^2.0.4",
54-
"webdriverio": "^8.38.2"
56+
"webdriverio": "^8.38.2",
57+
"zod": "^3.23.8"
5558
},
5659
"license": "LGPL-2.1",
5760
"main": "./lib/p5.min.js",
@@ -78,4 +81,4 @@
7881
"pre-commit": "lint-staged"
7982
}
8083
}
81-
}
84+
}

src/core/friendly_errors/param_validator.js

Lines changed: 153 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -2,15 +2,57 @@
22
* @for p5
33
* @requires core
44
*/
5-
// import p5 from '../main';
6-
import dataDoc from '../../../docs/parameterData.json';
5+
import p5 from '../main.js';
6+
import * as constants from '../constants.js';
77
import { z } from 'zod';
88
import { fromError } from 'zod-validation-error';
9+
import dataDoc from '../../../docs/parameterData.json' assert { type: 'json' };
910

1011
// Cache for Zod schemas
1112
let schemaRegistry = new Map();
1213
const arrDoc = JSON.parse(JSON.stringify(dataDoc));
1314

15+
// Mapping names of p5 types to their constructor functions.
16+
// p5Constructors:
17+
// - Color: f()
18+
// - Graphics: f()
19+
// - Vector: f()
20+
// and so on.
21+
const p5Constructors = {};
22+
// For speedup over many runs. `funcSpecificConstructors[func]` only has the
23+
// constructors for types which were seen earlier as args of `func`.
24+
const funcSpecificConstructors = {};
25+
26+
for (let [key, value] of Object.entries(p5)) {
27+
p5Constructors[key] = value;
28+
}
29+
30+
// window.addEventListener('load', () => {
31+
// // Make a list of all p5 classes to be used for argument validation
32+
// // This must be done only when everything has loaded otherwise we get
33+
// // an empty array.
34+
// for (let key of Object.keys(p5)) {
35+
// // Get a list of all constructors in p5. They are functions whose names
36+
// // start with a capital letter.
37+
// if (typeof p5[key] === 'function' && key[0] !== key[0].toLowerCase()) {
38+
// p5Constructors[key] = p5[key];
39+
// }
40+
// }
41+
// });
42+
43+
// `constantsMap` maps constants to their values, e.g.
44+
// {
45+
// ADD: 'lighter',
46+
// ALT: 18,
47+
// ARROW: 'default',
48+
// AUTO: 'auto',
49+
// ...
50+
// }
51+
const constantsMap = {};
52+
for (const [key, value] of Object.entries(constants)) {
53+
constantsMap[key] = value;
54+
}
55+
1456
const schemaMap = {
1557
'Any': z.any(),
1658
'Array': z.array(z.any()),
@@ -26,6 +68,30 @@ const schemaMap = {
2668
'String[]': z.array(z.string())
2769
};
2870

71+
const webAPIObjects = [
72+
'AudioNode',
73+
'HTMLCanvasElement',
74+
'HTMLElement',
75+
'KeyboardEvent',
76+
'MouseEvent',
77+
'TouchEvent',
78+
'UIEvent',
79+
'WheelEvent'
80+
];
81+
82+
function generateWebAPISchemas(apiObjects) {
83+
return apiObjects.map(obj => {
84+
return {
85+
name: obj,
86+
schema: z.custom((data) => data instanceof globalThis[obj], {
87+
message: `Expected a ${obj}`
88+
})
89+
};
90+
});
91+
}
92+
93+
const webAPISchemas = generateWebAPISchemas(webAPIObjects);
94+
2995
/**
3096
* This is a helper function that generates Zod schemas for a function based on
3197
* the parameter data from `docs/parameterData.json`.
@@ -46,8 +112,6 @@ const schemaMap = {
46112
*
47113
* TODO:
48114
* - [ ] Support for p5 constructors
49-
* - [ ] Support for p5 constants
50-
* - [ ] Support for generating multiple schemas for optional parameters
51115
* - [ ] Support for more obscure types, such as `lerpPalette` and optional
52116
* objects in `p5.Geometry.computeNormals()`
53117
* (see https://github.com/processing/p5.js/pull/7186#discussion_r1724983249)
@@ -68,45 +132,90 @@ function generateZodSchemasForFunc(func) {
68132
overloads = funcInfo.overloads;
69133
}
70134

71-
const createParamSchema = param => {
135+
// Returns a schema for a single type, i.e. z.boolean() for `boolean`.
136+
const generateTypeSchema = type => {
137+
// Type only contains uppercase letters and underscores -> type is a
138+
// constant. Note that because we're ultimately interested in the value of
139+
// the constant, mapping constants to their values via `constantsMap` is
140+
// necessary.
141+
if (/^[A-Z_]+$/.test(type)) {
142+
return z.literal(constantsMap[type]);
143+
} else if (schemaMap[type]) {
144+
return schemaMap[type];
145+
} else {
146+
// TODO: Make this throw an error once more types are supported.
147+
console.log(`Warning: Zod schema not found for type '${type}'. Skip mapping`);
148+
return undefined;
149+
}
150+
};
151+
152+
// Generate a schema for a single parameter. In the case where a parameter can
153+
// be of multiple types, `generateTypeSchema` is called for each type.
154+
const generateParamSchema = param => {
72155
const optional = param.endsWith('?');
73156
param = param.replace(/\?$/, '');
74157

158+
let schema;
159+
160+
// Generate a schema for a single parameter that can be of multiple
161+
// types / constants, i.e. `String|Number|Array`.
162+
//
163+
// Here, z.union() is used over z.enum() (which seems more intuitive) for
164+
// constants for the following reasons:
165+
// 1) z.enum() only allows a fixed set of allowable string values. However,
166+
// our constants sometimes have numeric or non-primitive values.
167+
// 2) In some cases, the type can be constants or strings, making z.enum()
168+
// insufficient for the use case.
75169
if (param.includes('|')) {
76170
const types = param.split('|');
77-
78-
/*
79-
* Note that for parameter types that are constants, such as for
80-
* `blendMode`, the parameters are always all caps, sometimes with
81-
* underscores, separated by `|`
82-
* (i.e. "BLEND|DARKEST|LIGHTEST|DIFFERENCE|MULTIPLY|EXCLUSION|SCREEN|
83-
* REPLACE|OVERLAY|HARD_LIGHT|SOFT_LIGHT|DODGE|BURN|ADD|REMOVE|SUBTRACT").
84-
* Use a regex check here to filter them out and distinguish them from
85-
* parameters that allow multiple types.
86-
*/
87-
return types.every(t => /^[A-Z_]+$/.test(t))
88-
? z.enum(types)
89-
: z.union(types
90-
.filter(t => {
91-
if (!schemaMap[t]) {
92-
console.warn(`Warning: Zod schema not found for type '${t}'. Skip mapping`);
93-
return false;
94-
}
95-
return true;
96-
})
97-
.map(t => schemaMap[t]));
171+
schema = z.union(types
172+
.map(t => generateTypeSchema(t))
173+
.filter(s => s !== undefined));
174+
} else {
175+
schema = generateTypeSchema(param);
98176
}
99177

100-
let schema = schemaMap[param];
101178
return optional ? schema.optional() : schema;
102179
};
103180

104-
const overloadSchemas = overloads.map(overload => {
105-
// For now, ignore schemas that cannot be mapped to a defined type
106-
return z.tuple(
107-
overload
108-
.map(p => createParamSchema(p))
109-
.filter(schema => schema !== undefined)
181+
// Note that in Zod, `optional()` only checks for undefined, not the absence
182+
// of value.
183+
//
184+
// Let's say we have a function with 3 parameters, and the last one is
185+
// optional, i.e. func(a, b, c?). If we only have a z.tuple() for the
186+
// parameters, where the third schema is optional, then we will only be able
187+
// to validate func(10, 10, undefined), but not func(10, 10), which is
188+
// a completely valid call.
189+
//
190+
// Therefore, on top of using `optional()`, we also have to generate parameter
191+
// combinations that are valid for all numbers of parameters.
192+
const generateOverloadCombinations = params => {
193+
// No optional parameters, return the original parameter list right away.
194+
if (!params.some(p => p.endsWith('?'))) {
195+
return [params];
196+
}
197+
198+
const requiredParamsCount = params.filter(p => !p.endsWith('?')).length;
199+
const result = [];
200+
201+
for (let i = requiredParamsCount; i <= params.length; i++) {
202+
result.push(params.slice(0, i));
203+
}
204+
205+
return result;
206+
}
207+
208+
// Generate schemas for each function overload and merge them
209+
const overloadSchemas = overloads.flatMap(overload => {
210+
const combinations = generateOverloadCombinations(overload);
211+
212+
return combinations.map(combo =>
213+
z.tuple(
214+
combo
215+
.map(p => generateParamSchema(p))
216+
// For now, ignore schemas that cannot be mapped to a defined type
217+
.filter(schema => schema !== undefined)
218+
)
110219
);
111220
});
112221

@@ -181,7 +290,7 @@ function findClosestSchema(schema, args) {
181290
return score;
182291
};
183292

184-
// Default to the first schema, so that we will always return something.
293+
// Default to the first schema, so that we are guaranteed to return a result.
185294
let closestSchema = schema._def.options[0];
186295
// We want to return the schema with the lowest score.
187296
let bestScore = Infinity;
@@ -198,25 +307,21 @@ function findClosestSchema(schema, args) {
198307
return closestSchema;
199308
}
200309

201-
202310
/**
203311
* Runs parameter validation by matching the input parameters to Zod schemas
204312
* generated from the parameter data from `docs/parameterData.json`.
205313
*
206-
* TODO:
207-
* - [ ] Turn it into a private method of `p5`.
208-
*
209314
* @param {String} func - Name of the function.
210315
* @param {Array} args - User input arguments.
211316
* @returns {Object} The validation result.
212317
* @returns {Boolean} result.success - Whether the validation was successful.
213318
* @returns {any} [result.data] - The parsed data if validation was successful.
214319
* @returns {import('zod-validation-error').ZodValidationError} [result.error] - The validation error if validation failed.
215320
*/
216-
export function validateParams(func, args) {
217-
// if (p5.disableFriendlyErrors) {
218-
// return; // skip FES
219-
// }
321+
p5._validateParams = function validateParams(func, args) {
322+
if (p5.disableFriendlyErrors) {
323+
return; // skip FES
324+
}
220325

221326
let funcSchemas = schemaRegistry.get(func);
222327
if (!funcSchemas) {
@@ -242,7 +347,12 @@ export function validateParams(func, args) {
242347
}
243348
}
244349

245-
const result = validateParams('p5.fill', [1]);
350+
p5.prototype._validateParams = p5._validateParams;
351+
export default p5;
352+
353+
const result = p5._validateParams('arc', [200, 100, 100, 80, 0, Math.PI, 'pie']);
246354
if (!result.success) {
247355
console.log(result.error.toString());
356+
} else {
357+
console.log('Validation successful');
248358
}

0 commit comments

Comments
 (0)