diff --git a/src/RaftDeviceTypeCatalogue.test.ts b/src/RaftDeviceTypeCatalogue.test.ts index 708ea6e..c8d4eb9 100644 --- a/src/RaftDeviceTypeCatalogue.test.ts +++ b/src/RaftDeviceTypeCatalogue.test.ts @@ -19,6 +19,7 @@ const site = { { label: "main", raftCore: "1.62.0", count: 2 }, { label: "v1.61.0", raftCore: "1.61.0", count: 2 }, { label: "v1.59.0", raftCore: "1.59.0", count: 1 }] }), [`${SITE}/v1.61.0/catalogue.json`]: JSON.stringify({ formatVersion: 1, label: "v1.61.0", raftCore: "1.61.0", recordFormat: 1, count: 2, + sets: { common: ["MCP9808"] }, records: [{ key: "LSM6DS3", file: "records/LSM6DS3.json", sha256: "x", summary: { name: "IMU", hasData: true, customCode: false, initCode: false } }, { key: "MCP9808", file: "records/MCP9808.json", sha256: "y", summary: { name: "Temp", hasData: true, customCode: false, initCode: false } }] }), [`${SITE}/v1.61.0/DeviceTypeRecords.json`]: '{"devTypes":{"LSM6DS3":{"addresses":"0x6a","m":{"12.5":1,"26":2}},"MCP9808":{"addresses":"0x18"}}}', @@ -39,6 +40,7 @@ describe("RaftCore's published catalogue", () => { const records = await catalogue.records(); expect(writeDevTypeJson(records.records.get("LSM6DS3") ?? null)).toBe('{"addresses":"0x6a","m":{"12.5":1,"26":2}}'); expect(records.raftCore).toBe("1.61.0"); + expect(Array.from(records.sets?.entries() ?? [])).toEqual([["common", ["MCP9808"]]]); expect((await catalogue.recordFile("MCP9808")).text).toContain("0x18"); await expect(catalogue.recordFile("Nope")).rejects.toThrow("no record"); await expect(RaftCoreCatalogue.open(fetchFn, "v9.9.9", SITE)).rejects.toThrow("HTTP 404"); diff --git a/src/RaftDeviceTypeCatalogue.ts b/src/RaftDeviceTypeCatalogue.ts index 7e3a1fa..5e7344a 100644 --- a/src/RaftDeviceTypeCatalogue.ts +++ b/src/RaftDeviceTypeCatalogue.ts @@ -184,6 +184,7 @@ export interface DeviceTypeCatalogueInfo { recordFormat?: number; count: number; records: DeviceTypeCatalogueEntry[]; + sets?: Record; // named sets of records ("raftcore:@"), from RaftCore 1.63.0 } export class RaftCoreCatalogue { @@ -238,7 +239,7 @@ export class RaftCoreCatalogue { throw new DeviceTypeSourceError("RaftCore catalogue: DeviceTypeRecords.json has no devTypes"); this._records = devTypes; } - return { records: this._records, raftCore: this.raftCore }; + return { records: this._records, raftCore: this.raftCore, sets: new Map(Object.entries(this.info.sets ?? {})) }; } // One record's file, as it is in RaftCore diff --git a/src/RaftDeviceTypeSources.test.ts b/src/RaftDeviceTypeSources.test.ts index f518790..5e5ed1f 100644 --- a/src/RaftDeviceTypeSources.test.ts +++ b/src/RaftDeviceTypeSources.test.ts @@ -30,7 +30,8 @@ describe("selection vectors", () => { const merged = mergeDeviceTypeSources(Object.entries(vec.files).map(([name, content]) => ({ name, content: content as string }))); const sources = new Map(); for (const [name, source] of Object.entries(vec.sources)) - sources.set(name, { records: recordsOf(source.records), raftCore: source.kind === "bundled" ? vectorsFile.raftCore : undefined }); + sources.set(name, { records: recordsOf(source.records), raftCore: source.kind === "bundled" ? vectorsFile.raftCore : undefined, + sets: source.sets ? new Map(Object.entries(source.sets)) : undefined }); return resolveDeviceTypeSelection(merged, vec.section as DeviceTypeSection, sources); }; if (vec.error !== undefined) { diff --git a/src/RaftDeviceTypeSources.ts b/src/RaftDeviceTypeSources.ts index bf4c9e8..d600c86 100644 --- a/src/RaftDeviceTypeSources.ts +++ b/src/RaftDeviceTypeSources.ts @@ -58,6 +58,7 @@ export interface DeviceTypeSourceRecords { records: Map; commit?: string; // a git source: the commit its ref resolved to raftCore?: string; // RaftCore's records: its version + sets?: Map; // RaftCore's records: its named sets (devtypes/sets), selected as ":@" } export interface DeviceTypeSelectionResult { @@ -188,7 +189,25 @@ export function resolveDeviceTypeSelection(merged: MergedDeviceTypeSources, sect sources.set(name, { records: loaded.records, isLocal: kind === "path" }); } const names = Array.from(sources.keys()); + const bundled = names.filter((n) => deviceTypeSourceKind(merged.sources.get(n) as DevTypeJsonObject) === "bundled"); const chosen = merged.sections[section]; + + // The keys of a named set (":@", RaftCore's records only) + const setKeys = (pattern: string, source: string | undefined, key: string, listName: string): string[] => { + if (source === undefined || !bundled.includes(source)) + throw new DeviceTypeSelectionError(`${section}.${listName} "${pattern}": named sets (@...) are only in RaftCore's records - ` + + `use ":${key}"`); + const sets = sourceRecords.get(source)?.sets ?? new Map(); + const keys = sets.get(key.slice(1)); + if (keys === undefined) + throw new DeviceTypeSelectionError(`${section}.${listName} "${pattern}": no set "${key.slice(1)}" in RaftCore's records ` + + `(sets: ${Array.from(sets.keys()).sort().join(", ") || "none"})`); + const records = (sources.get(source) as { records: Map }).records; + const missing = keys.filter((k) => !records.has(k)); + if (missing.length > 0) + throw new DeviceTypeSelectionError(`set "${key.slice(1)}" names ${missing.join(", ")}, not in RaftCore's records`); + return keys; + }; // builtin includes everything unless it says otherwise; a type file includes nothing unless it says otherwise const includes = chosen.hasInclude ? chosen.include : (section === "builtin" ? ["*"] : []); @@ -200,24 +219,26 @@ export function resolveDeviceTypeSelection(merged: MergedDeviceTypeSources, sect let matched = false; for (const name of source ? [source] : names) { const records = (sources.get(name) as { records: Map }).records; - const keys = key === "*" ? Array.from(records.keys()) : (records.has(key) ? [key] : []); + const keys = key.startsWith("@") ? setKeys(pattern, source, key, "include") : + (key === "*" ? Array.from(records.keys()) : (records.has(key) ? [key] : [])); for (const k of keys) { if (!candidates.has(k)) candidates.set(k, { named: [], any: [] }); const entry = candidates.get(k) as { named: string[]; any: string[] }; - addTo(source && key !== "*" ? entry.named : entry.any, name); + addTo(source && key !== "*" && !key.startsWith("@") ? entry.named : entry.any, name); matched = true; } } - if (!matched && key !== "*") { + if (!matched && key !== "*" && !key.startsWith("@")) { const where = source ? `source "${source}"` : "any source"; throw new DeviceTypeSelectionError(`${section}.include "${pattern}": no record "${key}" in ${where}`); } } for (const pattern of chosen.exclude) { const [source, key] = parsePattern(pattern, names); + const excluded = key.startsWith("@") ? new Set(setKeys(pattern, source, key, "exclude")) : undefined; for (const k of Array.from(candidates.keys())) { - if (key === "*" || k === key) { + if (key === "*" || k === key || (excluded !== undefined && excluded.has(k))) { const entry = candidates.get(k) as { named: string[]; any: string[] }; if (source === undefined) { entry.named = []; diff --git a/testdata/selection_vectors.json b/testdata/selection_vectors.json index 84e3a6e..7b66463 100644 --- a/testdata/selection_vectors.json +++ b/testdata/selection_vectors.json @@ -585,6 +585,248 @@ "policyOnly": {}, "messages": [], "provenance": "{\"formatVersion\":1,\"sources\":{\"raftcore\":{\"bundled\":true,\"raftCore\":\"1.62.0\"}},\"typeFile\":{\"include\":[\"raftcore:Alpha\"],\"exclude\":[\"local:*\"]}}" + }, + { + "name": "a named set", + "files": { + "systypes/Common/DeviceTypeSources.json": "{\"sources\": {\"raftcore\": {\"bundled\": true}}, \"builtin\": {\"include\": [\"raftcore:@small\"]}}" + }, + "sources": { + "raftcore": { + "kind": "bundled", + "records": "{\"devTypes\": {\"Alpha\": {\"addresses\": \"0x11\", \"detectionValues\": \"0x00=0x12\", \"devInfoJson\": {\"name\": \"Test\"}}, \"Bravo\": {\"addresses\": \"0x10\", \"detectionValues\": \"0x00=0x12\", \"devInfoJson\": {\"name\": \"Bravo core\"}}, \"Charlie\": {\"addresses\": \"0x12\", \"detectionValues\": \"0x00=0x12\", \"devInfoJson\": {\"name\": \"Test\"}}}}", + "sets": { + "small": [ + "Alpha", + "Charlie" + ], + "one": [ + "Charlie" + ], + "stale": [ + "Alpha", + "Zulu" + ] + } + } + }, + "section": "builtin", + "records": { + "Alpha": { + "source": "raftcore", + "record": "{\"addresses\":\"0x11\",\"detectionValues\":\"0x00=0x12\",\"devInfoJson\":{\"name\":\"Test\"}}" + }, + "Charlie": { + "source": "raftcore", + "record": "{\"addresses\":\"0x12\",\"detectionValues\":\"0x00=0x12\",\"devInfoJson\":{\"name\":\"Test\"}}" + } + }, + "replaced": {}, + "policyOnly": {}, + "messages": [], + "provenance": "{\"formatVersion\":1,\"sources\":{\"raftcore\":{\"bundled\":true,\"raftCore\":\"1.62.0\"}},\"builtin\":{\"include\":[\"raftcore:@small\"]}}" + }, + { + "name": "excluding a named set", + "files": { + "systypes/Common/DeviceTypeSources.json": "{\"sources\": {\"raftcore\": {\"bundled\": true}}, \"builtin\": {\"include\": [\"raftcore:*\"], \"exclude\": [\"raftcore:@one\"]}}" + }, + "sources": { + "raftcore": { + "kind": "bundled", + "records": "{\"devTypes\": {\"Alpha\": {\"addresses\": \"0x11\", \"detectionValues\": \"0x00=0x12\", \"devInfoJson\": {\"name\": \"Test\"}}, \"Bravo\": {\"addresses\": \"0x10\", \"detectionValues\": \"0x00=0x12\", \"devInfoJson\": {\"name\": \"Bravo core\"}}, \"Charlie\": {\"addresses\": \"0x12\", \"detectionValues\": \"0x00=0x12\", \"devInfoJson\": {\"name\": \"Test\"}}}}", + "sets": { + "small": [ + "Alpha", + "Charlie" + ], + "one": [ + "Charlie" + ], + "stale": [ + "Alpha", + "Zulu" + ] + } + } + }, + "section": "builtin", + "records": { + "Alpha": { + "source": "raftcore", + "record": "{\"addresses\":\"0x11\",\"detectionValues\":\"0x00=0x12\",\"devInfoJson\":{\"name\":\"Test\"}}" + }, + "Bravo": { + "source": "raftcore", + "record": "{\"addresses\":\"0x10\",\"detectionValues\":\"0x00=0x12\",\"devInfoJson\":{\"name\":\"Bravo core\"}}" + } + }, + "replaced": {}, + "policyOnly": {}, + "messages": [], + "provenance": "{\"formatVersion\":1,\"sources\":{\"raftcore\":{\"bundled\":true,\"raftCore\":\"1.62.0\"}},\"builtin\":{\"include\":[\"raftcore:*\"],\"exclude\":[\"raftcore:@one\"]}}" + }, + { + "name": "a named set in a type file, with a local copy replacing one record", + "files": { + "systypes/Common/DeviceTypeSources.json": "{\"sources\": {\"raftcore\": {\"bundled\": true}, \"local\": {\"path\": \"../../local\"}}, \"typeFile\": {\"include\": [\"raftcore:@small\", \"local:*\"]}}" + }, + "sources": { + "raftcore": { + "kind": "bundled", + "records": "{\"devTypes\": {\"Alpha\": {\"addresses\": \"0x11\", \"detectionValues\": \"0x00=0x12\", \"devInfoJson\": {\"name\": \"Test\"}}, \"Bravo\": {\"addresses\": \"0x10\", \"detectionValues\": \"0x00=0x12\", \"devInfoJson\": {\"name\": \"Bravo core\"}}, \"Charlie\": {\"addresses\": \"0x12\", \"detectionValues\": \"0x00=0x12\", \"devInfoJson\": {\"name\": \"Test\"}}}}", + "sets": { + "small": [ + "Alpha", + "Charlie" + ], + "one": [ + "Charlie" + ], + "stale": [ + "Alpha", + "Zulu" + ] + } + }, + "local": { + "kind": "path", + "records": "{\"devTypes\": {\"Alpha\": {\"addresses\": \"0x11\", \"detectionValues\": \"0x00=0x12\", \"devInfoJson\": {\"name\": \"Alpha local\"}}}}" + } + }, + "section": "typeFile", + "records": { + "Alpha": { + "source": "local", + "record": "{\"addresses\":\"0x11\",\"detectionValues\":\"0x00=0x12\",\"devInfoJson\":{\"name\":\"Alpha local\"}}" + }, + "Charlie": { + "source": "raftcore", + "record": "{\"addresses\":\"0x12\",\"detectionValues\":\"0x00=0x12\",\"devInfoJson\":{\"name\":\"Test\"}}" + } + }, + "replaced": { + "Alpha": [ + "raftcore" + ] + }, + "policyOnly": {}, + "messages": [ + "\"Alpha\" from local replaces the record from raftcore" + ], + "provenance": "{\"formatVersion\":1,\"sources\":{\"raftcore\":{\"bundled\":true,\"raftCore\":\"1.62.0\"},\"local\":{\"path\":\"../../local\"}},\"typeFile\":{\"include\":[\"raftcore:@small\",\"local:*\"]}}" + }, + { + "name": "error: no such set", + "files": { + "systypes/Common/DeviceTypeSources.json": "{\"sources\": {\"raftcore\": {\"bundled\": true}}, \"builtin\": {\"include\": [\"raftcore:@nope\"]}}" + }, + "sources": { + "raftcore": { + "kind": "bundled", + "records": "{\"devTypes\": {\"Alpha\": {\"addresses\": \"0x11\", \"detectionValues\": \"0x00=0x12\", \"devInfoJson\": {\"name\": \"Test\"}}, \"Bravo\": {\"addresses\": \"0x10\", \"detectionValues\": \"0x00=0x12\", \"devInfoJson\": {\"name\": \"Bravo core\"}}, \"Charlie\": {\"addresses\": \"0x12\", \"detectionValues\": \"0x00=0x12\", \"devInfoJson\": {\"name\": \"Test\"}}}}", + "sets": { + "small": [ + "Alpha", + "Charlie" + ], + "one": [ + "Charlie" + ], + "stale": [ + "Alpha", + "Zulu" + ] + } + } + }, + "section": "builtin", + "error": "builtin.include \"raftcore:@nope\": no set \"nope\" in RaftCore's records (sets: one, small, stale)" + }, + { + "name": "error: a set naming a record the source hasn't", + "files": { + "systypes/Common/DeviceTypeSources.json": "{\"sources\": {\"raftcore\": {\"bundled\": true}}, \"builtin\": {\"include\": [\"raftcore:@stale\"]}}" + }, + "sources": { + "raftcore": { + "kind": "bundled", + "records": "{\"devTypes\": {\"Alpha\": {\"addresses\": \"0x11\", \"detectionValues\": \"0x00=0x12\", \"devInfoJson\": {\"name\": \"Test\"}}, \"Bravo\": {\"addresses\": \"0x10\", \"detectionValues\": \"0x00=0x12\", \"devInfoJson\": {\"name\": \"Bravo core\"}}, \"Charlie\": {\"addresses\": \"0x12\", \"detectionValues\": \"0x00=0x12\", \"devInfoJson\": {\"name\": \"Test\"}}}}", + "sets": { + "small": [ + "Alpha", + "Charlie" + ], + "one": [ + "Charlie" + ], + "stale": [ + "Alpha", + "Zulu" + ] + } + } + }, + "section": "builtin", + "error": "set \"stale\" names Zulu, not in RaftCore's records" + }, + { + "name": "error: a set without a source", + "files": { + "systypes/Common/DeviceTypeSources.json": "{\"sources\": {\"raftcore\": {\"bundled\": true}}, \"builtin\": {\"include\": [\"@small\"]}}" + }, + "sources": { + "raftcore": { + "kind": "bundled", + "records": "{\"devTypes\": {\"Alpha\": {\"addresses\": \"0x11\", \"detectionValues\": \"0x00=0x12\", \"devInfoJson\": {\"name\": \"Test\"}}, \"Bravo\": {\"addresses\": \"0x10\", \"detectionValues\": \"0x00=0x12\", \"devInfoJson\": {\"name\": \"Bravo core\"}}, \"Charlie\": {\"addresses\": \"0x12\", \"detectionValues\": \"0x00=0x12\", \"devInfoJson\": {\"name\": \"Test\"}}}}", + "sets": { + "small": [ + "Alpha", + "Charlie" + ], + "one": [ + "Charlie" + ], + "stale": [ + "Alpha", + "Zulu" + ] + } + } + }, + "section": "builtin", + "error": "builtin.include \"@small\": named sets (@...) are only in RaftCore's records - use \":@small\"" + }, + { + "name": "error: a set from a path source", + "files": { + "systypes/Common/DeviceTypeSources.json": "{\"sources\": {\"raftcore\": {\"bundled\": true}, \"local\": {\"path\": \"../../local\"}}, \"builtin\": {\"include\": [\"local:@small\"]}}" + }, + "sources": { + "raftcore": { + "kind": "bundled", + "records": "{\"devTypes\": {\"Alpha\": {\"addresses\": \"0x11\", \"detectionValues\": \"0x00=0x12\", \"devInfoJson\": {\"name\": \"Test\"}}, \"Bravo\": {\"addresses\": \"0x10\", \"detectionValues\": \"0x00=0x12\", \"devInfoJson\": {\"name\": \"Bravo core\"}}, \"Charlie\": {\"addresses\": \"0x12\", \"detectionValues\": \"0x00=0x12\", \"devInfoJson\": {\"name\": \"Test\"}}}}", + "sets": { + "small": [ + "Alpha", + "Charlie" + ], + "one": [ + "Charlie" + ], + "stale": [ + "Alpha", + "Zulu" + ] + } + }, + "local": { + "kind": "path", + "records": "{\"devTypes\": {\"Delta\": {\"addresses\": \"0x13\", \"detectionValues\": \"0x00=0x12\", \"devInfoJson\": {\"name\": \"Test\"}}, \"Echo\": {\"addresses\": \"0x14\", \"detectionValues\": \"0x00=0x12\", \"devInfoJson\": {\"name\": \"Test\"}}}}" + } + }, + "section": "builtin", + "error": "builtin.include \"local:@small\": named sets (@...) are only in RaftCore's records - use \":@small\"" } ] }