Skip to content

Commit d93d97f

Browse files
authored
Merge pull request #1 from cloudflare/celso/const
adds const values support and changelog file
2 parents f46d121 + f48eda7 commit d93d97f

File tree

5 files changed

+126
-105
lines changed

5 files changed

+126
-105
lines changed

CHANGELOG.md

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
# Changelog
2+
3+
All notable changes to this project will be documented in this file.
4+
5+
## [0.0.18] - 2025-02-20
6+
7+
### Added
8+
9+
- Constant values support https://json-schema.org/understanding-json-schema/reference/const
10+
- CHANGELOG.md file

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@cloudflare/cabidela",
3-
"version": "0.0.17",
3+
"version": "0.0.18",
44
"description": "Cabidela is a small, fast, eval-less, Cloudflare Workers compatible, dynamic JSON Schema validator",
55
"main": "dist/index.js",
66
"module": "dist/index.mjs",

src/index.ts

Lines changed: 39 additions & 104 deletions
Original file line numberDiff line numberDiff line change
@@ -40,11 +40,7 @@ export class Cabidela {
4040

4141
throw(message: string, needle: SchemaNavigation) {
4242
const error = `${message}${this.options.fullErrors && needle.absorvErrors !== true && needle.errors.size > 0 ? `: ${Array.from(needle.errors).join(", ")}` : ``}`;
43-
throw new Error(
44-
this.options.errorMessages
45-
? (needle.schema.errorMessage ?? error)
46-
: error,
47-
);
43+
throw new Error(this.options.errorMessages ? (needle.schema.errorMessage ?? error) : error);
4844
}
4945

5046
parseAdditionalProperties(
@@ -53,14 +49,9 @@ export class Cabidela {
5349
contextEvaluatedProperties: Set<string>,
5450
): number {
5551
let matchCount = 0;
56-
const { metadata, resolvedObject } = resolvePayload(
57-
needle.path,
58-
needle.payload,
59-
);
52+
const { metadata, resolvedObject } = resolvePayload(needle.path, needle.payload);
6053

61-
const unevaluatedProperties = metadata.properties.difference(
62-
contextEvaluatedProperties,
63-
);
54+
const unevaluatedProperties = metadata.properties.difference(contextEvaluatedProperties);
6455

6556
// Setting the additionalProperties schema to false means no additional properties will be allowed.
6657
if (contextAdditionalProperties === false) {
@@ -142,10 +133,7 @@ export class Cabidela {
142133

143134
// unevaluatedProperties keyword is similar to additionalProperties except that it can recognize properties declared in subschemas.
144135
if (needle.schema.hasOwnProperty("unevaluatedProperties")) {
145-
needle.evaluatedProperties = new Set([
146-
...needle.evaluatedProperties,
147-
...localEvaluatedProperties,
148-
]);
136+
needle.evaluatedProperties = new Set([...needle.evaluatedProperties, ...localEvaluatedProperties]);
149137
matchCount += this.parseAdditionalProperties(
150138
needle,
151139
needle.schema.unevaluatedProperties,
@@ -156,14 +144,9 @@ export class Cabidela {
156144
// this has to be last
157145
if (needle.schema.hasOwnProperty("required")) {
158146
if (
159-
new Set(needle.schema.required).difference(
160-
needle.evaluatedProperties.union(localEvaluatedProperties),
161-
).size > 0
147+
new Set(needle.schema.required).difference(needle.evaluatedProperties.union(localEvaluatedProperties)).size > 0
162148
) {
163-
this.throw(
164-
`required properties at '${pathToString(needle.path)}' is '${needle.schema.required}'`,
165-
needle,
166-
);
149+
this.throw(`required properties at '${pathToString(needle.path)}' is '${needle.schema.required}'`, needle);
167150
}
168151
}
169152
return matchCount ? true : false;
@@ -206,10 +189,7 @@ export class Cabidela {
206189

207190
// To validate against anyOf, the given data must be valid against any (one or more) of the given subschemas.
208191
if (needle.schema.hasOwnProperty("anyOf")) {
209-
if (
210-
this.parseList(needle.schema.anyOf, needle, (r: number) => r !== 0) ===
211-
0
212-
) {
192+
if (this.parseList(needle.schema.anyOf, needle, (r: number) => r !== 0) === 0) {
213193
if (needle.path.length == 0) {
214194
this.throw(`anyOf at '${pathToString(needle.path)}' not met`, needle);
215195
}
@@ -220,10 +200,7 @@ export class Cabidela {
220200

221201
// To validate against allOf, the given data must be valid against all of the given subschemas.
222202
if (needle.schema.hasOwnProperty("allOf")) {
223-
const conditions = needle.schema.allOf.reduce(
224-
(r: any, c: any) => Object.assign(r, c),
225-
{},
226-
);
203+
const conditions = needle.schema.allOf.reduce((r: any, c: any) => Object.assign(r, c), {});
227204
try {
228205
this.parseSubSchema({
229206
...needle,
@@ -239,17 +216,10 @@ export class Cabidela {
239216
}
240217
}
241218

242-
const { metadata, resolvedObject } = resolvePayload(
243-
needle.path,
244-
needle.payload,
245-
);
219+
const { metadata, resolvedObject } = resolvePayload(needle.path, needle.payload);
246220

247221
// array, but object is not binary
248-
if (
249-
needle.schema.type === "array" &&
250-
!metadata.types.has("binary") &&
251-
!metadata.types.has("string")
252-
) {
222+
if (needle.schema.type === "array" && !metadata.types.has("binary") && !metadata.types.has("string")) {
253223
let matched = 0;
254224
for (let item in resolvedObject) {
255225
matched += this.parseSubSchema({
@@ -262,6 +232,19 @@ export class Cabidela {
262232
} else if (needle.schema.type === "object" || needle.schema.properties) {
263233
return this.parseObject(needle) ? 1 : 0;
264234
} else if (resolvedObject !== undefined) {
235+
// This has to be before type checking
236+
if (needle.schema.hasOwnProperty("const")) {
237+
if (resolvedObject !== needle.schema.const) {
238+
this.throw(
239+
`const ${resolvedObject} doesn't match ${needle.schema.const} at '${pathToString(needle.path)}'`,
240+
needle,
241+
);
242+
} else {
243+
// You can use const even without a type, to accept values of different types.
244+
// If that's the case, then skip type checking below
245+
if (needle.schema.type == undefined) return 1;
246+
}
247+
}
265248
// This has to be before type checking
266249
if (needle.schema.hasOwnProperty("enum")) {
267250
if (Array.isArray(needle.schema.enum)) {
@@ -276,17 +259,11 @@ export class Cabidela {
276259
if (needle.schema.type == undefined) return 1;
277260
}
278261
} else {
279-
this.throw(
280-
`enum should be an array at '${pathToString(needle.path)}'`,
281-
needle,
282-
);
262+
this.throw(`enum should be an array at '${pathToString(needle.path)}'`, needle);
283263
}
284264
}
285265
// This has to be after handling enum
286-
if (
287-
needle.schema.hasOwnProperty("type") &&
288-
!metadata.types.has(needle.schema.type)
289-
) {
266+
if (needle.schema.hasOwnProperty("type") && !metadata.types.has(needle.schema.type)) {
290267
this.throw(
291268
`Type mismatch of '${pathToString(needle.path)}', '${needle.schema.type}' not in ${JSON.stringify(Array.from(metadata.types))}`,
292269
needle,
@@ -297,19 +274,10 @@ export class Cabidela {
297274
/* Otherwise check schema type */
298275
switch (needle.schema.type) {
299276
case "string":
300-
if (
301-
needle.schema.hasOwnProperty("maxLength") &&
302-
metadata.size > needle.schema.maxLength
303-
) {
304-
this.throw(
305-
`Length of '${pathToString(needle.path)}' must be <= ${needle.schema.maxLength}`,
306-
needle,
307-
);
277+
if (needle.schema.hasOwnProperty("maxLength") && metadata.size > needle.schema.maxLength) {
278+
this.throw(`Length of '${pathToString(needle.path)}' must be <= ${needle.schema.maxLength}`, needle);
308279
}
309-
if (
310-
needle.schema.hasOwnProperty("minLength") &&
311-
metadata.size < needle.schema.minLength
312-
) {
280+
if (needle.schema.hasOwnProperty("minLength") && metadata.size < needle.schema.minLength) {
313281
this.throw(
314282
`Length of '${pathToString(needle.path)}' must be >= ${needle.schema.minLength} not met`,
315283
needle,
@@ -318,50 +286,20 @@ export class Cabidela {
318286
break;
319287
case "number":
320288
case "integer":
321-
if (
322-
needle.schema.hasOwnProperty("minimum") &&
323-
resolvedObject < needle.schema.minimum
324-
) {
325-
this.throw(
326-
`'${pathToString(needle.path)}' must be >= ${needle.schema.minimum}`,
327-
needle,
328-
);
289+
if (needle.schema.hasOwnProperty("minimum") && resolvedObject < needle.schema.minimum) {
290+
this.throw(`'${pathToString(needle.path)}' must be >= ${needle.schema.minimum}`, needle);
329291
}
330-
if (
331-
needle.schema.hasOwnProperty("exclusiveMinimum") &&
332-
resolvedObject <= needle.schema.exclusiveMinimum
333-
) {
334-
this.throw(
335-
`'${pathToString(needle.path)}' must be > ${needle.schema.exclusiveMinimum}`,
336-
needle,
337-
);
292+
if (needle.schema.hasOwnProperty("exclusiveMinimum") && resolvedObject <= needle.schema.exclusiveMinimum) {
293+
this.throw(`'${pathToString(needle.path)}' must be > ${needle.schema.exclusiveMinimum}`, needle);
338294
}
339-
if (
340-
needle.schema.hasOwnProperty("maximum") &&
341-
resolvedObject > needle.schema.maximum
342-
) {
343-
this.throw(
344-
`'${pathToString(needle.path)}' must be <= ${needle.schema.maximum}`,
345-
needle,
346-
);
295+
if (needle.schema.hasOwnProperty("maximum") && resolvedObject > needle.schema.maximum) {
296+
this.throw(`'${pathToString(needle.path)}' must be <= ${needle.schema.maximum}`, needle);
347297
}
348-
if (
349-
needle.schema.hasOwnProperty("exclusiveMaximum") &&
350-
resolvedObject >= needle.schema.exclusiveMaximum
351-
) {
352-
this.throw(
353-
`'${pathToString(needle.path)}' must be < ${needle.schema.exclusiveMaximum}`,
354-
needle,
355-
);
298+
if (needle.schema.hasOwnProperty("exclusiveMaximum") && resolvedObject >= needle.schema.exclusiveMaximum) {
299+
this.throw(`'${pathToString(needle.path)}' must be < ${needle.schema.exclusiveMaximum}`, needle);
356300
}
357-
if (
358-
needle.schema.hasOwnProperty("multipleOf") &&
359-
resolvedObject % needle.schema.multipleOf !== 0
360-
) {
361-
this.throw(
362-
`'${pathToString(needle.path)}' must be multiple of ${needle.schema.multipleOf}`,
363-
needle,
364-
);
301+
if (needle.schema.hasOwnProperty("multipleOf") && resolvedObject % needle.schema.multipleOf !== 0) {
302+
this.throw(`'${pathToString(needle.path)}' must be multiple of ${needle.schema.multipleOf}`, needle);
365303
}
366304
break;
367305
}
@@ -372,10 +310,7 @@ export class Cabidela {
372310
return 1;
373311
}
374312
// Apply defaults
375-
if (
376-
this.options.applyDefaults === true &&
377-
needle.schema.hasOwnProperty("default")
378-
) {
313+
if (this.options.applyDefaults === true && needle.schema.hasOwnProperty("default")) {
379314
needle.path.reduce(function (prev, curr, index) {
380315
// create objects as needed along the path, if they don't exist, so we can apply defaults at the end
381316
if (prev[curr] === undefined) {

tests/35-const.test.ts

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
import { expect, test, describe, it } from "vitest";
2+
import { FakeCabidela } from "./lib/fake-cabidela";
3+
4+
describe("const", () => {
5+
let schema = {
6+
type: "string",
7+
const: "red",
8+
};
9+
10+
let validator = new FakeCabidela(schema);
11+
12+
it("red in red", () => {
13+
expect(() => validator.validate("red")).not.toThrowError();
14+
});
15+
it("blue in not red", () => {
16+
expect(() => validator.validate("blue")).toThrowError();
17+
});
18+
});
19+
20+
describe("const without type", () => {
21+
let schema = {
22+
const: "red",
23+
};
24+
25+
let validator = new FakeCabidela(schema);
26+
27+
it("red in red", () => {
28+
expect(() => validator.validate("red")).not.toThrowError();
29+
});
30+
it("blue not in red", () => {
31+
expect(() => validator.validate("blue")).toThrowError();
32+
});
33+
});

tests/90-ai.test.ts

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -154,3 +154,46 @@ describe("Text embeddings", () => {
154154
expect(() => validator.validate({ text: "Tell me a joke about Cloudflare" })).not.toThrowError();
155155
});
156156
});
157+
158+
describe("Structured outputs", () => {
159+
let schema = {
160+
title: "JSON Mode",
161+
type: "object",
162+
oneOf: [
163+
{
164+
properties: {
165+
type: {
166+
type: "string",
167+
const: "json_object",
168+
},
169+
},
170+
required: ["type"],
171+
},
172+
{
173+
properties: {
174+
type: {
175+
type: "string",
176+
const: "json_schema",
177+
},
178+
json_schema: {},
179+
},
180+
required: ["type", "json_schema"],
181+
},
182+
],
183+
};
184+
185+
let validator = new FakeCabidela(schema, { errorMessages: true });
186+
187+
test("json_object type", () => {
188+
expect(() => validator.validate({ type: "json_object" })).not.toThrowError();
189+
});
190+
test("json_schema type", () => {
191+
expect(() => validator.validate({ type: "json_schema", json_schema: { something: "here" } })).not.toThrowError();
192+
});
193+
test("json_schema type", () => {
194+
expect(() => validator.validate({ type: "json_schema", json_schema: { something: "here" } })).not.toThrowError();
195+
});
196+
test("json_schema type without schema", () => {
197+
expect(() => validator.validate({ type: "json_schema" })).toThrowError();
198+
});
199+
});

0 commit comments

Comments
 (0)