fix(omptype): preserved TypeBox constraint keywords
Emit pattern, non-URL format, and multipleOf from the TypeBox adapter alongside the runtime narrows that enforce them. Embedded builders lowered by the legacy compatibility shim now retain identical validation constraints in the raw JSON-Schema validator. Added adapter-level wire assertions and an end-to-end unsafe embedding regression.
This commit is contained in:
@@ -280,6 +280,31 @@ describe("pi.typebox compatibility shim", () => {
|
||||
expect(schema.safeParse(true).success).toBe(false);
|
||||
});
|
||||
|
||||
it("preserves wire-only constraints on embedded schema builders", () => {
|
||||
const schema = Type.Unsafe({
|
||||
type: "object",
|
||||
properties: {
|
||||
code: Type.String({ pattern: "^x" }),
|
||||
count: Type.Number({ multipleOf: 2 }),
|
||||
},
|
||||
required: ["code", "count"],
|
||||
additionalProperties: false,
|
||||
});
|
||||
|
||||
expect(schema.toJsonSchema()).toEqual({
|
||||
type: "object",
|
||||
properties: {
|
||||
code: { type: "string", pattern: "^x" },
|
||||
count: { type: "number", multipleOf: 2 },
|
||||
},
|
||||
required: ["code", "count"],
|
||||
additionalProperties: false,
|
||||
});
|
||||
expect(schema.safeParse({ code: "xray", count: 4 }).success).toBe(true);
|
||||
expect(schema.safeParse({ code: "bad", count: 4 }).success).toBe(false);
|
||||
expect(schema.safeParse({ code: "xray", count: 3 }).success).toBe(false);
|
||||
});
|
||||
|
||||
it("recovers the wire schema when a builder is spread into a new document", () => {
|
||||
const base = Type.Unsafe({
|
||||
anyOf: [
|
||||
|
||||
@@ -2,6 +2,10 @@
|
||||
|
||||
## [Unreleased]
|
||||
|
||||
### Fixed
|
||||
|
||||
- Fixed the TypeBox adapter omitting `pattern`, non-URL `format`, and `multipleOf` from emitted JSON Schema even though runtime validation enforced them ([#8425](https://github.com/can1357/oh-my-pi/pull/8425)).
|
||||
|
||||
## [17.3.0] - 2026-08-13
|
||||
|
||||
### Added
|
||||
|
||||
@@ -220,7 +220,11 @@ function tString(opts?: StringOpts): TString {
|
||||
const valid = formatPredicate(format);
|
||||
schema = schema.narrow((value, ctx) => valid(value) || ctx.mustBe(`a string in ${format} format`));
|
||||
}
|
||||
return applyMeta(schema, opts);
|
||||
const result = applyMeta(schema, opts);
|
||||
const keywords: Record<string, unknown> = {};
|
||||
if (opts?.pattern !== undefined) keywords.pattern = opts.pattern;
|
||||
if (opts?.format !== undefined) keywords.format = opts.format === "url" ? "uri" : opts.format;
|
||||
return opts?.pattern !== undefined || opts?.format !== undefined ? withJsonSchemaKeywords(result, keywords) : result;
|
||||
}
|
||||
|
||||
function formatPredicate(format: string): (value: string) => boolean {
|
||||
@@ -290,7 +294,8 @@ function tNumber(opts?: NumberOpts, integer = false): TNumber {
|
||||
);
|
||||
});
|
||||
}
|
||||
return applyMeta(schema, opts);
|
||||
const result = applyMeta(schema, opts);
|
||||
return opts?.multipleOf !== undefined ? withJsonSchemaKeywords(result, { multipleOf: opts.multipleOf }) : result;
|
||||
}
|
||||
|
||||
function tLiteral<const V extends string | number | boolean | null>(value: V, opts?: Meta): TLiteral<V> {
|
||||
|
||||
@@ -46,11 +46,17 @@ describe("TypeBox adapter", () => {
|
||||
expect(valid(Type.String({ format: "email" }), "a@b.co")).toBe(true);
|
||||
expect(valid(Type.String({ format: "email" }), "nope")).toBe(false);
|
||||
expect(Type.String({ format: "url" }).toJsonSchema()).toEqual({ type: "string", format: "uri" });
|
||||
expect(Type.String({ pattern: "^[a-z]+$", format: "email" }).toJsonSchema()).toEqual({
|
||||
type: "string",
|
||||
pattern: "^[a-z]+$",
|
||||
format: "email",
|
||||
});
|
||||
|
||||
const number = Type.Number({ minimum: 1, maximum: 10, multipleOf: 2 });
|
||||
expect(valid(number, 4)).toBe(true);
|
||||
expect(valid(number, 0)).toBe(false);
|
||||
expect(valid(number, 3)).toBe(false);
|
||||
expect(number.toJsonSchema()).toEqual({ type: "number", minimum: 1, maximum: 10, multipleOf: 2 });
|
||||
const exclusive = Type.Number({ exclusiveMinimum: 1, exclusiveMaximum: 3 });
|
||||
expect(valid(exclusive, 2)).toBe(true);
|
||||
expect(valid(exclusive, 1)).toBe(false);
|
||||
|
||||
Reference in New Issue
Block a user