@whatsaaaa/form-builder-schema 0.5.0 → 0.6.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
@@ -85,22 +85,39 @@ function buildFieldSchema(checked, messages, ctx) {
85
85
  return required ? schema : v.optional(schema);
86
86
  }
87
87
  const pipe = (schema, items) => items.reduce((acc, item) => v.pipe(acc, item), schema);
88
- // src/rules/rule-params.ts
89
- const cache = /* @__PURE__ */ new WeakMap();
90
- function paramsFor(def, kind) {
91
- if (typeof def.params !== "function") return def.params;
92
- let byKind = cache.get(def);
93
- if (!byKind) {
94
- byKind = /* @__PURE__ */ new Map();
95
- cache.set(def, byKind);
96
- }
97
- let params = byKind.get(kind);
98
- if (!params) {
99
- params = def.params(kind);
100
- byKind.set(kind, params);
101
- }
102
- return params;
103
- }
88
+ // src/shared/reserved-keys.ts
89
+ /**
90
+ * Field keys this package owns. A field type's props can't use them.
91
+ */
92
+ const RESERVED_FIELD_KEYS = [
93
+ "name",
94
+ "type",
95
+ "label",
96
+ "description",
97
+ "placeholder",
98
+ "default",
99
+ "required",
100
+ "rules",
101
+ "ui"
102
+ ];
103
+ /**
104
+ * Rule keys this package owns. A rule's params can't use them.
105
+ */
106
+ const RESERVED_RULE_KEYS = ["rule", "message"];
107
+ /**
108
+ * Form rule keys this package owns. A form rule's params can't use them.
109
+ */
110
+ const RESERVED_FORM_RULE_KEYS = [
111
+ "rule",
112
+ "field",
113
+ "message"
114
+ ];
115
+ const fieldKeys = new Set(RESERVED_FIELD_KEYS);
116
+ const ruleKeys = new Set(RESERVED_RULE_KEYS);
117
+ const formRuleKeys = new Set(RESERVED_FORM_RULE_KEYS);
118
+ const isReservedFieldKey = (key) => fieldKeys.has(key);
119
+ const isReservedRuleKey = (key) => ruleKeys.has(key);
120
+ const isReservedFormRuleKey = (key) => formRuleKeys.has(key);
104
121
  // src/field-types/basic-types.ts
105
122
  /** Types with one plain value and no props. Each maps to a native input. */
