@whatsaaaa/form-builder-schema 0.1.0 → 0.3.0

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.
package/dist/index.js CHANGED
@@ -1,124 +1,884 @@
1
1
  import * as v from "valibot";
2
- // src/field-types.ts
2
+ // src/builder/build-field-schema.ts
3
+ /**
4
+ * The schema runs in this order: empty check, base schema, normalize rules, required check,
5
+ * then the type's default rules and the field's rules.
6
+ */
7
+ function buildFieldSchema(checked, messages) {
8
+ const { field, path, type, kind } = checked;
9
+ const required = !!field.required;
10
+ const requiredMessage = messages.resolve(typeof field.required === "object" && field.required.message || messages.required, {
11
+ field,
12
+ path
13
+ });
14
+ const normalize = [];
15
+ const validate = [];
16
+ for (const rule of checked.rules) {
17
+ const message = rule.message === void 0 ? void 0 : messages.resolve(rule.message, {
18
+ field,
19
+ rule: rule.name,
20
+ path
21
+ });
22
+ const action = rule.def.create(rule.params, message);
23
+ if (rule.def.stage === "normalize") normalize.push(action);
24
+ else validate.push(action);
25
+ }
26
+ const base = type.schema(field, { path });
27
+ const requiredCheck = required ? kind.required?.(requiredMessage) : void 0;
28
+ const head = pipe(base, requiredCheck ? [...normalize, requiredCheck] : normalize);
29
+ const empty = required ? v.never(requiredMessage) : v.pipe(v.unknown(), v.transform(() => void 0));
30
+ const whenFilled = (schema) => v.lazy((input) => kind.isEmpty(input) ? empty : schema);
31
+ const schema = whenFilled(v.pipe(head, whenFilled(pipe(v.unknown(), validate))));
32
+ return required ? schema : v.optional(schema);
33
+ }
34
+ const pipe = (schema, actions) => actions.reduce((acc, action) => v.pipe(acc, action), schema);
35
+ // src/field-types/default-field-types.ts
3
36
  const defaultFieldTypes = {
4
37
  text: {
5
38
  kind: "string",
6
- base: (m) => v.string(m)
39
+ schema: () => v.string()
7
40
  },
8
41
  password: {
9
42
  kind: "string",
10
- base: (m) => v.string(m)
43
+ schema: () => v.string()
11
44
  },
12
45
  email: {
13
46
  kind: "string",
14
- base: (m) => v.string(m),
15
- defaultRules: [{
16
- rule: "email",
17
- message: "Invalid email"
18
- }]
47
+ schema: () => v.string(),
48
+ defaultRules: [{ rule: "email" }]
19
49
  },
20
50
  number: {
21
51
  kind: "number",
22
- base: (m) => v.number(m)
52
+ schema: () => v.number()
23
53
  }
24
54
  };
25
- // src/rules.ts
55
+ // src/kinds/default-kinds.ts
56
+ const isNil = (input) => input === void 0 || input === null;
57
+ const defaultKinds = {
58
+ string: {
59
+ isEmpty: (input) => isNil(input) || input === "",
60
+ required: (message) => v.nonEmpty(message)
61
+ },
62
+ number: { isEmpty: (input) => isNil(input) || input === "" || Number.isNaN(input) },
63
+ boolean: {
64
+ isEmpty: isNil,
65
+ required: (message) => v.value(true, message)
66
+ },
67
+ date: {
68
+ isEmpty: (input) => isNil(input) || input === "",
69
+ required: (message) => v.nonEmpty(message)
70
+ },
71
+ array: {
72
+ isEmpty: (input) => isNil(input) || Array.isArray(input) && input.length === 0,
73
+ required: (message) => v.minLength(1, message)
74
+ },
75
+ file: { isEmpty: isNil },
76
+ object: { isEmpty: isNil },
77
+ none: { isEmpty: () => true }
78
+ };
79
+ // src/rules/define-rule.ts
26
80
  const defineRule = (def) => def;
