diff --git a/evaluate.ts b/evaluate.ts index 16eeac2..5ac12b3 100644 --- a/evaluate.ts +++ b/evaluate.ts @@ -85,7 +85,32 @@ export function createYSEnv(parent = global): PSEnv { } else if (value.type === "map") { let entries: [PSMapKey, PSValue][] = []; for (let [k, v] of value.value.entries()) { - entries.push([k, yield* env.eval(v)]); + let target = yield* env.eval(v); + if (k.type === "string" && !k.quote && k.value == "<<") { + if (target.type === "map") { + for (let [subkey, subvalue] of target.value.entries()) { + entries.push([subkey, subvalue]); + } + } else if (target.type === "list") { + for (let item of target.value) { + if (item.type === "map") { + for (let [subkey, subvalue] of item.value.entries()) { + entries.push([subkey, subvalue]); + } + } else { + throw new Error( + `merge key value must be either a map, or a sequence of maps`, + ); + } + } + } else { + throw new Error( + `merge key value must be either a map, or a sequence of maps`, + ); + } + } else { + entries.push([k, target]); + } } return { type: "map", value: new Map(entries) }; } else if (value.type === "list") { diff --git a/test/merge-key.test.ts b/test/merge-key.test.ts new file mode 100644 index 0000000..1b92631 --- /dev/null +++ b/test/merge-key.test.ts @@ -0,0 +1,60 @@ +import { describe, expect, it } from "./suite.ts"; + +import * as ps from "../mod.ts"; +import { lookup$ } from "../psmap.ts"; + +// https://yaml.org/type/merge.html + +describe("merge keys", () => { + it("mix in all the properties of a map", async () => { + let interp = ps.createPlatformScript(); + let program = ps.parse(` +<<: + one: 1 + two: 2 +`); + let map = await interp.eval(program) as ps.PSMap; + expect(lookup$("one", map)).toEqual(ps.number(1)); + expect(lookup$("two", map)).toEqual(ps.number(2)); + }); + it("mix in all the maps in a seq", async () => { + let interp = ps.createPlatformScript(); + let program = ps.parse(` +<<: + - + one: 1 + two: 2 + - + three: 3 + four: 4 +`); + let map = await interp.eval(program) as ps.PSMap; + expect(lookup$("one", map)).toEqual(ps.number(1)); + expect(lookup$("two", map)).toEqual(ps.number(2)); + expect(lookup$("three", map)).toEqual(ps.number(3)); + expect(lookup$("four", map)).toEqual(ps.number(4)); + }); + it("throw an error if the mapping points to a non-collection", async () => { + // TODO: this is a type error when we implement type system. + let program = ps.parse(`<<: not a map`); + let interp = ps.createPlatformScript(); + try { + await interp.eval(program); + throw new Error( + "expected mapping a non-collection to fail, but it did not", + ); + } catch (error) { + expect(error.message).toMatch(/merge key/); + } + }); + it("can invoke a function", async () => { + let program = ps.parse(` +$let: + id(x): $x +$do: {<<: { $id: { hello: world } } } +`); + let interp = ps.createPlatformScript(); + let map = await interp.eval(program) as ps.PSMap; + expect(lookup$("hello", map)).toEqual(ps.string("world")); + }); +});