106
123
  const basicTypes = {
@@ -331,7 +348,7 @@ const any = [
331
348
  "string",
332
349
  "boolean"
333
350
  ];
334
- const meta$6 = (label) => ({
351
+ const meta$7 = (label) => ({
335
352
  label,
336
353
  category: "comparison"
337
354
  });
@@ -342,54 +359,54 @@ const comparisonRules = {
342
359
  minValue: defineRule({
343
360
  kinds: ordered,
344
361
  params: valueParams,
345
- meta: meta$6("Minimum"),
362
+ meta: meta$7("Minimum"),
346
363
  create: (p, m) => v.minValue(p.value, m)
347
364
  }),
348
365
  maxValue: defineRule({
349
366
  kinds: ordered,
350
367
  params: valueParams,
351
- meta: meta$6("Maximum"),
368
+ meta: meta$7("Maximum"),
352
369
  create: (p, m) => v.maxValue(p.value, m)
353
370
  }),
354
371
  gtValue: defineRule({
355
372
  kinds: ordered,
356
373
  params: valueParams,
357
- meta: meta$6("Greater than"),
374
+ meta: meta$7("Greater than"),
358
375
  create: (p, m) => v.gtValue(p.value, m)
359
376
  }),
360
377
  ltValue: defineRule({
361
378
  kinds: ordered,
362
379
  params: valueParams,
363
- meta: meta$6("Less than"),
380
+ meta: meta$7("Less than"),
364
381
  create: (p, m) => v.ltValue(p.value, m)
365
382
  }),
366
383
  value: defineRule({
367
384
  kinds: any,
368
385
  params: valueParams,
369
- meta: meta$6("Equals"),
386
+ meta: meta$7("Equals"),
370
387
  create: (p, m) => v.value(p.value, m)
371
388
  }),
372
389
  notValue: defineRule({
373
390
  kinds: any,
374
391
  params: valueParams,
375
- meta: meta$6("Does not equal"),
392
+ meta: meta$7("Does not equal"),
376
393
  create: (p, m) => v.notValue(p.value, m)
377
394
  }),
378
395
  values: defineRule({
379
396
  kinds: ordered,
380
397
  params: valuesParams,
381
- meta: meta$6("One of"),
398
+ meta: meta$7("One of"),
382
399
  create: (p, m) => v.values(p.values, m)
383
400
  }),
384
401
  notValues: defineRule({
385
402
  kinds: ordered,
386
403
  params: valuesParams,
387
- meta: meta$6("None of"),
404
+ meta: meta$7("None of"),
388
405
  create: (p, m) => v.notValues(p.values, m)
389
406
  })
390
407
  };
391
408
  // src/rules/content-rules.ts
392
- const meta$5 = (label, description) => ({
409
+ const meta$6 = (label, description) => ({
393
410
  label,
394
411
  description,
395
412
  category: "content"
@@ -418,31 +435,31 @@ const contentRules = {
418
435
  regex: defineRule({
419
436
  kinds: ["string"],
420
437
  params: regexParams,
421
- meta: meta$5("Matches pattern", "A JavaScript regular expression, without the slashes."),
438
+ meta: meta$6("Matches pattern", "A JavaScript regular expression, without the slashes."),
422
439
  create: (p, m) => v.regex(new RegExp(p.pattern, p.flags?.replace(/[gy]/g, "")), m)
423
440
  }),
424
441
  startsWith: defineRule({
425
442
  kinds: ["string"],
426
443
  params: stringParams,
427
- meta: meta$5("Starts with"),
444
+ meta: meta$6("Starts with"),
428
445
  create: (p, m) => v.startsWith(p.value, m)
429
446
  }),
430
447
  endsWith: defineRule({
431
448
  kinds: ["string"],
432
449
  params: stringParams,
433
- meta: meta$5("Ends with"),
450
+ meta: meta$6("Ends with"),
434
451
  create: (p, m) => v.endsWith(p.value, m)
435
452
  }),
436
453
  includes: defineRule({
437
454
  kinds: ["string", "array"],
438
455
  params: contentParams,
439
- meta: meta$5("Contains"),
456
+ meta: meta$6("Contains"),
440
457
  create: (p, m) => v.includes(p.value, m)
441
458
  }),
442
459
  excludes: defineRule({
443
460
  kinds: ["string", "array"],
444
461
  params: contentParams,
445
- meta: meta$5("Does not contain"),
462
+ meta: meta$6("Does not contain"),
446
463
  create: (p, m) => v.excludes(p.value, m)
447
464
  })
448
465
  };
@@ -466,7 +483,7 @@ const dateRules = {
466
483
  };
467
484
  // src/rules/file-rules.ts
468
485
  const kinds$3 = ["file"];
469
- const meta$4 = (label, description) => ({
486
+ const meta$5 = (label, description) => ({
470
487
  label,
471
488
  description,
472
489
  category: "file"
@@ -478,31 +495,31 @@ const fileRules = {
478
495
  minSize: defineRule({
479
496
  kinds: kinds$3,
480
497
  params: countParams,
481
- meta: meta$4("Minimum size", bytes),
498
+ meta: meta$5("Minimum size", bytes),
482
499
  create: (p, m) => v.minSize(p.value, m)
483
500
  }),
484
501
  maxSize: defineRule({
485
502
  kinds: kinds$3,
486
503
  params: countParams,
487
- meta: meta$4("Maximum size", bytes),
504
+ meta: meta$5("Maximum size", bytes),
488
505
  create: (p, m) => v.maxSize(p.value, m)
489
506
  }),
490
507
  size: defineRule({
491
508
  kinds: kinds$3,
492
509
  params: countParams,
493
- meta: meta$4("Exact size", bytes),
510
+ meta: meta$5("Exact size", bytes),
494
511
  create: (p, m) => v.size(p.value, m)
495
512
  }),
496
513
  notSize: defineRule({
497
514
  kinds: kinds$3,
498
515
  params: countParams,
499
- meta: meta$4("Not this size", bytes),
516
+ meta: meta$5("Not this size", bytes),
500
517
  create: (p, m) => v.notSize(p.value, m)
501
518
  }),
502
519
  mimeType: defineRule({
503
520
  kinds: kinds$3,
504
521
  params: mimeTypeParams,
505
- meta: meta$4("File type"),
522
+ meta: meta$5("File type"),
506
523
  create: (p, m) => v.mimeType(p.types, m)
507
524
  })
508
525
  };
@@ -573,7 +590,7 @@ const formatRules = {
573
590
  };
574
591
  // src/rules/length-rules.ts
575
592
  const kinds$2 = ["string", "array"];
576
- const meta$3 = (label) => ({
593
+ const meta$4 = (label) => ({
577
594
  label,
578
595
  category: "length"
579
596
  });
@@ -582,31 +599,31 @@ const lengthRules = {
582
599
  length: defineRule({
583
600
  kinds: kinds$2,
584
601
  params: countParams,
585
- meta: meta$3("Exact length"),
602
+ meta: meta$4("Exact length"),
586
603
  create: (p, m) => v.length(p.value, m)
587
604
  }),
588
605
  minLength: defineRule({
589
606
  kinds: kinds$2,
590
607
  params: countParams,
591
- meta: meta$3("Minimum length"),
608
+ meta: meta$4("Minimum length"),
592
609
  create: (p, m) => v.minLength(p.value, m)
593
610
  }),
594
611
  maxLength: defineRule({
595
612
  kinds: kinds$2,
596
613
  params: countParams,
597
- meta: meta$3("Maximum length"),
614
+ meta: meta$4("Maximum length"),
598
615
  create: (p, m) => v.maxLength(p.value, m)
599
616
  }),
600
617
  notLength: defineRule({
601
618
  kinds: kinds$2,
602
619
  params: countParams,
603
- meta: meta$3("Not this length"),
620
+ meta: meta$4("Not this length"),
604
621
  create: (p, m) => v.notLength(p.value, m)
605
622
  })
606
623
  };
607
624
  // src/rules/normalize-rules.ts
608
625
  const stage = "normalize";
609
- const meta$2 = (label, description) => ({
626
+ const meta$3 = (label, description) => ({
610
627
  label,
611
628
  description,
612
629
  category: "normalize"
@@ -620,31 +637,31 @@ const normalizeRules = {
620
637
  trim: defineRule({
621
638
  kinds: ["string"],
622
639
  stage,
623
- meta: meta$2("Trim"),
640
+ meta: meta$3("Trim"),
624
641
  create: () => v.trim()
625
642
  }),
626
643
  trimStart: defineRule({
627
644
  kinds: ["string"],
628
645
  stage,
629
- meta: meta$2("Trim start"),
646
+ meta: meta$3("Trim start"),
630
647
  create: () => v.trimStart()
631
648
  }),
632
649
  trimEnd: defineRule({
633
650
  kinds: ["string"],
634
651
  stage,
635
- meta: meta$2("Trim end"),
652
+ meta: meta$3("Trim end"),
636
653
  create: () => v.trimEnd()
637
654
  }),
638
655
  toLowerCase: defineRule({
639
656
  kinds: ["string"],
640
657
  stage,
641
- meta: meta$2("Lowercase"),
658
+ meta: meta$3("Lowercase"),
642
659
  create: () => v.toLowerCase()
643
660
  }),
644
661
  toUpperCase: defineRule({
645
662
  kinds: ["string"],
646
663
  stage,
647
- meta: meta$2("Uppercase"),
664
+ meta: meta$3("Uppercase"),
648
665
  create: () => v.toUpperCase()
649
666
  }),
650
667
  normalize: defineRule({
@@ -656,27 +673,27 @@ const normalizeRules = {
656
673
  "NFKC",
657
674
  "NFKD"
658
675
  ])) }),
659
- meta: meta$2("Unicode normalize", "Defaults to NFC."),
676
+ meta: meta$3("Unicode normalize", "Defaults to NFC."),
660
677
  create: (p) => v.normalize(p.form)
661
678
  }),
662
679
  toMinValue: defineRule({
663
680
  kinds: ["number"],
664
681
  stage,
665
682
  params: limit,
666
- meta: meta$2("Raise to minimum", "Replaces a smaller number with the minimum."),
683
+ meta: meta$3("Raise to minimum", "Replaces a smaller number with the minimum."),
667
684
  create: (p) => v.toMinValue(p.value)
668
685
  }),
669
686
  toMaxValue: defineRule({
670
687
  kinds: ["number"],
671
688
  stage,
672
689
  params: limit,
673
- meta: meta$2("Lower to maximum", "Replaces a larger number with the maximum."),
690
+ meta: meta$3("Lower to maximum", "Replaces a larger number with the maximum."),
674
691
  create: (p) => v.toMaxValue(p.value)
675
692
  })
676
693
  };
677
694
  // src/rules/number-rules.ts
678
695
  const kinds$1 = ["number"];
679
- const meta$1 = (label) => ({
696
+ const meta$2 = (label) => ({
680
697
  label,
681
698
  category: "number"
682
699
  });
@@ -685,29 +702,29 @@ const divisor = v.pipe(v.number(), v.finite(), v.notValue(0, "Can't be 0"));
685
702
  const numberRules = {
686
703
  integer: defineRule({
687
704
  kinds: kinds$1,
688
- meta: meta$1("Whole number"),
705
+ meta: meta$2("Whole number"),
689
706
  create: (_, m) => v.integer(m)
690
707
  }),
691
708
  safeInteger: defineRule({
692
709
  kinds: kinds$1,
693
- meta: meta$1("Safe whole number"),
710
+ meta: meta$2("Safe whole number"),
694
711
  create: (_, m) => v.safeInteger(m)
695
712
  }),
696
713
  finite: defineRule({
697
714
  kinds: kinds$1,
698
- meta: meta$1("Finite number"),
715
+ meta: meta$2("Finite number"),
699
716
  create: (_, m) => v.finite(m)
700
717
  }),
701
718
  multipleOf: defineRule({
702
719
  kinds: kinds$1,
703
720
  params: v.object({ value: divisor }),
704
- meta: meta$1("Multiple of"),
721
+ meta: meta$2("Multiple of"),
705
722
  create: (p, m) => v.multipleOf(p.value, m)
706
723
  })
707
724
  };
708
725
  // src/rules/text-unit-rules.ts
709
726
  const kinds = ["string"];
710
- const meta = (label, description) => ({
727
+ const meta$1 = (label, description) => ({
711
728
  label,
712
729
  description,
713
730
  category: "text"
@@ -729,73 +746,73 @@ const textUnitRules = {
729
746
  graphemes: defineRule({
730
747
  kinds,
731
748
  params: countParams,
732
- meta: meta("Exact characters", graphemesInfo),
749
+ meta: meta$1("Exact characters", graphemesInfo),
733
750
  create: (p, m) => v.graphemes(p.value, m)
734
751
  }),
735
752
  minGraphemes: defineRule({
736
753
  kinds,
737
754
  params: countParams,
738
- meta: meta("Minimum characters", graphemesInfo),
755
+ meta: meta$1("Minimum characters", graphemesInfo),
739
756
  create: (p, m) => v.minGraphemes(p.value, m)
740
757
  }),
741
758
  maxGraphemes: defineRule({
742
759
  kinds,
743
760
  params: countParams,
744
- meta: meta("Maximum characters", graphemesInfo),
761
+ meta: meta$1("Maximum characters", graphemesInfo),
745
762
  create: (p, m) => v.maxGraphemes(p.value, m)
746
763
  }),
747
764
  notGraphemes: defineRule({
748
765
  kinds,
749
766
  params: countParams,
750
- meta: meta("Not this many characters", graphemesInfo),
767
+ meta: meta$1("Not this many characters", graphemesInfo),
751
768
  create: (p, m) => v.notGraphemes(p.value, m)
752
769
  }),
753
770
  words: defineRule({
754
771
  kinds,
755
772
  params: wordsParams,
756
- meta: meta("Exact words"),
773
+ meta: meta$1("Exact words"),
757
774
  create: (p, m) => v.words(p.locale, p.value, m)
758
775
  }),
759
776
  minWords: defineRule({
760
777
  kinds,
761
778
  params: wordsParams,
762
- meta: meta("Minimum words"),
779
+ meta: meta$1("Minimum words"),
763
780
  create: (p, m) => v.minWords(p.locale, p.value, m)
764
781
  }),
765
782
  maxWords: defineRule({
766
783
  kinds,
767
784
  params: wordsParams,
768
- meta: meta("Maximum words"),
785
+ meta: meta$1("Maximum words"),
769
786
  create: (p, m) => v.maxWords(p.locale, p.value, m)
770
787
  }),
771
788
  notWords: defineRule({
772
789
  kinds,
773
790
  params: wordsParams,
774
- meta: meta("Not this many words"),
791
+ meta: meta$1("Not this many words"),
775
792
  create: (p, m) => v.notWords(p.locale, p.value, m)
776
793
  }),
777
794
  bytes: defineRule({
778
795
  kinds,
779
796
  params: countParams,
780
- meta: meta("Exact bytes", bytesInfo),
797
+ meta: meta$1("Exact bytes", bytesInfo),
781
798
  create: (p, m) => v.bytes(p.value, m)
782
799
  }),
783
800
  minBytes: defineRule({
784
801
  kinds,
785
802
  params: countParams,
786
- meta: meta("Minimum bytes", bytesInfo),
803
+ meta: meta$1("Minimum bytes", bytesInfo),
787
804
  create: (p, m) => v.minBytes(p.value, m)
788
805
  }),
789
806
  maxBytes: defineRule({
790
807
  kinds,
791
808
  params: countParams,
792
- meta: meta("Maximum bytes", bytesInfo),
809
+ meta: meta$1("Maximum bytes", bytesInfo),
793
810
  create: (p, m) => v.maxBytes(p.value, m)
794
811
  }),
795
812
  notBytes: defineRule({
796
813
  kinds,
797
814
  params: countParams,
798
- meta: meta("Not this many bytes", bytesInfo),
815
+ meta: meta$1("Not this many bytes", bytesInfo),
799
816
  create: (p, m) => v.notBytes(p.value, m)
800
817
  })
801
818
  };
@@ -812,10 +829,71 @@ const defaultRules = {
812
829
  ...fileRules,
813
830
  ...normalizeRules
814
831
  };
832
+ // src/rules/define-form-rule.ts
833
+ const defineFormRule = (def) => def;
834
+ // src/rules/form-rules.ts
835
+ const meta = (label, description) => ({
836
+ label,
837
+ description,
838
+ category: "comparison"
839
+ });
840
+ const comparable = (value) => value instanceof Date ? value.getTime() : value;
841
+ const otherParams = v.object({ other: v.string() });
842
+ const operators = {
843
+ lt: (a, b) => a < b,
844
+ lte: (a, b) => a <= b,
845
+ gt: (a, b) => a > b,
846
+ gte: (a, b) => a >= b
847
+ };
848
+ /** Every built-in form rule. */
849
+ const defaultFormRules = {
850
+ sameAs: defineFormRule({
851
+ params: otherParams,
852
+ reads: (params) => [params.other],
853
+ kinds: [
854
+ "string",
855
+ "number",
856
+ "boolean",
857
+ "date"
858
+ ],
859
+ sameKind: true,
860
+ meta: meta("Same as", "Equals another field, e.g. to confirm a password"),
861
+ check: ([value, other]) => comparable(value) === comparable(other)
862
+ }),
863
+ differentFrom: defineFormRule({
864
+ params: otherParams,
865
+ reads: (params) => [params.other],
866
+ kinds: [
867
+ "string",
868
+ "number",
869
+ "boolean",
870
+ "date"
871
+ ],
872
+ sameKind: true,
873
+ meta: meta("Different from", "Differs from another field, e.g. a new password"),
874
+ check: ([value, other]) => comparable(value) !== comparable(other)
875
+ }),
876
+ compare: defineFormRule({
877
+ params: v.object({
878
+ op: v.picklist([
879
+ "lt",
880
+ "lte",
881
+ "gt",
882
+ "gte"
883
+ ]),
884
+ other: v.string()
885
+ }),
886
+ reads: (params) => [params.other],
887
+ kinds: ["number", "date"],
888
+ sameKind: true,
889
+ meta: meta("Compare", "Less or greater than another field, e.g. an end date after the start"),
890
+ check: ([value, other], { op }) => operators[op](comparable(value), comparable(other))
891
+ })
892
+ };
815
893
  // src/builder/registry.ts
816
894
  /** Consumer entries replace defaults with the same key. */
817
895
  function mergeRegistry(custom) {
818
- const rules = custom.includeDefaultRules === false ? {} : defaultRules;
896
+ const defaults = custom.includeDefaultRules !== false;
819
897
  return {
820
898
  kinds: Object.freeze({
821
899
  ...defaultKinds,
@@ -826,37 +904,34 @@ function mergeRegistry(custom) {
826
904
  ...custom.fieldTypes
827
905
  }),
828
906
  rules: Object.freeze({
829
- ...rules,
907
+ ...defaults ? defaultRules : {},
830
908
  ...custom.rules
909
+ }),
910
+ formRules: Object.freeze({
911
+ ...defaults ? defaultFormRules : {},
912
+ ...custom.formRules
831
913
  })
832
914
  };
833
915
  }
834
916
  function lookup(record, key) {
835
917
  return Object.hasOwn(record, key) ? record[key] : void 0;
836
918
  }
837
- // src/shared/reserved-keys.ts
838
- /**
839
- * Field keys this package owns. A field type's props can't use them.
840
- */
841
- const RESERVED_FIELD_KEYS = [
842
- "name",
843
- "type",
844
- "label",
845
- "description",
846
- "placeholder",
847
- "default",
848
- "required",
849
- "rules",
850
- "ui"
851
- ];
852
- /**
853
- * Rule keys this package owns. A rule's params can't use them.
854
- */
855
- const RESERVED_RULE_KEYS = ["rule", "message"];
856
- const fieldKeys = new Set(RESERVED_FIELD_KEYS);
857
- const ruleKeys = new Set(RESERVED_RULE_KEYS);
858
- const isReservedFieldKey = (key) => fieldKeys.has(key);
859
- const isReservedRuleKey = (key) => ruleKeys.has(key);
919
+ // src/rules/rule-params.ts
920
+ const cache = /* @__PURE__ */ new WeakMap();
921
+ function paramsFor(def, kind) {
922
+ if (typeof def.params !== "function") return def.params;
923
+ let byKind = cache.get(def);
924
+ if (!byKind) {
925
+ byKind = /* @__PURE__ */ new Map();
926
+ cache.set(def, byKind);
927
+ }
928
+ let params = byKind.get(kind);
929
+ if (!params) {
930
+ params = def.params(kind);
931
+ byKind.set(kind, params);
932
+ }
933
+ return params;
934
+ }
860
935
  // src/builder/validate-definition.ts
861
936
  const fieldShape = v.looseObject({
862
937
  name: v.optional(v.string()),
@@ -869,7 +944,7 @@ const fieldShape = v.looseObject({
869
944
  rules: v.optional(v.array(v.unknown())),
870
945
  ui: v.optional(v.record(v.string(), v.unknown()))
871
946
  });
872
- const ruleShape = v.looseObject({
947
+ const ruleShape$1 = v.looseObject({
873
948
  rule: v.string(),
874
949
  message: v.optional(v.string())
875
950
  });
@@ -985,7 +1060,7 @@ function checkProps(field, type, path, issues) {
985
1060
  issues.push(...toIssues("invalid_props", path, parsed.issues));
986
1061
  }
987
1062
  function checkRule(raw, { kind, itemKind }, path, registry, issues) {
988
- const shape = v.safeParse(ruleShape, raw);
1063
+ const shape = v.safeParse(ruleShape$1, raw);
989
1064
  if (!shape.success) {
990
1065
  issues.push(...toIssues("invalid_shape", path, shape.issues));
991
1066
  return;
@@ -1003,28 +1078,38 @@ function checkRule(raw, { kind, itemKind }, path, registry, issues) {
1003
1078
  issues.push(issue("kind_mismatch", [...path, "rule"], message));
1004
1079
  return;
1005
1080
  }
1081
+ const params = checkParams(rule, paramsFor(def, each ? itemKind : kind), isReservedRuleKey, `Rule "${rule.rule}"`, path, issues);
1082
+ if (!params) return void 0;
1083
+ return {
1084
+ name: rule.rule,
1085
+ def,
1086
+ params,
1087
+ message: rule.message,
1088
+ each
1089
+ };
1090
+ }
1091
+ /**
1092
+ * Checks the params of a rule or a form rule. Every key that isn't reserved must be one of
1093
+ * `params`, and together they must pass it. Returns the parsed params, or undefined when they
1094
+ * have issues. `at` names the rule in messages, e.g. `Rule "minLength"`.
1095
+ */
1096
+ function checkParams(raw, params, isReserved, at, path, issues) {
1006
1097
  const issueCount = issues.length;
1007
- const params = paramsFor(def, each ? itemKind : kind);
1008
1098
  const paramKeys = Object.keys(params?.entries ?? {});
1009
1099
  const input = {};
1010
- for (const key of Object.keys(rule)) {
1011
- if (isReservedRuleKey(key)) continue;
1012
- if (paramKeys.includes(key)) input[key] = rule[key];
1013
- else issues.push(issue("unknown_param", [...path, key], `Rule "${rule.rule}" has no param "${key}"`));
1100
+ for (const key of Object.keys(raw)) {
1101
+ if (isReserved(key)) continue;
1102
+ if (paramKeys.includes(key)) input[key] = raw[key];
1103
+ else issues.push(issue("unknown_param", [...path, key], `${at} has no param "${key}"`));
1014
1104
  }
1015
1105
  const parsed = v.safeParse(params ?? noParams, input);
1016
1106
  if (!parsed.success) issues.push(...toIssues("invalid_params", path, parsed.issues));
1017
1107
  if (!parsed.success || issues.length > issueCount) return void 0;
1018
- return {
1019
- name: rule.rule,
1020
- def,
1021
- params: parsed.output,
1022
- message: rule.message,
1023
- each
1024
- };
1108
+ return parsed.output;
1025
1109
  }
1026
1110
  function checkRegistry(registry) {
1027
1111
  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}"`);
1112
+ for (const [name, rule] of Object.entries(registry.formRules)) for (const key of Object.keys(rule.params?.entries ?? {})) if (isReservedFormRuleKey(key)) throw new TypeError(`Form rule "${name}": params can't use the reserved key "${key}"`);
1028
1113
  for (const [name, type] of Object.entries(registry.fieldTypes)) {
1029
1114
  const at = `Field type "${name}"`;
1030
1115
  for (const kind of [type.kind, type.itemKind]) if (typeof kind === "string" && !lookup(registry.kinds, kind)) throw new TypeError(`${at}: kind "${kind}" is not registered`);
@@ -1050,10 +1135,164 @@ function toIssues(code, path, valibotIssues) {
1050
1135
  return issue(code, [...path, ...keys], valibotIssue.message);
1051
1136
  });
1052
1137
  }
1138
+ // src/builder/check-form-rules.ts
1139
+ const ruleShape = v.looseObject({
1140
+ rule: v.string(),
1141
+ field: v.string(),
1142
+ message: v.string()
1143
+ });
1144
+ /**
1145
+ * Checks the definition's form rules against the fields the compiler built, and adds their
1146
+ * issues to `issues`. Returns the rules that are ready to build.
1147
+ */
1148
+ function checkFormRules(rules, registry, fieldsByPath, issues) {
1149
+ return rules.flatMap((raw, i) => checkFormRule(raw, ["formRules", i], registry, fieldsByPath, issues) ?? []);
1150
+ }
1151
+ function checkFormRule(raw, path, registry, fieldsByPath, issues) {
1152
+ const shape = v.safeParse(ruleShape, raw);
1153
+ if (!shape.success) {
1154
+ issues.push(...toIssues("invalid_shape", path, shape.issues));
1155
+ return;
1156
+ }
1157
+ const rule = shape.output;
1158
+ const at = `Form rule "${rule.rule}"`;
1159
+ const def = lookup(registry.formRules, rule.rule);
1160
+ if (!def) {
1161
+ issues.push(issue("unknown_rule", [...path, "rule"], `Unknown form rule "${rule.rule}"`));
1162
+ return;
1163
+ }
1164
+ const params = checkParams(rule, def.params, isReservedFormRuleKey, at, path, issues);
1165
+ if (!params) return void 0;
1166
+ const dotPaths = [rule.field, ...def.reads(params)];
1167
+ const paths = dotPaths.map((dotPath) => dotPath.split("."));
1168
+ const fieldKeys = paths[0];
1169
+ const issueCount = issues.length;
1170
+ const fields = dotPaths.map((dotPath, i) => {
1171
+ const found = findField(dotPath, fieldsByPath);
1172
+ const message = typeof found === "string" ? found : i > 0 && outsideItem(paths[i], fieldKeys);
1173
+ if (!message) return found;
1174
+ issues.push(issue("unknown_field", i === 0 ? [...path, "field"] : path, message));
1175
+ return null;
1176
+ });
1177
+ if (issues.length > issueCount) return void 0;
1178
+ if (fields.some((field) => !field)) return void 0;
1179
+ const checked = fields;
1180
+ const kinds = checked.map((field) => field.valueKind);
1181
+ const wrong = kinds.find((kind) => def.kinds && !def.kinds.includes(kind));
1182
+ if (wrong !== void 0) {
1183
+ const message = `${at} can't be used on ${wrong} fields`;
1184
+ issues.push(issue("kind_mismatch", [...path, "rule"], message));
1185
+ return;
1186
+ }
1187
+ if (def.sameKind && new Set(kinds).size > 1) {
1188
+ const message = `${at} needs fields of one kind, not ${[...new Set(kinds)].join(" and ")}`;
1189
+ issues.push(issue("kind_mismatch", [...path, "rule"], message));
1190
+ return;
1191
+ }
1192
+ const field = checked[0].field;
1193
+ return {
1194
+ name: rule.rule,
1195
+ def,
1196
+ params,
1197
+ message: rule.message,
1198
+ path,
1199
+ field,
1200
+ paths
1201
+ };
1202
+ }
1203
+ /**
1204
+ * The field with a value at `dotPath`. Null when a field on the way has issues of its own, and
1205
+ * the issue's message when there is no such field.
1206
+ */
1207
+ function findField(dotPath, fieldsByPath) {
1208
+ const keys = dotPath.split(".");
1209
+ let found;
1210
+ for (let i = 1; i <= keys.length; i++) {
1211
+ found = fieldsByPath.get(keys.slice(0, i).join("."));
1212
+ if (found === null) return null;
1213
+ if (found) continue;
1214
+ if (keys[i - 1] === "*" && i < keys.length) continue;
1215
+ if (fieldsByPath.get(keys.slice(0, i - 1).join("."))?.valueKind === "array" && keys[i - 1] !== "*") return `Unknown field "${dotPath}". A list's items are read with "*": "${[
1216
+ ...keys.slice(0, i - 1),
1217
+ "*",
1218
+ ...keys.slice(i - 1)
1219
+ ].join(".")}"`;
1220
+ return `Unknown field "${dotPath}"`;
1221
+ }
1222
+ return found ?? `Unknown field "${dotPath}"`;
1223
+ }
1224
+ /**
1225
+ * A path into the items of a list is read once per item, so `field` must be in the same items.
1226
+ * Returns the issue's message when it isn't.
1227
+ */
1228
+ function outsideItem(keys, fieldKeys) {
1229
+ const star = keys.lastIndexOf("*");
1230
+ const item = (path) => path.slice(0, star + 1).join(".");
1231
+ if (star === -1 || item(keys) === item(fieldKeys)) return void 0;
1232
+ const list = keys.slice(0, star).join(".");
1233
+ return `"${keys.join(".")}" is read from each item of "${list}", so "${fieldKeys.join(".")}" must be too`;
1234
+ }
1235
+ // src/builder/form-rule-action.ts
1236
+ /**
1237
+ * Runs a form rule on the form's value and puts its issue on `field`. A rule on the fields of a
1238
+ * list's items runs once per item, with the item's index in place of each `'*'`.
1239
+ *
1240
+ * The rule only runs when none of the fields it reads has an issue. `v.partialCheck` can't do
1241
+ * this: it runs whenever the form has no schema issues, even when a field it reads failed a rule.
1242
+ */
1243
+ function formRuleAction(rule, message) {
1244
+ const { def, params, paths } = rule;
1245
+ const fieldKeys = paths[0];
1246
+ return v.rawCheck(({ dataset, addIssue }) => {
1247
+ for (const indexes of itemIndexes(dataset.value, fieldKeys)) {
1248
+ const read = paths.map((keys) => withIndexes(keys, indexes));
1249
+ if (read.some((path) => hasIssue(dataset.issues, path))) continue;
1250
+ const values = read.map((path) => valueAt(dataset.value, path));
1251
+ if (values.includes(void 0) || def.check(values, params)) continue;
1252
+ const field = read[0];
1253
+ addIssue({
1254
+ message,
1255
+ input: values[0],
1256
+ path: issuePath(dataset.value, field)
1257
+ });
1258
+ }
1259
+ });
1260
+ }
1261
+ /** Replaces each `'*'` in `keys` with the next index. */
1262
+ const withIndexes = (keys, indexes) => {
1263
+ let next = 0;
1264
+ return keys.map((key) => key === "*" ? indexes[next++] : key);
1265
+ };
1266
+ const valueAt = (input, path) => path.reduce((value, key) => typeof value === "object" && value !== null ? value[key] : void 0, input);
1267
+ /** True when an issue is on `path`, inside it, or on a value that holds it. */
1268
+ const hasIssue = (issues, path) => !!issues?.some((issue) => (issue.path ?? []).every((item, i) => i >= path.length || item.key === path[i]));
1269
+ /** Valibot's path to `path` in `input`, the way an object or array schema reports it. */
1270
+ const issuePath = (input, path) => path.map((key, i) => {
1271
+ const parent = valueAt(input, path.slice(0, i));
1272
+ return {
1273
+ type: Array.isArray(parent) ? "array" : "object",
1274
+ origin: "value",
1275
+ input: parent,
1276
+ key,
1277
+ value: valueAt(parent, [key])
1278
+ };
1279
+ });
1280
+ /** The indexes for each `'*'` in `keys`, for every item `value` has there. */
1281
+ function* itemIndexes(value, keys, indexes = []) {
1282
+ const star = keys.indexOf("*");
1283
+ if (star === -1) {
1284
+ yield indexes;
1285
+ return;
1286
+ }
1287
+ const list = valueAt(value, keys.slice(0, star));
1288
+ if (!Array.isArray(list)) return;
1289
+ for (const [i, item] of list.entries()) yield* itemIndexes(item, keys.slice(star + 1), [...indexes, i]);
1290
+ }
1053
1291
  // src/builder/compile-definition.ts
1054
1292
  const formShape = v.object({
1055
1293
  version: v.string(),
1056
- fields: v.array(v.unknown())
1294
+ fields: v.array(v.unknown()),
1295
+ formRules: v.optional(v.array(v.unknown()))
1057
1296
  });
1058
1297
  /**
1059
1298
  * Checks a definition and builds its schema in the same pass, so `validate()` and `build()`
@@ -1078,9 +1317,23 @@ function compileDefinition(json, registry, messages) {
1078
1317
  schema: v.object({})
1079
1318
  };
1080
1319
  }
1320
+ const compiler = createCompiler(registry, messages, issues);
1321
+ const schema = compiler.fields(form.output.fields, ["fields"]);
1322
+ const rules = checkFormRules(form.output.formRules ?? [], registry, compiler.fieldsByPath, issues);
1323
+ if (issues.length || !rules.length) return {
1324
+ issues,
1325
+ schema
1326
+ };
1081
1327
  return {
1082
1328
  issues,
1083
- schema: createCompiler(registry, messages, issues).fields(form.output.fields, ["fields"])
1329
+ schema: rules.reduce((acc, rule) => {
1330
+ const ctx = {
1331
+ field: rule.field,
1332
+ rule: rule.name,
1333
+ path: rule.path
1334
+ };
1335
+ return v.pipe(acc, formRuleAction(rule, messages.resolve(rule.message, ctx)));
1336
+ }, schema)
1084
1337
  };
1085
1338
  }
1086
1339
  /** Every field the compiler checks adds its issues to `issues`. */
@@ -1090,29 +1343,54 @@ function createCompiler(registry, messages, issues) {
1090
1343
  buildFields: fields,
1091
1344
  buildField: field
1092
1345
  });
1346
+ const fieldsByPath = /* @__PURE__ */ new Map();
1347
+ const parents = [];
1348
+ const parentPath = () => parents.at(-1) ?? [];
1093
1349
  function fields(list, path) {
1094
1350
  const entries = {};
1095
1351
  const names = /* @__PURE__ */ new Set();
1096
1352
  list.forEach((raw, i) => {
1097
1353
  const checked = checkField(raw, [...path, i], registry, names, issues);
1098
- if (checked?.name) entries[checked.name] = build(checked);
1354
+ const name = checked ? checked.name : nameOf(raw);
1355
+ if (name === void 0) return;
1356
+ const dataPath = [...parentPath(), name];
1357
+ remember(raw, checked, dataPath);
1358
+ if (checked) entries[name] = build(checked, dataPath);
1099
1359
  });
1100
1360
  return v.object(entries);
1101
1361
  }
1102
1362
  function field(raw, path) {
1103
1363
  const checked = checkField(raw, path, registry, void 0, issues);
1104
- return checked ? build(checked) : v.never();
1364
+ const dataPath = parentPath();
1365
+ remember(raw, checked, dataPath);
1366
+ return checked ? build(checked, dataPath) : v.never();
1105
1367
  }
1106
- function build(checked) {
1107
- const schema = buildFieldSchema(checked, messages, context(checked.path));
1108
- checkDefault(checked, schema, issues);
1109
- return schema;
1368
+ function build(checked, dataPath) {
1369
+ parents.push(checked.valueKind === "array" ? [...dataPath, "*"] : dataPath);
1370
+ try {
1371
+ const schema = buildFieldSchema(checked, messages, context(checked.path));
1372
+ checkDefault(checked, schema, issues);
1373
+ return schema;
1374
+ } finally {
1375
+ parents.pop();
1376
+ }
1377
+ }
1378
+ /** The first field at a path wins, as with duplicate names. Display elements are left out. */
1379
+ function remember(raw, checked, dataPath) {
1380
+ const key = dataPath.join(".");
1381
+ if (fieldsByPath.has(key) || !checked && !hasValue(raw, registry)) return;
1382
+ fieldsByPath.set(key, checked ?? null);
1110
1383
  }
1111
1384
  return {
1112
1385
  fields,
1113
- field
1386
+ field,
1387
+ fieldsByPath
1114
1388
  };
1115
1389
  }
1390
+ const isRecord = (value) => typeof value === "object" && value !== null;
1391
+ const nameOf = (raw) => isRecord(raw) && typeof raw.name === "string" ? raw.name : void 0;
1392
+ /** An unknown type counts as one with a value, so a form rule that reads the field is fine. */
1393
+ const hasValue = (raw, registry) => isRecord(raw) && typeof raw.type === "string" && lookup(registry.fieldTypes, raw.type)?.kind !== "none";
1116
1394
  /** A default must be a value the field accepts. An empty one is the same as none. */
1117
1395
  function checkDefault({ field, kind, path }, schema, issues) {
1118
1396
  if (field.default === void 0 || kind.isEmpty(field.default)) return;
@@ -1180,7 +1458,8 @@ function createSchemaBuilder(options = {}) {
1180
1458
  rulesForKind,
1181
1459
  kinds: registry.kinds,
1182
1460
  fieldTypes: registry.fieldTypes,
1183
- rules: registry.rules
1461
+ rules: registry.rules,
1462
+ formRules: registry.formRules
1184
1463
  };
1185
1464
  }
1186
1465
  // src/definition/initial-input.ts
@@ -1239,6 +1518,6 @@ function* walkField(field, definitionPath, dataPath, depth) {
1239
1518
  }
1240
1519
  }
1241
1520
  const isField = (value) => typeof value === "object" && value !== null && "type" in value;
1242
- export { DefinitionError, RESERVED_FIELD_KEYS, RESERVED_RULE_KEYS, comparisonRules, contentRules, createBuildContext, createSchemaBuilder, dateRules, defaultFieldTypes, defaultKinds, defaultRules, defineFieldType, defineRule, fileRules, formatPath, formatRules, getInitialInput, lengthRules, normalizeRules, numberRules, textUnitRules, walkFields };
1521
+ export { DefinitionError, RESERVED_FIELD_KEYS, RESERVED_FORM_RULE_KEYS, RESERVED_RULE_KEYS, comparisonRules, contentRules, createBuildContext, createSchemaBuilder, dateRules, defaultFieldTypes, defaultFormRules, defaultKinds, defaultRules, defineFieldType, defineFormRule, defineRule, fileRules, formatPath, formatRules, getInitialInput, lengthRules, normalizeRules, numberRules, textUnitRules, walkFields };
1243
1522
 
1244
1523
  //# sourceMappingURL=index.js.map