@oh-my-pi/omptype 17.2.6 → 17.2.7

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/src/errors.ts CHANGED
@@ -8,33 +8,86 @@
8
8
  * All human-readable strings are built lazily on property access.
9
9
  */
10
10
 
11
+ /** Context supplied to configurable error formatters. */
12
+ export interface ErrorContext {
13
+ readonly code: string;
14
+ readonly path: readonly PropertyKey[];
15
+ readonly data: unknown;
16
+ readonly expected: string;
17
+ readonly actual: string;
18
+ readonly problem: string;
19
+ }
20
+
21
+ /** Per-schema overrides for validation error text. */
22
+ export interface ErrorConfig {
23
+ readonly expected?: string | ((context: ErrorContext) => string);
24
+ readonly actual?: string | ((context: ErrorContext) => string);
25
+ readonly problem?: string | ((context: ErrorContext) => string);
26
+ readonly message?: string | ((context: ErrorContext) => string);
27
+ }
28
+
29
+ function format(
30
+ override: string | ((context: ErrorContext) => string) | undefined,
31
+ context: ErrorContext,
32
+ fallback: string,
33
+ ): string {
34
+ return typeof override === "function" ? override(context) : (override ?? fallback);
35
+ }
36
+
11
37
  /** A single validation failure at one path. */
