diff --git a/packages/td-tools/.eslintrc.json b/packages/td-tools/.eslintrc.json index 7bb9b651d..8cc298a95 100644 --- a/packages/td-tools/.eslintrc.json +++ b/packages/td-tools/.eslintrc.json @@ -1,4 +1,7 @@ { "extends": "../../.eslintrc.js", - "ignorePatterns": "webpack.config.js" + "ignorePatterns": "webpack.config.js", + "rules": { + "@typescript-eslint/strict-boolean-expressions": ["error"] + } } diff --git a/packages/td-tools/src/td-parser.ts b/packages/td-tools/src/td-parser.ts index 2edf6e5c2..4ba73c973 100644 --- a/packages/td-tools/src/td-parser.ts +++ b/packages/td-tools/src/td-parser.ts @@ -158,7 +158,7 @@ export function parseTD(td: string, normalize?: boolean): Thing { throw new Error(`Form of Property '${propName}' has no href field`); } // check if base field required - if (!isAbsoluteUrl(form.href) && !thing.base) + if (!isAbsoluteUrl(form.href) && thing.base == null) throw new Error(`Form of Property '${propName}' has relative URI while TD has no base field`); // add allForms.push(form); @@ -176,7 +176,7 @@ export function parseTD(td: string, normalize?: boolean): Thing { throw new Error(`Form of Action '${actName}' has no href field`); } // check if base field required - if (!isAbsoluteUrl(form.href) && !thing.base) + if (!isAbsoluteUrl(form.href) && thing.base == null) throw new Error(`Form of Action '${actName}' has relative URI while TD has no base field`); // add allForms.push(form); @@ -194,7 +194,7 @@ export function parseTD(td: string, normalize?: boolean): Thing { throw new Error(`Form of Event '${evtName}' has no href field`); } // check if base field required - if (!isAbsoluteUrl(form.href) && !thing.base) + if (!isAbsoluteUrl(form.href) && thing.base == null) throw new Error(`Form of Event '${evtName}' has relative URI while TD has no base field`); // add allForms.push(form); @@ -222,20 +222,20 @@ export function serializeTD(thing: Thing): string { const copy = JSON.parse(JSON.stringify(thing)); // clean-ups - if (!copy.security || copy.security.length === 0) { + if (copy.security == null || copy.security.length === 0) { copy.securityDefinitions = { nosec_sc: { scheme: "nosec" }, }; copy.security = ["nosec_sc"]; } - if (copy.forms && copy.forms.length === 0) { + if (copy.forms?.length === 0) { delete copy.forms; } - if (copy.properties && Object.keys(copy.properties).length === 0) { + if (copy.properties != null && Object.keys(copy.properties).length === 0) { delete copy.properties; - } else if (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]; @@ -251,9 +251,9 @@ export function serializeTD(thing: Thing): string { } } - if (copy.actions && Object.keys(copy.actions).length === 0) { + if (copy.actions != null && Object.keys(copy.actions).length === 0) { delete copy.actions; - } else if (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]; @@ -265,11 +265,11 @@ export function serializeTD(thing: Thing): string { } } } - if (copy.events && Object.keys(copy.events).length === 0) { + if (copy.events != null && Object.keys(copy.events).length === 0) { delete copy.events; } - if (copy.links && copy.links.length === 0) { + if (copy?.links.length === 0) { delete copy.links; } diff --git a/packages/td-tools/src/thing-description.ts b/packages/td-tools/src/thing-description.ts index 34ce5bd51..b308fbddb 100644 --- a/packages/td-tools/src/thing-description.ts +++ b/packages/td-tools/src/thing-description.ts @@ -67,7 +67,7 @@ export class Form implements TDT.FormElementBase { constructor(href: string, contentType?: string) { this.href = href; - if (contentType) this.contentType = contentType; + if (contentType != null) this.contentType = contentType; } } export interface ExpectedResponse { diff --git a/packages/td-tools/src/thing-model-helpers.ts b/packages/td-tools/src/thing-model-helpers.ts index 1cb71213c..c5ea34b6d 100644 --- a/packages/td-tools/src/thing-model-helpers.ts +++ b/packages/td-tools/src/thing-model-helpers.ts @@ -110,7 +110,7 @@ export class ThingModelHelpers { } if ("links" in data && Array.isArray(data.links)) { const foundTmExtendsRel = data.links.find((link) => link.rel === "tm:extends"); - if (foundTmExtendsRel) return true; + if (foundTmExtendsRel != null) return true; } if (data.properties !== undefined) { @@ -136,17 +136,9 @@ export class ThingModelHelpers { * @experimental */ public static getModelVersion(data: ThingModel): string | undefined { - if ( - "version" in data && - data.version && - typeof data.version === "object" && - "model" in data.version && - typeof data.version.model === "string" - ) { - return data.version.model; - } else { - return undefined; - } + return typeof data?.version === "object" && typeof data?.version?.model === "string" + ? data.version.model + : undefined; } /** @@ -241,7 +233,7 @@ export class ThingModelHelpers { case "http": { return new Promise((resolve, reject) => { http.get(uri, (res) => { - if (!res.statusCode || res.statusCode !== 200) { + if (res.statusCode == null || res.statusCode !== 200) { reject(new Error(`http status code not 200 but ${res.statusCode} for ${uri}`)); } @@ -268,7 +260,7 @@ export class ThingModelHelpers { return new Promise((resolve, reject) => { https .get(uri, (res) => { - if (!res.statusCode || res.statusCode !== 200) { + if (res.statusCode == null || res.statusCode !== 200) { reject(new Error(`https status code not 200 but ${res.statusCode} for ${uri}`)); } @@ -343,7 +335,7 @@ export class ThingModelHelpers { for (const aff in affRefs) { const affUri = affRefs[aff] as string; const refObj = this.parseTmRef(affUri); - if (!refObj.uri) { + if (refObj.uri == null) { throw new Error(`Missing remote path in ${affUri}`); } let source = await this.fetchModel(refObj.uri); @@ -376,7 +368,7 @@ export class ThingModelHelpers { if (!options) { options = {} as CompositionOptions; } - if (!options.baseUrl) { + if (options.baseUrl == null) { options.baseUrl = "."; } const newTMHref = this.returnNewTMHref(options.baseUrl, title); @@ -405,7 +397,7 @@ export class ThingModelHelpers { for (const key in submodelObj) { const sub = submodelObj[key]; - if (options.selfComposition) { + if (options.selfComposition === true) { if (!data.links) { throw new Error( "You used self composition but links are missing; they are needed to extract the instance name" @@ -415,7 +407,7 @@ export class ThingModelHelpers { const index = data.links.findIndex((el) => el.href === key); const el = data.links[index]; const instanceName = el.instanceName; - if (!instanceName) { + if (instanceName == null) { throw new Error("Self composition is not possible without instance names"); } // self composition enabled, just one TD expected @@ -449,7 +441,7 @@ export class ThingModelHelpers { } } } - if (!data.links || options.selfComposition) { + if (!data.links || options.selfComposition === true) { data.links = []; } // add reference to the thing model @@ -475,7 +467,7 @@ export class ThingModelHelpers { private static getThingModelRef(data: Record): Record { const refs = {} as Record; - if (!data) { + if (data == null) { return refs; } for (const key in data) { @@ -508,7 +500,7 @@ export class ThingModelHelpers { extendedModel.properties = {}; } for (const key in properties) { - if (dest.properties && dest.properties[key]) { + if (dest.properties && dest.properties[key] != null) { extendedModel.properties[key] = { ...properties[key], ...dest.properties[key] }; } else { extendedModel.properties[key] = properties[key]; @@ -625,7 +617,7 @@ export class ThingModelHelpers { keys = keys.map((el) => el.replace("{{", "").replace("}}", "")); let isValid = true; let errors; - if (keys && keys.length > 0 && (map === undefined || map === null)) { + if (keys?.length > 0 && (map === undefined || map === null)) { isValid = false; errors = `No map provided for model ${model.title}`; } else if (keys.length > 0) { @@ -660,7 +652,7 @@ export class ThingModelHelpers { } private removeDependency(dep?: string) { - if (dep) { + if (dep != null) { this.deps = this.deps.filter((el) => el !== dep); } else { this.deps.pop(); diff --git a/packages/td-tools/src/util/asset-interface-description.ts b/packages/td-tools/src/util/asset-interface-description.ts index ec8504e79..b851af209 100644 --- a/packages/td-tools/src/util/asset-interface-description.ts +++ b/packages/td-tools/src/util/asset-interface-description.ts @@ -132,7 +132,7 @@ export class AssetInterfaceDescriptionUtil { public transformTD2SM(tdAsString: string, protocols?: string[]): string { const td: ThingDescription = TDParser.parseTD(tdAsString); - const aidID = td.id ? td.id : "ID" + Math.random(); + const aidID = td.id ?? "ID" + Math.random(); logInfo("TD " + td.title + " parsed..."); @@ -218,9 +218,9 @@ export class AssetInterfaceDescriptionUtil { } private updateProtocolPrefixes(forms: [FormElementBase, ...FormElementBase[]], protocols: string[]): void { - if (forms) { + if (forms != null) { for (const interactionForm of forms) { - if (interactionForm.href) { + if (interactionForm.href != null) { const positionColon = interactionForm.href.indexOf(":"); if (positionColon > 0) { const prefix = interactionForm.href.substring(0, positionColon); @@ -234,7 +234,7 @@ export class AssetInterfaceDescriptionUtil { } private getBaseFromEndpointMetadata(endpointMetadata?: Record): string { - if (endpointMetadata?.value && endpointMetadata.value instanceof Array) { + if (endpointMetadata?.value instanceof Array) { for (const v of endpointMetadata.value) { if (v.idShort === "base") { // e.g., "value": "modbus+tcp://192.168.1.187:502" @@ -246,7 +246,7 @@ export class AssetInterfaceDescriptionUtil { } private getContentTypeFromEndpointMetadata(endpointMetadata?: Record): string { - if (endpointMetadata?.value && endpointMetadata.value instanceof Array) { + if (endpointMetadata?.value instanceof Array) { for (const v of endpointMetadata.value) { if (v.idShort === "contentType") { // e.g., "value": "application/octet-stream;byteSeq=BIG_ENDIAN" @@ -264,18 +264,18 @@ export class AssetInterfaceDescriptionUtil { [k: string]: SecurityScheme; } = {}; - if (endpointMetadata?.value && endpointMetadata.value instanceof Array) { + if (endpointMetadata?.value instanceof Array) { for (const v of endpointMetadata.value) { if (v.idShort === "securityDefinitions") { // const securitySchemes: Array = []; - if (v.value && v.value instanceof Array) { + if (v.value instanceof Array) { for (const securityDefinitionsValues of v.value) { - if (securityDefinitionsValues.idShort) { + if (securityDefinitionsValues.idShort != null) { // key if (securityDefinitionsValues.value instanceof Array) { for (const securityDefinitionsValue of securityDefinitionsValues.value) { if (securityDefinitionsValue.idShort === "scheme") { - if (securityDefinitionsValue.value) { + if (securityDefinitionsValue.value != null) { const ss: SecurityScheme = { scheme: securityDefinitionsValue.value }; securityDefinitions[securityDefinitionsValues.idShort] = ss; } @@ -295,12 +295,12 @@ export class AssetInterfaceDescriptionUtil { endpointMetadata?: Record ): string | [string, ...string[]] { const security: string[] = []; - if (endpointMetadata?.value && endpointMetadata.value instanceof Array) { + if (endpointMetadata?.value instanceof Array) { for (const v of endpointMetadata.value) { if (v.idShort === "security") { - if (v.value && v.value instanceof Array) { + if (v.value instanceof Array) { for (const securityValue of v.value) { - if (securityValue.value) { + if (securityValue.value != null) { security.push(securityValue.value); } } @@ -346,17 +346,16 @@ export class AssetInterfaceDescriptionUtil { } } else if (typeof v.idShort === "string" && v.idShort.length > 0) { // TODO is this still relevant? - // pick *any* value (and possibly override, e.g, contentType) + // pick *any* value (and possibly override, e.g. contentType) // TODO Should we add all value's (e.g., dataMapping might be empty array) ? // if (typeof v.value === "string" ||typeof v.value === "number" || typeof v.value === "boolean") { - if (v.value) { + if (v.value != null) { form[v.idShort] = v.value; // use valueType to convert the string value if ( - v.valueType && - v.valueType && - v.valueType.dataObjectType && - v.valueType.dataObjectType.name && + v.valueType != null && + v.valueType.dataObjectType != null && + v.valueType.dataObjectType.name != null && typeof v.valueType.dataObjectType.name === "string" ) { // XSD schemaTypes, https://www.w3.org/TR/xmlschema-2/#built-in-datatypes @@ -400,12 +399,16 @@ export class AssetInterfaceDescriptionUtil { submodel: Record, submodelRegex?: string ): void { - if (submodel instanceof Object && submodel.idShort && submodel.idShort === "AssetInterfacesDescription") { - if (submodel.submodelElements && submodel.submodelElements instanceof Array) { + if ( + submodel instanceof Object && + submodel.idShort != null && + submodel.idShort === "AssetInterfacesDescription" + ) { + if (submodel.submodelElements instanceof Array) { for (const submodelElement of submodel.submodelElements) { if (submodelElement instanceof Object) { logDebug("SubmodelElement.idShort: " + submodelElement.idShort); - if (submodelRegex && typeof submodelRegex === "string" && submodelRegex.length > 0) { + if (typeof submodelRegex === "string" && submodelRegex.length > 0) { const regex = new RegExp(submodelRegex); if (!regex.test(submodelElement.idShort)) { logInfo("submodel not of interest"); @@ -422,7 +425,7 @@ export class AssetInterfaceDescriptionUtil { private processSubmodelElement(smInformation: SubmodelInformation, submodelElement: Record): void { // EndpointMetadata vs. InterfaceMetadata - if (submodelElement.value && submodelElement.value instanceof Array) { + if (submodelElement.value instanceof Array) { // Note: iterate twice over to collect first EndpointMetadata let endpointMetadata: Record = {}; for (const smValue of submodelElement.value) { @@ -446,7 +449,7 @@ export class AssetInterfaceDescriptionUtil { if (smValue instanceof Object) { if (smValue.idShort === "InterfaceMetadata") { logInfo("InterfaceMetadata"); - if (smValue.value && smValue.value instanceof Array) { + if (smValue.value instanceof Array) { for (const interactionValue of smValue.value) { if (interactionValue.idShort === "Properties") { if (interactionValue.value instanceof Array) { @@ -510,7 +513,7 @@ export class AssetInterfaceDescriptionUtil { thing: new Map>(), }; - if (aidModel instanceof Object && aidModel.submodels) { + if (aidModel instanceof Object && aidModel.submodels != null) { if (aidModel.submodels instanceof Array) { for (const submodel of aidModel.submodels) { this.processSubmodel(smInformation, submodel, submodelRegex); @@ -522,7 +525,7 @@ export class AssetInterfaceDescriptionUtil { } private _transform(smInformation: SubmodelInformation, template?: string): string { - const thing: Thing = template ? JSON.parse(template) : {}; + const thing: Thing = template != null ? JSON.parse(template) : {}; // walk over thing information and set them for (const [key, value] of smInformation.thing) { @@ -534,7 +537,7 @@ export class AssetInterfaceDescriptionUtil { } // required TD fields - if (!thing["@context"]) { + if (thing["@context"] == null) { thing["@context"] = "https://www.w3.org/2022/wot/td/v1.1"; } if (!thing.title) { @@ -544,7 +547,7 @@ export class AssetInterfaceDescriptionUtil { // Security in AID is defined for each submodel // add "securityDefinitions" globally and add them on form level if necessary // TODO: possible collisions for "security" names *could* be handled by cnt - if (!thing.securityDefinitions) { + if (thing.securityDefinitions == null) { thing.securityDefinitions = {}; } // let cnt = 1; @@ -595,7 +598,7 @@ export class AssetInterfaceDescriptionUtil { const tdDescription: Record = {}; if (aasDescription instanceof Array) { for (const aasDescriptionEntry of aasDescription) { - if (aasDescriptionEntry.language && aasDescriptionEntry.text) { + if (aasDescriptionEntry.language != null && aasDescriptionEntry.text != null) { const language: string = aasDescriptionEntry.language; const text: string = aasDescriptionEntry.text; tdDescription[language] = text; @@ -613,10 +616,10 @@ export class AssetInterfaceDescriptionUtil { thing.properties[key].type = interactionValue.value; } } else if (interactionValue.idShort === "range") { - if (interactionValue.min) { + if (interactionValue.min != null) { thing.properties[key].min = interactionValue.min; } - if (interactionValue.max) { + if (interactionValue.max != null) { thing.properties[key].max = interactionValue.max; } } else if (interactionValue.idShort === "observable") { @@ -694,7 +697,7 @@ export class AssetInterfaceDescriptionUtil { const values: Array = []; // base ? - if (td.base) { + if (td.base != null) { values.push({ idShort: "base", valueType: "xs:anyURI", @@ -715,7 +718,7 @@ export class AssetInterfaceDescriptionUtil { // security const securityValues: Array = []; - if (td.security) { + if (td.security != null) { for (const secKey of td.security) { securityValues.push({ valueType: "xs:string", @@ -779,7 +782,7 @@ export class AssetInterfaceDescriptionUtil { let formElementPicked: FormElementBase | undefined; if (propertyValue.forms) { for (const formElementProperty of propertyValue.forms) { - if (formElementProperty.href?.startsWith(protocol)) { + if (formElementProperty.href != null && formElementProperty.href.startsWith(protocol)) { formElementPicked = formElementProperty; // found matching form --> abort loop break; @@ -793,7 +796,7 @@ export class AssetInterfaceDescriptionUtil { const propertyValues: Array = []; // type - if (propertyValue.type) { + if (propertyValue.type != null) { propertyValues.push({ idShort: "type", valueType: "xs:string", @@ -802,7 +805,7 @@ export class AssetInterfaceDescriptionUtil { }); } // title - if (propertyValue.title) { + if (propertyValue.title != null) { propertyValues.push({ idShort: "title", valueType: "xs:string", @@ -811,7 +814,7 @@ export class AssetInterfaceDescriptionUtil { }); } // observable - if (propertyValue.observable) { + if (propertyValue.observable != null) { propertyValues.push({ idShort: "observable", valueType: "xs:boolean", @@ -823,7 +826,7 @@ export class AssetInterfaceDescriptionUtil { // range and others? Simply add them as is? // forms - if (formElementPicked) { + if (formElementPicked != null) { const propertyForm: Array = []; // TODO AID for now supports just *one* href/form @@ -861,7 +864,7 @@ export class AssetInterfaceDescriptionUtil { text: langValue, }); } - } else if (propertyValue.description) { + } else if (propertyValue.description != null) { // fallback description = []; description.push({ diff --git a/packages/td-tools/test/AssetInterfaceDescriptionTest.ts b/packages/td-tools/test/AssetInterfaceDescriptionTest.ts index bbe390ebc..3c582d087 100644 --- a/packages/td-tools/test/AssetInterfaceDescriptionTest.ts +++ b/packages/td-tools/test/AssetInterfaceDescriptionTest.ts @@ -343,7 +343,7 @@ class AssetInterfaceDescriptionUtilTest { expect(hasObservable).to.equal(true); expect(hasForms).to.equal(true); } - if (propertyValue.description) { + if (propertyValue.description != null) { hasPropertyStatusDescription = true; expect(propertyValue) .to.have.property("description") diff --git a/packages/td-tools/tsconfig.json b/packages/td-tools/tsconfig.json index b2c3d8edc..cbbaa970a 100644 --- a/packages/td-tools/tsconfig.json +++ b/packages/td-tools/tsconfig.json @@ -4,7 +4,8 @@ "resolveJsonModule": true, "types": ["node", "readable-stream"], "outDir": "dist", - "rootDir": "src" + "rootDir": "src", + "strictNullChecks": true }, "include": ["src/**/*"] }