toolcraft-schema 0.0.17 → 0.0.19

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 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 getRequiredKeyFingerprint(schema) {
5
- const requiredKeys = Object.keys(schema.shape)
4
+ function getRequiredKeys(schema) {
5
+ return Object.keys(schema.shape)
6
6
  .filter((key) => !isOptionalSchema(schema.shape[key]))
7
7
  .sort();
8
- return JSON.stringify(requiredKeys);
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
- const fingerprints = new Set();
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
- const discriminatorPath = [...path, schema.discriminator];
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
- addIssue(state, path, "exactly one union branch", matches.length === 0 ? "no matching branches" : `${matches.length} matching branches`, `Expected exactly one union branch at ${formatPath(path)}`);
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, message) {
290
- state.issues.push({ path, expected, received, message });
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
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "toolcraft-schema",
3
- "version": "0.0.17",
3
+ "version": "0.0.19",
4
4
  "type": "module",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",