12
38
  export class OmpError {
39
+ #rawExpected: string;
40
+ #config: ErrorConfig | undefined;
41
+
13
42
  constructor(
14
43
  /** Property path from the root to the failing value (empty at root). */
15
44
  readonly path: PropertyKey[],
16
- /** Human-readable expectation, e.g. `"a string"` or `"at most 3600"`. */
17
- readonly expected: string,
45
+ expected: string,
18
46
  /** The value that failed validation. */
19
47
  readonly data: unknown,
20
- ) {}
48
+ config?: ErrorConfig,
49
+ ) {
50
+ this.#rawExpected = expected;
51
+ this.#config = config;
52
+ }
53
+
54
+ /** Stable category for programmatic error handling. */
55
+ get code(): string {
56
+ return errorCode(this.#rawExpected, this.data);
57
+ }
58
+
59
+ #context(expected: string, actual: string, problem = ""): ErrorContext {
60
+ return { code: this.code, path: this.path, data: this.data, expected, actual, problem };
61
+ }
62
+
63
+ /** Human-readable expectation, including a configured override. */
64
+ get expected(): string {
65
+ const actual = describeValue(this.data);
66
+ return format(this.#config?.expected, this.#context(this.#rawExpected, actual), this.#rawExpected);
67
+ }
21
68
 
22
69
  /** Short description of the received value, e.g. `"a number"` or `"missing"`. */
23
70
  get actual(): string {
24
- return describeValue(this.data);
71
+ const actual = describeValue(this.data);
72
+ return format(this.#config?.actual, this.#context(this.expected, actual), actual);
25
73
  }
26
74
 
27
75
  /** Path-less problem statement: `must be <expected> (was <actual>)`. */
28
76
  get problem(): string {
29
- return this.data === MISSING
30
- ? `must be ${this.expected} (was missing)`
31
- : `must be ${this.expected} (was ${this.actual})`;
77
+ const expected = this.expected;
78
+ const actual = this.actual;
79
+ const fallback =
80
+ this.data === MISSING ? `must be ${expected} (was missing)` : `must be ${expected} (was ${actual})`;
81
+ return format(this.#config?.problem, this.#context(expected, actual, fallback), fallback);
32
82
  }
33
83
 
34
84
  /** Full message including the path prefix. */
35
85
  get message(): string {
86
+ const expected = this.expected;
87
+ const actual = this.actual;
88
+ const problem = this.problem;
36
89
  const at = this.path.length === 0 ? "" : `${this.path.map(String).join(".")} `;
37
- return `${at}${this.problem}`;
90
+ return format(this.#config?.message, this.#context(expected, actual, problem), `${at}${problem}`);
38
91
  }
39
92
 
40
93
  toString(): string {
@@ -68,48 +121,107 @@ function describeValue(data: unknown): string {
68
121
  }
69
122
  }
70
123
 
124
+ function errorCode(expected: string, data: unknown): string {
125
+ if (data === MISSING) return "required";
126
+ if (expected.includes("divisible by")) return "divisor";
127
+ if (expected.includes("at least") || expected.includes("more than")) return "min";
128
+ if (expected.includes("at most") || expected.includes("less than")) return "max";
129
+ if (expected.includes("matching") || expected.includes("format") || expected.includes("email")) return "pattern";
130
+ if (expected.includes("predicate") || expected.includes("satisfying")) return "predicate";
131
+ if (expected.startsWith('"') || expected.startsWith("the date ")) return "unit";
132
+ return "domain";
133
+ }
134
+
71
135
  /**
72
- * Aggregate of validation failures; the value returned by a schema call on
73
- * invalid input. Array-like so callers can `.map()` over entries.
136
+ * Single-failure validation result with a lazy array-like entry.
137
+ *
138
+ * Validators fast-fail, so allocating an `Array` subclass and a separate entry
139
+ * on every rejection only penalizes callers that inspect errors by identity.
140
+ * Indexing, iteration, and `map` materialize the entry on demand.
74
141
  */
75
- export class OmpErrors extends Array<OmpError> {
76
- // `.map()`/`.filter()` on an errors instance should produce plain arrays,
77
- // not attempt `new OmpErrors(n)`.
78
- static override get [Symbol.species](): typeof Array {
79
- return Array;
142
+ type StoredPath = PropertyKey[] | PropertyKey | undefined;
143
+
144
+ export class OmpErrors implements Iterable<OmpError> {
145
+ #path: StoredPath;
146
+ #expected: string;
147
+ #data: unknown;
148
+ #entry: OmpError | undefined;
149
+ #config: ErrorConfig | undefined;
150
+
151
+ /** Number of failures; omptype validators fast-fail on the first error. */
152
+ readonly length = 1;
153
+
154
+ constructor(path: StoredPath, expected: string, data: unknown, config?: ErrorConfig) {
155
+ this.#path = path;
156
+ this.#expected = expected;
157
+ this.#data = data;
158
+ this.#config = config;
80
159
  }
81
160
 
82
- /** Single-failure constructor used by generated validators. */
83
- static single(path: PropertyKey[], expected: string, data: unknown): OmpErrors {
84
- const out = new OmpErrors();
85
- out.push(new OmpError(path, expected, data));
86
- return out;
161
+ /** First and only validation failure, materialized on demand. */
162
+ get 0(): OmpError {
163
+ return this.#getEntry();
87
164
  }
88
165
 
89
- /** Prefix every entry's path with `key` (used when nesting sub-schemas). */
166
+ static single(path: PropertyKey[], expected: string, data: unknown, config?: ErrorConfig): OmpErrors {
167
+ return new OmpErrors(path, expected, data, config);
168
+ }
169
+
170
+ #getEntry(): OmpError {
171
+ if (this.#entry) return this.#entry;
172
+ const path = this.#path === undefined ? [] : Array.isArray(this.#path) ? [...this.#path] : [this.#path];
173
+ const entry = new OmpError(path, this.#expected, this.#data, this.#config);
174
+ this.#entry = entry;
175
+ return entry;
176
+ }
177
+
178
+ /** Prefix the failure path with `key` when nesting sub-schemas. */
90
179
  prefix(key: PropertyKey): this {
91
- for (let i = 0; i < this.length; i++) {
92
- const e = this[i];
93
- this[i] = new OmpError([key, ...e.path], e.expected, e.data);
94
- }
180
+ const path = this.#path;
181
+ this.#path = path === undefined ? [key] : Array.isArray(path) ? [key, ...path] : [key, path];
182
+ this.#entry = undefined;
183
+ return this;
184
+ }
185
+
186
+ /** Apply schema-local message formatting without rebuilding the failure. */
187
+ configure(config: ErrorConfig): this {
188
+ this.#config = { ...this.#config, ...config };
189
+ this.#entry = undefined;
95
190
  return this;
96
191
  }
97
192
 
98
- /** Human-readable digest of every failure, one per line. Built lazily. */
193
+ /** Index the failure by its dotted property path (`""` for the root). */
194
+ get byPath(): Readonly<Record<string, OmpError>> {
195
+ const entry = this.#getEntry();
196
+ return { [entry.path.map(String).join(".")]: entry };
197
+ }
198
+
199
+ /** Transform the failure entry into a plain array. */
200
+ map<result>(fn: (error: OmpError, index: number, errors: OmpErrors) => result): result[] {
201
+ return [fn(this.#getEntry(), 0, this)];
202
+ }
203
+
204
+ /** Select the failure entry into a plain array. */
205
+ filter(fn: (error: OmpError, index: number, errors: OmpErrors) => unknown): OmpError[] {
206
+ const entry = this.#getEntry();
207
+ return fn(entry, 0, this) ? [entry] : [];
208
+ }
209
+
210
+ /** Iterate over the single failure entry. */
211
+ *[Symbol.iterator](): IterableIterator<OmpError> {
212
+ yield this.#getEntry();
213
+ }
214
+
215
+ /** Human-readable failure text, materialized only when requested. */
99
216
  get summary(): string {
100
- let out = "";
101
- for (let i = 0; i < this.length; i++) {
102
- if (i > 0) out += "\n";
103
- out += this[i].message;
104
- }
105
- return out;
217
+ return this.#getEntry().message;
106
218
  }
107
219
 
108
- override toString(): string {
220
+ toString(): string {
109
221
  return this.summary;
110
222
  }
111
223
 
112
- /** Throw a `TraversalError` carrying the summary. */
224
+ /** Throw a `TraversalError` carrying this result. */
113
225
  throw(): never {
114
226
  throw new TraversalError(this);
115
227
  }
package/src/index.ts CHANGED
@@ -1,10 +1,9 @@
1
1
  /**
2
2
  * omptype — ArkType-compatible schema validation with a lazy JIT runtime.
3
3
  *
4
- * Drop-in for the arktype surface this repo uses:
5
- * `type()`, `Type`, `type.errors` / `OmpErrors`, `type.enumerated()`,
6
- * `.or/.and/.array/.pipe/.narrow/.describe/.default/.allows/.assert/.toJsonSchema`,
7
- * plus `typeof schema.infer` static inference.
4
+ * ArkType-compatible `type()`/`Type`, keyword modules, recursive scopes,
5
+ * composition and morph APIs, structured errors, input/output inference, and
6
+ * JSON Schema emission.
8
7
  */
9
8
  export * from "./errors";
10
9
  export * from "./infer";