"use strict"; // semantics(): the mappings of phrases to directives and of states to values, as discovery lists them. const { test } = require("node:test"); const assert = require("node:assert/strict"); const { Actions, AlexaActions, ModeController, RangeController, SchemaError, States, ToggleController, semantics } = require("alex2node"); const { doc } = require("../helpers/fixtures.js"); // The schema of the semantics option of an interface, and the message of the SchemaError it throws. const semanticsOf = (descriptor) => ({ parse: (input) => descriptor.options.parse({ ...COMPLETE[descriptor.namespace], semantics: input }).semantics, }); const COMPLETE = { "Alexa.RangeController": { range: { min: 0, max: 100, precision: 1 } }, "Alexa.ModeController": { supportedModes: ["Up", "Down"].map((name) => ({ value: `Position.${name}`, friendlyNames: [{ "@type": "text", value: { text: name, locale: "en-US" } }] })), }, "Alexa.ToggleController": {}, }; function refusal(descriptor, input) { try { semanticsOf(descriptor).parse(input); } catch (err) { assert.ok(err instanceof SchemaError, `threw ${err}`); return err.message; } return assert.fail("the semantics were accepted"); } test("semantics() builds the Semantics object of alexa-discovery-objects.html", () => { const built = semantics() .action(["Close", "Lower"], "SetRangeValue", { rangeValue: 0 }) .action(["Open", "Raise"], "SetRangeValue", { rangeValue: 100 }) .state("Closed", 0) .stateRange("Open", 1, 100); const { semantics: example } = doc("alexa-discovery-objects", "semantics"); assert.deepEqual(semanticsOf(RangeController).parse(built), example); assert.deepEqual(built.actionMappings[0], doc("alexa-discovery-objects", "actionMapping")); assert.deepEqual(built.stateMappings[1], doc("alexa-discovery-objects", "stateMapping")); // What the schema returns is the plain object, and it takes that object as it takes the builder assert.equal(Object.getPrototypeOf(semanticsOf(RangeController).parse(built)), Object.prototype); assert.deepEqual(semanticsOf(RangeController).parse(example), example); }); test("a phrase and a state are given by name, by id or by the AlexaActions enum of 1.x", () => { const { actionMappings, stateMappings } = semantics() .action("Open", "TurnOn") .action("Alexa.Actions.Close", "TurnOff") .action([AlexaActions.Raise, Actions[3]], "TurnOn") .state(["Closed", States[0]], "OFF"); assert.deepEqual(actionMappings.map((mapping) => mapping.actions), [ ["Alexa.Actions.Open"], ["Alexa.Actions.Close"], ["Alexa.Actions.Raise", "Alexa.Actions.Lower"], ]); assert.deepEqual(stateMappings[0].states, ["Alexa.States.Closed", "Alexa.States.Open"]); // No payload given, none in discovery; an empty one is kept, as in the ToggleController examples assert.deepEqual(actionMappings[0].directive, { name: "TurnOn" }); assert.deepEqual(semantics().action("Open", "TurnOn", {}).actionMappings[0].directive, { name: "TurnOn", payload: {} }); assert.deepEqual(semanticsOf(ToggleController).parse(semantics().action("Open", "TurnOn")).actionMappings[0].directive, { name: "TurnOn" }); }); test("a mapping is checked against the directives and the property of the interface", () => { const cases = [ [RangeController, semantics().action("Open", "SetRange", { rangeValue: 100 }), 'semantics.actionMappings[0].directive.name: expected a directive of the interface: SetRangeValue, AdjustRangeValue, got "SetRange"'], [RangeController, semantics().action("Open", "SetRangeValue", { rangeValue: "100" }), 'semantics.actionMappings[0].directive.payload.rangeValue: expected a number, got "100"'], [RangeController, semantics().action("Open", "SetRangeValue"), "semantics.actionMappings[0].directive.payload.rangeValue: expected a number, got nothing"], [RangeController, semantics().action("Lower", "AdjustRangeValue", { rangeValueDelta: -10 }), "semantics.actionMappings[0].directive.payload.rangeValueDeltaDefault: expected true or false, got nothing"], [RangeController, semantics().action("Shut", "SetRangeValue", { rangeValue: 0 }), 'semantics.actionMappings[0].actions[0]: expected a phrase of Alexa.Actions: Open, Close, Raise, Lower, got "Alexa.Actions.Shut"'], [RangeController, semantics().state("Ajar", 50), 'semantics.stateMappings[0].states[0]: expected a state of Alexa.States: Open, Closed, Low, Empty, Full, Done, Stuck, got "Alexa.States.Ajar"'], [RangeController, semantics().state("Closed", "OFF"), 'semantics.stateMappings[0].value: expected a number, got "OFF"'], [RangeController, semantics().stateRange("Open", 100, 1), "semantics.stateMappings[0].range: the minimumValue 100 is above the maximumValue 1"], [ToggleController, semantics().state("Open", "on"), 'semantics.stateMappings[0].value: expected ON | OFF, got "on"'], [ToggleController, semantics().stateRange("Open", 1, 100), 'semantics.stateMappings[0].@type: expected StatesToValue, got "StatesToRange"'], [ModeController, semantics().state("Closed", 0), "semantics.stateMappings[0].value: expected a string of 1 or more characters, got 0"], [ToggleController, semantics(), "semantics: expected semantics() with at least one action or state, got {}"], [ToggleController, "open", 'semantics: expected semantics() with at least one action or state, got "open"'], [ToggleController, { actionMappings: [{ "@type": "ActionToDirective", actions: ["Alexa.Actions.Open"], directive: { name: "TurnOn" } }] }, 'semantics.actionMappings[0].@type: expected "ActionsToDirective", got "ActionToDirective"'], ]; for (const [descriptor, input, message] of cases) assert.equal(refusal(descriptor, input), message); }); test("the phrases and states of Alexa hunches are for a ToggleController", () => { const eco = semantics().action("SetEcoOn", "TurnOn").action("SetEcoOff", "TurnOff").state("EcoOn", "ON").state("EcoOff", "OFF"); assert.deepEqual(semanticsOf(ToggleController).parse(eco), { actionMappings: [ { "@type": "ActionsToDirective", actions: ["Alexa.Actions.SetEcoOn"], directive: { name: "TurnOn" } }, { "@type": "ActionsToDirective", actions: ["Alexa.Actions.SetEcoOff"], directive: { name: "TurnOff" } }, ], stateMappings: [ { "@type": "StatesToValue", states: ["Alexa.States.EcoOn"], value: "ON" }, { "@type": "StatesToValue", states: ["Alexa.States.EcoOff"], value: "OFF" }, ], }); assert.match( refusal(RangeController, semantics().action("SetEcoOn", "SetRangeValue", { rangeValue: 20 })), /^semantics\.actionMappings\[0\]\.actions\[0\]: expected a phrase of Alexa\.Actions: Open, Close, Raise, Lower, got "Alexa\.Actions\.SetEcoOn"$/ ); assert.match(refusal(ModeController, semantics().state("EcoOn", "Position.Up")), /expected a state of Alexa\.States: Open, Closed, Low, Empty, Full, Done, Stuck, got/); });