Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 6 additions & 4 deletions src/utils/gbnfJson/utils/validateObjectAgainstGbnfSchema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -160,8 +160,10 @@ function validateObject<T extends GbnfJsonObjectSchema>(
defs: Record<string, GbnfJsonSchema> = {},
defScopeDefs: DefScopeDefs = new DefScopeDefs()
): object is GbnfJsonSchemaToType<T> {
if (typeof object !== "object" || object === null)
throw new TechnicalValidationError(`Expected an object but got "${typeof object}"`);
if (typeof object !== "object" || object === null || Array.isArray(object))
throw new TechnicalValidationError(
`Expected an object but got "${object === null ? "null" : Array.isArray(object) ? "array" : typeof object}"`
);

let res = true;

Expand Down Expand Up @@ -265,9 +267,9 @@ function validateBasicString<T extends GbnfJsonBasicStringSchema>(object: any, s
}

const dateRegex = /^\d{4}-(0[1-9]|1[012])-(0[1-9]|[12]\d|3[01])$/;
const timeRegex = /^([01]\d|2[0-3]):[0-5]\d:[0-5]\d(\.\d{3})?(Z|[+-]([01][0-9]|2[0-3])?:[0-5][0-9])$/;
const timeRegex = /^([01]\d|2[0-3]):[0-5]\d:[0-5]\d(\.\d{3})?(Z|[+-]([01][0-9]|2[0-3]):[0-5][0-9])$/;
const datetimeRegex =
/^\d{4}-(0[1-9]|1[012])-(0[1-9]|[12]\d|3[01])T([01]\d|2[0-3]):[0-5]\d:[0-5]\d(\.\d{3})?(Z|[+-]([01][0-9]|2[0-3])?:[0-5][0-9])$/;
/^\d{4}-(0[1-9]|1[012])-(0[1-9]|[12]\d|3[01])T([01]\d|2[0-3]):[0-5]\d:[0-5]\d(\.\d{3})?(Z|[+-]([01][0-9]|2[0-3]):[0-5][0-9])$/;

function validateFormatString<T extends GbnfJsonFormatStringSchema>(object: any, schema: T): object is GbnfJsonSchemaToType<T> {
if (typeof object !== "string")
Expand Down
93 changes: 93 additions & 0 deletions test/standalone/utils/validateObjectAgainstGbnfSchema.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
import {describe, expect, test} from "vitest";
import {
LlamaJsonSchemaValidationError, validateObjectAgainstGbnfSchema
} from "../../../src/utils/gbnfJson/utils/validateObjectAgainstGbnfSchema.js";
import {getGbnfGrammarForGbnfJsonSchema} from "../../../src/utils/gbnfJson/getGbnfGrammarForGbnfJsonSchema.js";


describe("utils", () => {
describe("validateObjectAgainstGbnfSchema", () => {
describe("format string", () => {
test("time with a valid timezone offset", () => {
const schema = {type: "string", format: "time"} as const;

expect(validateObjectAgainstGbnfSchema("12:00:00Z", schema)).toBe(true);
expect(validateObjectAgainstGbnfSchema("12:00:00+05:30", schema)).toBe(true);
expect(validateObjectAgainstGbnfSchema("12:00:00.123-23:59", schema)).toBe(true);
});

test("time rejects a timezone offset without an hour", () => {
const schema = {type: "string", format: "time"} as const;

expect(() => validateObjectAgainstGbnfSchema("12:00:00+:30", schema)).toThrowError(LlamaJsonSchemaValidationError);
expect(() => validateObjectAgainstGbnfSchema("12:00:00-:00", schema)).toThrowError(LlamaJsonSchemaValidationError);
expect(() => validateObjectAgainstGbnfSchema("12:00:00+24:00", schema)).toThrowError(LlamaJsonSchemaValidationError);
});

test("date-time with a valid timezone offset", () => {
const schema = {type: "string", format: "date-time"} as const;

expect(validateObjectAgainstGbnfSchema("2024-01-01T10:00:00Z", schema)).toBe(true);
expect(validateObjectAgainstGbnfSchema("2024-01-01T10:00:00-08:00", schema)).toBe(true);
});

test("date-time rejects a timezone offset without an hour", () => {
const schema = {type: "string", format: "date-time"} as const;

expect(() => validateObjectAgainstGbnfSchema("2024-01-01T10:00:00-:00", schema)).toThrowError(
LlamaJsonSchemaValidationError
);
expect(() => validateObjectAgainstGbnfSchema("2024-01-01T10:00:00+:30", schema)).toThrowError(
LlamaJsonSchemaValidationError
);
});

test("the grammar requires an hour in the timezone offset as well", () => {
for (const format of ["time", "date-time"] as const) {
const grammar = getGbnfGrammarForGbnfJsonSchema({type: "string", format});

expect(grammar).toContain('("+" | "-") ([01] [0-9] | "2" [0-3]) ":" [0-5] [0-9]');
}
});
});

describe("object", () => {
test("accepts a plain object", () => {
expect(validateObjectAgainstGbnfSchema({}, {type: "object", properties: {}})).toBe(true);
expect(validateObjectAgainstGbnfSchema(
{a: "b"},
{type: "object", properties: {a: {type: "string"}}}
)).toBe(true);
});

test("rejects an array", () => {
const schema = {type: "object", properties: {}} as const;

expect(() => validateObjectAgainstGbnfSchema([], schema)).toThrowError(LlamaJsonSchemaValidationError);
expect(() => validateObjectAgainstGbnfSchema([1, 2], schema)).toThrowError(LlamaJsonSchemaValidationError);
expect(() => validateObjectAgainstGbnfSchema([], schema)).toThrowError('Expected an object but got "array"');
});

test("rejects null and primitives", () => {
const schema = {type: "object", properties: {}} as const;

expect(() => validateObjectAgainstGbnfSchema(null, schema)).toThrowError('Expected an object but got "null"');
expect(() => validateObjectAgainstGbnfSchema("a", schema)).toThrowError('Expected an object but got "string"');
});

test("an empty array does not satisfy a oneOf of an object and null", () => {
const schema = {oneOf: [{type: "object", properties: {}}, {type: "null"}]} as const;

expect(validateObjectAgainstGbnfSchema({}, schema)).toBe(true);
expect(validateObjectAgainstGbnfSchema(null, schema)).toBe(true);
expect(() => validateObjectAgainstGbnfSchema([], schema)).toThrowError(LlamaJsonSchemaValidationError);
});

test("the grammar of an object schema starts with an opening brace", () => {
const grammar = getGbnfGrammarForGbnfJsonSchema({type: "object", properties: {}});

expect(grammar).toMatch(/^root ::= "\{"/);
});
});
});
});