27
- const nonNegInt = v.pipe(v.number(), v.integer(), v.minValue(0));
28
- const noParams = v.object({});
29
- const defaultRules = {
30
- minLength: defineRule({
81
+ // src/rules/param-schemas.ts
82
+ /** A length, count or size: a whole number from 0. */
83
+ const count = v.pipe(v.number(), v.integer(), v.minValue(0));
84
+ const countParams = v.object({ value: count });
85
+ const isIsoValue = (input) => v.ISO_DATE_REGEX.test(input) || v.ISO_DATE_TIME_REGEX.test(input) || v.ISO_TIME_REGEX.test(input);
86
+ /** Valibot compares dates as ISO strings, so the param must be one too. */
87
+ const isoValue = v.pipe(v.string(), v.check(isIsoValue, "Use an ISO date, date-time or time, such as 2026-01-01"));
88
+ const valueSchema = (kind) => {
89
+ switch (kind) {
90
+ case "date": return isoValue;
91
+ case "string": return v.string();
92
+ case "boolean": return v.boolean();
93
+ default: return v.number();
94
+ }
95
+ };
96
+ // src/rules/comparison-rules.ts
97
+ const ordered = ["number", "date"];
98
+ const any = [
99
+ "number",
100
+ "date",
101
+ "string",
102
+ "boolean"
103
+ ];
104
+ const meta$6 = (label) => ({
105
+ label,
106
+ category: "comparison"
107
+ });
108
+ const valueParams = (kind) => v.object({ value: valueSchema(kind) });
109
+ const valuesParams = (kind) => v.object({ values: v.pipe(v.array(valueSchema(kind)), v.minLength(1, "Give at least one value")) });
110
+ /** Compares the value with a param of the same type. */
111
+ const comparisonRules = {
112
+ minValue: defineRule({
113
+ kinds: ordered,
114
+ params: valueParams,
115
+ meta: meta$6("Minimum"),
116
+ create: (p, m) => v.minValue(p.value, m)
117
+ }),
118
+ maxValue: defineRule({
119
+ kinds: ordered,
120
+ params: valueParams,
121
+ meta: meta$6("Maximum"),
122
+ create: (p, m) => v.maxValue(p.value, m)
123
+ }),
124
+ gtValue: defineRule({
125
+ kinds: ordered,
126
+ params: valueParams,
127
+ meta: meta$6("Greater than"),
128
+ create: (p, m) => v.gtValue(p.value, m)
129
+ }),
130
+ ltValue: defineRule({
131
+ kinds: ordered,
132
+ params: valueParams,
133
+ meta: meta$6("Less than"),
134
+ create: (p, m) => v.ltValue(p.value, m)
135
+ }),
136
+ value: defineRule({
137
+ kinds: any,
138
+ params: valueParams,
139
+ meta: meta$6("Equals"),
140
+ create: (p, m) => v.value(p.value, m)
141
+ }),
142
+ notValue: defineRule({
143
+ kinds: any,
144
+ params: valueParams,
145
+ meta: meta$6("Does not equal"),
146
+ create: (p, m) => v.notValue(p.value, m)
147
+ }),
148
+ values: defineRule({
149
+ kinds: ordered,
150
+ params: valuesParams,
151
+ meta: meta$6("One of"),
152
+ create: (p, m) => v.values(p.values, m)
153
+ }),
154
+ notValues: defineRule({
155
+ kinds: ordered,
156
+ params: valuesParams,
157
+ meta: meta$6("None of"),
158
+ create: (p, m) => v.notValues(p.values, m)
159
+ })
160
+ };
161
+ // src/rules/content-rules.ts
162
+ const meta$5 = (label, description) => ({
163
+ label,
164
+ description,
165
+ category: "content"
166
+ });
167
+ const regexError = (pattern, flags = "") => {
168
+ try {
169
+ new RegExp(pattern, flags);
170
+ return;
171
+ } catch (error) {
172
+ return error.message;
173
+ }
174
+ };
175
+ const FLAGS = /^[dgimsuvy]*$/;
176
+ const regexParams = v.pipe(v.object({
177
+ pattern: v.string(),
178
+ flags: v.optional(v.pipe(v.string(), v.regex(FLAGS, "Flags can be d, g, i, m, s, u, v and y")))
179
+ }), v.forward(v.check((p) => !FLAGS.test(p.flags ?? "") || regexError(p.pattern, p.flags) === void 0, (issue) => regexError(issue.input.pattern, issue.input.flags) ?? "Invalid pattern"), ["pattern"]));
180
+ const item = v.union([
181
+ v.string(),
182
+ v.number(),
183
+ v.boolean()
184
+ ]);
185
+ const stringParams = v.object({ value: v.string() });
186
+ const contentParams = (kind) => v.object({ value: kind === "array" ? item : v.string() });
187
+ const contentRules = {
188
+ regex: defineRule({
189
+ kinds: ["string"],
190
+ params: regexParams,
191
+ meta: meta$5("Matches pattern", "A JavaScript regular expression, without the slashes."),
192
+ create: (p, m) => v.regex(new RegExp(p.pattern, p.flags?.replace(/[gy]/g, "")), m)
193
+ }),
194
+ startsWith: defineRule({
195
+ kinds: ["string"],
196
+ params: stringParams,
197
+ meta: meta$5("Starts with"),
198
+ create: (p, m) => v.startsWith(p.value, m)
199
+ }),
200
+ endsWith: defineRule({
201
+ kinds: ["string"],
202
+ params: stringParams,
203
+ meta: meta$5("Ends with"),
204
+ create: (p, m) => v.endsWith(p.value, m)
205
+ }),
206
+ includes: defineRule({
207
+ kinds: ["string", "array"],
208
+ params: contentParams,
209
+ meta: meta$5("Contains"),
210
+ create: (p, m) => v.includes(p.value, m)
211
+ }),
212
+ excludes: defineRule({
213
+ kinds: ["string", "array"],
214
+ params: contentParams,
215
+ meta: meta$5("Does not contain"),
216
+ create: (p, m) => v.excludes(p.value, m)
217
+ })
218
+ };
219
+ // src/rules/date-rules.ts
220
+ const iso = (label, description, create) => defineRule({
221
+ kinds: ["string", "date"],
222
+ meta: {
223
+ label,
224
+ description,
225
+ category: "date"
226
+ },
227
+ create: (_, message) => create(message)
228
+ });
229
+ const dateRules = {
230
+ isoDate: iso("ISO date", "Like 2026-10-28", (m) => v.isoDate(m)),
231
+ isoDateTime: iso("ISO date and time", "Like 2026-10-28T14:30", (m) => v.isoDateTime(m)),
232
+ isoTime: iso("ISO time", "Like 14:30", (m) => v.isoTime(m)),
233
+ isoTimeSecond: iso("ISO time with seconds", "Like 14:30:15", (m) => v.isoTimeSecond(m)),
234
+ isoTimestamp: iso("ISO timestamp", "Like 2026-10-28T14:30:15.000z", (m) => v.isoTimestamp(m)),
235
+ isoWeek: iso("ISO week", "Like 2026-W44", (m) => v.isoWeek(m))
236
+ };
237
+ // src/rules/file-rules.ts
238
+ const kinds$3 = ["file"];
239
+ const meta$4 = (label, description) => ({
240
+ label,
241
+ description,
242
+ category: "file"
243
+ });
244
+ const bytes = "In bytes, e.g. 1048576 for 1 MiB.";
245
+ const mimeTypeParams = v.object({ types: v.pipe(v.array(v.pipe(v.string(), v.regex(/^[\w.+-]+\/[\w.+-]+$/, "Use a MIME type, such as image/png"))), v.minLength(1, "Give at least one MIME type")) });
246
+ /** Size and type of a File or Blob. */
247
+ const fileRules = {
248
+ minSize: defineRule({
249
+ kinds: kinds$3,
250
+ params: countParams,
251
+ meta: meta$4("Minimum size", bytes),
252
+ create: (p, m) => v.minSize(p.value, m)
253
+ }),
254
+ maxSize: defineRule({
255
+ kinds: kinds$3,
256
+ params: countParams,
257
+ meta: meta$4("Maximum size", bytes),
258
+ create: (p, m) => v.maxSize(p.value, m)
259
+ }),
260
+ size: defineRule({
261
+ kinds: kinds$3,
262
+ params: countParams,
263
+ meta: meta$4("Exact size", bytes),
264
+ create: (p, m) => v.size(p.value, m)
265
+ }),
266
+ notSize: defineRule({
267
+ kinds: kinds$3,
268
+ params: countParams,
269
+ meta: meta$4("Not this size", bytes),
270
+ create: (p, m) => v.notSize(p.value, m)
271
+ }),
272
+ mimeType: defineRule({
273
+ kinds: kinds$3,
274
+ params: mimeTypeParams,
275
+ meta: meta$4("File type"),
276
+ create: (p, m) => v.mimeType(p.types, m)
277
+ })
278
+ };
279
+ // src/rules/format-rules.ts
280
+ /** A string format without params. */
281
+ const format = (label, create) => defineRule({
282
+ kinds: ["string"],
283
+ meta: {
284
+ label,
285
+ category: "format"
286
+ },
287
+ create: (_, message) => create(message)
288
+ });
289
+ const hashParams = v.object({ types: v.pipe(v.array(v.picklist([
290
+ "md4",
291
+ "md5",
292
+ "sha1",
293
+ "sha256",
294
+ "sha384",
295
+ "sha512",
296
+ "ripemd128",
297
+ "ripemd160",
298
+ "tiger128",
299
+ "tiger160",
300
+ "tiger192",
301
+ "crc32",
302
+ "crc32b",
303
+ "adler32"
304
+ ])), v.minLength(1, "Pick at least one hash type")) });
305
+ /** Formats Valibot can check on a string. */
306
+ const formatRules = {
307
+ email: format("Email", (m) => v.email(m)),
308
+ rfcEmail: format("Email (RFC 5322)", (m) => v.rfcEmail(m)),
309
+ url: format("URL", (m) => v.url(m)),
310
+ domain: format("Domain", (m) => v.domain(m)),
311
+ slug: format("Slug", (m) => v.slug(m)),
312
+ uuid: format("UUID", (m) => v.uuid(m)),
313
+ ulid: format("ULID", (m) => v.ulid(m)),
314
+ cuid2: format("CUID2", (m) => v.cuid2(m)),
315
+ nanoid: format("Nano ID", (m) => v.nanoid(m)),
316
+ ip: format("IP address", (m) => v.ip(m)),
317
+ ipv4: format("IPv4 address", (m) => v.ipv4(m)),
318
+ ipv6: format("IPv6 address", (m) => v.ipv6(m)),
319
+ mac: format("MAC address", (m) => v.mac(m)),
320
+ mac48: format("MAC-48 address", (m) => v.mac48(m)),
321
+ mac64: format("MAC-64 address", (m) => v.mac64(m)),
322
+ hexColor: format("Hex colour", (m) => v.hexColor(m)),
323
+ hexadecimal: format("Hexadecimal", (m) => v.hexadecimal(m)),
324
+ octal: format("Octal", (m) => v.octal(m)),
325
+ decimal: format("Decimal number", (m) => v.decimal(m)),
326
+ digits: format("Digits only", (m) => v.digits(m)),
327
+ base64: format("Base64", (m) => v.base64(m)),
328
+ bic: format("BIC (SWIFT code)", (m) => v.bic(m)),
329
+ creditCard: format("Credit card number", (m) => v.creditCard(m)),
330
+ imei: format("IMEI", (m) => v.imei(m)),
331
+ isbn: format("ISBN", (m) => v.isbn(m)),
332
+ isrc: format("ISRC", (m) => v.isrc(m)),
333
+ emoji: format("Emoji", (m) => v.emoji(m)),
334
+ hash: defineRule({
31
335
  kinds: ["string"],
32
- params: v.object({ value: nonNegInt }),
336
+ params: hashParams,
337
+ meta: {
338
+ label: "Hash",
339
+ category: "format"
340
+ },
341
+ create: (p, m) => v.hash(p.types, m)
342
+ })
343
+ };
344
+ // src/rules/length-rules.ts
345
+ const kinds$2 = ["string", "array"];
346
+ const meta$3 = (label) => ({
347
+ label,
348
+ category: "length"
349
+ });
350
+ /** Characters of a string, or items of an array */
351
+ const lengthRules = {
352
+ length: defineRule({
353
+ kinds: kinds$2,
354
+ params: countParams,
355
+ meta: meta$3("Exact length"),
356
+ create: (p, m) => v.length(p.value, m)
357
+ }),
358
+ minLength: defineRule({
359
+ kinds: kinds$2,
360
+ params: countParams,
361
+ meta: meta$3("Minimum length"),
33
362
  create: (p, m) => v.minLength(p.value, m)
34
363
  }),
35
364
  maxLength: defineRule({
36
- kinds: ["string"],
37
- params: v.object({ value: nonNegInt }),
365
+ kinds: kinds$2,
366
+ params: countParams,
367
+ meta: meta$3("Maximum length"),
38
368
  create: (p, m) => v.maxLength(p.value, m)
39
369
  }),
40
- regex: defineRule({
370
+ notLength: defineRule({
371
+ kinds: kinds$2,
372
+ params: countParams,
373
+ meta: meta$3("Not this length"),
374
+ create: (p, m) => v.notLength(p.value, m)
375
+ })
376
+ };
377
+ // src/rules/normalize-rules.ts
378
+ const stage = "normalize";
379
+ const meta$2 = (label, description) => ({
380
+ label,
381
+ description,
382
+ category: "normalize"
383
+ });
384
+ const limit = v.object({ value: v.number() });
385
+ /**
386
+ * Rules that change the value instead of checking it. They run before the required check, so
387
+ * `' '` with `trim` is empty. Their `message` is never shown.
388
+ */
389
+ const normalizeRules = {
390
+ trim: defineRule({
41
391
  kinds: ["string"],
42
- params: v.object({
43
- pattern: v.string(),
44
- flags: v.optional(v.string())
45
- }),
46
- create: (p, m) => v.regex(new RegExp(p.pattern, p.flags), m)
392
+ stage,
393
+ meta: meta$2("Trim"),
394
+ create: () => v.trim()
47
395
  }),
48
- email: defineRule({
396
+ trimStart: defineRule({
49
397
  kinds: ["string"],
50
- params: noParams,
51
- create: (_, m) => v.email(m)
398
+ stage,
399
+ meta: meta$2("Trim start"),
400
+ create: () => v.trimStart()
52
401
  }),
53
- url: defineRule({
402
+ trimEnd: defineRule({
54
403
  kinds: ["string"],
55
- params: noParams,
56
- create: (_, m) => v.url(m)
404
+ stage,
405
+ meta: meta$2("Trim end"),
406
+ create: () => v.trimEnd()
57
407
  }),
58
- minValue: defineRule({
59
- kinds: ["number"],
60
- params: v.object({ value: v.number() }),
61
- create: (p, m) => v.minValue(p.value, m)
408
+ toLowerCase: defineRule({
409
+ kinds: ["string"],
410
+ stage,
411
+ meta: meta$2("Lowercase"),
412
+ create: () => v.toLowerCase()
62
413
  }),
63
- maxValue: defineRule({
414
+ toUpperCase: defineRule({
415
+ kinds: ["string"],
416
+ stage,
417
+ meta: meta$2("Uppercase"),
418
+ create: () => v.toUpperCase()
419
+ }),
420
+ normalize: defineRule({
421
+ kinds: ["string"],
422
+ stage,
423
+ params: v.object({ form: v.optional(v.picklist([
424
+ "NFC",
425
+ "NFD",
426
+ "NFKC",
427
+ "NFKD"
428
+ ])) }),
429
+ meta: meta$2("Unicode normalize", "Defaults to NFC."),
430
+ create: (p) => v.normalize(p.form)
431
+ }),
432
+ toMinValue: defineRule({
64
433
  kinds: ["number"],
65
- params: v.object({ value: v.number() }),
66
- create: (p, m) => v.maxValue(p.value, m)
434
+ stage,
435
+ params: limit,
436
+ meta: meta$2("Raise to minimum", "Replaces a smaller number with the minimum."),
437
+ create: (p) => v.toMinValue(p.value)
67
438
  }),
68
- integer: defineRule({
439
+ toMaxValue: defineRule({
69
440
  kinds: ["number"],
70
- params: noParams,
441
+ stage,
442
+ params: limit,
443
+ meta: meta$2("Lower to maximum", "Replaces a larger number with the maximum."),
444
+ create: (p) => v.toMaxValue(p.value)
445
+ })
446
+ };
447
+ // src/rules/number-rules.ts
448
+ const kinds$1 = ["number"];
449
+ const meta$1 = (label) => ({
450
+ label,
451
+ category: "number"
452
+ });
453
+ const divisor = v.pipe(v.number(), v.finite(), v.notValue(0, "Can't be 0"));
454
+ /** Checks only numbers have. */
455
+ const numberRules = {
456
+ integer: defineRule({
457
+ kinds: kinds$1,
458
+ meta: meta$1("Whole number"),
71
459
  create: (_, m) => v.integer(m)
460
+ }),
461
+ safeInteger: defineRule({
462
+ kinds: kinds$1,
463
+ meta: meta$1("Safe whole number"),
464
+ create: (_, m) => v.safeInteger(m)
465
+ }),
466
+ finite: defineRule({
467
+ kinds: kinds$1,
468
+ meta: meta$1("Finite number"),
469
+ create: (_, m) => v.finite(m)
470
+ }),
471
+ multipleOf: defineRule({
472
+ kinds: kinds$1,
473
+ params: v.object({ value: divisor }),
474
+ meta: meta$1("Multiple of"),
475
+ create: (p, m) => v.multipleOf(p.value, m)
476
+ })
477
+ };
478
+ // src/rules/text-unit-rules.ts
479
+ const kinds = ["string"];
480
+ const meta = (label, description) => ({
481
+ label,
482
+ description,
483
+ category: "text"
484
+ });
485
+ const isLocale = (input) => {
486
+ try {
487
+ return Intl.getCanonicalLocales(input).length === 1;
488
+ } catch {
489
+ return false;
490
+ }
491
+ };
492
+ const wordsParams = v.object({
493
+ locale: v.optional(v.pipe(v.string(), v.check(isLocale, "Use a language tag, such as en or sr-Latn"))),
494
+ value: count
495
+ });
496
+ const graphemesInfo = "Counts characters as people see them, so an emoji is one.";
497
+ const bytesInfo = "Counts UTF-8 bytes, e.g. to fit a database column,";
498
+ const textUnitRules = {
499
+ graphemes: defineRule({
500
+ kinds,
501
+ params: countParams,
502
+ meta: meta("Exact characters", graphemesInfo),
503
+ create: (p, m) => v.graphemes(p.value, m)
504
+ }),
505
+ minGraphemes: defineRule({
506
+ kinds,
507
+ params: countParams,
508
+ meta: meta("Minimum characters", graphemesInfo),
509
+ create: (p, m) => v.minGraphemes(p.value, m)
510
+ }),
511
+ maxGraphemes: defineRule({
512
+ kinds,
513
+ params: countParams,
514
+ meta: meta("Maximum characters", graphemesInfo),
515
+ create: (p, m) => v.maxGraphemes(p.value, m)
516
+ }),
517
+ notGraphemes: defineRule({
518
+ kinds,
519
+ params: countParams,
520
+ meta: meta("Not this many characters", graphemesInfo),
521
+ create: (p, m) => v.notGraphemes(p.value, m)
522
+ }),
523
+ words: defineRule({
524
+ kinds,
525
+ params: wordsParams,
526
+ meta: meta("Exact words"),
527
+ create: (p, m) => v.words(p.locale, p.value, m)
528
+ }),
529
+ minWords: defineRule({
530
+ kinds,
531
+ params: wordsParams,
532
+ meta: meta("Minimum words"),
533
+ create: (p, m) => v.minWords(p.locale, p.value, m)
534
+ }),
535
+ maxWords: defineRule({
536
+ kinds,
537
+ params: wordsParams,
538
+ meta: meta("Maximum words"),
539
+ create: (p, m) => v.maxWords(p.locale, p.value, m)
540
+ }),
541
+ notWords: defineRule({
542
+ kinds,
543
+ params: wordsParams,
544
+ meta: meta("Not this many words"),
545
+ create: (p, m) => v.notWords(p.locale, p.value, m)
546
+ }),
547
+ bytes: defineRule({
548
+ kinds,
549
+ params: countParams,
550
+ meta: meta("Exact bytes", bytesInfo),
551
+ create: (p, m) => v.bytes(p.value, m)
552
+ }),
553
+ minBytes: defineRule({
554
+ kinds,
555
+ params: countParams,
556
+ meta: meta("Minimum bytes", bytesInfo),
557
+ create: (p, m) => v.minBytes(p.value, m)
558
+ }),
559
+ maxBytes: defineRule({
560
+ kinds,
561
+ params: countParams,
562
+ meta: meta("Maximum bytes", bytesInfo),
563
+ create: (p, m) => v.maxBytes(p.value, m)
564
+ }),
565
+ notBytes: defineRule({
566
+ kinds,
567
+ params: countParams,
568
+ meta: meta("Not this many bytes", bytesInfo),
569
+ create: (p, m) => v.notBytes(p.value, m)
72
570
  })
73
571
  };
74
- // src/create-schema-builder.ts
75
- function createSchemaBuilder(opts = {}) {
76
- const fieldTypes = {
77
- ...defaultFieldTypes,
78
- ...opts.fieldTypes
572
+ // src/rules/default-rules.ts
573
+ /** Every built-in rule. Register single families with `includeDefaultRules: false`. */
574
+ const defaultRules = {
575
+ ...lengthRules,
576
+ ...textUnitRules,
577
+ ...contentRules,
578
+ ...formatRules,
579
+ ...dateRules,
580
+ ...comparisonRules,
581
+ ...numberRules,
582
+ ...fileRules,
583
+ ...normalizeRules
584
+ };
585
+ // src/builder/registry.ts
586
+ /** Consumer entries replace defaults with the same key. */
587
+ function mergeRegistry(custom) {
588
+ const rules = custom.includeDefaultRules === false ? {} : defaultRules;
589
+ return {
590
+ kinds: Object.freeze({
591
+ ...defaultKinds,
592
+ ...custom.kinds
593
+ }),
594
+ fieldTypes: Object.freeze({
595
+ ...defaultFieldTypes,
596
+ ...custom.fieldTypes
597
+ }),
598
+ rules: Object.freeze({
599
+ ...rules,
600
+ ...custom.rules
601
+ })
602
+ };
603
+ }
604
+ function lookup(record, key) {
605
+ return Object.hasOwn(record, key) ? record[key] : void 0;
606
+ }
607
+ // src/errors/definition-error.ts
608
+ function formatPath(path) {
609
+ return path.reduce((out, key) => {
610
+ if (typeof key === "number") return `${out}[${key}]`;
611
+ return out ? `${out}.${key}` : key;
612
+ }, "");
613
+ }
614
+ /** Thrown by `build()` when the definition has issues. Lists all of them. */
615
+ var DefinitionError = class extends Error {
616
+ issues;
617
+ constructor(issues) {
618
+ const lines = issues.map((issue) => ` ${formatPath(issue.path) || "(root)"}: ${issue.message}`);
619
+ super(`Invalid form definition:\n${lines.join("\n")}`);
620
+ this.name = "DefinitionError";
621
+ this.issues = issues;
622
+ }
623
+ };
624
+ // src/rules/rule-params.ts
625
+ const cache = /* @__PURE__ */ new WeakMap();
626
+ function paramsFor(def, kind) {
627
+ if (typeof def.params !== "function") return def.params;
628
+ let byKind = cache.get(def);
629
+ if (!byKind) {
630
+ byKind = /* @__PURE__ */ new Map();
631
+ cache.set(def, byKind);
632
+ }
633
+ let params = byKind.get(kind);
634
+ if (!params) {
635
+ params = def.params(kind);
636
+ byKind.set(kind, params);
637
+ }
638
+ return params;
639
+ }
640
+ // src/shared/reserved-keys.ts
641
+ /**
642
+ * Field keys this package owns. A field type's props can't use them.
643
+ */
644
+ const RESERVED_FIELD_KEYS = [
645
+ "name",
646
+ "type",
647
+ "label",
648
+ "description",
649
+ "placeholder",
650
+ "default",
651
+ "required",
652
+ "rules",
653
+ "ui"
654
+ ];
655
+ /**
656
+ * Rule keys this package own. A rule's params can't use them.
657
+ */
658
+ const RESERVED_RULE_KEYS = ["rule", "message"];
659
+ const fieldKeys = new Set(RESERVED_FIELD_KEYS);
660
+ const ruleKeys = new Set(RESERVED_RULE_KEYS);
661
+ const isReservedFieldKey = (key) => fieldKeys.has(key);
662
+ const isReservedRuleKey = (key) => ruleKeys.has(key);
663
+ // src/builder/validate-definition.ts
664
+ const formShape = v.object({
665
+ version: v.string(),
666
+ fields: v.array(v.unknown())
667
+ });
668
+ const fieldShape = v.looseObject({
669
+ name: v.optional(v.string()),
670
+ type: v.string(),
671
+ label: v.optional(v.string()),
672
+ description: v.optional(v.string()),
673
+ placeholder: v.optional(v.string()),
674
+ default: v.optional(v.unknown()),
675
+ required: v.optional(v.union([v.boolean(), v.strictObject({ message: v.optional(v.string()) })])),
676
+ rules: v.optional(v.array(v.unknown())),
677
+ ui: v.optional(v.record(v.string(), v.unknown()))
678
+ });
679
+ const ruleShape = v.looseObject({
680
+ rule: v.string(),
681
+ message: v.optional(v.string())
682
+ });
683
+ const noParams = v.object({});
684
+ /**
685
+ * Checks a whole definition and collects every issue, in document order. Never throws.
686
+ */
687
+ function checkDefinition(json, registry) {
688
+ const issues = [];
689
+ const fields = [];
690
+ const form = v.safeParse(formShape, json);
691
+ if (!form.success) return {
692
+ issues: toIssues("invalid_shape", [], form.issues),
693
+ fields
694
+ };
695
+ if (form.output.version !== "1") return {
696
+ issues: [{
697
+ code: "unsupported_version",
698
+ path: ["version"],
699
+ message: `Unsupported version "${form.output.version}". This package reads version '1'.`
700
+ }],
701
+ fields
702
+ };
703
+ const names = /* @__PURE__ */ new Set();
704
+ form.output.fields.forEach((raw, i) => {
705
+ const field = checkField(raw, ["fields", i], registry, names, issues);
706
+ if (field) fields.push(field);
707
+ });
708
+ return {
709
+ issues,
710
+ fields
79
711
  };
80
- const rules = {
81
- ...defaultRules,
82
- ...opts.rules
712
+ }
713
+ function checkField(raw, path, registry, names, issues) {
714
+ const shape = v.safeParse(fieldShape, raw);
715
+ if (!shape.success) {
716
+ issues.push(...toIssues("invalid_shape", path, shape.issues));
717
+ return;
718
+ }
719
+ const field = shape.output;
720
+ const issueCount = issues.length;
721
+ const type = lookup(registry.fieldTypes, field.type);
722
+ const hasValue = type?.kind !== "none";
723
+ if (!field.name) {
724
+ if (type && hasValue) issues.push(issue("missing_name", [...path, "name"], "A field needs a name"));
725
+ } else if (hasValue) {
726
+ if (names.has(field.name)) issues.push(issue("duplicate_name", [...path, "name"], `Duplicate name "${field.name}"`));
727
+ names.add(field.name);
728
+ }
729
+ if (!type) {
730
+ issues.push(issue("unknown_type", [...path, "type"], `Unknown field type "${field.type}"`));
731
+ return;
732
+ }
733
+ const props = checkProps(field, type, path, issues);
734
+ if (!hasValue) {
735
+ for (const key of ["required", "rules"]) if (field[key] !== void 0) {
736
+ const message = `A "${field.type}" field has no value, so it can't have "${key}"`;
737
+ issues.push(issue("invalid_shape", [...path, key], message));
738
+ }
739
+ return;
740
+ }
741
+ const fieldRules = (shape.output.rules ?? []).flatMap((rule, j) => checkRule(rule, type?.kind, [
742
+ ...path,
743
+ "rules",
744
+ j
745
+ ], registry, issues) ?? []);
746
+ if (issues.length > issueCount || !field.name) return void 0;
747
+ const overridden = new Set(fieldRules.map((rule) => rule.name));
748
+ const defaultRules = (type?.defaultRules ?? []).filter((rule) => !overridden.has(rule.rule)).flatMap((rule) => checkRule(rule, type?.kind, path, registry, []) ?? []);
749
+ return {
750
+ name: field.name,
751
+ field: {
752
+ ...field,
753
+ ...props
754
+ },
755
+ path,
756
+ type,
757
+ kind: registry.kinds[type?.kind],
758
+ rules: [...defaultRules, ...fieldRules]
759
+ };
760
+ }
761
+ function checkProps(field, type, path, issues) {
762
+ const propKeys = Object.keys(type.props?.entries ?? {});
763
+ const input = {};
764
+ for (const key of Object.keys(field)) {
765
+ if (isReservedFieldKey(key)) continue;
766
+ if (propKeys.includes(key)) input[key] = field[key];
767
+ else issues.push(issue("unknown_key", [...path, key], `Unknown key "${key}" on a "${field.type}" field`));
768
+ }
769
+ if (!type.props) return {};
770
+ const parsed = v.safeParse(type.props, input);
771
+ if (parsed.success) return parsed.output;
772
+ issues.push(...toIssues("invalid_props", path, parsed.issues));
773
+ return {};
774
+ }
775
+ function checkRule(raw, kind, path, registry, issues) {
776
+ const shape = v.safeParse(ruleShape, raw);
777
+ if (!shape.success) {
778
+ issues.push(...toIssues("invalid_shape", path, shape.issues));
779
+ return;
780
+ }
781
+ const rule = shape.output;
782
+ const def = lookup(registry.rules, rule.rule);
783
+ if (!def) {
784
+ issues.push(issue("unknown_rule", [...path, "rule"], `Unknown rule "${rule.rule}"`));
785
+ return;
786
+ }
787
+ if (!def.kinds.includes(kind)) {
788
+ const message = `Rule "${rule.rule}" can't be used on ${kind} fields`;
789
+ issues.push(issue("kind_mismatch", [...path, "rule"], message));
790
+ return;
791
+ }
792
+ const issueCount = issues.length;
793
+ const params = paramsFor(def, kind);
794
+ const paramKeys = Object.keys(params?.entries ?? {});
795
+ const input = {};
796
+ for (const key of Object.keys(rule)) {
797
+ if (isReservedRuleKey(key)) continue;
798
+ if (paramKeys.includes(key)) input[key] = rule[key];
799
+ else issues.push(issue("unknown_param", [...path, key], `Rule "${rule.rule}" has no param "${key}"`));
800
+ }
801
+ const parsed = v.safeParse(params ?? noParams, input);
802
+ if (!parsed.success) issues.push(...toIssues("invalid_params", path, parsed.issues));
803
+ if (!parsed.success || issues.length > issueCount) return void 0;
804
+ return {
805
+ name: rule.rule,
806
+ def,
807
+ params: parsed.output,
808
+ message: rule.message
83
809
  };
84
- function buildAction(rule, kind, at) {
85
- const def = rules[rule.rule];
86
- if (!def) throw new Error(`${at}: unknown rule "${rule.rule}"`);
87
- if (!def.kinds.includes(kind)) throw new Error(`${at}: rule "${rule.rule}" can't be used on ${kind} fields`);
88
- const parsed = v.safeParse(def.params, rule);
89
- if (!parsed.success) throw new Error(`${at}: invalid params for "${rule.rule}"\n${v.summarize(parsed.issues)}`);
90
- return def.create(parsed.output, rule.message);
91
- }
92
- function buildField(field, i) {
93
- const at = `fields[${i}] "${field.name}"`;
94
- const type = fieldTypes[field.type];
95
- if (!type) throw new Error(`${at}: unknown field type "${field.type}"`);
96
- const userRules = field.rules ?? [];
97
- const overridden = new Set(userRules.map((r) => r.rule));
98
- const allRules = [...(type.defaultRules ?? []).filter((r) => !overridden.has(r.rule)), ...userRules];
99
- const required = !!field.required;
100
- const requiredMsg = typeof field.required === "object" ? field.required.message : void 0;
101
- const actions = [];
102
- if (required && type.kind === "string") actions.push(v.nonEmpty(requiredMsg ?? "Required"));
103
- allRules.forEach((r, j) => {
104
- actions.push(buildAction(r, type.kind, `${at}.rules[${j}]`));
810
+ }
811
+ function checkRegistry(registry) {
812
+ for (const [name, rule] of Object.entries(registry.rules)) for (const kind of rule.kinds) for (const key of Object.keys(paramsFor(rule, kind)?.entries ?? {})) if (isReservedRuleKey(key)) throw new TypeError(`Rule "${name}": params can't use the reserved key "${key}"`);
813
+ for (const [name, type] of Object.entries(registry.fieldTypes)) {
814
+ const at = `Field type "${name}"`;
815
+ if (!lookup(registry.kinds, type.kind)) throw new TypeError(`${at}: kind "${type.kind}" is not registered`);
816
+ if (type.kind !== "none" && !type.schema) throw new TypeError(`${at}: schema() is required unless the kind is 'none'`);
817
+ for (const key of Object.keys(type.props?.entries ?? {})) if (isReservedFieldKey(key)) throw new TypeError(`${at}: props can't use the reserved key "${key}"`);
818
+ const issues = [];
819
+ type.defaultRules?.forEach((rule, j) => {
820
+ checkRule(rule, type.kind, ["defaultRules", j], registry, issues);
105
821
  });
106
- const baseMsg = required && type.kind === "number" ? requiredMsg ?? "Required" : requiredMsg;
107
- const schema = actions.reduce((s, a) => v.pipe(s, a), type.base(baseMsg));
108
- if (required) return schema;
109
- if (type.kind === "string") return v.optional(v.union([v.literal(""), schema]));
110
- const empty = v.pipe(v.literal(""), v.transform(() => void 0));
111
- return v.optional(v.union([empty, schema]));
112
- }
113
- return function build(json) {
822
+ const [first] = issues;
823
+ if (first) throw new TypeError(`${at}: ${formatPath(first.path)}: ${first.message}`);
824
+ }
825
+ }
826
+ function issue(code, path, message) {
827
+ return {
828
+ code,
829
+ path,
830
+ message
831
+ };
832
+ }
833
+ function toIssues(code, path, valibotIssues) {
834
+ return valibotIssues.map((valibotIssue) => {
835
+ const keys = valibotIssue.path?.map((item) => item.key) ?? [];
836
+ return issue(code, [...path, ...keys], valibotIssue.message);
837
+ });
838
+ }
839
+ // src/builder/create-schema-builder.ts
840
+ function createSchemaBuilder(options = {}) {
841
+ const registry = mergeRegistry(options);
842
+ checkRegistry(registry);
843
+ const messages = {
844
+ required: options.messages?.required ?? "Required",
845
+ resolve: (text, ctx) => options.resolveMessage?.(text, ctx) ?? text
846
+ };
847
+ /** Every issue in the definition, in document order. Never throws on bad input. */
848
+ function validate(json) {
849
+ const { issues } = checkDefinition(json, registry);
850
+ return issues.length ? {
851
+ success: false,
852
+ issues
853
+ } : {
854
+ success: true,
855
+ issues: []
856
+ };
857
+ }
858
+ function build(json) {
859
+ const { issues, fields } = checkDefinition(json, registry);
860
+ if (issues.length) throw new DefinitionError(issues);
114
861
  const entries = {};
115
- json.fields.forEach((field, i) => {
116
- if (field.name in entries) throw new Error(`fields[${i}]: duplicate name "${field.name}"`);
117
- entries[field.name] = buildField(field, i);
118
- });
862
+ for (const field of fields) entries[field.name] = buildFieldSchema(field, messages);
119
863
  return v.object(entries);
864
+ }
865
+ /**
866
+ * The registered rules that can be used on a field of `kind`, e.g. for a rule picker.
867
+ */
868
+ function rulesForKind(kind) {
869
+ return Object.entries(registry.rules).filter(([, def]) => def.kinds.includes(kind));
870
+ }
871
+ return {
872
+ build,
873
+ validate,
874
+ rulesForKind,
875
+ kinds: registry.kinds,
876
+ fieldTypes: registry.fieldTypes,
877
+ rules: registry.rules
120
878
  };
121
879
  }
122
- export { createSchemaBuilder, defaultFieldTypes, defaultRules, defineRule };
880
+ // src/field-types/define-field-type.ts
881
+ const defineFieldType = (def) => def;
882
+ export { DefinitionError, RESERVED_FIELD_KEYS, RESERVED_RULE_KEYS, comparisonRules, contentRules, createSchemaBuilder, dateRules, defaultFieldTypes, defaultKinds, defaultRules, defineFieldType, defineRule, fileRules, formatPath, formatRules, lengthRules, normalizeRules, numberRules, textUnitRules };
123
883
 
124
884
  //# sourceMappingURL=index.js.map