Skip to content

Commit

Permalink
fix: shortId for AID does not allow any character (eclipse-thingweb#1147
Browse files Browse the repository at this point in the history
)

e.g., "+" is not allowed
  • Loading branch information
danielpeintner authored Nov 2, 2023
1 parent 04e59aa commit f737df4
Show file tree
Hide file tree
Showing 2 changed files with 208 additions and 8 deletions.
45 changes: 40 additions & 5 deletions packages/td-tools/src/util/asset-interface-description.ts
Original file line number Diff line number Diff line change
Expand Up @@ -92,7 +92,7 @@ export class AssetInterfaceDescriptionUtil {
const aas = {
assetAdministrationShells: [
{
idShort: aasName,
idShort: this.sanitizeIdShort(aasName),
id: aasId,
assetInformation: {
assetKind: "Type",
Expand Down Expand Up @@ -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"
),
Expand Down Expand Up @@ -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[] = [];

Expand Down Expand Up @@ -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",
});
}
Expand Down
171 changes: 168 additions & 3 deletions packages/td-tools/test/AssetInterfaceDescriptionTest.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down Expand Up @@ -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);
Expand All @@ -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);
Expand All @@ -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");
Expand Down

0 comments on commit f737df4

Please sign in to comment.