2
2
* @for p5
3
3
* @requires core
4
4
*/
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 ' ;
7
7
import { z } from 'zod' ;
8
8
import { fromError } from 'zod-validation-error' ;
9
+ import dataDoc from '../../../docs/parameterData.json' assert { type : 'json ' } ;
9
10
10
11
// Cache for Zod schemas
11
12
let schemaRegistry = new Map ( ) ;
12
13
const arrDoc = JSON . parse ( JSON . stringify ( dataDoc ) ) ;
13
14
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
+
14
56
const schemaMap = {
15
57
'Any' : z . any ( ) ,
16
58
'Array' : z . array ( z . any ( ) ) ,
@@ -26,6 +68,30 @@ const schemaMap = {
26
68
'String[]' : z . array ( z . string ( ) )
27
69
} ;
28
70
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
+
29
95
/**
30
96
* This is a helper function that generates Zod schemas for a function based on
31
97
* the parameter data from `docs/parameterData.json`.
@@ -46,8 +112,6 @@ const schemaMap = {
46
112
*
47
113
* TODO:
48
114
* - [ ] Support for p5 constructors
49
- * - [ ] Support for p5 constants
50
- * - [ ] Support for generating multiple schemas for optional parameters
51
115
* - [ ] Support for more obscure types, such as `lerpPalette` and optional
52
116
* objects in `p5.Geometry.computeNormals()`
53
117
* (see https://github.com/processing/p5.js/pull/7186#discussion_r1724983249)
@@ -68,45 +132,90 @@ function generateZodSchemasForFunc(func) {
68
132
overloads = funcInfo . overloads ;
69
133
}
70
134
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 => {
72
155
const optional = param . endsWith ( '?' ) ;
73
156
param = param . replace ( / \? $ / , '' ) ;
74
157
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.
75
169
if ( param . includes ( '|' ) ) {
76
170
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 ) ;
98
176
}
99
177
100
- let schema = schemaMap [ param ] ;
101
178
return optional ? schema . optional ( ) : schema ;
102
179
} ;
103
180
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
+ )
110
219
) ;
111
220
} ) ;
112
221
@@ -181,7 +290,7 @@ function findClosestSchema(schema, args) {
181
290
return score ;
182
291
} ;
183
292
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 .
185
294
let closestSchema = schema . _def . options [ 0 ] ;
186
295
// We want to return the schema with the lowest score.
187
296
let bestScore = Infinity ;
@@ -198,25 +307,21 @@ function findClosestSchema(schema, args) {
198
307
return closestSchema ;
199
308
}
200
309
201
-
202
310
/**
203
311
* Runs parameter validation by matching the input parameters to Zod schemas
204
312
* generated from the parameter data from `docs/parameterData.json`.
205
313
*
206
- * TODO:
207
- * - [ ] Turn it into a private method of `p5`.
208
- *
209
314
* @param {String } func - Name of the function.
210
315
* @param {Array } args - User input arguments.
211
316
* @returns {Object } The validation result.
212
317
* @returns {Boolean } result.success - Whether the validation was successful.
213
318
* @returns {any } [result.data] - The parsed data if validation was successful.
214
319
* @returns {import('zod-validation-error').ZodValidationError } [result.error] - The validation error if validation failed.
215
320
*/
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
+ }
220
325
221
326
let funcSchemas = schemaRegistry . get ( func ) ;
222
327
if ( ! funcSchemas ) {
@@ -242,7 +347,12 @@ export function validateParams(func, args) {
242
347
}
243
348
}
244
349
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' ] ) ;
246
354
if ( ! result . success ) {
247
355
console . log ( result . error . toString ( ) ) ;
356
+ } else {
357
+ console . log ( 'Validation successful' ) ;
248
358
}
0 commit comments