@orkestrel/form 0.0.11 → 0.0.12

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.
@@ -15,6 +15,7 @@ var FIELD_CONTROLS = Object.freeze([
15
15
  "confirm",
16
16
  "select",
17
17
  "checkbox",
18
+ "list",
18
19
  "file"
19
20
  ]);
20
21
  /** Lists the members every field declares, whatever its control. */
@@ -67,6 +68,7 @@ var FIELD_KEYS = Object.freeze({
67
68
  "choices",
68
69
  "default"
69
70
  ]),
71
+ list: Object.freeze([...FIELD_BASE_KEYS, "default"]),
70
72
  file: Object.freeze([
71
73
  ...FIELD_BASE_KEYS,
72
74
  "accept",
@@ -325,6 +327,7 @@ function isFormField(input) {
325
327
  const fallback = hasDefault ? input.default : void 0;
326
328
  return (0, _orkestrel_contract.arrayOf)(isFieldChoice)(choices) && (!hasDefault || (0, _orkestrel_contract.arrayOf)(_orkestrel_contract.isString)(fallback));
327
329
  }
330
+ case "list": return !Object.hasOwn(input, "default") || (0, _orkestrel_contract.arrayOf)(_orkestrel_contract.isString)(input.default);
328
331
  case "file": {
329
332
  const hasAccept = Object.hasOwn(input, "accept");
330
333
  const accept = hasAccept ? input.accept : void 0;
@@ -449,6 +452,12 @@ function cloneFormField(field) {
449
452
  choices: cloneChoices(field.choices),
450
453
  ...field.default === void 0 ? {} : { default: cloneValue(field.default) }
451
454
  });
455
+ case "list": return Object.freeze({
456
+ ...field,
457
+ ...rule,
458
+ ...meta,
459
+ ...field.default === void 0 ? {} : { default: cloneValue(field.default) }
460
+ });
452
461
  case "file": return Object.freeze({
453
462
  ...field,
454
463
  ...rule,
@@ -573,6 +582,7 @@ function matchesField(field, value) {
573
582
  case "confirm": return (0, _orkestrel_contract.isBoolean)(value);
574
583
  case "select": return (0, _orkestrel_contract.isString)(value) && !field.choices.some((choice) => choice.value === value && choice.disabled === true) && (field.open === true || field.choices.some((choice) => choice.value === value && choice.disabled !== true));
575
584
  case "checkbox": return entries !== void 0 && entries.every((entry) => (0, _orkestrel_contract.isString)(entry) && field.choices.some((choice) => choice.value === entry && choice.disabled !== true)) && new Set(entries).size === entries.length;
585
+ case "list": return entries !== void 0;
576
586
  case "file": return entries !== void 0 && (field.multiple === true || entries.length <= 1);
577
587
  }
578
588
  }
@@ -583,7 +593,7 @@ function matchesField(field, value) {
583
593
  * Bind with `fill(name, matchesAnswer(raw) ? raw : undefined)`. This projection treats an absent
584
594
  * value and a string containing only whitespace as unanswered. Every other field value is an
585
595
  * answer, including an empty list, `false`, and zero. Core evaluation does not use this projection:
586
- * its `required` rule remains presence-only.
596
+ * its `required` rule remains presence-only except that `list` requires an entry.
587
597
  *
588
598
  * @param value - The raw field value, or absence.
589
599
  * @returns True if the binding preserves the value as an answer; false otherwise.
@@ -631,7 +641,7 @@ function appliesRule(control, rule) {
631
641
  function evaluateField(field, value, values, messages) {
632
642
  const errors = [];
633
643
  const rule = field.rule;
634
- if (value === void 0) {
644
+ if (value === void 0 || field.control === "list" && (0, _orkestrel_contract.isArray)(value) && value.length === 0) {
635
645
  if (rule?.required === true && appliesRule(field.control, "required")) errors.push(createFieldError(field, "required", void 0, messages));
636
646
  }
637
647
  if (rule === void 0) return Object.freeze(errors);
@@ -652,6 +662,7 @@ function evaluateField(field, value, values, messages) {
652
662
  failed = (0, _orkestrel_contract.isString)(value) && (0, _orkestrel_contract.isString)(rule.minimum) && value < rule.minimum;
653
663
  break;
654
664
  case "checkbox":
665
+ case "list":
655
666
  case "file": failed = (0, _orkestrel_contract.isArray)(value) && (0, _orkestrel_contract.isFiniteNumber)(rule.minimum) && value.length < rule.minimum;
656
667
  }
657
668
  if (failed) errors.push(createFieldError(field, "minimum", rule.minimum, messages));
@@ -673,6 +684,7 @@ function evaluateField(field, value, values, messages) {
673
684
  failed = (0, _orkestrel_contract.isString)(value) && (0, _orkestrel_contract.isString)(rule.maximum) && value > rule.maximum;
674
685
  break;
675
686
  case "checkbox":
687
+ case "list":
676
688
  case "file": failed = (0, _orkestrel_contract.isArray)(value) && (0, _orkestrel_contract.isFiniteNumber)(rule.maximum) && value.length > rule.maximum;
677
689
  }
678
690
  if (failed) errors.push(createFieldError(field, "maximum", rule.maximum, messages));
@@ -681,19 +693,45 @@ function evaluateField(field, value, values, messages) {
681
693
  const multiple = (value - ((0, _orkestrel_contract.isFiniteNumber)(rule.minimum) ? rule.minimum : 0)) / rule.step;
682
694
  if (!(0, _orkestrel_contract.isFiniteNumber)(rule.step) || rule.step === 0 || !(0, _orkestrel_contract.isFiniteNumber)(multiple) || Math.abs(multiple - Math.round(multiple)) > 1e-9) errors.push(createFieldError(field, "step", rule.step, messages));
683
695
  }
684
- if ((0, _orkestrel_contract.isString)(value)) {
685
- if (rule.pattern !== void 0 && appliesRule(field.control, "pattern")) {
686
- const pattern = rule.pattern;
687
- if (pattern.length > 256) errors.push(createFieldError(field, "pattern", void 0, messages));
688
- else {
689
- const outcome = (0, _orkestrel_contract.attempt)(() => new RegExp(pattern).test(value));
690
- if (!outcome.success || !outcome.value) errors.push(createFieldError(field, "pattern", void 0, messages));
696
+ const entries = (0, _orkestrel_contract.isString)(value) ? [value] : field.control === "list" && (0, _orkestrel_contract.isArray)(value) ? value : [];
697
+ for (const constraint of [
698
+ "pattern",
699
+ "email",
700
+ "url",
701
+ "alphanumeric",
702
+ "integer"
703
+ ]) {
704
+ if (!appliesRule(field.control, constraint)) continue;
705
+ for (const entry of entries) {
706
+ let failed = false;
707
+ switch (constraint) {
708
+ case "pattern": {
709
+ const pattern = rule.pattern;
710
+ if (pattern !== void 0) {
711
+ const outcome = (0, _orkestrel_contract.attempt)(() => pattern.length <= 256 && new RegExp(pattern).test(entry));
712
+ failed = !outcome.success || !outcome.value;
713
+ }
714
+ break;
715
+ }
716
+ case "email":
717
+ failed = rule.email === true && !EMAIL_PATTERN.test(entry);
718
+ break;
719
+ case "url":
720
+ failed = rule.url === true && !URL_PATTERN.test(entry);
721
+ break;
722
+ case "alphanumeric":
723
+ failed = rule.alphanumeric === true && !ALPHANUMERIC_PATTERN.test(entry);
724
+ break;
725
+ case "integer": failed = rule.integer === true && !INTEGER_PATTERN.test(entry);
726
+ }
727
+ if (failed) {
728
+ const error = createFieldError(field, constraint, void 0, messages);
729
+ errors.push(field.control === "list" ? Object.freeze({
730
+ ...error,
731
+ message: `${JSON.stringify(entry)}: ${error.message}`
732
+ }) : error);
691
733
  }
692
734
  }
693
- if (rule.email === true && appliesRule(field.control, "email") && !EMAIL_PATTERN.test(value)) errors.push(createFieldError(field, "email", void 0, messages));
694
- if (rule.url === true && appliesRule(field.control, "url") && !URL_PATTERN.test(value)) errors.push(createFieldError(field, "url", void 0, messages));
695
- if (rule.alphanumeric === true && appliesRule(field.control, "alphanumeric") && !ALPHANUMERIC_PATTERN.test(value)) errors.push(createFieldError(field, "alphanumeric", void 0, messages));
696
- if (rule.integer === true && appliesRule(field.control, "integer") && !INTEGER_PATTERN.test(value)) errors.push(createFieldError(field, "integer", void 0, messages));
697
735
  }
698
736
  if (value !== void 0 && field.control === "number" && rule.integer === true && appliesRule(field.control, "integer") && !(0, _orkestrel_contract.isInteger)(value)) errors.push(createFieldError(field, "integer", void 0, messages));
699
737
  if (rule.custom !== void 0) {
@@ -737,6 +775,7 @@ function computeDefaults(schema) {
737
775
  const defaults = {};
738
776
  for (const field of schema.fields) switch (field.control) {
739
777
  case "checkbox":
778
+ case "list":
740
779
  if (field.default !== void 0) defineEntry(defaults, field.name, cloneValue(field.default));
741
780
  break;
742
781
  case "password":
@@ -924,6 +963,9 @@ function serializeForm(schema) {
924
963
  });
925
964
  if (field.default !== void 0) entry.default = field.default;
926
965
  break;
966
+ case "list":
967
+ if (field.default !== void 0) entry.default = field.default;
968
+ break;
927
969
  case "file":
928
970
  if (field.accept !== void 0) entry.accept = field.accept;
929
971
  if (field.multiple !== void 0) entry.multiple = field.multiple;
@@ -1075,7 +1117,8 @@ function auditSchema(schema) {
1075
1117
  case "color":
1076
1118
  case "confirm":
1077
1119
  case "select":
1078
- case "checkbox": if (field.default !== void 0 && !matchesField(field, field.default)) faults.push(`Field "${field.name}" has an invalid default`);
1120
+ case "checkbox":
1121
+ case "list": if (field.default !== void 0 && !matchesField(field, field.default)) faults.push(`Field "${field.name}" has an invalid default`);
1079
1122
  }
1080
1123
  if (field.control === "select" || field.control === "checkbox") {
1081
1124
  const choices = /* @__PURE__ */ new Set();
@@ -1094,6 +1137,11 @@ function auditSchema(schema) {
1094
1137
  }
1095
1138
  const rule = field.rule;
1096
1139
  if (rule === void 0) continue;
1140
+ if (field.control === "list") {
1141
+ for (const bound of [rule.minimum, rule.maximum]) if ((0, _orkestrel_contract.isFiniteNumber)(bound) && (!(0, _orkestrel_contract.isInteger)(bound) || bound < 0)) faults.push(`Field "${field.name}" has a count that is not a nonnegative integer`);
1142
+ if ((0, _orkestrel_contract.isFiniteNumber)(rule.minimum) && rule.minimum > 1024) faults.push(`Field "${field.name}" has minimum greater than ${LIST_LIMIT}`);
1143
+ if (rule.required === true && (0, _orkestrel_contract.isFiniteNumber)(rule.maximum) && rule.maximum < 1) faults.push(`Field "${field.name}" is required but permits no entries`);
1144
+ }
1097
1145
  const temporal = field.control === "date" || field.control === "time" || field.control === "datetime";
1098
1146
  if (rule.minimum !== void 0 && !appliesRule(field.control, "minimum")) faults.push(`Field "${field.name}" has minimum on ${field.control}`);
1099
1147
  else if ((0, _orkestrel_contract.isString)(rule.minimum) && !temporal) faults.push(`Field "${field.name}" has a string minimum on ${field.control}`);