@orkestrel/form 0.0.10 → 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.
@@ -354,7 +354,7 @@ export declare interface FieldChoice {
354
354
  * const control: FieldControl = 'select'
355
355
  * ```
356
356
  */
357
- export declare type FieldControl = 'text' | 'editor' | 'password' | 'number' | 'date' | 'time' | 'datetime' | 'color' | 'confirm' | 'select' | 'checkbox' | 'file';
357
+ export declare type FieldControl = 'text' | 'editor' | 'password' | 'number' | 'date' | 'time' | 'datetime' | 'color' | 'confirm' | 'select' | 'checkbox' | 'list' | 'file';
358
358
 
359
359
  /**
360
360
  * Represents one failed check against one field.
@@ -377,11 +377,12 @@ export declare interface FieldError {
377
377
  * `minimum` and `maximum` measure whatever the control makes countable: characters for
378
378
  * `text`, `editor`, and `password`; magnitude for `number`; chronology for `date`, `time`,
379
379
  * and `datetime`, whose operand is a string written in that control's own format; and the
380
- * number of selections for `checkbox` and `file`.
380
+ * number of entries for `checkbox`, `list`, and `file`.
381
381
  *
382
382
  * `step` is the interval a numeric value must land on. `pattern` is regular-expression
383
383
  * source. `email`, `url`, `integer`, and `alphanumeric` each assert one shape over the whole
384
- * value. `custom` runs last and is the only rule that sees the rest of the form.
384
+ * value, or each entry of a `list`. `required` requires an entry for `list` and presence
385
+ * for other controls. `custom` runs last and is the only rule that sees the rest of the form.
385
386
  *
386
387
  * @example
387
388
  * ```ts
@@ -440,7 +441,7 @@ export declare type FieldValidator = (value: FieldValue | undefined, values: For
440
441
  *
441
442
  * @remarks
442
443
  * The variant follows the control: text-like controls hold a `string`, `number` holds a
443
- * `number`, `confirm` holds a `boolean`, and `checkbox` holds its checked values as a
444
+ * `number`, `confirm` holds a `boolean`, and `checkbox`, `list`, and `file` hold a
444
445
  * `readonly string[]`.
445
446
  */
446
447
  export declare type FieldValue = string | number | boolean | readonly string[];
@@ -753,7 +754,7 @@ export declare type FormEventMap = {
753
754
  * }
754
755
  * ```
755
756
  */
756
- export declare type FormField = TextField | EditorField | PasswordField | NumberField | DateField | TimeField | DatetimeField | ColorField | ConfirmField | SelectField | CheckboxField | FileField;
757
+ export declare type FormField = TextField | EditorField | PasswordField | NumberField | DateField | TimeField | DatetimeField | ColorField | ConfirmField | SelectField | CheckboxField | ListField | FileField;
757
758
 
758
759
  /**
759
760
  * Represents a named section of a form.
@@ -1202,6 +1203,26 @@ export declare function isFormValues(input: unknown): input is FormValues;
1202
1203
  /** Caps the number of entries one list-valued answer may hold, at 1024. */
1203
1204
  export declare const LIST_LIMIT = 1024;
1204
1205
 
1206
+ /**
1207
+ * Represents an ordered list of free-text strings.
1208
+ *
1209
+ * @remarks
1210
+ * `required` requires at least one entry. `minimum` and `maximum` count entries.
1211
+ * `pattern`, `email`, `url`, `integer`, and `alphanumeric` check each entry and name
1212
+ * the failing value in its message. `custom` checks the whole list. Duplicates are retained.
1213
+ *
1214
+ * @example
1215
+ * ```ts
1216
+ * const to: ListField = {
1217
+ * control: 'list', name: 'to', rule: { required: true, email: true },
1218
+ * }
1219
+ * ```
1220
+ */
1221
+ export declare interface ListField extends FieldBase {
1222
+ readonly control: 'list';
1223
+ readonly default?: readonly string[];
1224
+ }
1225
+
1205
1226
  /**
1206
1227
  * Decides whether a raw binding value projects to an answered field.
1207
1228
  *
@@ -1209,7 +1230,7 @@ export declare const LIST_LIMIT = 1024;
1209
1230
  * Bind with `fill(name, matchesAnswer(raw) ? raw : undefined)`. This projection treats an absent
1210
1231
  * value and a string containing only whitespace as unanswered. Every other field value is an
1211
1232
  * answer, including an empty list, `false`, and zero. Core evaluation does not use this projection:
1212
- * its `required` rule remains presence-only.
1233
+ * its `required` rule remains presence-only except that `list` requires an entry.
1213
1234
  *
1214
1235
  * @param value - The raw field value, or absence.
1215
1236
  * @returns True if the binding preserves the value as an answer; false otherwise.
@@ -354,7 +354,7 @@ export declare interface FieldChoice {
354
354
  * const control: FieldControl = 'select'
355
355
  * ```
356
356
  */
357
- export declare type FieldControl = 'text' | 'editor' | 'password' | 'number' | 'date' | 'time' | 'datetime' | 'color' | 'confirm' | 'select' | 'checkbox' | 'file';
357
+ export declare type FieldControl = 'text' | 'editor' | 'password' | 'number' | 'date' | 'time' | 'datetime' | 'color' | 'confirm' | 'select' | 'checkbox' | 'list' | 'file';
358
358
 
359
359
  /**
360
360
  * Represents one failed check against one field.
@@ -377,11 +377,12 @@ export declare interface FieldError {
377
377
  * `minimum` and `maximum` measure whatever the control makes countable: characters for
378
378
  * `text`, `editor`, and `password`; magnitude for `number`; chronology for `date`, `time`,
379
379
  * and `datetime`, whose operand is a string written in that control's own format; and the
380
- * number of selections for `checkbox` and `file`.
380
+ * number of entries for `checkbox`, `list`, and `file`.
381
381
  *
382
382
  * `step` is the interval a numeric value must land on. `pattern` is regular-expression
383
383
  * source. `email`, `url`, `integer`, and `alphanumeric` each assert one shape over the whole
384
- * value. `custom` runs last and is the only rule that sees the rest of the form.
384
+ * value, or each entry of a `list`. `required` requires an entry for `list` and presence
385
+ * for other controls. `custom` runs last and is the only rule that sees the rest of the form.
385
386
  *
386
387
  * @example
387
388
  * ```ts
@@ -440,7 +441,7 @@ export declare type FieldValidator = (value: FieldValue | undefined, values: For
440
441
  *
441
442
  * @remarks
442
443
  * The variant follows the control: text-like controls hold a `string`, `number` holds a
443
- * `number`, `confirm` holds a `boolean`, and `checkbox` holds its checked values as a
444
+ * `number`, `confirm` holds a `boolean`, and `checkbox`, `list`, and `file` hold a
444
445
  * `readonly string[]`.
445
446
  */
446
447
  export declare type FieldValue = string | number | boolean | readonly string[];
@@ -753,7 +754,7 @@ export declare type FormEventMap = {
753
754
  * }
754
755
  * ```
755
756
  */
756
- export declare type FormField = TextField | EditorField | PasswordField | NumberField | DateField | TimeField | DatetimeField | ColorField | ConfirmField | SelectField | CheckboxField | FileField;
757
+ export declare type FormField = TextField | EditorField | PasswordField | NumberField | DateField | TimeField | DatetimeField | ColorField | ConfirmField | SelectField | CheckboxField | ListField | FileField;
757
758
 
758
759
  /**
759
760
  * Represents a named section of a form.
@@ -1202,6 +1203,26 @@ export declare function isFormValues(input: unknown): input is FormValues;
1202
1203
  /** Caps the number of entries one list-valued answer may hold, at 1024. */
1203
1204
  export declare const LIST_LIMIT = 1024;
1204
1205
 
1206
+ /**
1207
+ * Represents an ordered list of free-text strings.
1208
+ *
1209
+ * @remarks
1210
+ * `required` requires at least one entry. `minimum` and `maximum` count entries.
1211
+ * `pattern`, `email`, `url`, `integer`, and `alphanumeric` check each entry and name
1212
+ * the failing value in its message. `custom` checks the whole list. Duplicates are retained.
1213
+ *
1214
+ * @example
1215
+ * ```ts
1216
+ * const to: ListField = {
1217
+ * control: 'list', name: 'to', rule: { required: true, email: true },
1218
+ * }
1219
+ * ```
1220
+ */
1221
+ export declare interface ListField extends FieldBase {
1222
+ readonly control: 'list';
1223
+ readonly default?: readonly string[];
1224
+ }
1225
+
1205
1226
  /**
1206
1227
  * Decides whether a raw binding value projects to an answered field.
1207
1228
  *
@@ -1209,7 +1230,7 @@ export declare const LIST_LIMIT = 1024;
1209
1230
  * Bind with `fill(name, matchesAnswer(raw) ? raw : undefined)`. This projection treats an absent
1210
1231
  * value and a string containing only whitespace as unanswered. Every other field value is an
1211
1232
  * answer, including an empty list, `false`, and zero. Core evaluation does not use this projection:
1212
- * its `required` rule remains presence-only.
1233
+ * its `required` rule remains presence-only except that `list` requires an entry.
1213
1234
  *
1214
1235
  * @param value - The raw field value, or absence.
1215
1236
  * @returns True if the binding preserves the value as an answer; false otherwise.
@@ -14,6 +14,7 @@ var FIELD_CONTROLS = Object.freeze([
14
14
  "confirm",
15
15
  "select",
16
16
  "checkbox",
17
+ "list",
17
18
  "file"
18
19
  ]);
19
20
  /** Lists the members every field declares, whatever its control. */
@@ -66,6 +67,7 @@ var FIELD_KEYS = Object.freeze({
66
67
  "choices",
67
68
  "default"
68
69
  ]),
70
+ list: Object.freeze([...FIELD_BASE_KEYS, "default"]),
69
71
  file: Object.freeze([
70
72
  ...FIELD_BASE_KEYS,
71
73
  "accept",
@@ -324,6 +326,7 @@ function isFormField(input) {
324
326
  const fallback = hasDefault ? input.default : void 0;
325
327
  return arrayOf(isFieldChoice)(choices) && (!hasDefault || arrayOf(isString)(fallback));
326
328
  }
329
+ case "list": return !Object.hasOwn(input, "default") || arrayOf(isString)(input.default);
327
330
  case "file": {
328
331
  const hasAccept = Object.hasOwn(input, "accept");
329
332
  const accept = hasAccept ? input.accept : void 0;
@@ -448,6 +451,12 @@ function cloneFormField(field) {
448
451
  choices: cloneChoices(field.choices),
449
452
  ...field.default === void 0 ? {} : { default: cloneValue(field.default) }
450
453
  });
454
+ case "list": return Object.freeze({
455
+ ...field,
456
+ ...rule,
457
+ ...meta,
458
+ ...field.default === void 0 ? {} : { default: cloneValue(field.default) }
459
+ });
451
460
  case "file": return Object.freeze({
452
461
  ...field,
453
462
  ...rule,
@@ -572,6 +581,7 @@ function matchesField(field, value) {
572
581
  case "confirm": return isBoolean(value);
573
582
  case "select": return 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));
574
583
  case "checkbox": return entries !== void 0 && entries.every((entry) => isString(entry) && field.choices.some((choice) => choice.value === entry && choice.disabled !== true)) && new Set(entries).size === entries.length;
584
+ case "list": return entries !== void 0;
575
585
  case "file": return entries !== void 0 && (field.multiple === true || entries.length <= 1);
576
586
  }
577
587
  }
@@ -582,7 +592,7 @@ function matchesField(field, value) {
582
592
  * Bind with `fill(name, matchesAnswer(raw) ? raw : undefined)`. This projection treats an absent
583
593
  * value and a string containing only whitespace as unanswered. Every other field value is an
584
594
  * answer, including an empty list, `false`, and zero. Core evaluation does not use this projection:
585
- * its `required` rule remains presence-only.
595
+ * its `required` rule remains presence-only except that `list` requires an entry.
586
596
  *
587
597
  * @param value - The raw field value, or absence.
588
598
  * @returns True if the binding preserves the value as an answer; false otherwise.
@@ -630,7 +640,7 @@ function appliesRule(control, rule) {
630
640
  function evaluateField(field, value, values, messages) {
631
641
  const errors = [];
632
642
  const rule = field.rule;
633
- if (value === void 0) {
643
+ if (value === void 0 || field.control === "list" && isArray(value) && value.length === 0) {
634
644
  if (rule?.required === true && appliesRule(field.control, "required")) errors.push(createFieldError(field, "required", void 0, messages));
635
645
  }
636
646
  if (rule === void 0) return Object.freeze(errors);
@@ -651,6 +661,7 @@ function evaluateField(field, value, values, messages) {
651
661
  failed = isString(value) && isString(rule.minimum) && value < rule.minimum;
652
662
  break;
653
663
  case "checkbox":
664
+ case "list":
654
665
  case "file": failed = isArray(value) && isFiniteNumber(rule.minimum) && value.length < rule.minimum;
655
666
  }
656
667
  if (failed) errors.push(createFieldError(field, "minimum", rule.minimum, messages));
@@ -672,6 +683,7 @@ function evaluateField(field, value, values, messages) {
672
683
  failed = isString(value) && isString(rule.maximum) && value > rule.maximum;
673
684
  break;
674
685
  case "checkbox":
686
+ case "list":
675
687
  case "file": failed = isArray(value) && isFiniteNumber(rule.maximum) && value.length > rule.maximum;
676
688
  }
677
689
  if (failed) errors.push(createFieldError(field, "maximum", rule.maximum, messages));
@@ -680,19 +692,45 @@ function evaluateField(field, value, values, messages) {
680
692
  const multiple = (value - (isFiniteNumber(rule.minimum) ? rule.minimum : 0)) / rule.step;
681
693
  if (!isFiniteNumber(rule.step) || rule.step === 0 || !isFiniteNumber(multiple) || Math.abs(multiple - Math.round(multiple)) > 1e-9) errors.push(createFieldError(field, "step", rule.step, messages));
682
694
  }
683
- if (isString(value)) {
684
- if (rule.pattern !== void 0 && appliesRule(field.control, "pattern")) {
685
- const pattern = rule.pattern;
686
- if (pattern.length > 256) errors.push(createFieldError(field, "pattern", void 0, messages));
687
- else {
688
- const outcome = attempt(() => new RegExp(pattern).test(value));
689
- if (!outcome.success || !outcome.value) errors.push(createFieldError(field, "pattern", void 0, messages));
695
+ const entries = isString(value) ? [value] : field.control === "list" && isArray(value) ? value : [];
696
+ for (const constraint of [
697
+ "pattern",
698
+ "email",
699
+ "url",
700
+ "alphanumeric",
701
+ "integer"
702
+ ]) {
703
+ if (!appliesRule(field.control, constraint)) continue;
704
+ for (const entry of entries) {
705
+ let failed = false;
706
+ switch (constraint) {
707
+ case "pattern": {
708
+ const pattern = rule.pattern;
709
+ if (pattern !== void 0) {
710
+ const outcome = attempt(() => pattern.length <= 256 && new RegExp(pattern).test(entry));
711
+ failed = !outcome.success || !outcome.value;
712
+ }
713
+ break;
714
+ }
715
+ case "email":
716
+ failed = rule.email === true && !EMAIL_PATTERN.test(entry);
717
+ break;
718
+ case "url":
719
+ failed = rule.url === true && !URL_PATTERN.test(entry);
720
+ break;
721
+ case "alphanumeric":
722
+ failed = rule.alphanumeric === true && !ALPHANUMERIC_PATTERN.test(entry);
723
+ break;
724
+ case "integer": failed = rule.integer === true && !INTEGER_PATTERN.test(entry);
725
+ }
726
+ if (failed) {
727
+ const error = createFieldError(field, constraint, void 0, messages);
728
+ errors.push(field.control === "list" ? Object.freeze({
729
+ ...error,
730
+ message: `${JSON.stringify(entry)}: ${error.message}`
731
+ }) : error);
690
732
  }
691
733
  }
692
- if (rule.email === true && appliesRule(field.control, "email") && !EMAIL_PATTERN.test(value)) errors.push(createFieldError(field, "email", void 0, messages));
693
- if (rule.url === true && appliesRule(field.control, "url") && !URL_PATTERN.test(value)) errors.push(createFieldError(field, "url", void 0, messages));
694
- if (rule.alphanumeric === true && appliesRule(field.control, "alphanumeric") && !ALPHANUMERIC_PATTERN.test(value)) errors.push(createFieldError(field, "alphanumeric", void 0, messages));
695
- if (rule.integer === true && appliesRule(field.control, "integer") && !INTEGER_PATTERN.test(value)) errors.push(createFieldError(field, "integer", void 0, messages));
696
734
  }
697
735
  if (value !== void 0 && field.control === "number" && rule.integer === true && appliesRule(field.control, "integer") && !isInteger(value)) errors.push(createFieldError(field, "integer", void 0, messages));
698
736
  if (rule.custom !== void 0) {
@@ -736,6 +774,7 @@ function computeDefaults(schema) {
736
774
  const defaults = {};
737
775
  for (const field of schema.fields) switch (field.control) {
738
776
  case "checkbox":
777
+ case "list":
739
778
  if (field.default !== void 0) defineEntry(defaults, field.name, cloneValue(field.default));
740
779
  break;
741
780
  case "password":
@@ -923,6 +962,9 @@ function serializeForm(schema) {
923
962
  });
924
963
  if (field.default !== void 0) entry.default = field.default;
925
964
  break;
965
+ case "list":
966
+ if (field.default !== void 0) entry.default = field.default;
967
+ break;
926
968
  case "file":
927
969
  if (field.accept !== void 0) entry.accept = field.accept;
928
970
  if (field.multiple !== void 0) entry.multiple = field.multiple;
@@ -1074,7 +1116,8 @@ function auditSchema(schema) {
1074
1116
  case "color":
1075
1117
  case "confirm":
1076
1118
  case "select":
1077
- case "checkbox": if (field.default !== void 0 && !matchesField(field, field.default)) faults.push(`Field "${field.name}" has an invalid default`);
1119
+ case "checkbox":
1120
+ case "list": if (field.default !== void 0 && !matchesField(field, field.default)) faults.push(`Field "${field.name}" has an invalid default`);
1078
1121
  }
1079
1122
  if (field.control === "select" || field.control === "checkbox") {
1080
1123
  const choices = /* @__PURE__ */ new Set();
@@ -1093,6 +1136,11 @@ function auditSchema(schema) {
1093
1136
  }
1094
1137
  const rule = field.rule;
1095
1138
  if (rule === void 0) continue;
1139
+ if (field.control === "list") {
1140
+ for (const bound of [rule.minimum, rule.maximum]) if (isFiniteNumber(bound) && (!isInteger(bound) || bound < 0)) faults.push(`Field "${field.name}" has a count that is not a nonnegative integer`);
1141
+ if (isFiniteNumber(rule.minimum) && rule.minimum > 1024) faults.push(`Field "${field.name}" has minimum greater than ${LIST_LIMIT}`);
1142
+ if (rule.required === true && isFiniteNumber(rule.maximum) && rule.maximum < 1) faults.push(`Field "${field.name}" is required but permits no entries`);
1143
+ }
1096
1144
  const temporal = field.control === "date" || field.control === "time" || field.control === "datetime";
1097
1145
  if (rule.minimum !== void 0 && !appliesRule(field.control, "minimum")) faults.push(`Field "${field.name}" has minimum on ${field.control}`);
1098
1146
  else if (isString(rule.minimum) && !temporal) faults.push(`Field "${field.name}" has a string minimum on ${field.control}`);