@terreno/blocks 57.10.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +62 -0
- package/biome.jsonc +18 -0
- package/bunfig.toml +6 -0
- package/dist/asks/errors.d.ts +77 -0
- package/dist/asks/errors.js +364 -0
- package/dist/asks/files.d.ts +53 -0
- package/dist/asks/files.js +171 -0
- package/dist/asks/formValues.d.ts +32 -0
- package/dist/asks/formValues.js +229 -0
- package/dist/asks/headless.d.ts +227 -0
- package/dist/asks/headless.js +94 -0
- package/dist/asks/jsonSchema.d.ts +6 -0
- package/dist/asks/jsonSchema.js +62 -0
- package/dist/asks/limits.d.ts +58 -0
- package/dist/asks/limits.js +61 -0
- package/dist/asks/prompt.d.ts +10 -0
- package/dist/asks/prompt.js +144 -0
- package/dist/asks/schema.d.ts +826 -0
- package/dist/asks/schema.js +864 -0
- package/dist/asks/simpleCard.d.ts +85 -0
- package/dist/asks/simpleCard.js +279 -0
- package/dist/asks/validateInput.d.ts +13 -0
- package/dist/asks/validateInput.js +19 -0
- package/dist/asks/validateResponse.d.ts +13 -0
- package/dist/asks/validateResponse.js +278 -0
- package/dist/cli.d.ts +11 -0
- package/dist/cli.js +58 -0
- package/dist/errors.d.ts +51 -0
- package/dist/errors.js +106 -0
- package/dist/index.d.ts +31 -0
- package/dist/index.js +111 -0
- package/dist/jsonSchema.d.ts +5 -0
- package/dist/jsonSchema.js +12 -0
- package/dist/limits.d.ts +27 -0
- package/dist/limits.js +30 -0
- package/dist/lint.d.ts +21 -0
- package/dist/lint.js +388 -0
- package/dist/parse.d.ts +16 -0
- package/dist/parse.js +95 -0
- package/dist/parsePartial.d.ts +12 -0
- package/dist/parsePartial.js +105 -0
- package/dist/prompt.d.ts +9 -0
- package/dist/prompt.js +95 -0
- package/dist/schema.d.ts +199 -0
- package/dist/schema.js +277 -0
- package/dist/validate.d.ts +17 -0
- package/dist/validate.js +331 -0
- package/package.json +60 -0
- package/schemas/pendingAsksResponse.schema.json +195 -0
- package/schemas/simpleCard.schema.json +135 -0
- package/schemas/turnRequest.schema.json +108 -0
- package/schemas/turnResponse.schema.json +204 -0
- package/scripts/writeJsonSchemas.ts +11 -0
- package/src/asks/docParity.test.ts +65 -0
- package/src/asks/errors.test.ts +250 -0
- package/src/asks/errors.ts +449 -0
- package/src/asks/files.test.ts +294 -0
- package/src/asks/files.ts +216 -0
- package/src/asks/fixtures/invalid/choice-blank-text.json +20 -0
- package/src/asks/fixtures/invalid/choice-default-not-in-options.json +13 -0
- package/src/asks/fixtures/invalid/choice-default-selects-two.json +16 -0
- package/src/asks/fixtures/invalid/choice-description-too-long.json +24 -0
- package/src/asks/fixtures/invalid/choice-duplicate-ids.json +17 -0
- package/src/asks/fixtures/invalid/choice-label-too-long.json +23 -0
- package/src/asks/fixtures/invalid/choice-many-bounds-not-whole.json +17 -0
- package/src/asks/fixtures/invalid/choice-many-bounds-out-of-range.json +17 -0
- package/src/asks/fixtures/invalid/choice-many-default-duplicate.json +17 -0
- package/src/asks/fixtures/invalid/choice-many-default-too-many.json +15 -0
- package/src/asks/fixtures/invalid/choice-many-max-above-options.json +14 -0
- package/src/asks/fixtures/invalid/choice-many-min-above-choices.json +13 -0
- package/src/asks/fixtures/invalid/choice-many-min-above-max.json +15 -0
- package/src/asks/fixtures/invalid/choice-missing-required.json +14 -0
- package/src/asks/fixtures/invalid/choice-mixed-errors.json +19 -0
- package/src/asks/fixtures/invalid/choice-not-an-object.json +5 -0
- package/src/asks/fixtures/invalid/choice-one-with-many-fields.json +19 -0
- package/src/asks/fixtures/invalid/choice-option-id-format.json +35 -0
- package/src/asks/fixtures/invalid/choice-options-not-objects.json +12 -0
- package/src/asks/fixtures/invalid/choice-other-label-too-long.json +25 -0
- package/src/asks/fixtures/invalid/choice-other-label-without-allow-other.json +14 -0
- package/src/asks/fixtures/invalid/choice-prompt-empty.json +12 -0
- package/src/asks/fixtures/invalid/choice-prompt-too-long.json +23 -0
- package/src/asks/fixtures/invalid/choice-select-unknown.json +12 -0
- package/src/asks/fixtures/invalid/choice-submit-label-too-long.json +13 -0
- package/src/asks/fixtures/invalid/choice-title-too-long.json +24 -0
- package/src/asks/fixtures/invalid/choice-too-few-options.json +9 -0
- package/src/asks/fixtures/invalid/choice-too-many-options.json +219 -0
- package/src/asks/fixtures/invalid/choice-unknown-key-option.json +15 -0
- package/src/asks/fixtures/invalid/choice-unknown-key-root.json +13 -0
- package/src/asks/fixtures/invalid/choice-wrong-types.json +16 -0
- package/src/asks/fixtures/invalid/confirm-emoji-label-too-long.json +13 -0
- package/src/asks/fixtures/invalid/confirm-label-too-long.json +14 -0
- package/src/asks/fixtures/invalid/confirm-labels-alike-after-trim.json +14 -0
- package/src/asks/fixtures/invalid/confirm-labels-alike.json +13 -0
- package/src/asks/fixtures/invalid/confirm-missing-prompt-blank-label.json +16 -0
- package/src/asks/fixtures/invalid/confirm-unknown-keys.json +23 -0
- package/src/asks/fixtures/invalid/confirm-wrong-types.json +18 -0
- package/src/asks/fixtures/invalid/files-accept-duplicate.json +13 -0
- package/src/asks/fixtures/invalid/files-accept-empty.json +13 -0
- package/src/asks/fixtures/invalid/files-accept-unknown.json +13 -0
- package/src/asks/fixtures/invalid/files-counts-out-of-range.json +19 -0
- package/src/asks/fixtures/invalid/files-min-above-max.json +15 -0
- package/src/asks/fixtures/invalid/files-missing-accept-unknown-key.json +17 -0
- package/src/asks/fixtures/invalid/form-default-bad-format.json +40 -0
- package/src/asks/fixtures/invalid/form-default-invalid-date.json +40 -0
- package/src/asks/fixtures/invalid/form-default-not-in-options.json +58 -0
- package/src/asks/fixtures/invalid/form-default-out-of-range.json +33 -0
- package/src/asks/fixtures/invalid/form-default-text-bounds.json +42 -0
- package/src/asks/fixtures/invalid/form-default-wrong-type.json +30 -0
- package/src/asks/fixtures/invalid/form-duplicate-field-ids.json +24 -0
- package/src/asks/fixtures/invalid/form-field-id-format.json +19 -0
- package/src/asks/fixtures/invalid/form-field-text-limits.json +33 -0
- package/src/asks/fixtures/invalid/form-keys-of-another-type.json +39 -0
- package/src/asks/fixtures/invalid/form-length-above-cap.json +30 -0
- package/src/asks/fixtures/invalid/form-min-above-max.json +32 -0
- package/src/asks/fixtures/invalid/form-no-fields.json +13 -0
- package/src/asks/fixtures/invalid/form-select-options.json +44 -0
- package/src/asks/fixtures/invalid/form-too-many-fields.json +59 -0
- package/src/asks/fixtures/invalid/form-unknown-field-type.json +27 -0
- package/src/asks/fixtures/invalid/markdown-bounds-out-of-range.json +18 -0
- package/src/asks/fixtures/invalid/markdown-initial-above-cap.json +13 -0
- package/src/asks/fixtures/invalid/markdown-max-above-cap.json +13 -0
- package/src/asks/fixtures/invalid/markdown-min-above-cap.json +13 -0
- package/src/asks/fixtures/invalid/markdown-min-above-max.json +14 -0
- package/src/asks/fixtures/invalid/markdown-placeholder-too-long.json +13 -0
- package/src/asks/fixtures/invalid/markdown-wrong-types-unknown-key.json +27 -0
- package/src/asks/fixtures/valid/choice-emoji-label-cut.json +32 -0
- package/src/asks/fixtures/valid/choice-labels-collide-after-cut.json +21 -0
- package/src/asks/fixtures/valid/choice-long-text-cut.json +41 -0
- package/src/asks/fixtures/valid/choice-many-default-below-min.json +24 -0
- package/src/asks/fixtures/valid/choice-many-every-choice-with-other.json +24 -0
- package/src/asks/fixtures/valid/choice-many-no-default.json +21 -0
- package/src/asks/fixtures/valid/choice-many-optional-no-buttons.json +20 -0
- package/src/asks/fixtures/valid/choice-many-options-default-label-collides.json +23 -0
- package/src/asks/fixtures/valid/choice-many-options-no-buttons.json +21 -0
- package/src/asks/fixtures/valid/choice-many-options-no-default.json +22 -0
- package/src/asks/fixtures/valid/choice-many-options-with-default.json +30 -0
- package/src/asks/fixtures/valid/choice-many-required-with-default.json +29 -0
- package/src/asks/fixtures/valid/choice-many-single-pick-or-other.json +29 -0
- package/src/asks/fixtures/valid/choice-many-toppings-with-default.json +36 -0
- package/src/asks/fixtures/valid/choice-max-limits.json +245 -0
- package/src/asks/fixtures/valid/choice-plan-with-default.json +42 -0
- package/src/asks/fixtures/valid/choice-two-options-no-decline.json +32 -0
- package/src/asks/fixtures/valid/choice-two-options.json +32 -0
- package/src/asks/fixtures/valid/confirm-allow-decline.json +40 -0
- package/src/asks/fixtures/valid/confirm-default-labels.json +36 -0
- package/src/asks/fixtures/valid/confirm-destructive-delete.json +41 -0
- package/src/asks/fixtures/valid/confirm-emoji-labels.json +38 -0
- package/src/asks/fixtures/valid/confirm-long-text-cut.json +40 -0
- package/src/asks/fixtures/valid/files-every-type.json +28 -0
- package/src/asks/fixtures/valid/files-one-csv-no-decline.json +16 -0
- package/src/asks/fixtures/valid/files-receipt-photos.json +24 -0
- package/src/asks/fixtures/valid/form-boolean-newsletter.json +30 -0
- package/src/asks/fixtures/valid/form-date-due.json +44 -0
- package/src/asks/fixtures/valid/form-datetime-meeting.json +44 -0
- package/src/asks/fixtures/valid/form-datetime-minutes-offset.json +44 -0
- package/src/asks/fixtures/valid/form-eight-fields.json +71 -0
- package/src/asks/fixtures/valid/form-email-contact.json +44 -0
- package/src/asks/fixtures/valid/form-invoice-details.json +68 -0
- package/src/asks/fixtures/valid/form-multiselect-channels.json +64 -0
- package/src/asks/fixtures/valid/form-number-budget.json +45 -0
- package/src/asks/fixtures/valid/form-phone-callback.json +44 -0
- package/src/asks/fixtures/valid/form-select-shipping.json +58 -0
- package/src/asks/fixtures/valid/form-text-required-default.json +47 -0
- package/src/asks/fixtures/valid/form-textarea-notes.json +23 -0
- package/src/asks/fixtures/valid/form-time-reminder.json +44 -0
- package/src/asks/fixtures/valid/form-url-website.json +30 -0
- package/src/asks/fixtures/valid/markdown-announcement-draft.json +39 -0
- package/src/asks/fixtures/valid/markdown-empty-draft.json +34 -0
- package/src/asks/fixtures/valid/markdown-initial-blank-below-min.json +24 -0
- package/src/asks/fixtures/valid/markdown-initial-over-max.json +24 -0
- package/src/asks/fixtures/valid/markdown-long-text-cut.json +37 -0
- package/src/asks/fixtures/valid/markdown-no-decline.json +28 -0
- package/src/asks/fixtures/valid/markdown-no-initial-with-min.json +25 -0
- package/src/asks/formValues.ts +320 -0
- package/src/asks/headless.test.ts +146 -0
- package/src/asks/headless.ts +130 -0
- package/src/asks/jsonSchema.test.ts +171 -0
- package/src/asks/jsonSchema.ts +89 -0
- package/src/asks/limits.ts +58 -0
- package/src/asks/prompt.test.ts +200 -0
- package/src/asks/prompt.ts +173 -0
- package/src/asks/schema.test.ts +198 -0
- package/src/asks/schema.ts +1203 -0
- package/src/asks/simpleCard.test.ts +998 -0
- package/src/asks/simpleCard.ts +341 -0
- package/src/asks/validateInput.test.ts +875 -0
- package/src/asks/validateInput.ts +24 -0
- package/src/asks/validateResponse.test.ts +740 -0
- package/src/asks/validateResponse.ts +341 -0
- package/src/blocksDocParity.test.ts +21 -0
- package/src/cli.test.ts +134 -0
- package/src/cli.ts +65 -0
- package/src/display.test.ts +68 -0
- package/src/errors.test.ts +29 -0
- package/src/errors.ts +125 -0
- package/src/fixtures/invalid/column-not-found.yaml +14 -0
- package/src/fixtures/invalid/column-type.yaml +14 -0
- package/src/fixtures/invalid/dataset-not-found.yaml +7 -0
- package/src/fixtures/invalid/dataset-too-large.yaml +20 -0
- package/src/fixtures/invalid/depth-exceeded.yaml +10 -0
- package/src/fixtures/invalid/duplicate-id.yaml +6 -0
- package/src/fixtures/invalid/invalid-enum.yaml +5 -0
- package/src/fixtures/invalid/key-order.yaml +4 -0
- package/src/fixtures/invalid/missing-required.yaml +3 -0
- package/src/fixtures/invalid/not-a-document.txt +1 -0
- package/src/fixtures/invalid/row-arity.yaml +11 -0
- package/src/fixtures/invalid/select-target.yaml +12 -0
- package/src/fixtures/invalid/table-too-wide.yaml +9 -0
- package/src/fixtures/invalid/too-many-points.yaml +8 -0
- package/src/fixtures/invalid/unknown-key.yaml +5 -0
- package/src/fixtures/invalid/unsupported-version.yaml +4 -0
- package/src/fixtures/invalid/yaml-anchor.yaml +6 -0
- package/src/fixtures/partial/cut-after-divider.yaml +5 -0
- package/src/fixtures/valid/actions.yaml +32 -0
- package/src/fixtures/valid/chart.yaml +20 -0
- package/src/fixtures/valid/layout.yaml +26 -0
- package/src/fixtures/valid/prose.yaml +4 -0
- package/src/html.test.ts +52 -0
- package/src/index.test.ts +114 -0
- package/src/index.ts +175 -0
- package/src/jsonSchema.test.ts +33 -0
- package/src/jsonSchema.ts +11 -0
- package/src/limits.ts +27 -0
- package/src/lint.test.ts +289 -0
- package/src/lint.ts +484 -0
- package/src/parse.test.ts +281 -0
- package/src/parse.ts +104 -0
- package/src/parsePartial.test.ts +53 -0
- package/src/parsePartial.ts +116 -0
- package/src/prompt.test.ts +24 -0
- package/src/prompt.ts +104 -0
- package/src/schema.ts +509 -0
- package/src/tests/askFixtures.ts +40 -0
- package/src/validate.perf.test.ts +43 -0
- package/src/validate.ts +369 -0
- package/tsconfig.json +18 -0
|
@@ -0,0 +1,449 @@
|
|
|
1
|
+
import type {z} from "zod";
|
|
2
|
+
import {ASK_LIMITS} from "./limits";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Every ask error code, with its meaning. `validateAskInput` and `validateAskResponse` return all
|
|
6
|
+
* of them except `UNKNOWN_BUTTON`, which the headless `turn` endpoint returns, and
|
|
7
|
+
* `FILE_NOT_OWNED`, which the server returns when it loads an uploaded file.
|
|
8
|
+
*/
|
|
9
|
+
export const ASK_ERROR_CODES = {
|
|
10
|
+
CHANGED_MISMATCH:
|
|
11
|
+
"A markdown answer's changed flag does not match whether its text differs from the draft.",
|
|
12
|
+
DECLINE_NOT_ALLOWED: "The answer skips an ask that does not allow skipping.",
|
|
13
|
+
DEFAULT_NOT_IN_OPTIONS: "A default names an option id that the ask does not offer.",
|
|
14
|
+
DUPLICATE_ID:
|
|
15
|
+
"An id appears twice where ids must be unique: options, default, accept, or an answer.",
|
|
16
|
+
DUPLICATE_LABEL:
|
|
17
|
+
"Two buttons would share a label: options of a compact ask, or a confirm's approve and deny.",
|
|
18
|
+
FIELD_TYPE_MISMATCH:
|
|
19
|
+
"A form value or default does not fit its field's type: the wrong JSON type, not a whole number, or not a valid email, URL, or phone number.",
|
|
20
|
+
FILE_COUNT: "A files answer has fewer files than minFiles or more than maxFiles.",
|
|
21
|
+
FILE_NOT_OWNED:
|
|
22
|
+
"A files answer names a fileId that is not an upload of the caller, or the host has no file storage.",
|
|
23
|
+
FILE_TOO_LARGE: "A file is larger than the host's per-file upload cap.",
|
|
24
|
+
FILE_TYPE_NOT_ACCEPTED: "A file's declared type is not one the ask's accept list allows.",
|
|
25
|
+
INVALID_DATE:
|
|
26
|
+
"A form date, time, or datetime value or default is not a real ISO 8601 value in the field's format.",
|
|
27
|
+
INVALID_ENUM: "A value is not one of the allowed values.",
|
|
28
|
+
INVALID_FORMAT: "A string does not match its required format.",
|
|
29
|
+
INVALID_TYPE: "A value has the wrong type.",
|
|
30
|
+
MIME_MISMATCH:
|
|
31
|
+
"A file's bytes, or its data URL's media type, do not match the type the answer declares.",
|
|
32
|
+
MISSING_REQUIRED: "A required field is missing.",
|
|
33
|
+
OPTION_NOT_OFFERED: "The answer selects an option id that the ask did not offer.",
|
|
34
|
+
OTHER_NOT_ALLOWED: "An ask or an answer uses Other where the ask does not allow it.",
|
|
35
|
+
OUT_OF_RANGE: "A form number value or default is below the field's min or above its max.",
|
|
36
|
+
RANGE_INVALID:
|
|
37
|
+
"A count or length bound is out of range: below its minimum, above what the ask offers, or a minimum above its maximum.",
|
|
38
|
+
REQUIRED_FIELD: "A form answer leaves a required field missing or blank.",
|
|
39
|
+
SELECTION_COUNT: "A default or an answer selects the wrong number of options.",
|
|
40
|
+
TOO_FEW: "A list has fewer items than allowed.",
|
|
41
|
+
TOO_LONG: "A string is longer than allowed.",
|
|
42
|
+
TOO_MANY: "A list has more items than allowed.",
|
|
43
|
+
TOO_SHORT: "A string is empty, only whitespace, or shorter than its minimum.",
|
|
44
|
+
UNKNOWN_BUTTON: "The pressed button is not on the pending ask's simple card.",
|
|
45
|
+
UNKNOWN_KEY: "An object has a field that its schema does not define.",
|
|
46
|
+
} as const;
|
|
47
|
+
|
|
48
|
+
export type AskErrorCode = keyof typeof ASK_ERROR_CODES;
|
|
49
|
+
|
|
50
|
+
export interface AskValidationError {
|
|
51
|
+
/** A key of `ASK_ERROR_CODES`. */
|
|
52
|
+
code: AskErrorCode;
|
|
53
|
+
/** One instruction that fixes the error. */
|
|
54
|
+
fix: string;
|
|
55
|
+
/** What is wrong, in one sentence. */
|
|
56
|
+
message: string;
|
|
57
|
+
/** Where the error is, such as `options[2].id`. The root is an empty string. */
|
|
58
|
+
path: string;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/** An error before sorting; `segments` keeps array indexes numeric so they sort correctly. */
|
|
62
|
+
export interface AskErrorDraft {
|
|
63
|
+
code: AskErrorCode;
|
|
64
|
+
fix: string;
|
|
65
|
+
message: string;
|
|
66
|
+
segments: readonly PropertyKey[];
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
const MAX_QUOTED_LENGTH = 40;
|
|
70
|
+
|
|
71
|
+
const isAskErrorCode = (value: unknown): value is AskErrorCode =>
|
|
72
|
+
typeof value === "string" && Object.hasOwn(ASK_ERROR_CODES, value);
|
|
73
|
+
|
|
74
|
+
export const formatAskPath = (segments: readonly PropertyKey[]): string =>
|
|
75
|
+
segments.reduce<string>((path, segment) => {
|
|
76
|
+
if (typeof segment === "number") {
|
|
77
|
+
return `${path}[${segment}]`;
|
|
78
|
+
}
|
|
79
|
+
const key = String(segment);
|
|
80
|
+
return path === "" ? key : `${path}.${key}`;
|
|
81
|
+
}, "");
|
|
82
|
+
|
|
83
|
+
/** A Zod custom issue that carries an ask error code and its fix. */
|
|
84
|
+
export const askIssue = ({
|
|
85
|
+
code,
|
|
86
|
+
fix,
|
|
87
|
+
message,
|
|
88
|
+
segments,
|
|
89
|
+
}: AskErrorDraft): {
|
|
90
|
+
code: "custom";
|
|
91
|
+
message: string;
|
|
92
|
+
params: {askCode: AskErrorCode; fix: string};
|
|
93
|
+
path: PropertyKey[];
|
|
94
|
+
} => ({code: "custom", message, params: {askCode: code, fix}, path: [...segments]});
|
|
95
|
+
|
|
96
|
+
export const quoteValue = (value: unknown): string => {
|
|
97
|
+
if (typeof value !== "string") {
|
|
98
|
+
return String(value);
|
|
99
|
+
}
|
|
100
|
+
const shortened =
|
|
101
|
+
value.length > MAX_QUOTED_LENGTH ? `${value.slice(0, MAX_QUOTED_LENGTH - 1)}…` : value;
|
|
102
|
+
return JSON.stringify(shortened);
|
|
103
|
+
};
|
|
104
|
+
|
|
105
|
+
const sentenceSubject = (path: string): string => (path === "" ? "The value" : path);
|
|
106
|
+
|
|
107
|
+
const inlineSubject = (path: string): string => (path === "" ? "the value" : path);
|
|
108
|
+
|
|
109
|
+
const describeExpected = (expected: string): string => {
|
|
110
|
+
switch (expected) {
|
|
111
|
+
case "array":
|
|
112
|
+
case "tuple":
|
|
113
|
+
return "a list";
|
|
114
|
+
case "boolean":
|
|
115
|
+
return "true or false";
|
|
116
|
+
case "int":
|
|
117
|
+
return "a whole number";
|
|
118
|
+
case "number":
|
|
119
|
+
return "a number";
|
|
120
|
+
case "map":
|
|
121
|
+
case "object":
|
|
122
|
+
case "record":
|
|
123
|
+
return "an object";
|
|
124
|
+
case "string":
|
|
125
|
+
return "a string";
|
|
126
|
+
default:
|
|
127
|
+
return expected;
|
|
128
|
+
}
|
|
129
|
+
};
|
|
130
|
+
|
|
131
|
+
const describeReceived = (value: unknown): string => {
|
|
132
|
+
if (value === null) {
|
|
133
|
+
return "null";
|
|
134
|
+
}
|
|
135
|
+
if (Array.isArray(value)) {
|
|
136
|
+
return "a list";
|
|
137
|
+
}
|
|
138
|
+
switch (typeof value) {
|
|
139
|
+
case "boolean":
|
|
140
|
+
return "a boolean";
|
|
141
|
+
case "number":
|
|
142
|
+
return "a number";
|
|
143
|
+
case "object":
|
|
144
|
+
return "an object";
|
|
145
|
+
case "string":
|
|
146
|
+
return "a string";
|
|
147
|
+
default:
|
|
148
|
+
return typeof value;
|
|
149
|
+
}
|
|
150
|
+
};
|
|
151
|
+
|
|
152
|
+
const isPrimitive = (value: unknown): boolean =>
|
|
153
|
+
value === null || ["boolean", "number", "string"].includes(typeof value);
|
|
154
|
+
|
|
155
|
+
const lookup = (
|
|
156
|
+
root: unknown,
|
|
157
|
+
segments: readonly PropertyKey[]
|
|
158
|
+
): {found: boolean; value: unknown} => {
|
|
159
|
+
let current: unknown = root;
|
|
160
|
+
for (const segment of segments) {
|
|
161
|
+
if (current === null || typeof current !== "object" || !(segment in current)) {
|
|
162
|
+
return {found: false, value: undefined};
|
|
163
|
+
}
|
|
164
|
+
current = (current as Record<PropertyKey, unknown>)[segment];
|
|
165
|
+
}
|
|
166
|
+
return {found: current !== undefined, value: current};
|
|
167
|
+
};
|
|
168
|
+
|
|
169
|
+
const missingRequired = (segments: readonly PropertyKey[]): AskErrorDraft => {
|
|
170
|
+
const path = formatAskPath(segments);
|
|
171
|
+
const lastSegment = segments.at(-1);
|
|
172
|
+
if (typeof lastSegment !== "string") {
|
|
173
|
+
return {
|
|
174
|
+
code: "MISSING_REQUIRED",
|
|
175
|
+
fix: `Send ${inlineSubject(path)}.`,
|
|
176
|
+
message: `${sentenceSubject(path)} is required.`,
|
|
177
|
+
segments,
|
|
178
|
+
};
|
|
179
|
+
}
|
|
180
|
+
const parentPath = formatAskPath(segments.slice(0, -1));
|
|
181
|
+
return {
|
|
182
|
+
code: "MISSING_REQUIRED",
|
|
183
|
+
fix: parentPath === "" ? `Add "${lastSegment}".` : `Add "${lastSegment}" to ${parentPath}.`,
|
|
184
|
+
message: `${path} is required.`,
|
|
185
|
+
segments,
|
|
186
|
+
};
|
|
187
|
+
};
|
|
188
|
+
|
|
189
|
+
const invalidEnum = (
|
|
190
|
+
segments: readonly PropertyKey[],
|
|
191
|
+
allowed: readonly unknown[],
|
|
192
|
+
received: unknown
|
|
193
|
+
): AskErrorDraft => {
|
|
194
|
+
const path = formatAskPath(segments);
|
|
195
|
+
const allowedText =
|
|
196
|
+
allowed.length === 1
|
|
197
|
+
? quoteValue(allowed[0])
|
|
198
|
+
: `one of ${allowed.map((value) => quoteValue(value)).join(", ")}`;
|
|
199
|
+
const receivedText = isPrimitive(received) ? `, not ${quoteValue(received)}` : "";
|
|
200
|
+
return {
|
|
201
|
+
code: "INVALID_ENUM",
|
|
202
|
+
fix: `Use ${allowedText}.`,
|
|
203
|
+
message: `${sentenceSubject(path)} must be ${allowedText}${receivedText}.`,
|
|
204
|
+
segments,
|
|
205
|
+
};
|
|
206
|
+
};
|
|
207
|
+
|
|
208
|
+
const tooShort = (segments: readonly PropertyKey[]): AskErrorDraft => {
|
|
209
|
+
const path = formatAskPath(segments);
|
|
210
|
+
return {
|
|
211
|
+
code: "TOO_SHORT",
|
|
212
|
+
fix: `Write at least one visible character in ${inlineSubject(path)}.`,
|
|
213
|
+
message: `${sentenceSubject(path)} must contain visible text.`,
|
|
214
|
+
segments,
|
|
215
|
+
};
|
|
216
|
+
};
|
|
217
|
+
|
|
218
|
+
const numberOutOfRange = (segments: readonly PropertyKey[], bound: string): AskErrorDraft => {
|
|
219
|
+
const path = formatAskPath(segments);
|
|
220
|
+
return {
|
|
221
|
+
code: "RANGE_INVALID",
|
|
222
|
+
fix: `Make ${inlineSubject(path)} ${bound}.`,
|
|
223
|
+
message: `${sentenceSubject(path)} must be ${bound}.`,
|
|
224
|
+
segments,
|
|
225
|
+
};
|
|
226
|
+
};
|
|
227
|
+
|
|
228
|
+
const unionOptions = (issue: z.core.$ZodIssueInvalidUnion): readonly unknown[] =>
|
|
229
|
+
"options" in issue && Array.isArray(issue.options) ? issue.options : [];
|
|
230
|
+
|
|
231
|
+
const describeIssue = (
|
|
232
|
+
issue: z.core.$ZodIssue,
|
|
233
|
+
root: unknown,
|
|
234
|
+
prefix: readonly PropertyKey[]
|
|
235
|
+
): AskErrorDraft[] => {
|
|
236
|
+
const segments = [...prefix, ...issue.path];
|
|
237
|
+
const path = formatAskPath(segments);
|
|
238
|
+
const target = lookup(root, issue.path);
|
|
239
|
+
switch (issue.code) {
|
|
240
|
+
case "unrecognized_keys":
|
|
241
|
+
return issue.keys.map((key) => ({
|
|
242
|
+
code: "UNKNOWN_KEY",
|
|
243
|
+
fix: `Remove "${key}".`,
|
|
244
|
+
message: `"${key}" is not a field of ${path === "" ? "this object" : path}.`,
|
|
245
|
+
segments: [...segments, key],
|
|
246
|
+
}));
|
|
247
|
+
case "invalid_type": {
|
|
248
|
+
if (!target.found) {
|
|
249
|
+
return [missingRequired(segments)];
|
|
250
|
+
}
|
|
251
|
+
const expected = describeExpected(issue.expected);
|
|
252
|
+
const received =
|
|
253
|
+
issue.expected === "int" && typeof target.value === "number"
|
|
254
|
+
? String(target.value)
|
|
255
|
+
: describeReceived(target.value);
|
|
256
|
+
return [
|
|
257
|
+
{
|
|
258
|
+
code: "INVALID_TYPE",
|
|
259
|
+
fix: `Make ${inlineSubject(path)} ${expected}.`,
|
|
260
|
+
message: `${sentenceSubject(path)} must be ${expected}, not ${received}.`,
|
|
261
|
+
segments,
|
|
262
|
+
},
|
|
263
|
+
];
|
|
264
|
+
}
|
|
265
|
+
case "invalid_value":
|
|
266
|
+
if (!target.found) {
|
|
267
|
+
return [missingRequired(segments)];
|
|
268
|
+
}
|
|
269
|
+
return [invalidEnum(segments, issue.values, target.value)];
|
|
270
|
+
case "invalid_union": {
|
|
271
|
+
if (!target.found) {
|
|
272
|
+
return [missingRequired(segments)];
|
|
273
|
+
}
|
|
274
|
+
const allowed = unionOptions(issue);
|
|
275
|
+
if (allowed.length === 0) {
|
|
276
|
+
return [
|
|
277
|
+
{
|
|
278
|
+
code: "INVALID_TYPE",
|
|
279
|
+
fix: `Check ${inlineSubject(path)}.`,
|
|
280
|
+
message: issue.message,
|
|
281
|
+
segments,
|
|
282
|
+
},
|
|
283
|
+
];
|
|
284
|
+
}
|
|
285
|
+
return [invalidEnum(segments, allowed, target.value)];
|
|
286
|
+
}
|
|
287
|
+
case "too_big": {
|
|
288
|
+
const maximum = String(issue.maximum);
|
|
289
|
+
if (issue.origin === "number") {
|
|
290
|
+
return [numberOutOfRange(segments, `at most ${maximum}`)];
|
|
291
|
+
}
|
|
292
|
+
if (issue.origin === "array" || issue.origin === "set") {
|
|
293
|
+
return [
|
|
294
|
+
{
|
|
295
|
+
code: "TOO_MANY",
|
|
296
|
+
fix: `Remove items from ${inlineSubject(path)} until it has ${maximum} or fewer.`,
|
|
297
|
+
message: `${sentenceSubject(path)} has more than ${maximum} items.`,
|
|
298
|
+
segments,
|
|
299
|
+
},
|
|
300
|
+
];
|
|
301
|
+
}
|
|
302
|
+
return [
|
|
303
|
+
{
|
|
304
|
+
code: "TOO_LONG",
|
|
305
|
+
fix: `Shorten ${inlineSubject(path)} to ${maximum} characters or fewer.`,
|
|
306
|
+
message: `${sentenceSubject(path)} is longer than ${maximum} characters.`,
|
|
307
|
+
segments,
|
|
308
|
+
},
|
|
309
|
+
];
|
|
310
|
+
}
|
|
311
|
+
case "too_small": {
|
|
312
|
+
if (issue.origin === "number") {
|
|
313
|
+
return [numberOutOfRange(segments, `at least ${String(issue.minimum)}`)];
|
|
314
|
+
}
|
|
315
|
+
if (issue.origin === "array" || issue.origin === "set") {
|
|
316
|
+
const minimum = String(issue.minimum);
|
|
317
|
+
return [
|
|
318
|
+
{
|
|
319
|
+
code: "TOO_FEW",
|
|
320
|
+
fix: `Add items to ${inlineSubject(path)} until it has at least ${minimum}.`,
|
|
321
|
+
message: `${sentenceSubject(path)} has fewer than ${minimum} items.`,
|
|
322
|
+
segments,
|
|
323
|
+
},
|
|
324
|
+
];
|
|
325
|
+
}
|
|
326
|
+
return [tooShort(segments)];
|
|
327
|
+
}
|
|
328
|
+
case "invalid_format":
|
|
329
|
+
if (issue.pattern === String(ASK_LIMITS.choice.optionIdPattern)) {
|
|
330
|
+
const idKind = issue.path.length === 3 && issue.path[0] === "fields" ? "field" : "option";
|
|
331
|
+
return [
|
|
332
|
+
{
|
|
333
|
+
code: "INVALID_FORMAT",
|
|
334
|
+
fix: `Use 1-${ASK_LIMITS.choice.optionIdMaxLength} lowercase letters, digits, "_", or "-", starting with a letter or digit.`,
|
|
335
|
+
message: `${sentenceSubject(path)} ${quoteValue(target.value)} is not a valid ${idKind} id.`,
|
|
336
|
+
segments,
|
|
337
|
+
},
|
|
338
|
+
];
|
|
339
|
+
}
|
|
340
|
+
return [
|
|
341
|
+
{
|
|
342
|
+
code: "INVALID_FORMAT",
|
|
343
|
+
fix: `Change ${inlineSubject(path)} to match the ${issue.format} format.`,
|
|
344
|
+
message: `${sentenceSubject(path)} does not match the ${issue.format} format.`,
|
|
345
|
+
segments,
|
|
346
|
+
},
|
|
347
|
+
];
|
|
348
|
+
case "custom": {
|
|
349
|
+
const askCode = issue.params?.askCode;
|
|
350
|
+
const fix = issue.params?.fix;
|
|
351
|
+
if (askCode === "TOO_SHORT") {
|
|
352
|
+
return [tooShort(segments)];
|
|
353
|
+
}
|
|
354
|
+
if (isAskErrorCode(askCode) && typeof fix === "string") {
|
|
355
|
+
return [{code: askCode, fix, message: issue.message, segments}];
|
|
356
|
+
}
|
|
357
|
+
return [
|
|
358
|
+
{
|
|
359
|
+
code: "INVALID_FORMAT",
|
|
360
|
+
fix: `Check ${inlineSubject(path)}.`,
|
|
361
|
+
message: issue.message,
|
|
362
|
+
segments,
|
|
363
|
+
},
|
|
364
|
+
];
|
|
365
|
+
}
|
|
366
|
+
default:
|
|
367
|
+
return [
|
|
368
|
+
{
|
|
369
|
+
code: "INVALID_TYPE",
|
|
370
|
+
fix: `Check ${inlineSubject(path)}.`,
|
|
371
|
+
message: issue.message,
|
|
372
|
+
segments,
|
|
373
|
+
},
|
|
374
|
+
];
|
|
375
|
+
}
|
|
376
|
+
};
|
|
377
|
+
|
|
378
|
+
/**
|
|
379
|
+
* Maps Zod issues to ask errors. `root` is the value that was parsed; it tells a missing key apart
|
|
380
|
+
* from a key with the wrong type. `prefix` is prepended to every path.
|
|
381
|
+
*/
|
|
382
|
+
export const issuesToAskErrors = ({
|
|
383
|
+
issues,
|
|
384
|
+
prefix = [],
|
|
385
|
+
root,
|
|
386
|
+
}: {
|
|
387
|
+
issues: readonly z.core.$ZodIssue[];
|
|
388
|
+
prefix?: readonly PropertyKey[];
|
|
389
|
+
root: unknown;
|
|
390
|
+
}): AskErrorDraft[] => {
|
|
391
|
+
const typeIssuePaths = new Set(
|
|
392
|
+
issues
|
|
393
|
+
.filter((issue) => issue.code === "invalid_type")
|
|
394
|
+
.map((issue) => formatAskPath(issue.path))
|
|
395
|
+
);
|
|
396
|
+
return issues.flatMap((issue) => {
|
|
397
|
+
if (issue.code !== "invalid_type" && typeIssuePaths.has(formatAskPath(issue.path))) {
|
|
398
|
+
return [];
|
|
399
|
+
}
|
|
400
|
+
return describeIssue(issue, root, prefix);
|
|
401
|
+
});
|
|
402
|
+
};
|
|
403
|
+
|
|
404
|
+
const compareStrings = (left: string, right: string): number => {
|
|
405
|
+
if (left === right) {
|
|
406
|
+
return 0;
|
|
407
|
+
}
|
|
408
|
+
return left < right ? -1 : 1;
|
|
409
|
+
};
|
|
410
|
+
|
|
411
|
+
const compareSegments = (left: readonly PropertyKey[], right: readonly PropertyKey[]): number => {
|
|
412
|
+
const length = Math.min(left.length, right.length);
|
|
413
|
+
for (let index = 0; index < length; index += 1) {
|
|
414
|
+
const a = left[index];
|
|
415
|
+
const b = right[index];
|
|
416
|
+
if (a === b) {
|
|
417
|
+
continue;
|
|
418
|
+
}
|
|
419
|
+
if (typeof a === "number" && typeof b === "number") {
|
|
420
|
+
return a - b;
|
|
421
|
+
}
|
|
422
|
+
if (typeof a === "number") {
|
|
423
|
+
return -1;
|
|
424
|
+
}
|
|
425
|
+
if (typeof b === "number") {
|
|
426
|
+
return 1;
|
|
427
|
+
}
|
|
428
|
+
return compareStrings(String(a), String(b));
|
|
429
|
+
}
|
|
430
|
+
return left.length - right.length;
|
|
431
|
+
};
|
|
432
|
+
|
|
433
|
+
/** Sorts drafts by path (array indexes in numeric order), then by code, and drops duplicates. */
|
|
434
|
+
export const finalizeAskErrors = (drafts: readonly AskErrorDraft[]): AskValidationError[] => {
|
|
435
|
+
const sorted = [...drafts].sort(
|
|
436
|
+
(left, right) =>
|
|
437
|
+
compareSegments(left.segments, right.segments) || compareStrings(left.code, right.code)
|
|
438
|
+
);
|
|
439
|
+
const seen = new Set<string>();
|
|
440
|
+
return sorted.flatMap((draft) => {
|
|
441
|
+
const path = formatAskPath(draft.segments);
|
|
442
|
+
const key = `${path}\u0000${draft.code}`;
|
|
443
|
+
if (seen.has(key)) {
|
|
444
|
+
return [];
|
|
445
|
+
}
|
|
446
|
+
seen.add(key);
|
|
447
|
+
return [{code: draft.code, fix: draft.fix, message: draft.message, path}];
|
|
448
|
+
});
|
|
449
|
+
};
|