From c451a6b0ecf2bd624a4a9b2bd743960cefe613ec Mon Sep 17 00:00:00 2001 From: Charles Lowell Date: Thu, 26 Jan 2023 10:55:18 -0600 Subject: [PATCH] add `<<` merge key to "spread" mappings into a mapping. In order to gracefully compose data structures, you need a way to insert an entire set of mappings into another at a single place. This is equivalent to the javascript `...` operator. YAML allows support for this with the language independent type "merge-key" feature. This implements the spec here https://yaml.org/type/merge.html Thus evaluating this example: ```yaml composed: <<: one: 1 two: 2 <<: three: 3 four: 4 <<: - five: 5 - six: 6 ``` is: ```yaml composed: one: 1 two: 2 three: 3 four: 4 five: 5 six: 6 ``` Although ubiquitous, this syntax is officially deprecated and will be replaced by something else in YAML 1.3 but nobody is for sure what that will be. --- evaluate.ts | 27 ++++++++++++++++++- test/merge-key.test.ts | 60 ++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 86 insertions(+), 1 deletion(-) create mode 100644 test/merge-key.test.ts 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")); + }); +});