From 34604c77c107db5119c71bf63845e047e6e4d534 Mon Sep 17 00:00:00 2001 From: Joseph Mearman Date: Sun, 1 Nov 2026 05:27:21 +0000 Subject: [PATCH] feat: export ruleSides and its RuleSides type ruleSides lists the paths a two-way rule reads and writes, the same list the executor and the validator use, so a consumer that has to reason about a crosswalk's paths (to tell whether it can be applied a page at a time, or to show its rules) handles every operation without keeping its own copy of which field of which rule holds a path. --- src/index.ts | 2 ++ src/sides.unit.test.ts | 32 ++++++++++++++++++++++++++++++++ 2 files changed, 34 insertions(+) create mode 100644 src/sides.unit.test.ts diff --git a/src/index.ts b/src/index.ts index dc9467e..536bd44 100644 --- a/src/index.ts +++ b/src/index.ts @@ -17,6 +17,8 @@ export type { TwoWayRule, When, } from "./crosswalk"; +export { ruleSides } from "./sides"; +export type { RuleSides } from "./sides"; export { CrosswalkStructureError, CrosswalkSyntaxError, RuleApplicationError, RoundTripMismatchError, CrossmapError } from "./errors"; export type { CrosswalkIssue, SyntaxIssue } from "./errors"; export { compileCrosswalk, decode, encode } from "./executor"; diff --git a/src/sides.unit.test.ts b/src/sides.unit.test.ts new file mode 100644 index 0000000..b65f243 --- /dev/null +++ b/src/sides.unit.test.ts @@ -0,0 +1,32 @@ +import { describe, expect, it } from "vitest"; +import { parseCrosswalk, ruleSides } from "./index"; + +/** + * Parses one two-way rule inside a crosswalk, so the rule under test is what the schema accepts. + * @param rule - The rule's document. + * @returns The rule as parsed. + */ +function parsed(rule: unknown) { + const [first] = parseCrosswalk({ source: "native", target: "shared", rules: [rule], oneWay: [] }).rules; + if (first === undefined) { + return expect.unreachable("the crosswalk has the one rule"); + } + + return first; +} + +describe("ruleSides", () => { + it("gives the one path on each side of a rule that has one", () => { + expect(ruleSides(parsed({ op: "move", from: "meta", to: "info" }))).toEqual({ from: ["meta"], to: ["info"] }); + expect(ruleSides(parsed({ op: "scale", from: "a", to: "b", factor: 2 }))).toEqual({ from: ["a"], to: ["b"] }); + expect(ruleSides(parsed({ op: "split", from: "a", to: "b", separator: "," }))).toEqual({ from: ["a"], to: ["b"] }); + }); + + it("gives a template's fields in its record's key order and a copy's every target", () => { + expect(ruleSides(parsed({ op: "template", from: { family: "people[].family", given: "people[].given" }, to: "names[]", template: "{family}, {given}" }))).toEqual({ + from: ["people[].family", "people[].given"], + to: ["names[]"], + }); + expect(ruleSides(parsed({ op: "copy", from: "items[].id", to: ["nodes[].id", "labels[].text"] }))).toEqual({ from: ["items[].id"], to: ["nodes[].id", "labels[].text"] }); + }); +});