registry: describe an Alexa interface as data

src/registry/ holds what the library knows about an interface: namespace, version, the page it was read from,
properties with their value schemas and directives with their payload schemas. Five interfaces are described
(Alexa, PowerController, BrightnessController, TemperatureSensor, EndpointHealth); the other 65 names of
AlexaInterfaceType are stubs with the version and property names of 1.5.2. schema.ts is the run-time check behind
it (241 lines, no new dependency), catalog.ts the vocabularies of the pages: 103 assets (23 units), 6 actions,
9 states, 56 display categories, 22 reserved words, 73 error types under 11 namespaces.

AlexaInterface.getVersion() and getProps() read the registry; the two switch statements are gone (-167 lines).
On the wire: Alexa.EndpointHealth is announced at 3.1 (was 3.3; the page is titled 3.1 and no page mentions 3.3),
and TimeHoldController and Camera.LiveViewController at 3 and 1.7 (1.5.2 sent the string "UNKNOWN").
DisplayCategory gains VACUUM. New exports: registry, DeclarationError, SchemaError, Assets, Units, Actions,
States, DisplayCategories and the descriptor types.

Tests: 20 JSON examples of the five pages under test/fixtures/alexa-docs; every directive payload and property
value in them parses with its descriptor. npm test: 57 pass (was 30) in 10.8 s, also on Node 18.20.8 and 20.20.2.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
David 2026-09-28 15:18:22 +00:00
parent 128ca35c2a
commit aa0ffd64ea
93 changed files with 4058 additions and 490 deletions

View file

@ -0,0 +1,72 @@
"use strict";
// The vocabularies copied from Amazon's pages: their sizes as counted on the pages on 2026-09-28, and how they
// relate to the 1.x enums.
const { test } = require("node:test");
const assert = require("node:assert/strict");
const {
Actions, AlexaActions, AlexaErrorType, Assets, DisplayCategories, DisplayCategory, States, Units,
} = require("alex2node");
const { ERROR_TYPES, RESERVED_WORDS, LIMITS } = require("../../dist/cjs/registry/catalog.js");
const unique = (list) => new Set(list).size === list.length;
test("assets: the 103 ids of the global Alexa catalog, 23 of them units of measure", () => {
assert.equal(Assets.length, 103);
assert.ok(unique(Assets));
for (const id of Assets) assert.match(id, /^Alexa\.(Actions|Button|DeviceName|Gestures?|Setting|Shower|Unit|Value)\.[A-Za-z0-9.]+$/);
assert.equal(Units.length, 23);
assert.deepEqual(Units, Assets.filter((id) => id.startsWith("Alexa.Unit.")));
assert.ok(Units.includes("Alexa.Unit.Percent"));
// The page has "Gesture" seven times and "Gestures" once
assert.deepEqual(Assets.filter((id) => id.startsWith("Alexa.Gestures.")), ["Alexa.Gestures.DoubleTap"]);
});
test("semantics: six actions, the ones the AlexaActions enum of 1.x has, and nine states", () => {
assert.deepEqual([...Actions].sort(), Object.values(AlexaActions).sort());
assert.deepEqual(States.map((id) => id.replace("Alexa.States.", "")), ["Open", "Closed", "EcoOn", "EcoOff", "Low", "Empty", "Full", "Done", "Stuck"]);
});
test("display categories: the 56 of the page; the enum has them all and keeps VEHICLE", () => {
assert.equal(DisplayCategories.length, 56);
assert.ok(unique(DisplayCategories));
const inEnum = Object.values(DisplayCategory);
assert.deepEqual(DisplayCategories.filter((category) => !inEnum.includes(category)), []);
assert.deepEqual(inEnum.filter((category) => !DisplayCategories.includes(category)), ["VEHICLE"]);
assert.equal(DisplayCategory.VACUUM, "VACUUM");
for (const [key, value] of Object.entries(DisplayCategory)) assert.equal(key, value);
});
test("error types: the 73 of the error type table, each under the namespace of its interface", () => {
assert.equal(Object.keys(ERROR_TYPES).length, 73);
assert.equal(ERROR_TYPES.ENDPOINT_UNREACHABLE, "Alexa");
assert.equal(ERROR_TYPES.INVALID_VALUE, "Alexa");
assert.equal(ERROR_TYPES.THERMOSTAT_IS_OFF, "Alexa.ThermostatController");
assert.equal(ERROR_TYPES.UNAUTHORIZED, "Alexa.SecurityPanelController");
assert.equal(ERROR_TYPES.OBSTACLE_DETECTED, "Alexa.Safety");
assert.equal(ERROR_TYPES.TEMPERATURE_VALUE_OUT_OF_RANGE, "Alexa", "the generic one, not a thermostat error");
const namespaces = [...new Set(Object.values(ERROR_TYPES))].sort();
assert.equal(namespaces.length, 11);
for (const namespace of namespaces) assert.match(namespace, /^Alexa(\.[A-Za-z]+)*$/);
// AlexaErrorType of 1.x has every type of the table and two that the table does not list
const inEnum = Object.values(AlexaErrorType);
assert.deepEqual(Object.keys(ERROR_TYPES).filter((type) => !inEnum.includes(type)), []);
assert.deepEqual(inEnum.filter((type) => !(type in ERROR_TYPES)).sort(), ["EXCEEDED_PIN_ATTEMPTS", "PIN_SETUP_REQUIRED"]);
});
test("reserved words and limits", () => {
assert.equal(RESERVED_WORDS.length, 22);
assert.ok(unique(RESERVED_WORDS));
for (const word of RESERVED_WORDS) assert.equal(word, word.toLowerCase());
assert.deepEqual(LIMITS, {
endpointsPerCustomer: 300,
capabilitiesPerEndpoint: 100,
endpointIdLength: 256,
friendlyNameLength: 256,
sceneFriendlyNameLength: 128,
manufacturerNameLength: 128,
descriptionLength: 128,
additionalAttributeLength: 256,
cookieBytes: 5000,
});
});