From f737df4f266011b2129e9f69398b98312fe28742 Mon Sep 17 00:00:00 2001 From: danielpeintner Date: Thu, 2 Nov 2023 10:06:52 +0100 Subject: [PATCH] fix: shortId for AID does not allow any character (#1147) e.g., "+" is not allowed --- .../src/util/asset-interface-description.ts | 45 ++++- .../test/AssetInterfaceDescriptionTest.ts | 171 +++++++++++++++++- 2 files changed, 208 insertions(+), 8 deletions(-) diff --git a/packages/td-tools/src/util/asset-interface-description.ts b/packages/td-tools/src/util/asset-interface-description.ts index 7b1b67064..5d4bddee6 100644 --- a/packages/td-tools/src/util/asset-interface-description.ts +++ b/packages/td-tools/src/util/asset-interface-description.ts @@ -92,7 +92,7 @@ export class AssetInterfaceDescriptionUtil { const aas = { assetAdministrationShells: [ { - idShort: aasName, + idShort: this.sanitizeIdShort(aasName), id: aasId, assetInformation: { assetKind: "Type", @@ -159,7 +159,7 @@ export class AssetInterfaceDescriptionUtil { } const submdelElement = { - idShort: submodelElementIdShort, + idShort: this.sanitizeIdShort(submodelElementIdShort), semanticId: this.createSemanticId( "https://admin-shell.io/idta/AssetInterfacesDescription/1/0/Interface" ), @@ -233,6 +233,37 @@ export class AssetInterfaceDescriptionUtil { }; } + private replaceCharAt(str: string, index: number, char: string) { + if (index > str.length - 1) return str; + return str.substring(0, index) + char + str.substring(index + 1); + } + + private sanitizeIdShort(value: string): string { + // idShort of Referables shall only feature letters, digits, underscore ("_"); + // starting mandatory with a letter, i.e. [a-zA-Z][a-zA-Z0-9]*. + // + // see https://github.com/eclipse-thingweb/node-wot/issues/1145 + // and https://github.com/admin-shell-io/aas-specs/issues/295 + if (value != null) { + for (let i = 0; i < value.length; i++) { + const char = value.charCodeAt(i); + if (i !== 0 && char === " ".charCodeAt(0)) { + // underscore -> fine as is + } else if (char >= "0".charCodeAt(0) && char <= "9".charCodeAt(0)) { + // digit -> fine as is + } else if (char >= "A".charCodeAt(0) && char <= "Z".charCodeAt(0)) { + // small letter -> fine as is + } else if (char >= "a".charCodeAt(0) && char <= "z".charCodeAt(0)) { + // capital letter -> fine as is + } else { + // replace with underscore "_" + value = this.replaceCharAt(value, i, "_"); + } + } + } + return value; + } + private getProtocolPrefixes(td: ThingDescription): string[] { const protocols: string[] = []; @@ -1207,20 +1238,24 @@ export class AssetInterfaceDescriptionUtil { // TODO are there more characters we need to deal with? formTerm = formTerm.replace(":", "_"); - if (typeof formValue === "string") { + if ( + typeof formValue === "string" || + typeof formValue === "number" || + typeof formValue === "boolean" + ) { if (semanticId !== undefined) { propertyForm.push({ idShort: formTerm, semanticId: this.createSemanticId(semanticId), valueType: "xs:string", - value: formValue, + value: formValue.toString(), modelType: "Property", }); } else { propertyForm.push({ idShort: formTerm, valueType: "xs:string", - value: formValue, + value: formValue.toString(), modelType: "Property", }); } diff --git a/packages/td-tools/test/AssetInterfaceDescriptionTest.ts b/packages/td-tools/test/AssetInterfaceDescriptionTest.ts index 5d9144f05..75f313448 100644 --- a/packages/td-tools/test/AssetInterfaceDescriptionTest.ts +++ b/packages/td-tools/test/AssetInterfaceDescriptionTest.ts @@ -512,7 +512,7 @@ class AssetInterfaceDescriptionUtilTest { }, }; - @test async "should correctly transform sample TD into AID submodel"() { + @test async "should correctly transform sample TD1 into AID submodel"() { const sm = this.assetInterfaceDescriptionUtil.transformTD2SM(JSON.stringify(this.td1), ["https"]); const smObj = JSON.parse(sm); @@ -734,7 +734,7 @@ class AssetInterfaceDescriptionUtilTest { } @test - async "should transform sample TD into JSON submodel without any properties due to unknown protocol prefix"() { + async "should transform sample TD1 into JSON submodel without any properties due to unknown protocol prefix"() { const sm = this.assetInterfaceDescriptionUtil.transformTD2SM(JSON.stringify(this.td1), ["unknown"]); const smObj = JSON.parse(sm); @@ -759,7 +759,7 @@ class AssetInterfaceDescriptionUtilTest { expect(hasInterfaceMetadata, "No InterfaceMetadata").to.equal(true); } - @test async "should correctly transform sample TD into JSON AAS"() { + @test async "should correctly transform sample TD1 into JSON AAS"() { const sm = this.assetInterfaceDescriptionUtil.transformTD2AAS(JSON.stringify(this.td1), ["http"]); const aasObj = JSON.parse(sm); @@ -769,6 +769,171 @@ class AssetInterfaceDescriptionUtilTest { // Note: proper AID submodel checks done in previous test-cases } + td2: ThingDescription = { + "@context": "https://www.w3.org/2022/wot/td/v1.1", + title: "ModbusTD", + securityDefinitions: { + nosec_sc: { + scheme: "nosec", + }, + }, + security: "nosec_sc", + properties: { + prop: { + forms: [ + { + href: "modbus+tcp://127.0.0.1:60000/1", + op: "readproperty", + "modbus:function": "readCoil", + "modbus:address": 1, + }, + ], + }, + }, + }; + + @test async "should correctly transform sample TD2 into AID submodel"() { + const sm = this.assetInterfaceDescriptionUtil.transformTD2SM(JSON.stringify(this.td2)); + + const smObj = JSON.parse(sm); + expect(smObj).to.have.property("idShort").that.equals("AssetInterfacesDescription"); + expect(smObj).to.have.property("semanticId"); + expect(smObj).to.have.property("submodelElements").to.be.an("array").to.have.lengthOf.greaterThan(0); + const smInterface = smObj.submodelElements[0]; + expect(smInterface).to.have.property("idShort").to.equal("InterfaceMODBUS_TCP"); // AID does not allow "+" in idShort, see InterfaceMODBUS+TCP + expect(smInterface).to.have.property("value").to.be.an("array").to.have.lengthOf.greaterThan(0); + expect(smInterface) + .to.have.property("semanticId") + .to.be.an("object") + .with.property("keys") + .to.be.an("array") + .to.have.lengthOf.greaterThan(0); + expect(smInterface) + .to.have.property("supplementalSemanticIds") + .to.be.an("array") + .to.have.lengthOf.greaterThan(1); // default WoT-TD and http + let hasThingTitle = false; + let hasEndpointMetadata = false; + for (const smValue of smInterface.value) { + if (smValue.idShort === "title") { + hasThingTitle = true; + expect(smValue).to.have.property("value").to.equal("ModbusTD"); + } else if (smValue.idShort === "EndpointMetadata") { + hasEndpointMetadata = true; + const endpointMetadata = smValue; + expect(endpointMetadata).to.have.property("value").to.be.an("array").to.have.lengthOf.greaterThan(0); + let hasBase = false; + let hasContentType = false; + let hasSecurity = false; + let hasSecurityDefinitions = false; + for (const endpointMetadataValue of endpointMetadata.value) { + if (endpointMetadataValue.idShort === "base") { + hasBase = true; + } else if (endpointMetadataValue.idShort === "contentType") { + hasContentType = true; + } else if (endpointMetadataValue.idShort === "security") { + hasSecurity = true; + expect(endpointMetadataValue) + .to.have.property("value") + .to.be.an("array") + .to.have.lengthOf.greaterThan(0); + expect(endpointMetadataValue.value[0].value).to.equal("nosec_sc"); + } else if (endpointMetadataValue.idShort === "securityDefinitions") { + hasSecurityDefinitions = true; + } + } + expect(hasBase).to.equal(false); + expect(hasContentType).to.equal(false); + expect(hasSecurity).to.equal(true); + expect(hasSecurityDefinitions).to.equal(true); + } + } + expect(hasThingTitle, "No thing title").to.equal(true); + expect(hasEndpointMetadata, "No EndpointMetadata").to.equal(true); + + // InterfaceMetadata with properties etc + let hasInterfaceMetadata = false; + for (const smValue of smInterface.value) { + if (smValue.idShort === "InterfaceMetadata") { + hasInterfaceMetadata = true; + expect(smValue).to.have.property("value").to.be.an("array").to.have.lengthOf.greaterThan(0); + let hasProperties = false; + for (const interactionValues of smValue.value) { + if (interactionValues.idShort === "properties") { + hasProperties = true; + expect(interactionValues) + .to.have.property("value") + .to.be.an("array") + .to.have.lengthOf.greaterThan(0); + let hasPropertyProp = false; + for (const propertyValue of interactionValues.value) { + if (propertyValue.idShort === "prop") { + hasPropertyProp = true; + expect(propertyValue) + .to.have.property("value") + .to.be.an("array") + .to.have.lengthOf.greaterThan(0); + let hasType = false; + let hasTitle = false; + let hasObservable = false; + let hasForms = false; + for (const propProperty of propertyValue.value) { + if (propProperty.idShort === "type") { + hasType = true; + } else if (propProperty.idShort === "title") { + hasTitle = true; + } else if (propProperty.idShort === "observable") { + hasObservable = true; + } else if (propProperty.idShort === "forms") { + hasForms = true; + expect(propProperty) + .to.have.property("value") + .to.be.an("array") + .to.have.lengthOf.greaterThan(0); + let hasHref = false; + let hasContentType = false; + let hasOp = false; + let hasModbusFunction = false; + let hasModbusAddress = false; + for (const formEntry of propProperty.value) { + if (formEntry.idShort === "href") { + hasHref = true; + expect(formEntry.value).to.equal("modbus+tcp://127.0.0.1:60000/1"); + } else if (formEntry.idShort === "contentType") { + hasContentType = true; + } else if (formEntry.idShort === "op") { + hasOp = true; + expect(formEntry.value).to.equal("readproperty"); + } else if (formEntry.idShort === "modbus_function") { + hasModbusFunction = true; + expect(formEntry.value).to.equal("readCoil"); + } else if (formEntry.idShort === "modbus_address") { + hasModbusAddress = true; + expect(formEntry.value).to.equal("1"); + } + } + expect(hasHref).to.equal(true); + expect(hasContentType).to.equal(false); + expect(hasOp).to.equal(true); + expect(hasModbusFunction).to.equal(true); + expect(hasModbusAddress).to.equal(true); + } + } + expect(hasType).to.equal(false); + expect(hasTitle).to.equal(false); + expect(hasObservable).to.equal(false); + expect(hasForms).to.equal(true); + } + } + expect(hasPropertyProp).to.equal(true); + } + } + expect(hasProperties).to.equal(true); + } + } + expect(hasInterfaceMetadata, "No InterfaceMetadata").to.equal(true); + } + @test.skip async "should correctly transform counter TD into JSON AAS"() { // built-in fetch requires Node.js 18+ const response = await fetch("http://plugfest.thingweb.io:8083/counter");