wysiwyv 0.1.0 → 0.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,576 @@
1
+ | [README](README.md) | [USAGE](README-USAGE.md) | _INCLUDED PLUGINS_ | [PLUGIN AUTHORING](README-PLUGIN-AUTHORING.md) | [INTEGRATION](README-INTEGRATION.md) |
2
+ | :------------------ | :----------------------- | :----------------- | :--------------------------------------------- | :----------------------------------- |
3
+
4
+ # Included Plugins
5
+
6
+ - [Basic Syntax](#basic-syntax)
7
+ - [$and](#and) / [$or](#or)
8
+ - [$any](#any)
9
+ - [$array](#array) / [$object](#object) / [$plainobject](#plain-object)
10
+ - [$basicisodate](#basic-iso-date) / [$isodate](#iso-date) / [$strictisodate](#strict-iso-date)
11
+ - [$bool](#bool)
12
+ - [$email](#email)
13
+ - [$int](#int) / [$number](#number)
14
+ - [$string](#string)
15
+ - [$uuid](#uuid)
16
+ - [$val](#val)
17
+
18
+ ## BASIC SYNTAX
19
+
20
+ Each plugin has a unique tag starting with a dollar sign, e.g. `$myplugin`.
21
+
22
+ Syntax varies by number of parameters being passed to the plugin.
23
+
24
+ ### No Parameters
25
+
26
+ Matcher is a simple string with just the plugin tag:
27
+
28
+ ```js
29
+ { "id": "$uuid" }
30
+ ```
31
+
32
+ ### One Parameter
33
+
34
+ Matcher is an object with the tag as its only key, and parameter as its value. (Here, the UUID version)
35
+
36
+ ```js
37
+ { "id": { "$uuid": 4 } }
38
+ ```
39
+
40
+ ### Named Parameters
41
+
42
+ Matcher is an object with the tag as its only key, and a sub-object with the named parameters. Parameter names also start with `$`
43
+
44
+ ```js
45
+ { "age": {
46
+ "$int": {
47
+ "$min": 20,
48
+ "$max": 80
49
+ }
50
+ }}
51
+ ```
52
+
53
+ [↑ top](#included-plugins)
54
+
55
+ ## AND
56
+
57
+ Key: `$and`
58
+
59
+ Takes an array of predicates.
60
+
61
+ - If array is empty, returns success
62
+ - If ALL predicates are passed, returns success
63
+ - If ANY predicates fail:
64
+ - One error is recorded for the $and block itself
65
+ - All errors from failed predicates are returned
66
+ - The same path will be set for all for easy association
67
+ - No short-circuiting; all predicates are evaluated
68
+
69
+ ```js
70
+ {
71
+ "id": {
72
+ "$and": [
73
+ "$uuid",
74
+ { "$val": "store_id" }
75
+ ],
76
+ },
77
+ }
78
+ ```
79
+
80
+ See [and-plugin.test.ts](tests/plugins/and-plugin.test.ts) for exhaustive cases
81
+
82
+ [↑ top](#included-plugins)
83
+
84
+ ## ANY
85
+
86
+ Key: `$any`
87
+
88
+ Succeeds on any input value.
89
+
90
+ ```js
91
+ { "biography": "$any" }
92
+ ```
93
+
94
+ See [any-plugin.test.ts](tests/plugins/any-plugin.test.ts) for exhaustive cases
95
+
96
+ [↑ top](#included-plugins)
97
+
98
+ ## ARRAY
99
+
100
+ Key: `$array`
101
+
102
+ Validates that the input is an array.
103
+
104
+ - No parameters: accepts any array
105
+ - Optional named parameters:
106
+ - `$length` — exact length match
107
+ - `$minlength` — minimum length (inclusive)
108
+ - `$maxlength` — maximum length (inclusive)
109
+ - `$each` — validate every element against a template
110
+ If _none_ of these are present, any parameter provided will be treated as `$each`
111
+ - All length constraints are checked independently; a value can fail multiple constraints
112
+ - ⚠️ Unknown parameters alongside recognized ones generate a config error
113
+
114
+ ```js
115
+ // bare — any array
116
+ { "tags": "$array" }
117
+ ```
118
+
119
+ ```js
120
+ // length constraints
121
+ { "tags": { "$array": { "$minlength": 1, "$maxlength": 10 } } }
122
+ ```
123
+
124
+ ```js
125
+ // explicit $each — validate every element
126
+ { "tags": {
127
+ "$array": {
128
+ "$each": "$string"
129
+ }
130
+ }}
131
+ ```
132
+
133
+ ```js
134
+ // implicit $each — validate every element
135
+ { "ages": {
136
+ "$array": "$number"
137
+ }}
138
+ ```
139
+
140
+ ```js
141
+ // implicit each shorthand — if no recognized parameters are present,
142
+ // the params object itself becomes the element template
143
+ { "team": {
144
+ "$array": {
145
+ "name": "$string",
146
+ "role": "$string"
147
+ }
148
+ }}
149
+ ```
150
+
151
+ **Note:** The implicit shorthand only activates when _no_ recognized parameters (`$length`, `$minlength`, `$maxlength`, `$each`) are present. Mixing recognized parameters with extra keys produces a config error.
152
+
153
+ See [array-plugin.test.ts](tests/plugins/array-plugin.test.ts) for exhaustive cases
154
+
155
+ [↑ top](#included-plugins)
156
+
157
+ ## BASIC ISO DATE
158
+
159
+ Key: `$basicisodate`
160
+
161
+ Validates ISO 8601 Basic format — the compact form with no separators.
162
+
163
+ - Must be a string
164
+ - Format: `YYYYMMDDTHHmmssZ` (no dashes or colons)
165
+ - Seconds and fractional seconds are optional
166
+ - Timezone designator is required (`Z` or `±HHmm`)
167
+ - Case-insensitive `T` separator
168
+ - Validates numeric ranges for date/time components
169
+
170
+ ```js
171
+ // accepts "20201209T160953Z"
172
+ { "timestamp": "$basicisodate" }
173
+ ```
174
+
175
+ See [datetime-plugin.test.ts](tests/plugins/datetime-plugin.test.ts) for exhaustive cases
176
+
177
+ [↑ top](#included-plugins)
178
+
179
+ ## BOOL
180
+
181
+ Key: `$bool`
182
+
183
+ Validates that the input is a boolean.
184
+
185
+ - Accepts `true` and `false` only
186
+ - Rejects truthy/falsy stand-ins: `"true"`, `"false"`, `0`, `1`, etc.
187
+ - No parameters
188
+
189
+ ```js
190
+ { "active": "$bool" }
191
+ ```
192
+
193
+ See [bool-plugin.test.ts](tests/plugins/bool-plugin.test.ts) for exhaustive cases
194
+
195
+ [↑ top](#included-plugins)
196
+
197
+ ## EMAIL
198
+
199
+ Key: `$email`
200
+
201
+ Validates email address format.
202
+
203
+ - Must be a string
204
+ - Covers common email formats; not an exhaustive RFC 5322 implementation
205
+ - Rejects: missing `@`, double `@`, trailing dot in domain, consecutive dots in local part, spaces, bare addresses without a domain dot
206
+ - No parameters
207
+
208
+ ```js
209
+ { "contact": "$email" }
210
+ ```
211
+
212
+ See [email-plugin.test.ts](tests/plugins/email-plugin.test.ts) for exhaustive cases
213
+
214
+ [↑ top](#included-plugins)
215
+
216
+ ## INT
217
+
218
+ Key: `$int`
219
+
220
+ Validates that the input is an integer.
221
+
222
+ - Uses `Number.isInteger()` under the hood
223
+ - Accepts whole-valued floats like `1.0` and `-0`
224
+ - Rejects floats, NaN, Infinity, bigints, and non-numbers
225
+ - Optional named parameters:
226
+ - `$min` — inclusive lower bound (≥)
227
+ - `$max` — inclusive upper bound (≤)
228
+ - Both constraints are checked independently; a value can fail both
229
+
230
+ ```js
231
+ // bare — any integer
232
+ { "count": "$int" }
233
+ ```
234
+
235
+ ```js
236
+ // with bounds
237
+ { "age": {
238
+ "$int": {
239
+ "$min": 0,
240
+ "$max": 150
241
+ }
242
+ }}
243
+ ```
244
+
245
+ See [int-plugin.test.ts](tests/plugins/int-plugin.test.ts) for exhaustive cases
246
+
247
+ [↑ top](#included-plugins)
248
+
249
+ ## ISO DATE
250
+
251
+ Key: `$isodate`
252
+
253
+ Validates ISO 8601 Extended format — the common form with separators.
254
+
255
+ - Must be a string
256
+ - Format: `YYYY-MM-DDTHH:mm:ssZ` (with dashes and colons)
257
+ - Seconds and fractional seconds are optional
258
+ - Timezone designator is required (`Z` or `±HH:mm`)
259
+ - Case-insensitive `T` separator
260
+ - Validates numeric ranges for date/time components
261
+ - Also checks that the string parses to a valid JS Date (catches things like Feb 30)
262
+
263
+ ```js
264
+ // accepts "2026-05-05T20:41:00Z", "2026-05-05T13:41:00-07:00"
265
+ { "createdAt": "$isodate" }
266
+ ```
267
+
268
+ See [datetime-plugin.test.ts](tests/plugins/datetime-plugin.test.ts) for exhaustive cases
269
+
270
+ [↑ top](#included-plugins)
271
+
272
+ ## NUMBER
273
+
274
+ Key: `$number`
275
+
276
+ Validates that the input is a finite number.
277
+
278
+ - Accepts integers, floats, `-0`
279
+ - Rejects NaN, Infinity, -Infinity, bigints, and non-numbers
280
+ - Optional named parameters:
281
+ - `$min` — inclusive lower bound (≥)
282
+ - `$max` — inclusive upper bound (≤)
283
+ - `$gt` — exclusive lower bound (>)
284
+ - `$lt` — exclusive upper bound (<)
285
+ - All constraints are checked independently
286
+
287
+ ```js
288
+ // bare — any finite number
289
+ { "score": "$number" }
290
+ ```
291
+
292
+ ```js
293
+ // inclusive bounds
294
+ { "rating": {
295
+ "$number": {
296
+ "$min": 0,
297
+ "$max": 5
298
+ }
299
+ }}
300
+ ```
301
+
302
+ ```js
303
+ // exclusive bounds
304
+ { "temperature": {
305
+ "$number": {
306
+ "$gt": -273.15
307
+ }
308
+ }}
309
+ ```
310
+
311
+ See [number-plugin.test.ts](tests/plugins/number-plugin.test.ts) for exhaustive cases
312
+
313
+ [↑ top](#included-plugins)
314
+
315
+ ## OBJECT
316
+
317
+ Key: `$object`
318
+
319
+ Validates that the input is an object in the broad sense — includes Date, RegExp, Map, Set, wrapped primitives, etc.
320
+
321
+ - Rejects `null`, arrays, functions, and classes
322
+ - For plain objects only, see [$plainobject](#plain-object)
323
+ - Optional named parameters (mutually exclusive):
324
+ - `$partial` — validate a subset of keys; extra keys in the candidate are allowed; missing expected keys produce errors
325
+ - `$eachElement` — validate every value in the object against a template
326
+ - Using both `$partial` and `$eachElement` together produces a config error
327
+ - Unknown parameters produce a config error
328
+
329
+ ```js
330
+ // bare — accepts any object, including Date, Map, Set, etc.
331
+ { "metadata": "$object" }
332
+ ```
333
+
334
+ ```js
335
+ // partial match — only check specified keys, ignore extras
336
+ { "user": {
337
+ "$object": {
338
+ "$partial": {
339
+ "name": "$string",
340
+ "email": "$email"
341
+ }
342
+ }
343
+ }}
344
+ ```
345
+
346
+ ```js
347
+ // each element — every value must match the template
348
+ { "scores": {
349
+ "$object": {
350
+ "$eachElement": "$number"
351
+ }
352
+ }}
353
+ ```
354
+
355
+ ```js
356
+ // each element with nesting
357
+ { "teams": {
358
+ "$object": {
359
+ "$eachElement": { "$array": { "$length": 2 } }
360
+ }
361
+ }}
362
+ ```
363
+
364
+ See [object-plugin.test.ts](tests/plugins/object-plugin.test.ts) for exhaustive cases
365
+
366
+ [↑ top](#included-plugins)
367
+
368
+ ## OR
369
+
370
+ Key: `$or`
371
+
372
+ Takes an array of predicates.
373
+
374
+ - If array is empty, returns an error
375
+ - Boolean algebra defines the OR identity as false
376
+ - If ANY predicates are passed, returns success
377
+ - Short-Circuiting: Further predicates are not evaluated
378
+ - If ALL predicates fail:
379
+ - One error is recorded for the $or block itself
380
+ - All errors from all predicates are returned
381
+ - The same path will be set for all for easy association
382
+
383
+ ```js
384
+ {
385
+ "id": {
386
+ "$or": [
387
+ "$uuid",
388
+ "$number"
389
+ ]
390
+ }
391
+ }
392
+ ```
393
+
394
+ See [or-plugin.test.ts](tests/plugins/or-plugin.test.ts) for exhaustive cases
395
+
396
+ [↑ top](#included-plugins)
397
+
398
+ ## PLAIN OBJECT
399
+
400
+ Key: `$plainobject`
401
+
402
+ Validates that the input is a plain object — only literal `{}`, `new Object()`, and `Object.create(null)`.
403
+
404
+ - Rejects Date, RegExp, Map, Set, wrapped primitives, and other non-plain objects
405
+ - Stricter than [$object](#object), which accepts any object type
406
+ - Same optional parameters as `$object`:
407
+ - `$partial` — validate a subset of keys
408
+ - `$eachElement` — validate every value
409
+ - `$partial` and `$eachElement` are mutually exclusive
410
+
411
+ ```js
412
+ // bare — any plain object
413
+ { "config": "$plainobject" }
414
+ ```
415
+
416
+ ```js
417
+ // with $partial
418
+ { "config": {
419
+ "$plainobject": {
420
+ "$partial": {
421
+ "debug": "$bool",
422
+ "port": "$int"
423
+ }
424
+ }
425
+ }}
426
+ ```
427
+
428
+ See [object-plugin.test.ts](tests/plugins/object-plugin.test.ts) for exhaustive cases
429
+
430
+ [↑ top](#included-plugins)
431
+
432
+ ## STRICT ISO DATE
433
+
434
+ Key: `$strictisodate`
435
+
436
+ Validates RFC 3339 format — a strict profile of ISO 8601 commonly used in APIs.
437
+
438
+ - Must be a string
439
+ - Format: `YYYY-MM-DDTHH:mm:ss.sssZ`
440
+ - Allows space as date/time separator (not just `T`)
441
+ - Seconds and fractional seconds are optional
442
+ - Timezone designator is required (`Z` or `±HH:mm`, including `-00:00`)
443
+ - Case-insensitive `T`/`t` separator
444
+ - Also checks that the string parses to a valid JS Date
445
+
446
+ ```js
447
+ // accepts "2026-05-05T20:41:00Z", "2026-05-05 20:41:00Z"
448
+ { "timestamp": "$strictisodate" }
449
+ ```
450
+
451
+ See [datetime-plugin.test.ts](tests/plugins/datetime-plugin.test.ts) for exhaustive cases
452
+
453
+ [↑ top](#included-plugins)
454
+
455
+ ## STRING
456
+
457
+ Key: `$string`
458
+
459
+ Validates that the input is a string.
460
+
461
+ - Accepts any primitive string value
462
+ - Rejects non-strings, including numbers, booleans, null, undefined, and boxed `new String()` objects
463
+ - No parameters
464
+
465
+ ```js
466
+ { "name": "$string" }
467
+ ```
468
+
469
+ See [string-plugin.test.ts](tests/plugins/string-plugin.test.ts) for exhaustive cases
470
+
471
+ [↑ top](#included-plugins)
472
+
473
+ ## UUID
474
+
475
+ Key: `$uuid`
476
+
477
+ Validates UUID format.
478
+
479
+ - No parameter: accepts any valid UUID (versions 1–8, NIL, and max)
480
+ - Single parameter (version): validates a specific UUID version
481
+ - `0` — NIL UUID (all zeros)
482
+ - `1` through `8` — specific version
483
+ - `"F"` — max UUID (all f's)
484
+ - Invalid version numbers produce a config error
485
+ - Must be a string
486
+ - Case-insensitive
487
+
488
+ ```js
489
+ // any UUID
490
+ { "id": "$uuid" }
491
+ ```
492
+
493
+ ```js
494
+ // specific version
495
+ { "id": { "$uuid": 4 } }
496
+ ```
497
+
498
+ ```js
499
+ // NIL UUID
500
+ { "id": { "$uuid": 0 } }
501
+ ```
502
+
503
+ See [uuid-plugin.test.ts](tests/plugins/uuid-plugin.test.ts) for exhaustive cases
504
+
505
+ [↑ top](#included-plugins)
506
+
507
+ ## VAL
508
+
509
+ Key: `$val`
510
+
511
+ Matches values by name — either against predefined values supplied at setup time, or by capturing and back-referencing values seen during validation.
512
+
513
+ ### Predefined Values
514
+
515
+ Supply known values when creating the validator. The template checks that candidate values match.
516
+
517
+ ```js
518
+ const wyv = makeWysiwyv({
519
+ pluginSetups: {
520
+ $val: {
521
+ expectedTeam: "Yankees",
522
+ fruit: "apple",
523
+ },
524
+ },
525
+ });
526
+
527
+ const expected = {
528
+ team: { $val: "expectedTeam" },
529
+ snack: { $val: "fruit" },
530
+ };
531
+
532
+ // passes if team === "Yankees" and snack === "apple"
533
+ ```
534
+
535
+ ### Back-References
536
+
537
+ If a val name hasn't been predefined, the first occurrence captures whatever value appears in the candidate. Subsequent uses of the same name check that the value matches.
538
+
539
+ ```js
540
+ const expected = {
541
+ billing: {
542
+ customerId: { $val: "cid" },
543
+ },
544
+ shipping: {
545
+ customerId: { $val: "cid" },
546
+ },
547
+ };
548
+
549
+ // passes if billing.customerId === shipping.customerId
550
+ // (whatever the value is)
551
+ ```
552
+
553
+ ### Combined
554
+
555
+ Predefined values and back-references work together. Predefined keys are checked immediately; undefined keys are captured on first encounter and enforced on subsequent encounters.
556
+
557
+ ```js
558
+ const wyv = makeWysiwyv({
559
+ pluginSetups: {
560
+ $val: { region: "us-east-1" },
561
+ },
562
+ });
563
+
564
+ const expected = {
565
+ region: { $val: "region" },
566
+ primaryId: { $val: "id" },
567
+ backupId: { $val: "id" },
568
+ };
569
+
570
+ // region must be "us-east-1" (predefined)
571
+ // primaryId and backupId must match each other (back-reference)
572
+ ```
573
+
574
+ See [vals-plugin.test.ts](tests/plugins/vals-plugin.test.ts) for exhaustive cases
575
+
576
+ [↑ top](#included-plugins)
@@ -0,0 +1,45 @@
1
+ | [README](README.md) | [USAGE](README-USAGE.md) | [INCLUDED PLUGINS](README-INCLUDED-PLUGINS.md) | [PLUGIN AUTHORING](README-PLUGIN-AUTHORING.md) | _INTEGRATION_ |
2
+ | :------------------ | :----------------------- | :--------------------------------------------- | :--------------------------------------------- | ------------- |
3
+
4
+ # Integration
5
+
6
+ ## jest
7
+
8
+ See [test-util](./test-util.ts)
9
+
10
+ ## express
11
+
12
+ ```ts
13
+ const wyv = makeWysiwyv();
14
+
15
+ export const validateBody =
16
+ (template: HookValue) =>
17
+ (req: Request, res: Response, next: NextFunction) => {
18
+ const result = wyv.validate(template, req.body);
19
+ if (result.success) return next();
20
+ res.status(400).json({
21
+ error: "Validation failed",
22
+ details: result.errors,
23
+ });
24
+ };
25
+
26
+ const newUserTemplate = {
27
+ name: "$string",
28
+ email: "$email",
29
+ age: { $int: { $min: 18 } },
30
+ };
31
+
32
+ app.post("/users", validateBody(newUserTemplate), createUserHandler);
33
+ ```
34
+
35
+ ## zod
36
+
37
+ ```ts
38
+ const toZodLike = (assessment: HookAssessment) => ({
39
+ success: assessment.success,
40
+ issues: assessment.errors.map((e) => ({
41
+ message: e.message,
42
+ path: e.path.split(".").filter(Boolean), // convert ".age" to ["age"]
43
+ })),
44
+ });
45
+ ```
@@ -0,0 +1,177 @@
1
+ | [README](README.md) | [USAGE](README-USAGE.md) | [INCLUDED PLUGINS](README-INCLUDED-PLUGINS.md) | _PLUGIN AUTHORING_ | [INTEGRATION](README-INTEGRATION.md) |
2
+ | :------------------ | :----------------------- | :--------------------------------------------- | :----------------- | :----------------------------------- |
3
+
4
+ # Plugin Authoring
5
+
6
+ ## 1. Pick a key.
7
+
8
+ Select a string to identify your plugin. It should start with a dollar sign and should be unique among loaded plugins. We can use `$us-ssn` as an example.
9
+ Your key will be used in several contexts:
10
+
11
+ 1. In a matching template, to select your plugin for matching a given node.
12
+ 2. In initial Wysiwyv factory, to specify plugin setup data.
13
+ 3. When the engine is calling your plugin, to give you exclusive access to setup and scratch data.
14
+
15
+ [↑ top](#plugin-authoring)
16
+
17
+ ## 2. Scaffold your plugin.
18
+
19
+ 1. Start with a `WyvPlugin` object.
20
+ 2. Populate `handles` with a function that will return true when passed your key.
21
+ 3. Populate `handlers` with an object that maps your key to a function that should evaluate the node being evaluated.
22
+
23
+ ```ts
24
+ export const WYV_KEY_US_SSN = "$us-ssn";
25
+ const ssnWyvern: WyvPlugin = {
26
+ handles: (value) => [WYV_KEY_US_SSN].includes(value),
27
+ handlers: {
28
+ [WYV_KEY_US_SSN]: (value, expected, options) => {
29
+ // ...
30
+ },
31
+ },
32
+ };
33
+ ```
34
+
35
+ [↑ top](#plugin-authoring)
36
+
37
+ ## 3. Evaluate data and return your assessment.
38
+
39
+ 1. The `value` parameter is the value to be evaluated.
40
+ The `expected` parameter is the template node the value should be assessed against.
41
+ 1. If you have determined the value is acceptable, just
42
+ `return HookAssessor.SUCCESS;`
43
+ 1. If you have one reason for rejecting a value
44
+ 1. create an error with one of the helpers from `HookError.ts`:
45
+ `const e = errValue(3, 6, '.employee[3].id');`
46
+ 2. return it with
47
+ `return HookAssessor.fault(myError);`
48
+ 1. If you have multiple reasons for rejecting a value
49
+ 1. start an assessement with:
50
+ `const errors = HookAssessment.start()`
51
+ 2. add encountered errors one at a time with `errors.fault(myNextError);`
52
+ 3. incorporate another assessent into your open one with `errors.include(myOtherErrors);`
53
+ 4. when finished, just `return errors;`
54
+ 1. All assessments follow the same format:
55
+
56
+ if no issues encountered:
57
+
58
+ ```ts
59
+ {
60
+ success: true,
61
+ errors: [],
62
+ }
63
+ ```
64
+
65
+ if faults are found:
66
+
67
+ ```ts
68
+ {
69
+ success: false,
70
+ errors: [
71
+ {message: '...', path: '...'},
72
+ ...,
73
+ ],
74
+ }
75
+ ```
76
+
77
+ [↑ top](#plugin-authoring)
78
+
79
+ ## 4. Know your Options
80
+
81
+ The `options` parameter has several fields.
82
+
83
+ ```ts
84
+ options = {
85
+ path: string;
86
+ params: ParamsType;
87
+ setup: SetupType;
88
+ context: ContextType;
89
+ shared: Record<string, unknown>;
90
+ evaluate: WysiwyvEvaluatorFunction;
91
+ }
92
+ ```
93
+
94
+ - `path`:
95
+ - Your current path in the original data structure.
96
+ - Components look like `.fieldname` for objects or `[#]` for arrays.
97
+ - Use this as the path value when making HookErrors
98
+ - `params`:
99
+ - Parameters to your plugin specified at the current position in the matching template object.
100
+ - Customize by defining a `MyHookParams` type reflecting how they should be passed.
101
+ - This can be any kind of data representable in JSON since it comes from the template.
102
+ `type WyvParams = { version: number }`
103
+ - `setup`:
104
+ - Parameters to your plugin specified at the time the matching engine is instantiated (makeWysiwyv)
105
+ - Customize by defining a `MyHookSetup` type similar to that for `params`
106
+ - How to provide the data is shown below, at [Register Plugin](#6-register-plugin)
107
+ - `context`:
108
+ - A private data store for any information your plugin needs to track from one instance to the next.
109
+ - Only plugins with the same `key` will see this data.
110
+ - Customize with a `MyHookContext` type similar to that for `params`.
111
+ - `shared`:
112
+ - A shared data store for any information that needs to be reused across multiple plugin types.
113
+ - This is the Wild West. No type enforcement, no privacy.
114
+ - `evaluate`:
115
+ - A callback function provided by the engine. See [Recurse and Descend](#5-recurse-and-descend)
116
+
117
+ ⚠️ If you customize any types, all should be provided as type params when defining your plugin:
118
+
119
+ ```ts
120
+ const ssnWyvern: WyvPlugin<MyParams, MySetup, MyContext> = {...};
121
+ ```
122
+
123
+ [↑ top](#plugin-authoring)
124
+
125
+ ## 5. Recurse and Descend
126
+
127
+ Your plugin can behave like `$array` or `$object`, descending recursively into a subtree of the data. This could even be a virtual tree, generated during validation by your custom logic.
128
+
129
+ 1. Open an assessment:
130
+ `const errors = HookAssessment.start();`
131
+
132
+ 2. Call `options.evaluate` on the subtree:
133
+
134
+ - This is a function provided by the engine for recursive descent. Call it when you need to hand control back, for example in the case of evaluating individual array or object children.
135
+ - Call with the following arguments:
136
+ - `expected` A Wysiwyv template object to be evaluated on the subtree
137
+ - `candidate` The child (or derived) field or subtree being evaluated
138
+ - `path` Use the `options.path` parameter you received, appending information about how the child element is being selected (key, index, hash, etc)
139
+
140
+ `const subErrors = options.evaluate(subTemplate, subTree, path + 'my selector');`
141
+
142
+ 3. Include any errors back into your assessment:
143
+
144
+ `errors.include(subErrors);`
145
+
146
+ 4. Return your final assessment with errors from your level plus any errors from the recursion.
147
+
148
+ `return errors;`
149
+
150
+ [↑ top](#plugin-authoring)
151
+
152
+ ## 6. Register Plugin
153
+
154
+ Pass your plugin into `config.plugins` when making your engine instance.
155
+
156
+ If you have preconfig data to provide, pass it in to `config.pluginSetups.$myPlugin`.
157
+
158
+ ```ts
159
+ import { makeWysiwyv } from "./src/wysiwyv-core";
160
+ // or, if you want to include all default plugins:
161
+ // import { makeWysiwyv } from "./src/wysiwyv";
162
+
163
+ import usSsnWyvern from "./src/hooks/us-ssn";
164
+
165
+ const wyv = makeWysiwyv({
166
+ plugins: [usSsnWyvern],
167
+ pluginSetups: {
168
+ "$us-ssn": {
169
+ defaultFormat: "###-##-####",
170
+ },
171
+ },
172
+ });
173
+
174
+ const result = wyv.validate(myExpected, myCandidate);
175
+ ```
176
+
177
+ [↑ top](#plugin-authoring)
@@ -0,0 +1,352 @@
1
+ | [README](README.md) | _USAGE_ | [INCLUDED PLUGINS](README-INCLUDED-PLUGINS.md) | [PLUGIN AUTHORING](README-PLUGIN-AUTHORING.md) | [INTEGRATION](README-INTEGRATION.md) |
2
+ | :------------------ | :------ | :--------------------------------------------- | :--------------------------------------------- | :----------------------------------- |
3
+
4
+ # WYSIWYV - Usage
5
+
6
+ - [Basics](#basics)
7
+ - [Result Format](#result-format)
8
+ - [Literal Matching](#literal-matching)
9
+ - [Plugins](#plugins)
10
+ - [Escaping](#escaping)
11
+ - [Plugin Parameters](#plugin-parameters)
12
+ - [Initialization](#initialization)
13
+ - [Template Parameters](#template-parameters)
14
+ - [Composition](#composition)
15
+
16
+ ## Basics
17
+
18
+ 1. Get your data.
19
+
20
+ Any data will do; it does not need to be an object at root.
21
+
22
+ `const data = {name: "Joe"};`
23
+
24
+ 2. Write your template.
25
+
26
+ You can start by copy-pasting your data, if that helps.
27
+
28
+ Anything that should be dynamic, replace with a matcher.
29
+
30
+ `const template = {name: "$string"};`
31
+
32
+ 3. Make your validator
33
+
34
+ You can select custom plugin sets, add your own, or initialize data for smart plugins
35
+
36
+ `const wyv = makeWysiwyv();`
37
+
38
+ 4. Run the validation
39
+
40
+ `const result = wyv.validate(template, data);`
41
+
42
+ 5. Enjoy Success!
43
+
44
+ `console.log(result.success);`
45
+
46
+ ```json
47
+ true
48
+ ```
49
+
50
+ [↑ top](#wysiwyv---usage)
51
+
52
+ ## Result Format
53
+
54
+ The result format is consistent whether successful, partially successful, or if a validation is fully rejected.
55
+
56
+ Every error includes a detailed message and a full path to where in the comparison the error was encountered.
57
+
58
+ - Object keys look like `.key`
59
+ - Array indexes look like `[10]`
60
+
61
+ On success:
62
+
63
+ ```js
64
+ { success: true, errors: [] }
65
+ ```
66
+
67
+ On errors:
68
+
69
+ ```js
70
+ {
71
+ success: false,
72
+ errors:[
73
+ {
74
+ message: "Type: Expected 'number', got value 'old enough'",
75
+ path: ".age"
76
+ },
77
+ {
78
+ message: "Value: Expected 'Sales', got value 'Delivery'",
79
+ path: ".jobs[1].dept.name"
80
+ }
81
+ ]
82
+ }
83
+ ```
84
+
85
+ [↑ top](#wysiwyv---usage)
86
+
87
+ ## Literal Matching
88
+
89
+ The core behavior is just a simple deep-comparison matcher. Your data should look exactly like your template. Only an exact match will achieve `success`!
90
+
91
+ All templates must consist only of values representable by JSON; the data being matched can be arbitrary, but plugins must be used to validate anything not representable in JSON.
92
+
93
+ ### Valid Types:
94
+
95
+ - _scalars_: null, number, string, boolean
96
+ - _arrays_ with elements of any valid type
97
+ - _objects_ with string keys and values of any valid type
98
+
99
+ ### Example
100
+
101
+ data:
102
+
103
+ ```js
104
+ {
105
+ name: "Jawn Deaux",
106
+ jobs:[
107
+ {
108
+ title: "CEO",
109
+ dept: {
110
+ name: "Delivery"
111
+ }
112
+ },
113
+ {
114
+ title: "Director",
115
+ dept: {
116
+ name: "Sales"
117
+ }
118
+ }
119
+ ]
120
+ }
121
+ ```
122
+
123
+ template:
124
+
125
+ ```json
126
+ {
127
+ "name": "Jawn Deaux",
128
+ "jobs": [
129
+ {
130
+ "title": "CEO",
131
+ "dept": {
132
+ "name": "Delivery"
133
+ }
134
+ },
135
+ {
136
+ "title": "Director",
137
+ "dept": {
138
+ "name": "Sales"
139
+ }
140
+ }
141
+ ]
142
+ }
143
+ ```
144
+
145
+ [↑ top](#wysiwyv---usage)
146
+
147
+ ## Plugins
148
+
149
+ Plugins are where everything gets flexible. You can assess data types, sub-object shapes, array length, array elements, combine assessments with boolean logic, or write your own plugins for anything we didn't think of.
150
+
151
+ We will gladly include or link any plugins you care to share!
152
+
153
+ - Every plugin is identified by a `key` -- a string starting with a dollar sign (`$`). For example: `$and`, `$uuid`.
154
+
155
+ - Plugins are used in place of values in the matching template.
156
+
157
+ static:
158
+
159
+ ```json
160
+ {
161
+ "id": "John"
162
+ }
163
+ ```
164
+
165
+ loose:
166
+
167
+ ```json
168
+ {
169
+ "id": "$string"
170
+ }
171
+ ```
172
+
173
+ [↑ top](#wysiwyv---usage)
174
+
175
+ ## Escaping
176
+
177
+ Sometimes you really will want to match a string starting with a dollar sign. Just use a second dollar sign (`$$`) in your matching string to indicate it's not meant to be a plugin key.
178
+
179
+ data (starts with single dollar):
180
+
181
+ ```json
182
+ {
183
+ "name": "$heka"
184
+ }
185
+ ```
186
+
187
+ matching template (double dollar):
188
+
189
+ ```json
190
+ {
191
+ "name": "$$heka"
192
+ }
193
+ ```
194
+
195
+ [↑ top](#wysiwyv---usage)
196
+
197
+ ## Plugin Parameters
198
+
199
+ When referencing a plugin, it can be passed 0, 1, or more parameters.
200
+
201
+ 0 Params: Just use the plugin key as a string.
202
+
203
+ ```json
204
+ {
205
+ "datum-list": "$array"
206
+ }
207
+ ```
208
+
209
+ 1 Param: Use a simple object with plugin as key and parameter as value. The plugin's docs will tell you what argument a single parameter will correspond to, if there are multiple.
210
+
211
+ ```js
212
+ {
213
+ // default arg is $uuid.$version
214
+ "id": { "$uuid": 4 },
215
+ }
216
+ ```
217
+
218
+ More Params: Use a nested object. The outer just has the plugin key, and the inner has the parameters paired with named keys.
219
+
220
+ ```json
221
+ {
222
+ "datum-list": {
223
+ "$array": {
224
+ "$minlength": 2,
225
+ "$maxlength": 10
226
+ }
227
+ }
228
+ }
229
+ ```
230
+
231
+ [↑ top](#wysiwyv---usage)
232
+
233
+ ## Initialization
234
+
235
+ Plugins can accept pre-filled setup objects. For example, when matching values, you could have some that you got from another source during a test, but after setting up the validation template.
236
+
237
+ ```js
238
+ const wyv = makeWysiwyv({
239
+ pluginSetups: {
240
+ $val: {
241
+ entityId: 24601,
242
+ },
243
+ },
244
+ });
245
+
246
+ const data = {
247
+ id: 24601,
248
+ };
249
+
250
+ const template = {
251
+ id: { $val: "entityId" },
252
+ };
253
+
254
+ const result = wyv.validate(template, data);
255
+
256
+ // result.success === true
257
+ ```
258
+
259
+ [↑ top](#wysiwyv---usage)
260
+
261
+ ## Template Parameters
262
+
263
+ ### (Meta-Plugins)
264
+
265
+ Some plugins accept matcher templates as parameters.
266
+
267
+ - `$and` and `$or` each take an array of templates to evaluate
268
+ - `$array.$each` is an optional template that every element of an array must individually match
269
+ - `$object.$eachElement` and `$plainobject.$eachElement` are similar, every value (regardless of key) must match
270
+ - `$object.$partial` and `$plainobject.$partial` are optional (mutually exclusive with `.$eachElement`) templates. Every k-v present in the provided template must be present in the candidate data being evaluated. Extra candidate-object keys beyond those listed are ignored (accepted).
271
+
272
+ ### $or template:
273
+
274
+ ```json
275
+ {
276
+ "department": {
277
+ "$or": ["Sales", "Marketing", "Ideation", "Finance"]
278
+ }
279
+ }
280
+ ```
281
+
282
+ ### $object.$partial
283
+
284
+ #### candidate data:
285
+
286
+ ```js
287
+ {
288
+ id: 24680,
289
+ name: "Jane Doe-Smith",
290
+ }
291
+ ```
292
+
293
+ #### matcher template:
294
+
295
+ ```json
296
+ {
297
+ "$object": {
298
+ "$partial": {
299
+ "id": 24680
300
+ // ignores .name
301
+ }
302
+ }
303
+ }
304
+ ```
305
+
306
+ [↑ top](#wysiwyv---usage)
307
+
308
+ ## Composition
309
+
310
+ Any Meta-Plugins can be nested and alternated arbitrarily with other plugins and with core behavior. Because control is handed back to the engine for each node in descent, nested claims are treated identically to ones in the base tree.
311
+
312
+ data:
313
+
314
+ ```js
315
+ {
316
+ people: [
317
+ {
318
+ name: "Jon Deo",
319
+ title: "CEO",
320
+ salary: 800_000,
321
+ },
322
+ {
323
+ name: "Don Jo",
324
+ department: "Burger-Flippin",
325
+ wage: 3.75,
326
+ },
327
+ ],
328
+ }
329
+ ```
330
+
331
+ template:
332
+
333
+ ```js
334
+ {
335
+ people: {
336
+ $array: { $each: {
337
+ $and: [
338
+ { $object: { $partial:
339
+ { name: "$string" }
340
+ }},
341
+ { $or: [
342
+ { name: "$any", title: "$string", salary: "$number" },
343
+ { name: "$any", department: "$string", wage: "$number" },
344
+ ]},
345
+ ],
346
+ },
347
+ },
348
+ },
349
+ }
350
+ ```
351
+
352
+ [↑ top](#wysiwyv---usage)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "wysiwyv",
3
- "version": "0.1.0",
3
+ "version": "0.1.1",
4
4
  "main": "./dist/wysiwyv.js",
5
5
  "devDependencies": {
6
6
  "@eslint/js": "^10.0.1",
@@ -15,6 +15,11 @@
15
15
  "peerDependencies": {
16
16
  "typescript": "^5"
17
17
  },
18
+ "peerDependenciesMeta": {
19
+ "typescript": {
20
+ "optional": true
21
+ }
22
+ },
18
23
  "exports": {
19
24
  ".": {
20
25
  "types": "./dist/wysiwyv.d.ts",
@@ -28,7 +33,7 @@
28
33
  "files": [
29
34
  "dist",
30
35
  "LICENSE",
31
- "README.md"
36
+ "README*.md"
32
37
  ],
33
38
  "jsdelivr": "./dist/wysiwyv.min.js",
34
39
  "scripts": {