toolcraft-schema 0.0.16 → 0.0.18
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.d.ts +2 -0
- package/dist/union.d.ts +1 -0
- package/dist/union.js +20 -11
- package/dist/validate.js +41 -8
- package/package.json +1 -1
package/dist/index.d.ts
CHANGED
|
@@ -21,10 +21,12 @@ type StringMetadata = {
|
|
|
21
21
|
maxLength?: number;
|
|
22
22
|
minLength?: number;
|
|
23
23
|
pattern?: string;
|
|
24
|
+
secret?: boolean;
|
|
24
25
|
};
|
|
25
26
|
type NumberMetadata = {
|
|
26
27
|
maximum?: number;
|
|
27
28
|
minimum?: number;
|
|
29
|
+
secret?: boolean;
|
|
28
30
|
};
|
|
29
31
|
type ArrayMetadata = {
|
|
30
32
|
maxItems?: number;
|
package/dist/union.d.ts
CHANGED
|
@@ -3,5 +3,6 @@ type UnionStatic<TBranches extends readonly ObjectSchema<any>[]> = Static<TBranc
|
|
|
3
3
|
export interface UnionSchema<TBranches extends readonly ObjectSchema<any>[]> extends SchemaBase<"union", UnionStatic<TBranches>> {
|
|
4
4
|
readonly branches: TBranches;
|
|
5
5
|
}
|
|
6
|
+
export declare function getRequiredKeyFingerprint(schema: ObjectSchema<any>): string;
|
|
6
7
|
export declare function Union<const TBranches extends readonly ObjectSchema<any>[]>(branches: TBranches): UnionSchema<TBranches>;
|
|
7
8
|
export {};
|
package/dist/union.js
CHANGED
|
@@ -1,24 +1,33 @@
|
|
|
1
1
|
function isOptionalSchema(schema) {
|
|
2
2
|
return schema.kind === "optional";
|
|
3
3
|
}
|
|
4
|
-
function
|
|
5
|
-
|
|
4
|
+
function getRequiredKeys(schema) {
|
|
5
|
+
return Object.keys(schema.shape)
|
|
6
6
|
.filter((key) => !isOptionalSchema(schema.shape[key]))
|
|
7
7
|
.sort();
|
|
8
|
-
|
|
8
|
+
}
|
|
9
|
+
export function getRequiredKeyFingerprint(schema) {
|
|
10
|
+
return getRequiredKeys(schema).join("+");
|
|
11
|
+
}
|
|
12
|
+
function assertUniqueRequiredKeyFingerprints(branches) {
|
|
13
|
+
const fingerprints = new Map();
|
|
14
|
+
branches.forEach((branch, index) => {
|
|
15
|
+
const fingerprint = getRequiredKeyFingerprint(branch);
|
|
16
|
+
const indices = fingerprints.get(fingerprint) ?? [];
|
|
17
|
+
indices.push(index);
|
|
18
|
+
fingerprints.set(fingerprint, indices);
|
|
19
|
+
});
|
|
20
|
+
for (const [fingerprint, indices] of fingerprints) {
|
|
21
|
+
if (indices.length > 1) {
|
|
22
|
+
throw new Error(`Union branches [${indices.join(", ")}] share required-key fingerprint "${fingerprint}". Each branch must require a distinct set of keys.`);
|
|
23
|
+
}
|
|
24
|
+
}
|
|
9
25
|
}
|
|
10
26
|
function assertValidBranches(branches) {
|
|
11
27
|
if (branches.length === 0) {
|
|
12
28
|
throw new Error("Union schema requires at least one branch");
|
|
13
29
|
}
|
|
14
|
-
|
|
15
|
-
for (const branch of branches) {
|
|
16
|
-
const fingerprint = getRequiredKeyFingerprint(branch);
|
|
17
|
-
if (fingerprints.has(fingerprint)) {
|
|
18
|
-
throw new Error("Union schema branches must have unique required-key fingerprints");
|
|
19
|
-
}
|
|
20
|
-
fingerprints.add(fingerprint);
|
|
21
|
-
}
|
|
30
|
+
assertUniqueRequiredKeyFingerprints(branches);
|
|
22
31
|
}
|
|
23
32
|
export function Union(branches) {
|
|
24
33
|
assertValidBranches(branches);
|
package/dist/validate.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { getRequiredKeyFingerprint } from "./union.js";
|
|
1
2
|
const missingValue = Symbol("missingValue");
|
|
2
3
|
export function validate(schema, value) {
|
|
3
4
|
const state = { issues: [] };
|
|
@@ -164,11 +165,16 @@ function walkOneOf(schema, value, path, state) {
|
|
|
164
165
|
return { present: true, value };
|
|
165
166
|
}
|
|
166
167
|
const discriminatorValue = value[schema.discriminator];
|
|
168
|
+
const discriminatorPath = [...path, schema.discriminator];
|
|
169
|
+
const branchValues = Object.keys(schema.branches);
|
|
170
|
+
const expected = `one of ${branchValues.join(", ")}`;
|
|
171
|
+
if (!Object.hasOwn(value, schema.discriminator)) {
|
|
172
|
+
addIssueWithMessage(state, discriminatorPath, expected, "missing", `Missing discriminator "${schema.discriminator}" at ${formatPath(path)}. Expected one of: ${branchValues.join(", ")}.`);
|
|
173
|
+
return { present: true, value };
|
|
174
|
+
}
|
|
167
175
|
if (typeof discriminatorValue !== "string" ||
|
|
168
176
|
!Object.hasOwn(schema.branches, discriminatorValue)) {
|
|
169
|
-
|
|
170
|
-
const expected = `one of ${Object.keys(schema.branches).join(", ")}`;
|
|
171
|
-
addIssue(state, discriminatorPath, expected, receivedValue(discriminatorValue), `Expected ${expected} at ${formatPath(discriminatorPath)}`);
|
|
177
|
+
addIssueWithMessage(state, discriminatorPath, expected, receivedValue(discriminatorValue), `Expected ${expected} at ${formatPath(discriminatorPath)}, got ${formatReceivedDiscriminator(discriminatorValue)}`);
|
|
172
178
|
return { present: true, value };
|
|
173
179
|
}
|
|
174
180
|
return walkObject(schema.branches[discriminatorValue], value, path, state, {
|
|
@@ -187,13 +193,18 @@ function walkUnion(schema, value, path, state) {
|
|
|
187
193
|
const branchState = { issues: [] };
|
|
188
194
|
const result = walkObject(branch, value, path, branchState);
|
|
189
195
|
if (branchState.issues.length === 0 && result.present) {
|
|
190
|
-
matches.push(result.value);
|
|
196
|
+
matches.push({ fingerprint: getRequiredKeyFingerprint(branch), value: result.value });
|
|
191
197
|
}
|
|
192
198
|
}
|
|
193
199
|
if (matches.length === 1) {
|
|
194
|
-
return { present: true, value: matches[0] };
|
|
200
|
+
return { present: true, value: matches[0].value };
|
|
201
|
+
}
|
|
202
|
+
if (matches.length === 0) {
|
|
203
|
+
const branchDescriptions = schema.branches.map((branch) => getRequiredKeyFingerprint(branch));
|
|
204
|
+
addIssueWithMessage(state, path, "exactly one union branch", "0 matching branches", `No union branch matched at ${formatPath(path)}. Tried ${schema.branches.length} branches. Expected one of: ${branchDescriptions.join(" | ")}.`);
|
|
205
|
+
return { present: true, value };
|
|
195
206
|
}
|
|
196
|
-
|
|
207
|
+
addIssueWithMessage(state, path, "exactly one union branch", `${matches.length} matching branches`, `Expected exactly one union branch at ${formatPath(path)}, but matched more than one branch: ${matches.map((match) => match.fingerprint).join(" | ")}`);
|
|
197
208
|
return { present: true, value };
|
|
198
209
|
}
|
|
199
210
|
function hasRequiredKeys(schema, value) {
|
|
@@ -286,8 +297,24 @@ function addExpectedIssue(state, path, expected, value) {
|
|
|
286
297
|
function addUnexpectedPropertyIssue(state, path) {
|
|
287
298
|
addIssue(state, path, "no additional properties", "unknown property", `Unexpected property ${formatPath(path)}`);
|
|
288
299
|
}
|
|
289
|
-
function addIssue(state, path, expected, received,
|
|
290
|
-
state.issues.push({
|
|
300
|
+
function addIssue(state, path, expected, received, _message) {
|
|
301
|
+
state.issues.push({
|
|
302
|
+
path,
|
|
303
|
+
expected,
|
|
304
|
+
received,
|
|
305
|
+
message: formatIssueMessage(expected, path, received)
|
|
306
|
+
});
|
|
307
|
+
}
|
|
308
|
+
function addIssueWithMessage(state, path, expected, received, message) {
|
|
309
|
+
state.issues.push({
|
|
310
|
+
path,
|
|
311
|
+
expected,
|
|
312
|
+
received,
|
|
313
|
+
message
|
|
314
|
+
});
|
|
315
|
+
}
|
|
316
|
+
function formatIssueMessage(expected, path, received) {
|
|
317
|
+
return `Expected ${expected} at ${formatPath(path)}, got ${received}`;
|
|
291
318
|
}
|
|
292
319
|
function formatPath(path) {
|
|
293
320
|
return path.length === 0 ? "value" : path.join(".");
|
|
@@ -321,3 +348,9 @@ function receivedValue(value) {
|
|
|
321
348
|
}
|
|
322
349
|
return String(value);
|
|
323
350
|
}
|
|
351
|
+
function formatReceivedDiscriminator(value) {
|
|
352
|
+
if (typeof value === "string") {
|
|
353
|
+
return JSON.stringify(value);
|
|
354
|
+
}
|
|
355
|
+
return receivedValue(value);
|
|
356
|
+
}
|