diff --git a/packages/td-tools/src/thing-description.ts b/packages/td-tools/src/thing-description.ts index f5c2d989c..31cef8fac 100644 --- a/packages/td-tools/src/thing-description.ts +++ b/packages/td-tools/src/thing-description.ts @@ -39,6 +39,8 @@ export default class Thing implements TDT.ThingDescription { constructor() { this["@context"] = [DEFAULT_CONTEXT_V1, DEFAULT_CONTEXT_V11]; this["@type"] = DEFAULT_THING_TYPE; + this.title = ""; + this.securityDefinitions = {}; this.security = ""; this.properties = {}; this.actions = {}; diff --git a/packages/td-tools/src/thing-model-helpers.ts b/packages/td-tools/src/thing-model-helpers.ts index 4a42df268..fed6c02c3 100644 --- a/packages/td-tools/src/thing-model-helpers.ts +++ b/packages/td-tools/src/thing-model-helpers.ts @@ -66,7 +66,7 @@ export class ThingModelHelpers { static tsSchemaValidator = ajv.compile(tmSchema) as ValidateFunction; private deps: string[] = [] as string[]; - private resolver: Resolver = undefined; + private resolver?: Resolver = undefined; constructor(_resolver?: Resolver) { if (_resolver) { @@ -129,9 +129,9 @@ export class ThingModelHelpers { * * @experimental */ - public static getModelVersion(data: ThingModel): string { - if (!("version" in data) || !("model" in data.version)) { - return null; + public static getModelVersion(data: ThingModel): string | undefined { + if (!("version" in data) || !(data.version && "model" in data.version)) { + return undefined; } return data.version.model as string; } @@ -146,11 +146,11 @@ export class ThingModelHelpers { * * @experimental */ - public static validateThingModel(data: ThingModel): { valid: boolean; errors: string } { + public static validateThingModel(data: ThingModel): { valid: boolean; errors?: string } { const isValid = ThingModelHelpers.tsSchemaValidator(data); let errors; if (!isValid) { - errors = ThingModelHelpers.tsSchemaValidator.errors.map((o: ErrorObject) => o.message).join("\n"); + errors = ThingModelHelpers.tsSchemaValidator.errors?.map((o: ErrorObject) => o.message).join("\n"); } return { valid: isValid, @@ -239,7 +239,7 @@ export class ThingModelHelpers { console.debug("[td-tools]", "http fetched:", parsedData); resolve(parsedData); } catch (e) { - console.error(e.message); + console.error("[td-tools]", (e as Error).message); } }); }).on("error", (e) => { @@ -262,7 +262,7 @@ export class ThingModelHelpers { console.debug("[td-tools]", "https fetched:", parsedData); resolve(parsedData); } catch (e) { - console.error(e.message); + console.error("[td-tools]", (e as Error).message); } }); }) @@ -322,10 +322,13 @@ export class ThingModelHelpers { for (const aff in affRefs) { const affUri = affRefs[aff] as string; const refObj = this.parseTmRef(affUri); + if (!refObj.uri) { + 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); + const importedAffordance = this.getRefAffordance(refObj, source) ?? {}; refObj.name = aff; // update the name of the affordance modelInput.imports.push({ affordance: importedAffordance, ...refObj }); } @@ -348,7 +351,7 @@ export class ThingModelHelpers { options?: CompositionOptions ): Promise { let tmpThingModels = [] as ThingModel[]; - const title = data.title.replace(/ /g, ""); + const title = (data.title ?? "").replace(/ /g, ""); if (!options) { options = {} as CompositionOptions; } @@ -358,19 +361,22 @@ export class ThingModelHelpers { const newTMHref = this.returnNewTMHref(options.baseUrl, title); const newTDHref = this.returnNewTDHref(options.baseUrl, title); if ("extends" in modelObject) { - const extendObjs = modelObject.extends; - for (const key in extendObjs) { - const el = extendObjs[key]; - data = ThingModelHelpers.extendThingModel(el, data); + const extendObjs = modelObject.extends ?? []; + for (const extendObj of extendObjs) { + data = ThingModelHelpers.extendThingModel(extendObj, data); } // remove the tm:extends links - data.links = data.links.filter((link) => link.rel !== "tm:extends"); + data.links = data.links?.filter((link) => link.rel !== "tm:extends"); } if ("imports" in modelObject) { - const importObjs = modelObject.imports; - for (const key in importObjs) { - const el = importObjs[key]; - data = ThingModelHelpers.importAffordance(el.type, el.name, el.affordance, data); + const importObjs = modelObject.imports ?? []; + for (const importedObj of importObjs) { + data = ThingModelHelpers.importAffordance( + importedObj.type, + importedObj.name, + importedObj.affordance, + data + ); } } if ("submodel" in modelObject) { @@ -379,6 +385,12 @@ export class ThingModelHelpers { for (const key in submodelObj) { const sub = submodelObj[key]; if (options.selfComposition) { + if (!data.links) { + throw new Error( + "You used self composition but links are missing; they are needed to extract the instance name" + ); + } + const index = data.links.findIndex((el) => el.href === key); const el = data.links[index]; const instanceName = el.instanceName; @@ -400,9 +412,9 @@ export class ThingModelHelpers { } } } else { - const subTitle = sub.title.replace(/ /g, ""); + const subTitle = (sub.title ?? "").replace(/ /g, ""); const subNewHref = this.returnNewTDHref(options.baseUrl, subTitle); - if (!("links" in sub)) { + if (!sub.links) { sub.links = []; } sub.links.push({ @@ -416,7 +428,7 @@ export class ThingModelHelpers { } } } - if (!("links" in data) || options.selfComposition) { + if (!data.links || options.selfComposition) { data.links = []; } // add reference to the thing model @@ -433,7 +445,7 @@ export class ThingModelHelpers { data = this.fillPlaceholder(data, options.map); } tmpThingModels.unshift(data); // put itself as first element - tmpThingModels = tmpThingModels.map((el) => this.fillPlaceholder(el, options.map)); // TODO: make more efficient, since repeated each recursive call + tmpThingModels = tmpThingModels.map((el) => this.fillPlaceholder(el, options?.map)); // TODO: make more efficient, since repeated each recursive call if (this.deps.length > 0) { this.removeDependency(); } @@ -471,8 +483,11 @@ export class ThingModelHelpers { extendedModel = { ...source, ...dest }; // TODO: implement validation for extending if (properties) { + if (!extendedModel.properties) { + extendedModel.properties = {}; + } for (const key in properties) { - if (dest.properties && key in dest.properties) { + if (dest.properties && dest.properties[key]) { extendedModel.properties[key] = { ...properties[key], ...dest.properties[key] }; } else { extendedModel.properties[key] = properties[key]; @@ -480,6 +495,9 @@ export class ThingModelHelpers { } } if (actions) { + if (!extendedModel.actions) { + extendedModel.actions = {}; + } for (const key in actions) { if (dest.actions && key in dest.actions) { extendedModel.actions[key] = { ...actions[key], ...dest.actions[key] }; @@ -489,6 +507,9 @@ export class ThingModelHelpers { } } if (events) { + if (!extendedModel.events) { + extendedModel.events = {}; + } for (const key in events) { if (dest.events && key in dest.events) { extendedModel.events[key] = { ...events[key], ...dest.events[key] }; @@ -506,18 +527,32 @@ export class ThingModelHelpers { source: DataSchema, dest: ThingModel ): ThingModel { - const d = dest[affordanceType][affordanceName]; - dest[affordanceType][affordanceName] = { ...source, ...d }; - for (const key in dest[affordanceType][affordanceName]) { - if (dest[affordanceType][affordanceName][key] === null) { - delete dest[affordanceType][affordanceName][key]; + if (!dest[affordanceType]) { + dest[affordanceType] = {}; + } + /* eslint-disable @typescript-eslint/no-non-null-assertion */ + // tsc doesn't know that dest[affordanceType] is not null + const d = dest[affordanceType]![affordanceName]; + dest[affordanceType]![affordanceName] = { ...source, ...d }; + for (const key in dest[affordanceType]![affordanceName]) { + if (dest[affordanceType]![affordanceName][key] === undefined) { + delete dest[affordanceType]![affordanceName][key]; } } + /* eslint-enable @typescript-eslint/no-non-null-assertion */ return dest; } private static formatSubmodelLink(source: ThingModel, oldHref: string, newHref: string) { + if (!source.links) { + throw new Error("Links are missing"); + } + const index = source.links.findIndex((el) => el.href === oldHref); + if (index === -1) { + throw new Error("Link not found"); + } + const el = source.links[index]; if ("instanceName" in el) { delete el.instanceName; @@ -539,26 +574,29 @@ export class ThingModelHelpers { return { uri: thingModelUri, type: affordaceType, name: affordaceName }; } - private getRefAffordance(obj: ModelImportsInput, thing: ThingModel): DataSchema { + private getRefAffordance(obj: ModelImportsInput, thing: ThingModel): DataSchema | undefined { const affordanceType = obj.type; const affordanceKey = obj.name; if (!(affordanceType in thing)) { - return null; + return undefined; } const affordances = thing[affordanceType] as DataSchema; if (!(affordanceKey in affordances)) { - return null; + return undefined; } return affordances[affordanceKey]; } - private fillPlaceholder(data: Record, map: Record): ThingModel { + private fillPlaceholder(data: Record, map: Record = {}): ThingModel { const placeHolderReplacer = new JsonPlaceholderReplacer(); placeHolderReplacer.addVariableMap(map); return placeHolderReplacer.replace(data) as ThingModel; } - private checkPlaceholderMap(model: ThingModel, map: Record): { valid: boolean; errors: string } { + private checkPlaceholderMap( + model: ThingModel, + map: Record = {} + ): { valid: boolean; errors?: string } { const regex = "{{.*?}}"; const modelString = JSON.stringify(model); // first check if model needs map diff --git a/packages/td-tools/test/ThingModelHelperCompositionTest.ts b/packages/td-tools/test/ThingModelHelperCompositionTest.ts index 0e40ba274..7996118dd 100644 --- a/packages/td-tools/test/ThingModelHelperCompositionTest.ts +++ b/packages/td-tools/test/ThingModelHelperCompositionTest.ts @@ -25,10 +25,7 @@ import { ExposedThingInit } from "wot-typescript-definitions"; chai.use(chaiAsPromised); @suite("tests to verify the composition feature of Thing Model Helper") class ThingModelHelperCompositionTest { - private thingModelHelpers: ThingModelHelpers; - async before() { - this.thingModelHelpers = new ThingModelHelpers(); - } + private thingModelHelpers: ThingModelHelpers = new ThingModelHelpers(); async fetch(uri: string): Promise { const data = await fsPromises.readFile(uri, "utf-8"); @@ -102,9 +99,9 @@ class ThingModelHelperCompositionTest { // eslint-disable-next-line dot-notation const extendedModel = await this.thingModelHelpers["composeModel"](model, modelInput, options); expect(extendedModel.length).to.be.equal(3); - expect(extendedModel[0]).to.be.deep.equal(finalModel); - expect(extendedModel[1]).to.be.deep.equal(finalModel1); - expect(extendedModel[2]).to.be.deep.equal(finalModel2); + expect(extendedModel[0]).to.be.deep.equal(finalModel, "FinalModel is not deep equal"); + expect(extendedModel[1]).to.be.deep.equal(finalModel1, "FinalModel1 is not deep equal"); + expect(extendedModel[2]).to.be.deep.equal(finalModel2, "FinalModel2 is not deep equal"); } @test async "should correctly compose recursively a Thing Model with multiple partialTDs and extend/import"() { @@ -123,7 +120,9 @@ class ThingModelHelperCompositionTest { baseUrl: "http://test.com", selfComposition: false, }; - finalModel2.links[0].href = "http://test.com/VentilatorThingModelRecursive.td.jsonld"; + + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion -- the links in the model are not null + finalModel2.links![0]!.href = "http://test.com/VentilatorThingModelRecursive.td.jsonld"; // eslint-disable-next-line dot-notation const extendedModel = await this.thingModelHelpers["composeModel"](model, modelInput, options); expect(extendedModel.length).to.be.equal(3); diff --git a/packages/td-tools/test/ThingModelHelperTest.ts b/packages/td-tools/test/ThingModelHelperTest.ts index 2849a097f..c785bbc47 100644 --- a/packages/td-tools/test/ThingModelHelperTest.ts +++ b/packages/td-tools/test/ThingModelHelperTest.ts @@ -22,10 +22,7 @@ import { ThingModelHelpers, CompositionOptions, modelComposeInput } from "../src import { promises as fs } from "fs"; @suite("tests to verify the Thing Model Helper") class ThingModelHelperTest { - private thingModelHelpers: ThingModelHelpers; - async before() { - this.thingModelHelpers = new ThingModelHelpers(); - } + private thingModelHelpers = new ThingModelHelpers(); @test "should correctly validate tm schema with ThingModel in @type"() { const model = { @@ -135,7 +132,7 @@ class ThingModelHelperTest { }; version = ThingModelHelpers.getModelVersion(thing); - expect(version).to.be.null; + expect(version).to.be.undefined; thing = { title: "thingTest", @@ -144,7 +141,7 @@ class ThingModelHelperTest { }; version = ThingModelHelpers.getModelVersion(thing); - expect(version).to.be.null; + expect(version).to.be.undefined; } @test async "should correctly extend a thing model with properties"() { @@ -223,7 +220,7 @@ class ThingModelHelperTest { properties: { timestamp1: { "tm:ref": "file://./test/thing-model/tmodels/OnOff.jsonld#/properties/timestamp", - description: null, + description: undefined, }, }, }; diff --git a/packages/td-tools/test/tsconfig.json b/packages/td-tools/test/tsconfig.json index 38a46d26c..786b1b616 100644 --- a/packages/td-tools/test/tsconfig.json +++ b/packages/td-tools/test/tsconfig.json @@ -1,4 +1,7 @@ { "extends": "../tsconfig.json", + "compilerOptions": { + "rootDir": ".." + }, "include": ["*.ts", "**/*.ts", "../src/**/*.ts"] } diff --git a/packages/td-tools/tsconfig.json b/packages/td-tools/tsconfig.json index b2c3d8edc..e0a29e71a 100644 --- a/packages/td-tools/tsconfig.json +++ b/packages/td-tools/tsconfig.json @@ -1,6 +1,8 @@ { "extends": "../../tsconfig.json", "compilerOptions": { + "strict": true, + "strictFunctionTypes": true, "resolveJsonModule": true, "types": ["node", "readable-stream"], "outDir": "dist",