toolcraft-schema 0.0.22 → 0.0.24

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.js CHANGED
@@ -80,6 +80,30 @@ function assertValidEnumValues(values) {
80
80
  if (uniqueValues.size !== values.length) {
81
81
  throw new Error("Enum schema values must be unique");
82
82
  }
83
+ if (values.some((value) => typeof value === "number" && !Number.isFinite(value))) {
84
+ throw new Error("Enum schema numeric values must be finite");
85
+ }
86
+ }
87
+ function assertNonNegativeInteger(value, name) {
88
+ if (value !== undefined && (!Number.isInteger(value) || value < 0)) {
89
+ throw new Error(`${name} must be a non-negative integer`);
90
+ }
91
+ }
92
+ function assertFiniteNumber(value, name) {
93
+ if (value !== undefined && !Number.isFinite(value)) {
94
+ throw new Error(`${name} must be finite`);
95
+ }
96
+ }
97
+ function assertPattern(pattern) {
98
+ if (pattern === undefined) {
99
+ return;
100
+ }
101
+ try {
102
+ new RegExp(pattern);
103
+ }
104
+ catch {
105
+ throw new Error("pattern must be a valid regular expression");
106
+ }
83
107
  }
84
108
  function unwrapOptional(schema) {
85
109
  if (isOptionalSchema(schema)) {
@@ -106,12 +130,21 @@ function withInjectedDiscriminator(schema, discriminator, branchName) {
106
130
  }
107
131
  export const S = {
108
132
  String(options = {}) {
133
+ assertNonNegativeInteger(options.minLength, "minLength");
134
+ assertNonNegativeInteger(options.maxLength, "maxLength");
135
+ assertPattern(options.pattern);
109
136
  return {
110
137
  kind: "string",
111
138
  ...options,
112
139
  };
113
140
  },
114
141
  Number(options = {}) {
142
+ assertFiniteNumber(options.minimum, "minimum");
143
+ assertFiniteNumber(options.maximum, "maximum");
144
+ assertFiniteNumber(options.default, "default");
145
+ if (options.jsonType === "integer" && options.default !== undefined && !Number.isInteger(options.default)) {
146
+ throw new Error("default must be an integer");
147
+ }
115
148
  return {
116
149
  kind: "number",
117
150
  ...options,
@@ -125,6 +158,9 @@ export const S = {
125
158
  },
126
159
  Enum(values, options = {}) {
127
160
  assertValidEnumValues(values);
161
+ if (options.jsonType === "integer" && values.some((value) => typeof value !== "number" || !Number.isInteger(value))) {
162
+ throw new Error("Integer enum values must be integers");
163
+ }
128
164
  return {
129
165
  kind: "enum",
130
166
  values,
@@ -132,6 +168,8 @@ export const S = {
132
168
  };
133
169
  },
134
170
  Array(item, options = {}) {
171
+ assertNonNegativeInteger(options.minItems, "minItems");
172
+ assertNonNegativeInteger(options.maxItems, "maxItems");
135
173
  return {
136
174
  kind: "array",
137
175
  item,
@@ -186,7 +224,12 @@ export function toJsonSchema(schema) {
186
224
  const properties = {};
187
225
  const required = [];
188
226
  for (const [key, propertySchema] of Object.entries(unwrappedSchema.shape)) {
189
- properties[key] = toJsonSchema(propertySchema);
227
+ Object.defineProperty(properties, key, {
228
+ enumerable: true,
229
+ configurable: true,
230
+ writable: true,
231
+ value: toJsonSchema(propertySchema)
232
+ });
190
233
  if (!isOptionalSchema(propertySchema)) {
191
234
  required.push(key);
192
235
  }
package/dist/oneof.js CHANGED
@@ -1,10 +1,15 @@
1
- function assertValidBranches(branches) {
1
+ function assertValidBranches(branches, discriminator) {
2
2
  if (Object.keys(branches).length === 0) {
3
3
  throw new Error("OneOf schema requires at least one branch");
4
4
  }
5
+ for (const [branchName, branch] of Object.entries(branches)) {
6
+ if (Object.prototype.hasOwnProperty.call(branch.shape, discriminator)) {
7
+ throw new Error(`OneOf branch "${branchName}" must not declare discriminator field "${discriminator}".`);
8
+ }
9
+ }
5
10
  }
6
11
  export function OneOf(config) {
7
- assertValidBranches(config.branches);
12
+ assertValidBranches(config.branches, config.discriminator);
8
13
  return {
9
14
  kind: "oneOf",
10
15
  discriminator: config.discriminator,
package/dist/union.js CHANGED
@@ -12,14 +12,21 @@ export function getRequiredKeyFingerprint(schema) {
12
12
  function assertUniqueRequiredKeyFingerprints(branches) {
13
13
  const fingerprints = new Map();
14
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);
15
+ const requiredKeys = getRequiredKeys(branch);
16
+ const fingerprint = JSON.stringify(requiredKeys);
17
+ const existing = fingerprints.get(fingerprint);
18
+ if (existing === undefined) {
19
+ fingerprints.set(fingerprint, {
20
+ display: requiredKeys.join("+"),
21
+ indices: [index],
22
+ });
23
+ return;
24
+ }
25
+ existing.indices.push(index);
19
26
  });
20
- for (const [fingerprint, indices] of fingerprints) {
27
+ for (const { display, indices } of fingerprints.values()) {
21
28
  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.`);
29
+ throw new Error(`Union branches [${indices.join(", ")}] share required-key fingerprint "${display}". Each branch must require a distinct set of keys.`);
23
30
  }
24
31
  }
25
32
  }
package/dist/validate.js CHANGED
@@ -46,7 +46,7 @@ function walkOptional(schema, value, path, state) {
46
46
  if (value === missingValue || value === undefined) {
47
47
  const defaultValue = getDefault(schema.inner);
48
48
  if (defaultValue.present) {
49
- return { present: true, value: defaultValue.value };
49
+ return walkSchema(schema.inner, cloneDefault(defaultValue.value), path, state);
50
50
  }
51
51
  return { present: false };
52
52
  }
@@ -75,7 +75,7 @@ function walkString(schema, value, path, state) {
75
75
  return { present: true, value };
76
76
  }
77
77
  function walkNumber(schema, value, path, state) {
78
- if (typeof value !== "number" || Number.isNaN(value)) {
78
+ if (typeof value !== "number" || !Number.isFinite(value)) {
79
79
  addExpectedIssue(state, path, schema.jsonType === "integer" ? "integer" : "number", value);
80
80
  return { present: true, value };
81
81
  }
@@ -135,15 +135,15 @@ function walkObject(schema, value, path, state, injectedProperties = {}) {
135
135
  const propertyValue = Object.hasOwn(value, key) ? value[key] : missingValue;
136
136
  const result = walkSchema(propertySchema, propertyValue, [...path, key], state);
137
137
  if (result.present) {
138
- nextValue[key] = result.value;
138
+ setOwnValue(nextValue, key, result.value);
139
139
  }
140
140
  }
141
141
  for (const [key, injectedValue] of Object.entries(injectedProperties)) {
142
142
  if (Object.hasOwn(value, key)) {
143
- nextValue[key] = value[key];
143
+ setOwnValue(nextValue, key, value[key]);
144
144
  }
145
145
  else {
146
- nextValue[key] = injectedValue;
146
+ setOwnValue(nextValue, key, injectedValue);
147
147
  }
148
148
  }
149
149
  for (const [key, propertyValue] of Object.entries(value)) {
@@ -151,7 +151,7 @@ function walkObject(schema, value, path, state, injectedProperties = {}) {
151
151
  continue;
152
152
  }
153
153
  if (schema.additionalProperties === true) {
154
- nextValue[key] = propertyValue;
154
+ setOwnValue(nextValue, key, propertyValue);
155
155
  }
156
156
  else {
157
157
  addUnexpectedPropertyIssue(state, [...path, key]);
@@ -224,7 +224,7 @@ function walkRecord(schema, value, path, state) {
224
224
  for (const [key, propertyValue] of Object.entries(value)) {
225
225
  const result = walkSchema(schema.value, propertyValue, [...path, key], state);
226
226
  if (result.present) {
227
- nextValue[key] = result.value;
227
+ setOwnValue(nextValue, key, result.value);
228
228
  }
229
229
  }
230
230
  return { present: true, value: nextValue };
@@ -252,7 +252,7 @@ function isPlainRecord(value) {
252
252
  const prototype = Object.getPrototypeOf(value);
253
253
  return prototype === Object.prototype || prototype === null;
254
254
  }
255
- function isJsonValue(value) {
255
+ function isJsonValue(value, ancestors = new Set()) {
256
256
  if (value === null ||
257
257
  typeof value === "string" ||
258
258
  typeof value === "number" ||
@@ -260,13 +260,36 @@ function isJsonValue(value) {
260
260
  return typeof value !== "number" || Number.isFinite(value);
261
261
  }
262
262
  if (Array.isArray(value)) {
263
- return value.every((item) => isJsonValue(item));
263
+ if (ancestors.has(value)) {
264
+ return false;
265
+ }
266
+ ancestors.add(value);
267
+ const result = value.every((item) => isJsonValue(item, ancestors));
268
+ ancestors.delete(value);
269
+ return result;
264
270
  }
265
271
  if (isPlainRecord(value)) {
266
- return Object.values(value).every((item) => isJsonValue(item));
272
+ if (ancestors.has(value)) {
273
+ return false;
274
+ }
275
+ ancestors.add(value);
276
+ const result = Object.values(value).every((item) => isJsonValue(item, ancestors));
277
+ ancestors.delete(value);
278
+ return result;
267
279
  }
268
280
  return false;
269
281
  }
282
+ function cloneDefault(value) {
283
+ return structuredClone(value);
284
+ }
285
+ function setOwnValue(target, key, value) {
286
+ Object.defineProperty(target, key, {
287
+ configurable: true,
288
+ enumerable: true,
289
+ writable: true,
290
+ value
291
+ });
292
+ }
270
293
  function expectedFor(schema) {
271
294
  switch (schema.kind) {
272
295
  case "string":
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "toolcraft-schema",
3
- "version": "0.0.22",
3
+ "version": "0.0.24",
4
4
  "type": "module",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",