diff --git a/.eslintrc.js b/.eslintrc.js index dd139b4f6..37fa88490 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -32,6 +32,7 @@ module.exports = { "@typescript-eslint/prefer-nullish-coalescing": "error", "unused-imports/no-unused-imports": "error", "@typescript-eslint/strict-boolean-expressions": "error", + "guard-for-in": "error", "unused-imports/no-unused-vars": [ "warn", { diff --git a/packages/binding-coap/src/mdns-introducer.ts b/packages/binding-coap/src/mdns-introducer.ts index 05876ede8..e2979881c 100644 --- a/packages/binding-coap/src/mdns-introducer.ts +++ b/packages/binding-coap/src/mdns-introducer.ts @@ -73,8 +73,8 @@ export class MdnsIntroducer { private determineTarget(): string { const interfaces = networkInterfaces(); - for (const iface in interfaces) { - for (const entry of interfaces[iface] ?? []) { + for (const iface of Object.values(interfaces ?? {})) { + for (const entry of iface ?? []) { if (entry.internal === false) { if (entry.family === this.ipAddressFamily) { return entry.address; diff --git a/packages/binding-http/src/http-browser.ts b/packages/binding-http/src/http-browser.ts index 712f325c3..05c6392bc 100644 --- a/packages/binding-http/src/http-browser.ts +++ b/packages/binding-http/src/http-browser.ts @@ -54,8 +54,8 @@ export class HttpForm extends TD.Form { Headers.prototype.raw = function () { const result: { [key: string]: string[] } = {}; - for (const h in this.entries()) { - result[h[0]] = h[1].split(","); + for (const [headerKey, headerValue] of this.entries()) { + result[headerKey] = headerValue.split(","); } return result; }; diff --git a/packages/binding-http/src/routes/thing-description.ts b/packages/binding-http/src/routes/thing-description.ts index ef1c6618a..74759ed9b 100644 --- a/packages/binding-http/src/routes/thing-description.ts +++ b/packages/binding-http/src/routes/thing-description.ts @@ -26,35 +26,31 @@ function resetMultiLangInteraction( interactions: ThingDescription["properties"] | ThingDescription["actions"] | ThingDescription["events"], prefLang: string ) { - if (interactions) { - for (const interName in interactions) { + if (interactions != null) { + for (const interaction of Object.values(interactions)) { // unset any current title and/or description - delete interactions[interName].title; - delete interactions[interName].description; + delete interaction.title; + delete interaction.description; // use new language title - const titles = interactions[interName].titles; - if (titles) { - for (const titleLang in titles) { - if (titleLang.startsWith(prefLang)) { - interactions[interName].title = titles[titleLang]; - } + for (const [titleLang, titleValue] of Object.entries(interaction.titles ?? {})) { + if (titleLang.startsWith(prefLang)) { + interaction.title = titleValue; + break; } } // use new language description - const descriptions = interactions[interName].descriptions; - if (descriptions) { - for (const descLang in descriptions) { - if (descLang.startsWith(prefLang)) { - interactions[interName].description = descriptions[descLang]; - } + for (const [descLang, descriptionValue] of Object.entries(interaction.descriptions ?? {})) { + if (descLang.startsWith(prefLang)) { + interaction.description = descriptionValue; + break; } } // unset any multilanguage titles and/or descriptions - delete interactions[interName].titles; - delete interactions[interName].descriptions; + delete interaction.titles; + delete interaction.descriptions; } } } diff --git a/packages/binding-mqtt/src/mqtt-broker-server.ts b/packages/binding-mqtt/src/mqtt-broker-server.ts index af092ae13..3821bc88b 100644 --- a/packages/binding-mqtt/src/mqtt-broker-server.ts +++ b/packages/binding-mqtt/src/mqtt-broker-server.ts @@ -108,15 +108,15 @@ export default class MqttBrokerServer implements ProtocolServer { this.things.set(name, thing); - for (const propertyName in thing.properties) { + for (const propertyName of Object.keys(thing.properties)) { this.exposeProperty(name, propertyName, thing); } - for (const actionName in thing.actions) { + for (const actionName of Object.keys(thing.actions)) { this.exposeAction(name, actionName, thing); } - for (const eventName in thing.events) { + for (const eventName of Object.keys(thing.events)) { this.exposeEvent(name, eventName, thing); } diff --git a/packages/binding-netconf/src/codecs/netconf-codec.ts b/packages/binding-netconf/src/codecs/netconf-codec.ts index fc33c21cd..a0af778e0 100644 --- a/packages/binding-netconf/src/codecs/netconf-codec.ts +++ b/packages/binding-netconf/src/codecs/netconf-codec.ts @@ -126,8 +126,9 @@ export default class NetconfCodec { let nsFound = false; let aliasNs = ""; let value; - for (const key in properties) { - const el = properties[key]; + // TODO: Use correct type for el + // eslint-disable-next-line @typescript-eslint/no-explicit-any + for (const [key, el] of Object.entries(properties) as [string, any]) { const payloadField = payload[key]; if (payloadField == null) { throw new Error(`Payload is missing '${key}' field specified in TD`); diff --git a/packages/binding-websockets/src/ws-server.ts b/packages/binding-websockets/src/ws-server.ts index 049428fe8..d3b7bd2fc 100644 --- a/packages/binding-websockets/src/ws-server.ts +++ b/packages/binding-websockets/src/ws-server.ts @@ -120,8 +120,8 @@ export default class WebSocketServer implements ProtocolServer { public stop(): Promise { debug(`WebSocketServer stopping on port ${this.port}`); return new Promise((resolve, reject) => { - for (const pathSocket in this.socketServers) { - this.socketServers[pathSocket].close(); + for (const socketServer of Object.values(this.socketServers)) { + socketServer.close(); } // stop promise handles all errors from now on @@ -162,7 +162,7 @@ export default class WebSocketServer implements ProtocolServer { // TODO more efficient routing to ExposedThing without ResourceListeners in each server - for (const propertyName in thing.properties) { + for (const [propertyName, property] of Object.entries(thing.properties)) { const path = "/" + encodeURIComponent(urlPath) + @@ -170,7 +170,6 @@ export default class WebSocketServer implements ProtocolServer { this.PROPERTY_DIR + "/" + encodeURIComponent(propertyName); - const property = thing.properties[propertyName]; // Populate forms related to the property for (const address of Helpers.getAddresses()) { @@ -231,26 +230,22 @@ export default class WebSocketServer implements ProtocolServer { }); } - for (const actionName in thing.actions) { + for (const [actionName, action] of Object.entries(thing.actions)) { const path = "/" + encodeURIComponent(urlPath) + "/" + this.ACTION_DIR + "/" + encodeURIComponent(actionName); - // eslint-disable-next-line unused-imports/no-unused-vars - const action = thing.actions[actionName]; for (const address of Helpers.getAddresses()) { const href = `${this.scheme}://${address}:${this.getPort()}${path}`; const form = new TD.Form(href, ContentSerdes.DEFAULT); form.op = ["invokeaction"]; - thing.actions[actionName].forms.push(form); + action.forms.push(form); debug(`WebSocketServer on port ${this.getPort()} assigns '${href}' to Action '${actionName}'`); } } - for (const eventName in thing.events) { + for (const [eventName, event] of Object.entries(thing.events)) { const path = "/" + encodeURIComponent(urlPath) + "/" + this.EVENT_DIR + "/" + encodeURIComponent(eventName); - // eslint-disable-next-line unused-imports/no-unused-vars - const event = thing.events[eventName]; // Populate forms related to the event for (const address of Helpers.getAddresses()) { diff --git a/packages/core/src/codecs/netconf-codec.ts b/packages/core/src/codecs/netconf-codec.ts index 45f793e66..05552cef0 100644 --- a/packages/core/src/codecs/netconf-codec.ts +++ b/packages/core/src/codecs/netconf-codec.ts @@ -79,8 +79,9 @@ export default class NetconfCodec implements ContentCodec { let nsFound = false; let aliasNs = ""; let value; - for (const key in properties) { - const el = properties[key]; + // TODO: Use correct type for el + // eslint-disable-next-line @typescript-eslint/no-explicit-any + for (const [key, el] of Object.entries(properties) as [string, any]) { if (payload[key] == null) { throw new Error(`Payload is missing '${key}' field specified in TD`); } diff --git a/packages/core/src/codecs/octetstream-codec.ts b/packages/core/src/codecs/octetstream-codec.ts index e1bb481c6..aae832c51 100644 --- a/packages/core/src/codecs/octetstream-codec.ts +++ b/packages/core/src/codecs/octetstream-codec.ts @@ -604,11 +604,12 @@ export default class OctetstreamCodec implements ContentCodec { } result = result ?? Buffer.alloc(length); - for (const propertyName in schema.properties) { + // TODO: Use correct type for propertySchema + // eslint-disable-next-line @typescript-eslint/no-explicit-any + for (const [propertyName, propertySchema] of Object.entries(schema.properties) as [string, any]) { if (Object.hasOwnProperty.call(value, propertyName) === false) { throw new Error(`Missing property '${propertyName}'`); } - const propertySchema = schema.properties[propertyName]; const propertyValue = value[propertyName]; const propertyOffset = parseInt(propertySchema["ex:bitOffset"]); const propertyLength = parseInt(propertySchema["ex:bitLength"]); diff --git a/packages/core/src/consumed-thing.ts b/packages/core/src/consumed-thing.ts index 445373ae8..6b5e42063 100644 --- a/packages/core/src/consumed-thing.ts +++ b/packages/core/src/consumed-thing.ts @@ -381,19 +381,16 @@ export default class ConsumedThing extends TD.Thing implements IConsumedThing { } extendInteractions(): void { - for (const propertyName in this.properties) { - const newProp = Helpers.extend( - this.properties[propertyName], - new ConsumedThingProperty(propertyName, this) - ); + for (const [propertyName, property] of Object.entries(this.properties)) { + const newProp = Helpers.extend(property, new ConsumedThingProperty(propertyName, this)); this.properties[propertyName] = newProp; } - for (const actionName in this.actions) { - const newAction = Helpers.extend(this.actions[actionName], new ConsumedThingAction(actionName, this)); + for (const [actionName, action] of Object.entries(this.actions)) { + const newAction = Helpers.extend(action, new ConsumedThingAction(actionName, this)); this.actions[actionName] = newAction; } - for (const eventName in this.events) { - const newEvent = Helpers.extend(this.events[eventName], new ConsumedThingEvent(eventName, this)); + for (const [eventName, event] of Object.entries(this.events)) { + const newEvent = Helpers.extend(event, new ConsumedThingEvent(eventName, this)); this.events[eventName] = newEvent; } } @@ -612,14 +609,15 @@ export default class ConsumedThing extends TD.Thing implements IConsumedThing { readAllProperties(options?: WoT.InteractionOptions): Promise { const propertyNames: string[] = []; - for (const propertyName in this.properties) { + + for (const [propertyName, property] of Object.entries(this.properties)) { // collect attributes that are "readable" only - const tp = this.properties[propertyName]; - const { form } = this.getClientFor(tp.forms, "readproperty", Affordance.PropertyAffordance, options); + const { form } = this.getClientFor(property.forms, "readproperty", Affordance.PropertyAffordance, options); if (form != null) { propertyNames.push(propertyName); } } + return this._readProperties(propertyNames); } @@ -656,9 +654,7 @@ export default class ConsumedThing extends TD.Thing implements IConsumedThing { async writeMultipleProperties(valueMap: WoT.PropertyWriteMap, options?: WoT.InteractionOptions): Promise { // collect all single promises into array const promises: Promise[] = []; - for (const propertyName in valueMap) { - // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - const value = valueMap.get(propertyName)!; + for (const [propertyName, value] of valueMap.entries()) { promises.push(this.writeProperty(propertyName, value)); } // wait for all promises to succeed and create response diff --git a/packages/core/src/exposed-thing.ts b/packages/core/src/exposed-thing.ts index 57074c84d..b8c44b778 100644 --- a/packages/core/src/exposed-thing.ts +++ b/packages/core/src/exposed-thing.ts @@ -458,10 +458,7 @@ export default class ExposedThing extends TD.Thing implements WoT.ExposedThing { public async handleReadAllProperties( options: WoT.InteractionOptions & { formIndex: number } ): Promise { - const propertyNames: string[] = []; - for (const propertyName in this.properties) { - propertyNames.push(propertyName); - } + const propertyNames = Object.keys(this.properties); return await this._handleReadProperties(propertyNames, options); } @@ -513,16 +510,15 @@ export default class ExposedThing extends TD.Thing implements WoT.ExposedThing { ): Promise { // collect all single promises into array const promises: Promise[] = []; - for (const propertyName in valueMap) { + for (const [propertyName, property] of Object.entries(valueMap)) { // Note: currently only DataSchema properties are supported const form = this.properties[propertyName].forms.find( (form) => form.contentType === "application/json" || form.contentType == null ); - if (!form) { + if (form == null) { continue; } - // eslint-disable-next-line @typescript-eslint/no-non-null-assertion -- we know that the property exists - promises.push(this.handleWriteProperty(propertyName, valueMap.get(propertyName)!, options)); + promises.push(this.handleWriteProperty(propertyName, property, options)); } try { await Promise.all(promises); diff --git a/packages/core/src/helpers.ts b/packages/core/src/helpers.ts index a02a389e1..0514698ca 100644 --- a/packages/core/src/helpers.ts +++ b/packages/core/src/helpers.ts @@ -87,8 +87,8 @@ export default class Helpers implements Resolver { } else { const interfaces = os.networkInterfaces(); - for (const iface in interfaces) { - interfaces[iface]?.forEach((entry) => { + for (const iface of Object.values(interfaces)) { + iface?.forEach((entry) => { debug(`AddressHelper found ${entry.address}`); if (entry.internal === false) { if (entry.family === "IPv4") { @@ -197,12 +197,12 @@ export default class Helpers implements Resolver { */ public static extend(first: T, second: U): T & U { const result = {}; - for (const id in first) { - (>result)[id] = (>first)[id]; + for (const [id, value] of Object.entries(first as Record)) { + (>result)[id] = value; } - for (const id in second) { + for (const [id, value] of Object.entries(second as Record)) { if (!Object.prototype.hasOwnProperty.call(result, id)) { - (>result)[id] = (>second)[id]; + (>result)[id] = value; } } return result; @@ -242,9 +242,9 @@ export default class Helpers implements Resolver { } } - if (tdSchemaCopy.definitions !== undefined) { - for (const prop in tdSchemaCopy.definitions) { - tdSchemaCopy.definitions[prop] = this.createExposeThingInitSchema(tdSchemaCopy.definitions[prop]); + if (tdSchemaCopy.definitions != null) { + for (const [prop, propValue] of Object.entries(tdSchemaCopy.definitions) ?? []) { + tdSchemaCopy.definitions[prop] = this.createExposeThingInitSchema(propValue); } } @@ -307,9 +307,7 @@ export default class Helpers implements Resolver { options = { uriVariables: {} }; } - for (const varKey in thingUriVariables) { - const varValue = thingUriVariables[varKey]; - + for (const [varKey, varValue] of Object.entries(thingUriVariables)) { if (!(varKey in uriVariables) && "default" in varValue) { uriVariables[varKey] = varValue.default; } diff --git a/packages/core/src/servient.ts b/packages/core/src/servient.ts index a92bd238e..99261b4d2 100644 --- a/packages/core/src/servient.ts +++ b/packages/core/src/servient.ts @@ -54,20 +54,20 @@ export default class Servient { // initializing forms fields thing.forms = []; - for (const name in thing.properties) { + for (const property of Object.values(thing.properties)) { // eslint-disable-next-line @typescript-eslint/ban-ts-comment // @ts-ignore - thing.properties[name].forms = []; + property.forms = []; } - for (const name in thing.actions) { + for (const action of Object.values(thing.actions)) { // eslint-disable-next-line @typescript-eslint/ban-ts-comment // @ts-ignore - thing.actions[name].forms = []; + action.forms = []; } - for (const name in thing.events) { + for (const event of Object.values(thing.events)) { // eslint-disable-next-line @typescript-eslint/ban-ts-comment // @ts-ignore - thing.events[name].forms = []; + event.forms = []; } const serverPromises: Promise[] = []; @@ -178,16 +178,13 @@ export default class Servient { } public addCredentials(credentials: Record): void { - if (typeof credentials === "object") { - for (const i in credentials) { - debug(`Servient storing credentials for '${i}'`); - let currentCredentials = this.credentialStore.get(i); - if (!currentCredentials) { - currentCredentials = []; - this.credentialStore.set(i, currentCredentials); - } - currentCredentials.push(credentials[i]); + for (const [credentialKey, credentialValue] of Object.entries(credentials ?? {})) { + debug(`Servient storing credentials for '${credentialKey}'`); + const currentCredentials = this.credentialStore.get(credentialKey) ?? []; + if (currentCredentials.length === 0) { + this.credentialStore.set(credentialKey, currentCredentials); } + currentCredentials.push(credentialValue); } } diff --git a/packages/td-tools/src/td-parser.ts b/packages/td-tools/src/td-parser.ts index 886a536b3..03d24dba0 100644 --- a/packages/td-tools/src/td-parser.ts +++ b/packages/td-tools/src/td-parser.ts @@ -19,7 +19,7 @@ import * as TDHelpers from "./td-helpers"; import isAbsoluteUrl = require("is-absolute-url"); import URLToolkit = require("url-toolkit"); -import { ThingContext } from "wot-thing-description-types"; +import { ThingContext, PropertyElement, ActionElement, EventElement } from "wot-thing-description-types"; // TODO: Refactor and reuse debug solution from core package import debug from "debug"; @@ -27,6 +27,34 @@ const namespace = "node-wot:td-tools:td-parser"; const logDebug = debug(`${namespace}:debug`); const logWarn = debug(`${namespace}:warn`); +type AffordanceElement = PropertyElement | ActionElement | EventElement; + +/** + * Initializes the affordances field of a thing with an empty object if its + * type should be incorrect or undefined. + * + * This avoids potential errors that could occur due to an undefined + * affordance field. + * + * @param thing The Thing whose affordance field is being adjusted. + * @param affordanceKey The key of the affordance field. + */ +function adjustAffordanceField(thing: Thing, affordanceKey: string) { + const affordance = thing[affordanceKey]; + + if (typeof affordance !== "object" || affordance == null) { + thing[affordanceKey] = {}; + } +} + +function adjustBooleanField(affordance: AffordanceElement, key: string) { + const currentValue = affordance[key]; + + if (currentValue === undefined || typeof currentValue !== "boolean") { + affordance[key] = false; + } +} + /** Parses a TD into a Thing object */ export function parseTD(td: string, normalize?: boolean): Thing { logDebug(`parseTD() parsing\n\`\`\`\n${td}\n\`\`\``); @@ -98,42 +126,20 @@ export function parseTD(td: string, normalize?: boolean): Thing { thing["@type"] = [TD.DEFAULT_THING_TYPE, semType]; } - if (thing.properties !== undefined && thing.properties instanceof Object) { - for (const propName in thing.properties) { - const prop: TD.ThingProperty = thing.properties[propName]; - if (prop.readOnly === undefined || typeof prop.readOnly !== "boolean") { - prop.readOnly = false; - } - if (prop.writeOnly === undefined || typeof prop.writeOnly !== "boolean") { - prop.writeOnly = false; - } - if (prop.observable === undefined || typeof prop.observable !== "boolean") { - prop.observable = false; - } + for (const property of Object.values(thing.properties ?? {})) { + for (const key of ["readOnly", "writeOnly", "observable"]) { + adjustBooleanField(property, key); } } - if (thing.actions !== undefined && thing.actions instanceof Object) { - for (const actName in thing.actions) { - const act: TD.ThingAction = thing.actions[actName]; - if (act.safe === undefined || typeof act.safe !== "boolean") { - act.safe = false; - } - if (act.idempotent === undefined || typeof act.idempotent !== "boolean") { - act.idempotent = false; - } + for (const action of Object.values(thing.actions ?? {})) { + for (const key of ["safe", "idempotent"]) { + adjustBooleanField(action, key); } } - // avoid errors due to 'undefined' - if (typeof thing.properties !== "object" || thing.properties === null) { - thing.properties = {}; - } - if (typeof thing.actions !== "object" || thing.actions === null) { - thing.actions = {}; - } - if (typeof thing.events !== "object" || thing.events === null) { - thing.events = {}; + for (const affordanceKey of ["properties", "actions", "events"]) { + adjustAffordanceField(thing, affordanceKey); } if (thing.security === undefined) { @@ -147,10 +153,9 @@ export function parseTD(td: string, normalize?: boolean): Thing { // collect all forms for normalization and use iterations also for checking const allForms = []; // properties - for (const propName in thing.properties) { - const prop: TD.ThingProperty = thing.properties[propName]; + for (const [propName, prop] of Object.entries(thing.properties ?? {})) { // ensure forms mandatory forms field - if (!prop.forms) { + if (prop.forms == null) { throw new Error(`Property '${propName}' has no forms field`); } for (const form of prop.forms) { @@ -165,10 +170,9 @@ export function parseTD(td: string, normalize?: boolean): Thing { } } // actions - for (const actName in thing.actions) { - const act: TD.ThingProperty = thing.actions[actName]; + for (const [actName, act] of Object.entries(thing.actions ?? {})) { // ensure forms mandatory forms field - if (!act.forms) { + if (act.forms == null) { throw new Error(`Action '${actName}' has no forms field`); } for (const form of act.forms) { @@ -183,10 +187,9 @@ export function parseTD(td: string, normalize?: boolean): Thing { } } // events - for (const evtName in thing.events) { - const evt: TD.ThingProperty = thing.events[evtName]; + for (const [evtName, evt] of Object.entries(thing.events ?? {})) { // ensure forms mandatory forms field - if (!evt.forms) { + if (evt.forms == null) { throw new Error(`Event '${evtName}' has no forms field`); } for (const form of evt.forms) { @@ -219,7 +222,7 @@ export function parseTD(td: string, normalize?: boolean): Thing { /** Serializes a Thing object into a TD */ export function serializeTD(thing: Thing): string { - const copy = JSON.parse(JSON.stringify(thing)); + const copy: Thing = JSON.parse(JSON.stringify(thing)); // clean-ups if (copy.security == null || copy.security.length === 0) { @@ -237,16 +240,9 @@ export function serializeTD(thing: Thing): string { delete copy.properties; } else if (copy.properties != null) { // add mandatory fields (if missing): observable, writeOnly, and readOnly - for (const propName in copy.properties) { - const prop = copy.properties[propName]; - if (prop.readOnly === undefined || typeof prop.readOnly !== "boolean") { - prop.readOnly = false; - } - if (prop.writeOnly === undefined || typeof prop.writeOnly !== "boolean") { - prop.writeOnly = false; - } - if (prop.observable === undefined || typeof prop.observable !== "boolean") { - prop.observable = false; + for (const property of Object.values(copy.properties)) { + for (const key of ["readOnly", "writeOnly", "observable"]) { + adjustBooleanField(property, key); } } } @@ -255,13 +251,9 @@ export function serializeTD(thing: Thing): string { delete copy.actions; } else if (copy.actions != null) { // add mandatory fields (if missing): idempotent and safe - for (const actName in copy.actions) { - const act = copy.actions[actName]; - if (act.idempotent === undefined || typeof act.idempotent !== "boolean") { - act.idempotent = false; - } - if (act.safe === undefined || typeof act.safe !== "boolean") { - act.safe = false; + for (const action of Object.values(copy.actions)) { + for (const key of ["safe", "idempotent"]) { + adjustBooleanField(action, key); } } } diff --git a/packages/td-tools/src/thing-description.ts b/packages/td-tools/src/thing-description.ts index adc234b37..a21dcb757 100644 --- a/packages/td-tools/src/thing-description.ts +++ b/packages/td-tools/src/thing-description.ts @@ -36,6 +36,12 @@ export default class Thing implements TDT.ThingDescription { // eslint-disable-next-line @typescript-eslint/no-explicit-any [key: string]: any; + properties?: { [k: string]: TDT.PropertyElement } | undefined; + + actions?: { [k: string]: TDT.ActionElement } | undefined; + + events?: { [k: string]: TDT.EventElement } | undefined; + constructor() { this["@context"] = [DEFAULT_CONTEXT_V1, DEFAULT_CONTEXT_V11]; this["@type"] = DEFAULT_THING_TYPE; diff --git a/packages/td-tools/src/thing-model-helpers.ts b/packages/td-tools/src/thing-model-helpers.ts index 3ccbce578..784243915 100644 --- a/packages/td-tools/src/thing-model-helpers.ts +++ b/packages/td-tools/src/thing-model-helpers.ts @@ -319,20 +319,17 @@ export class ThingModelHelpers { modelInput.imports = []; for (const affType of affordanceTypes) { const affRefs = ThingModelHelpers.getThingModelRef(data[affType] as DataSchema); - if (Object.keys(affRefs).length > 0) { - for (const aff in affRefs) { - const affUri = affRefs[aff] as string; - const refObj = this.parseTmRef(affUri); - if (refObj.uri == null) { - throw new Error(`Missing remote path in ${affUri}`); - } - let source = await this.fetchModel(refObj.uri); - [source] = await this._getPartialTDs(source); - delete (data[affType] as DataSchema)[aff]["tm:ref"]; - const importedAffordance = this.getRefAffordance(refObj, source) ?? {}; - refObj.name = aff; // update the name of the affordance - modelInput.imports.push({ affordance: importedAffordance, ...refObj }); + for (const [aff, affUri] of Object.entries(affRefs)) { + const refObj = this.parseTmRef(affUri); + if (refObj.uri == null) { + throw new Error(`Missing remote path in ${affUri}`); } + let source = await this.fetchModel(refObj.uri); + [source] = await this._getPartialTDs(source); + delete (data[affType] as DataSchema)[aff]["tm:ref"]; + const importedAffordance = this.getRefAffordance(refObj, source) ?? {}; + refObj.name = aff; // update the name of the affordance + modelInput.imports.push({ affordance: importedAffordance, ...refObj }); } } const tmLinks = ThingModelHelpers.getThingModelLinks(data, "tm:submodel"); @@ -383,8 +380,7 @@ export class ThingModelHelpers { if ("submodel" in modelObject) { const submodelObj = modelObject.submodel; - for (const key in submodelObj) { - const sub = submodelObj[key]; + for (const [key, sub] of Object.entries(submodelObj ?? {})) { if (options.selfComposition === true) { if (!data.links) { throw new Error( @@ -402,11 +398,9 @@ export class ThingModelHelpers { const [subPartialTD] = await this._getPartialTDs(sub, options); const affordanceTypes = ["properties", "actions", "events"]; for (const affType of affordanceTypes) { - for (const affKey in subPartialTD[affType] as DataSchema) { + for (const affKey of Object.keys((subPartialTD[affType] ?? {}) as DataSchema)) { + data[affType] ??= {} as DataSchema; const newAffKey = `${instanceName}_${affKey}`; - if (!(affType in data)) { - data[affType] = {} as DataSchema; - } (data[affType] as DataSchema)[newAffKey] = (subPartialTD[affType] as DataSchema)[ affKey ] as DataSchema; @@ -453,15 +447,15 @@ export class ThingModelHelpers { return tmpThingModels; } - private static getThingModelRef(data: Record): Record { - const refs = {} as Record; + private static getThingModelRef(data: Record): Record { + const refs = {} as Record; if (data == null) { return refs; } - for (const key in data) { - for (const key1 in data[key] as Record) { - if (key1 === "tm:ref") { - refs[key] = (data[key] as Record)["tm:ref"] as string; + for (const [key, value] of Object.entries(data)) { + for (const valueKey of Object.keys(value as Record)) { + if (valueKey === "tm:ref") { + refs[key] = (value as Record)["tm:ref"] as string; } } } diff --git a/packages/td-tools/test/TDParseTest.ts b/packages/td-tools/test/TDParseTest.ts index 6c336fe15..553fd124f 100644 --- a/packages/td-tools/test/TDParseTest.ts +++ b/packages/td-tools/test/TDParseTest.ts @@ -509,13 +509,14 @@ class TDParserTest { expect(thing).to.have.property("title").that.equals("MyTemperatureThing"); expect(thing).to.not.have.property("base"); - expect(thing.properties).to.have.property("temperature"); - expect(thing.properties.temperature).to.have.property("readOnly").that.equals(false); - expect(thing.properties.temperature).to.have.property("observable").that.equals(false); - - expect(thing.properties.temperature).to.have.property("forms").to.have.lengthOf(1); - expect(thing.properties.temperature.forms[0]).to.have.property("contentType").that.equals("application/json"); - expect(thing.properties.temperature.forms[0]) + const thingProperties = thing.properties!; + expect(thingProperties).to.have.property("temperature"); + expect(thingProperties.temperature).to.have.property("readOnly").that.equals(false); + expect(thingProperties.temperature).to.have.property("observable").that.equals(false); + + expect(thingProperties.temperature).to.have.property("forms").to.have.lengthOf(1); + expect(thingProperties.temperature.forms[0]).to.have.property("contentType").that.equals("application/json"); + expect(thingProperties.temperature.forms[0]) .to.have.property("href") .that.equals("coap://mytemp.example.com:5683/temp"); } @@ -529,13 +530,14 @@ class TDParserTest { expect(thing).to.have.property("title").that.equals("MyTemperatureThing2"); expect(thing).to.not.have.property("base"); - expect(thing.properties).to.have.property("temperature"); - expect(thing.properties.temperature).to.have.property("readOnly").that.equals(false); - expect(thing.properties.temperature).to.have.property("observable").that.equals(false); + const thingProperties = thing.properties!; + expect(thingProperties).to.have.property("temperature"); + expect(thingProperties.temperature).to.have.property("readOnly").that.equals(false); + expect(thingProperties.temperature).to.have.property("observable").that.equals(false); - expect(thing.properties.temperature).to.have.property("forms").to.have.lengthOf(1); - expect(thing.properties.temperature.forms[0]).to.have.property("contentType").that.equals("application/json"); - expect(thing.properties.temperature.forms[0]) + expect(thingProperties.temperature).to.have.property("forms").to.have.lengthOf(1); + expect(thingProperties.temperature.forms[0]).to.have.property("contentType").that.equals("application/json"); + expect(thingProperties.temperature.forms[0]) .to.have.property("href") .that.equals("coap://mytemp.example.com:5683/temp"); } @@ -549,23 +551,24 @@ class TDParserTest { expect(thing).to.have.property("title").that.equals("MyTemperatureThing3"); expect(thing).to.have.property("base").that.equals("coap://mytemp.example.com:5683/interactions/"); - expect(thing.properties).to.have.property("temperature"); - expect(thing.properties.temperature).to.have.property("readOnly").that.equals(false); - expect(thing.properties.temperature).to.have.property("observable").that.equals(false); - expect(thing.properties.temperature).to.have.property("forms").to.have.lengthOf(1); - expect(thing.properties.temperature.forms[0]).to.have.property("contentType").that.equals("application/json"); - - expect(thing.properties).to.have.property("temperature2"); - expect(thing.properties.temperature2).to.have.property("readOnly").that.equals(true); - expect(thing.properties.temperature2).to.have.property("observable").that.equals(false); - expect(thing.properties.temperature2).to.have.property("forms").to.have.lengthOf(1); - expect(thing.properties.temperature2.forms[0]).to.have.property("contentType").that.equals("application/json"); - - expect(thing.properties).to.have.property("humidity"); - expect(thing.properties.humidity).to.have.property("readOnly").that.equals(false); - expect(thing.properties.humidity).to.have.property("observable").that.equals(false); - expect(thing.properties.humidity).to.have.property("forms").to.have.lengthOf(1); - expect(thing.properties.humidity.forms[0]).to.have.property("contentType").that.equals("application/json"); + const thingProperties = thing.properties!; + expect(thingProperties).to.have.property("temperature"); + expect(thingProperties.temperature).to.have.property("readOnly").that.equals(false); + expect(thingProperties.temperature).to.have.property("observable").that.equals(false); + expect(thingProperties.temperature).to.have.property("forms").to.have.lengthOf(1); + expect(thingProperties.temperature.forms[0]).to.have.property("contentType").that.equals("application/json"); + + expect(thingProperties).to.have.property("temperature2"); + expect(thingProperties.temperature2).to.have.property("readOnly").that.equals(true); + expect(thingProperties.temperature2).to.have.property("observable").that.equals(false); + expect(thingProperties.temperature2).to.have.property("forms").to.have.lengthOf(1); + expect(thingProperties.temperature2.forms[0]).to.have.property("contentType").that.equals("application/json"); + + expect(thingProperties).to.have.property("humidity"); + expect(thingProperties.humidity).to.have.property("readOnly").that.equals(false); + expect(thingProperties.humidity).to.have.property("observable").that.equals(false); + expect(thingProperties.humidity).to.have.property("forms").to.have.lengthOf(1); + expect(thingProperties.humidity.forms[0]).to.have.property("contentType").that.equals("application/json"); } // TODO: wait for exclude https://github.com/chaijs/chai/issues/885 @@ -623,17 +626,18 @@ class TDParserTest { // thing metadata "reference": "myTempThing" in metadata expect(thing).to.have.property("reference").that.equals("myTempThing"); - expect(thing.properties).to.have.property("myTemp"); - expect(thing.properties.myTemp).to.have.property("readOnly").that.equals(true); - expect(thing.properties.myTemp).to.have.property("observable").that.equals(false); - expect(thing.properties.myTemp).to.have.property("forms").to.have.lengthOf(1); - expect(thing.properties.myTemp.forms[0]).to.have.property("contentType").that.equals("application/json"); + const thingProperties = thing.properties!; + expect(thingProperties).to.have.property("myTemp"); + expect(thingProperties.myTemp).to.have.property("readOnly").that.equals(true); + expect(thingProperties.myTemp).to.have.property("observable").that.equals(false); + expect(thingProperties.myTemp).to.have.property("forms").to.have.lengthOf(1); + expect(thingProperties.myTemp.forms[0]).to.have.property("contentType").that.equals("application/json"); // metadata // metadata "unit": "celsius" - expect(thing.properties.myTemp).to.have.property("unit").that.equals("celsius"); + expect(thingProperties.myTemp).to.have.property("unit").that.equals("celsius"); // metadata "reference": "threshold" - expect(thing.properties.myTemp).to.have.property("reference").that.equals("threshold"); + expect(thingProperties.myTemp).to.have.property("reference").that.equals("threshold"); // serialize // const newJson = TDParser.serializeTD(thing); @@ -645,21 +649,22 @@ class TDParserTest { expect(thing).to.have.property("base").that.equals("coap://mytemp.example.com:5683/interactions/"); - expect(thing.properties.temperature.forms[0]) + const thingProperties = thing.properties!; + expect(thingProperties.temperature.forms[0]) .to.have.property("href") .that.equals("coap://mytemp.example.com:5683/interactions/temp"); - expect(thing.properties.temperature2.forms[0]) + expect(thingProperties.temperature2.forms[0]) .to.have.property("href") .that.equals("coap://mytemp.example.com:5683/interactions/temp"); - expect(thing.properties.humidity.forms[0]) + expect(thingProperties.humidity.forms[0]) .to.have.property("href") .that.equals("coap://mytemp.example.com:5683/humid"); - expect(thing.actions.reset.forms[0]) + expect(thing.actions!.reset.forms[0]) .to.have.property("href") .that.equals("coap://mytemp.example.com:5683/actions/reset"); - expect(thing.events.update.forms[0]) + expect(thing.events!.update.forms[0]) .to.have.property("href") .that.equals("coap://mytemp.example.com:5683/interactions/events/update"); } @@ -678,9 +683,11 @@ class TDParserTest { expect(thing).to.have.property("events"); logDebug(`${thing["@context"]}`); - expect(thing.properties).to.have.property("status"); - expect(thing.properties.status.readOnly).equals(true); - expect(thing.properties.status.observable).equals(false); + + const thingProperties = thing.properties!; + expect(thingProperties).to.have.property("status"); + expect(thingProperties.status.readOnly).equals(true); + expect(thingProperties.status.observable).equals(false); } @test "simplified TD 1.1"() { @@ -697,9 +704,11 @@ class TDParserTest { expect(thing).to.have.property("events"); logDebug(`${thing["@context"]}`); - expect(thing.properties).to.have.property("status"); - expect(thing.properties.status.readOnly).equals(true); - expect(thing.properties.status.observable).equals(false); + + const thingProperties = thing.properties!; + expect(thingProperties).to.have.property("status"); + expect(thingProperties.status.readOnly).equals(true); + expect(thingProperties.status.observable).equals(false); } @test "should detect broken TDs"() { @@ -775,14 +784,16 @@ class TDParserTest { // interaction arrays expect(thing).to.have.property("properties"); - expect(thing.properties).to.have.property("without"); - expect(thing.properties.without.forms[0].href).equals("coap://localhost:8080/uv/" + "without{?step}"); + const thingProperties = thing.properties!; - expect(thing.properties).to.have.property("with1"); - expect(thing.properties.with1.forms[0].href).equals("coap://localhost:8080/uv/" + "with1{?step}"); + expect(thingProperties).to.have.property("without"); + expect(thingProperties.without.forms[0].href).equals("coap://localhost:8080/uv/" + "without{?step}"); - expect(thing.properties).to.have.property("with2"); - expect(thing.properties.with2.forms[0].href).equals("coap://localhost:8080/uv/" + "with2{?step,a}"); + expect(thingProperties).to.have.property("with1"); + expect(thingProperties.with1.forms[0].href).equals("coap://localhost:8080/uv/" + "with1{?step}"); + + expect(thingProperties).to.have.property("with2"); + expect(thingProperties.with2.forms[0].href).equals("coap://localhost:8080/uv/" + "with2{?step,a}"); } @test "uriVarables in combination with and without coap base"() { @@ -832,13 +843,15 @@ class TDParserTest { // interaction arrays expect(thing).to.have.property("properties"); - expect(thing.properties).to.have.property("without"); - expect(thing.properties.without.forms[0].href).equals("http://localhost:8080/uv/" + "without{?step}"); + const thingProperties = thing.properties!; + + expect(thingProperties).to.have.property("without"); + expect(thingProperties.without.forms[0].href).equals("http://localhost:8080/uv/" + "without{?step}"); - expect(thing.properties).to.have.property("with1"); - expect(thing.properties.with1.forms[0].href).equals("http://localhost:8080/uv/" + "with1{?step}"); + expect(thingProperties).to.have.property("with1"); + expect(thingProperties.with1.forms[0].href).equals("http://localhost:8080/uv/" + "with1{?step}"); - expect(thing.properties).to.have.property("with2"); - expect(thing.properties.with2.forms[0].href).equals("http://localhost:8080/uv/" + "with2{?step,a}"); + expect(thingProperties).to.have.property("with2"); + expect(thingProperties.with2.forms[0].href).equals("http://localhost:8080/uv/" + "with2{?step,a}"); } }