From 082de6b5bad9e11115aee43da84539feeef14c73 Mon Sep 17 00:00:00 2001 From: Joseph Mearman Date: Sun, 1 Nov 2026 05:31:12 +0000 Subject: [PATCH] feat(crosswalk): accept a $schema property naming the document's schema A crosswalk document can now declare the JSON Schema it is written against, which editors read to validate and complete it. The property is carried on the parsed crosswalk and has no part in encoding, decoding or validation. --- src/crosswalk.ts | 3 +++ src/crosswalk.unit.test.ts | 14 ++++++++++++++ 2 files changed, 17 insertions(+) diff --git a/src/crosswalk.ts b/src/crosswalk.ts index 46bffae..1a5bd68 100644 --- a/src/crosswalk.ts +++ b/src/crosswalk.ts @@ -200,6 +200,7 @@ export type OneWayRule = z.infer; export const CrosswalkSchema: z.ZodType & { is: (value: unknown) => value is Crosswalk } = define( z .object({ + $schema: z.string().optional(), source: z.string(), target: z.string(), rules: z.array(TwoWayRuleSchema).default([]), @@ -219,6 +220,8 @@ function keptPath(path: string): MoveRule { return { op: "move", from: path, to: path }; } export interface Crosswalk { + /** The address of the JSON Schema the document declares itself written against, which editors read to validate and complete it. It has no part in encoding, decoding or validation. */ + readonly $schema?: string | undefined; readonly source: string; readonly target: string; readonly rules: readonly TwoWayRule[]; diff --git a/src/crosswalk.unit.test.ts b/src/crosswalk.unit.test.ts index 9f9c983..ab921cc 100644 --- a/src/crosswalk.unit.test.ts +++ b/src/crosswalk.unit.test.ts @@ -157,6 +157,20 @@ describe("parseCrosswalk", () => { }); }); +describe("$schema", () => { + it("is accepted as the address of the document's own schema and carries no rule", () => { + const parsed = parseCrosswalk({ $schema: "https://example.com/crossmap.schema.json", source: "a", target: "b", keep: ["x"] }); + expect(parsed.$schema).toBe("https://example.com/crossmap.schema.json"); + expect(parsed.rules).toEqual([{ op: "move", from: "x", to: "x" }]); + expect(validateCrosswalk(parsed)).toEqual([]); + }); + + it("is optional and must be text", () => { + expect(parseCrosswalk({ source: "a", target: "b" }).$schema).toBeUndefined(); + expect(() => parseCrosswalk({ $schema: 1, source: "a", target: "b" })).toThrow(CrosswalkSyntaxError); + }); +}); + describe("keep", () => { it("reads each kept path as an identity move appended after the rules, in order", () => { const parsed = parseCrosswalk({ source: "a", target: "b", rules: [{ op: "scale", from: "w", to: "weight", factor: 2 }], keep: ["meta", "nodes[].id"] });