Skip to content

Commit 05485b1

Browse files
committed
Enforce explicit cardinality in templates and validation
- Fix cardinality typos in XML files ("ZerotToOne" → "ZeroToOne", etc.) - Require explicit cardinality: throw on "Unknown" and log error - Add/adjust tests for cardinality validation and error handling - Update SemanticIdResolver to require ILogger and validate cardinality - Update .bru files and scripts to use MaintenanceInstructions submodel - Set root node cardinality to One in SemanticTreeExtractor - Improve InternalDataProcessingException with message constructor
1 parent 5ab54c7 commit 05485b1

13 files changed

Lines changed: 155 additions & 81 deletions

File tree

example/aas/MaintenanceInstructions.xml

Lines changed: 12 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -324,7 +324,7 @@
324324
<qualifier>
325325
<type>SMT/Cardinality</type>
326326
<valueType>xs:string</valueType>
327-
<value>ZerotToOne</value>
327+
<value>ZeroToOne</value>
328328
</qualifier>
329329
</qualifiers>
330330
<valueType>xs:string</valueType>
@@ -574,7 +574,7 @@
574574
<qualifier>
575575
<type>SMT/Cardinality</type>
576576
<valueType>xs:string</valueType>
577-
<value>ZerotoMany</value>
577+
<value>ZeroToMany</value>
578578
</qualifier>
579579
</qualifiers>
580580
<value>
@@ -2232,7 +2232,7 @@
22322232
<qualifier>
22332233
<type>SMT/Cardinality</type>
22342234
<valueType>xs:string</valueType>
2235-
<value>ZerotToOne</value>
2235+
<value>ZeroToOne</value>
22362236
</qualifier>
22372237
</qualifiers>
22382238
<value>
@@ -2262,7 +2262,7 @@
22622262
<qualifier>
22632263
<type>SMT/Cardinality</type>
22642264
<valueType>xs:string</valueType>
2265-
<value>ZerotToOne</value>
2265+
<value>ZeroToOne</value>
22662266
</qualifier>
22672267
</qualifiers>
22682268
<value>
@@ -2574,7 +2574,7 @@
25742574
<qualifier>
25752575
<type>SMT/Cardinality</type>
25762576
<valueType>xs:string</valueType>
2577-
<value>ZerotToOne</value>
2577+
<value>ZeroToOne</value>
25782578
</qualifier>
25792579
</qualifiers>
25802580
<value>
@@ -2604,7 +2604,7 @@
26042604
<qualifier>
26052605
<type>SMT/Cardinality</type>
26062606
<valueType>xs:string</valueType>
2607-
<value>ZerotToOne</value>
2607+
<value>ZeroToOne</value>
26082608
</qualifier>
26092609
</qualifiers>
26102610
<value>
@@ -2669,7 +2669,7 @@
26692669
<qualifier>
26702670
<type>SMT/Cardinality</type>
26712671
<valueType>xs:string</valueType>
2672-
<value>ZerotToOne</value>
2672+
<value>ZeroToOne</value>
26732673
</qualifier>
26742674
</qualifiers>
26752675
<valueType>xs:string</valueType>
@@ -2947,7 +2947,7 @@
29472947
<qualifier>
29482948
<type>SMT/Cardinality</type>
29492949
<valueType>xs:string</valueType>
2950-
<value>ZerotToOne</value>
2950+
<value>ZeroToOne</value>
29512951
</qualifier>
29522952
</qualifiers>
29532953
</multiLanguageProperty>
@@ -2977,7 +2977,7 @@
29772977
<qualifier>
29782978
<type>SMT/Cardinality</type>
29792979
<valueType>xs:string</valueType>
2980-
<value>ZerotToOne</value>
2980+
<value>ZeroToOne</value>
29812981
</qualifier>
29822982
</qualifiers>
29832983
<value>
@@ -3007,7 +3007,7 @@
30073007
<qualifier>
30083008
<type>SMT/Cardinality</type>
30093009
<valueType>xs:string</valueType>
3010-
<value>ZerotToOne</value>
3010+
<value>ZeroToOne</value>
30113011
</qualifier>
30123012
</qualifiers>
30133013
<value>
@@ -3037,7 +3037,7 @@
30373037
<qualifier>
30383038
<type>SMT/Cardinality</type>
30393039
<valueType>xs:string</valueType>
3040-
<value>ZerotToOne</value>
3040+
<value>ZeroToOne</value>
30413041
</qualifier>
30423042
</qualifiers>
30433043
<valueType>xs:decimal</valueType>
@@ -3098,7 +3098,7 @@
30983098
<qualifier>
30993099
<type>SMT/Cardinality</type>
31003100
<valueType>xs:string</valueType>
3101-
<value>ZerotToOne</value>
3101+
<value>ZeroToOne</value>
31023102
</qualifier>
31033103
</qualifiers>
31043104
<valueType>xs:string</valueType>

example/aas/TechnicalData.xml

Lines changed: 16 additions & 54 deletions
Original file line numberDiff line numberDiff line change
@@ -1203,6 +1203,22 @@
12031203
</keys>
12041204
</reference>
12051205
</supplementalSemanticIds>
1206+
<qualifiers>
1207+
<qualifier>
1208+
<semanticId>
1209+
<type>ExternalReference</type>
1210+
<keys>
1211+
<key>
1212+
<type>GlobalReference</type>
1213+
<value>https://admin-shell.io/SubmodelTemplates/SMT/Cardinality/1/0</value>
1214+
</key>
1215+
</keys>
1216+
</semanticId>
1217+
<type>SMT/Cardinality</type>
1218+
<valueType>xs:string</valueType>
1219+
<value>ZeroToMany</value>
1220+
</qualifier>
1221+
</qualifiers>
12061222
</submodelElementCollection>
12071223
</value>
12081224
</submodelElementList>
@@ -1422,60 +1438,6 @@
14221438
</keys>
14231439
</semanticIdListElement>
14241440
<typeValueListElement>SubmodelElementCollection</typeValueListElement>
1425-
<value>
1426-
<submodelElementCollection>
1427-
<displayName>
1428-
<langStringNameType>
1429-
<language>en</language>
1430-
<text>Specific description</text>
1431-
</langStringNameType>
1432-
<langStringNameType>
1433-
<language>de</language>
1434-
<text>Spezifische Beschreibung</text>
1435-
</langStringNameType>
1436-
</displayName>
1437-
<semanticId>
1438-
<type>ExternalReference</type>
1439-
<keys>
1440-
<key>
1441-
<type>GlobalReference</type>
1442-
<value>0173-1#02-ABM221#001/0173-1#01-AHY912#001</value>
1443-
</key>
1444-
</keys>
1445-
</semanticId>
1446-
<supplementalSemanticIds>
1447-
<reference>
1448-
<type>ExternalReference</type>
1449-
<keys>
1450-
<key>
1451-
<type>GlobalReference</type>
1452-
<value>0173-1#02-ABM221#001~0/0173-1#01-AHY912#001</value>
1453-
</key>
1454-
<key>
1455-
<type>GlobalReference</type>
1456-
<value>https://api.eclass-cdp.com/0173-1-02-ABM221-001/0173-1-01-AHY912-001</value>
1457-
</key>
1458-
</keys>
1459-
</reference>
1460-
</supplementalSemanticIds>
1461-
<qualifiers>
1462-
<qualifier>
1463-
<semanticId>
1464-
<type>ExternalReference</type>
1465-
<keys>
1466-
<key>
1467-
<type>GlobalReference</type>
1468-
<value>https://admin-shell.io/SubmodelTemplates/Naming/1/0</value>
1469-
</key>
1470-
</keys>
1471-
</semanticId>
1472-
<type>Naming</type>
1473-
<valueType>xs:string</valueType>
1474-
<value>Name of SMC is use case or application</value>
1475-
</qualifier>
1476-
</qualifiers>
1477-
</submodelElementCollection>
1478-
</value>
14791441
</submodelElementList>
14801442
</submodelElements>
14811443
</submodel>

example/apiCollection/Submodel Registry/Get Submodel Descriptor By Id - Contact.bru

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ get {
1111
}
1212

1313
params:path {
14-
submodelIdentifier: {{submodelIdentifierContact-1}}
14+
submodelIdentifier: {{submodelIdentifierMaintenanceInstructions-1}}
1515
}
1616

1717
settings {

example/apiCollection/Submodel Repository/Serialization/Get appropriate serialization - Product1.bru

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,14 +5,14 @@ meta {
55
}
66

77
get {
8-
url: {{DataEngineBaseUrl}}/serialization?aasIds={{aasIdentifier-1}}&submodelIds={{submodelIdentifierContact-1}}&submodelIds={{submodelIdentifierNameplate-1}}&submodelIds={{submodelIdentifierCarbonFootprint-1}}&submodelIds={{submodelIdentifierHandoverDocumentation-1}}&submodelIds={{submodelIdentifierTechnicalData-1}}&includeConceptDescriptions=false
8+
url: {{DataEngineBaseUrl}}/serialization?aasIds={{aasIdentifier-1}}&submodelIds={{submodelIdentifierMaintenanceInstructions-1}}&submodelIds={{submodelIdentifierNameplate-1}}&submodelIds={{submodelIdentifierCarbonFootprint-1}}&submodelIds={{submodelIdentifierHandoverDocumentation-1}}&submodelIds={{submodelIdentifierTechnicalData-1}}&includeConceptDescriptions=false
99
body: none
1010
auth: inherit
1111
}
1212

1313
params:query {
1414
aasIds: {{aasIdentifier-1}}
15-
submodelIds: {{submodelIdentifierContact-1}}
15+
submodelIds: {{submodelIdentifierMaintenanceInstructions-1}}
1616
submodelIds: {{submodelIdentifierNameplate-1}}
1717
submodelIds: {{submodelIdentifierCarbonFootprint-1}}
1818
submodelIds: {{submodelIdentifierHandoverDocumentation-1}}

example/apiCollection/Submodel Repository/Submodel/Get Submodel - ContactInfo.bru

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ get {
1111
}
1212

1313
params:path {
14-
submodelIdentifier: {{submodelIdentifierContact-1}}
14+
submodelIdentifier: {{submodelIdentifierMaintenanceInstructions-1}}
1515
}
1616

1717
settings {

example/apiCollection/collection.bru

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ script:pre-request {
2121
'aasIdentifier-1',
2222
'aasIdentifier-2',
2323
'aasIdentifier-3',
24-
'submodelIdentifierContact-1',
24+
'submodelIdentifierMaintenanceInstructions-1',
2525
'submodelIdentifierNameplate-1',
2626
'submodelIdentifierHandoverDocumentation-1',
2727
'submodelIdentifierTechnicalData-1',

source/AAS.TwinEngine.DataEngine.UnitTests/ApplicationLogic/Services/SubmodelRepository/SemanticId/Extraction/SemanticTreeExtractorTests.cs

Lines changed: 90 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -82,7 +82,7 @@ public void Extract_ElementWithNoHandler_CreatesLeafNodeFallback()
8282
_resolver.ResolveSemanticId(submodel, "Test").Returns("http://test/root");
8383
_resolver.ResolveElementSemanticId(element, "UnknownElement").Returns("http://test/unknown");
8484
_resolver.GetValueType(element).Returns(DataType.Unknown);
85-
_resolver.GetCardinality(element).Returns(Cardinality.Unknown);
85+
_resolver.GetCardinality(element).Returns(Cardinality.One);
8686

8787
var result = _sut.Extract(submodel) as SemanticBranchNode;
8888

@@ -93,6 +93,62 @@ public void Extract_ElementWithNoHandler_CreatesLeafNodeFallback()
9393
Equal(DataType.Unknown, leaf.DataType);
9494
}
9595

96+
[Fact]
97+
public void Extract_ElementWithUnknownCardinality_ThrowsInternalDataProcessingException()
98+
{
99+
var element = Substitute.For<ISubmodelElement>();
100+
element.IdShort.Returns("ElementWithUnknownCard");
101+
var submodel = Substitute.For<ISubmodel>();
102+
submodel.IdShort.Returns("Test");
103+
submodel.SubmodelElements.Returns([element]);
104+
_resolver.ResolveSemanticId(submodel, "Test").Returns("http://test/root");
105+
_resolver.GetCardinality(Arg.Is(element)).Returns(x => throw new InternalDataProcessingException("Cardinality is mandatory for SubmodelElement 'ElementWithUnknownCard' in template. Found: Unknown"));
106+
_resolver.ResolveElementSemanticId(element, "ElementWithUnknownCard").Returns("http://test/elem");
107+
_resolver.GetValueType(element).Returns(DataType.String);
108+
109+
var exception = Throws<InternalDataProcessingException>(() => _sut.Extract(submodel));
110+
Contains("Cardinality is mandatory", exception.Message);
111+
}
112+
113+
[Fact]
114+
public void Extract_SubmodelWithUnknownCardinality_ThrowsInternalDataProcessingException()
115+
{
116+
var submodel = Substitute.For<ISubmodel>();
117+
submodel.IdShort.Returns("TestSubmodel");
118+
submodel.SubmodelElements.Returns([]);
119+
_resolver.ResolveSemanticId(submodel, "TestSubmodel").Returns("http://test/root");
120+
121+
var result = _sut.Extract(submodel) as SemanticBranchNode;
122+
123+
NotNull(result);
124+
Equal(Cardinality.One, result!.Cardinality);
125+
}
126+
127+
[Theory]
128+
[InlineData(Cardinality.ZeroToOne)]
129+
[InlineData(Cardinality.One)]
130+
[InlineData(Cardinality.ZeroToMany)]
131+
[InlineData(Cardinality.OneToMany)]
132+
public void Extract_ElementWithValidCardinality_CreatesLeafNode(Cardinality validCardinality)
133+
{
134+
var element = Substitute.For<ISubmodelElement>();
135+
element.IdShort.Returns("ValidElement");
136+
var submodel = Substitute.For<ISubmodel>();
137+
submodel.IdShort.Returns("Test");
138+
submodel.SubmodelElements.Returns([element]);
139+
_resolver.ResolveSemanticId(submodel, "Test").Returns("http://test/root");
140+
_resolver.ResolveElementSemanticId(element, "ValidElement").Returns("http://test/valid");
141+
_resolver.GetValueType(element).Returns(DataType.String);
142+
_resolver.GetCardinality(element).Returns(validCardinality);
143+
144+
var result = _sut.Extract(submodel) as SemanticBranchNode;
145+
146+
NotNull(result);
147+
Single(result!.Children);
148+
var leaf = IsType<SemanticLeafNode>(result.Children[0]);
149+
Equal(validCardinality, leaf.Cardinality);
150+
}
151+
96152
[Fact]
97153
public void Extract_ByIdShortPath_NullSubmodel_ThrowsInvalidDependencyException() => Throws<InvalidDependencyException>(() => _sut.Extract(null!, "path"));
98154

@@ -153,4 +209,37 @@ public void Extract_ByIdShortPath_ChildElementsNull_ThrowsException()
153209

154210
Throws<InternalDataProcessingException>(() => _sut.Extract(submodel, "Prop.Child"));
155211
}
212+
213+
[Fact]
214+
public void ExtractElement_WithUnknownCardinality_ThrowsInternalDataProcessingException()
215+
{
216+
var element = Substitute.For<ISubmodelElement>();
217+
element.IdShort.Returns("TestElement");
218+
_resolver.ResolveElementSemanticId(element, "TestElement").Returns("http://test/elem");
219+
_resolver.GetValueType(element).Returns(DataType.String);
220+
_resolver.GetCardinality(Arg.Is(element)).Returns(x => throw new InternalDataProcessingException("Cardinality is mandatory for SubmodelElement 'TestElement' in template. Found: Unknown"));
221+
222+
var exception = Throws<InternalDataProcessingException>(() => _sut.ExtractElement(element));
223+
Contains("Cardinality is mandatory", exception.Message);
224+
}
225+
226+
[Theory]
227+
[InlineData(Cardinality.ZeroToOne)]
228+
[InlineData(Cardinality.One)]
229+
[InlineData(Cardinality.ZeroToMany)]
230+
[InlineData(Cardinality.OneToMany)]
231+
public void ExtractElement_WithValidCardinality_CreatesLeafNode(Cardinality validCardinality)
232+
{
233+
var element = Substitute.For<ISubmodelElement>();
234+
element.IdShort.Returns("ValidElement");
235+
_resolver.ResolveElementSemanticId(element, "ValidElement").Returns("http://test/valid");
236+
_resolver.GetValueType(element).Returns(DataType.Integer);
237+
_resolver.GetCardinality(element).Returns(validCardinality);
238+
239+
var result = _sut.ExtractElement(element);
240+
241+
var leaf = IsType<SemanticLeafNode>(result);
242+
Equal(validCardinality, leaf.Cardinality);
243+
Equal(DataType.Integer, leaf.DataType);
244+
}
156245
}

source/AAS.TwinEngine.DataEngine.UnitTests/ApplicationLogic/Services/SubmodelRepository/SemanticId/Helpers/ReferenceHelperTests.cs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,8 @@ public ReferenceHelperTests()
2828
SubmodelElementIndexContextPrefix = "_aastwinengineindex_"
2929
});
3030
var templateManagementConfig = Options.Create(new TemplateManagementConfig());
31-
_resolver = new SemanticIdResolver(pluginsConfig, templateManagementConfig);
31+
var logger = Substitute.For<ILogger<SemanticIdResolver>>();
32+
_resolver = new SemanticIdResolver(pluginsConfig, templateManagementConfig, logger);
3233
_logger = Substitute.For<ILogger<ReferenceHelper>>();
3334
_sut = new ReferenceHelper(_resolver, _logger);
3435
}

source/AAS.TwinEngine.DataEngine.UnitTests/ApplicationLogic/Services/SubmodelRepository/SemanticId/Helpers/SemanticIdResolverTests.cs

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,11 @@
1-
using AAS.TwinEngine.DataEngine.ApplicationLogic.Services.SubmodelRepository.SemanticId.Helpers;
1+
using AAS.TwinEngine.DataEngine.ApplicationLogic.Services.SubmodelRepository.SemanticId.Helpers;
22
using AAS.TwinEngine.DataEngine.DomainModel.SubmodelRepository;
33
using AAS.TwinEngine.DataEngine.ServiceConfiguration.Config;
44

55
using AasCore.Aas3_0;
66

77
using Microsoft.Extensions.Options;
8+
using Microsoft.Extensions.Logging;
89

910
using NSubstitute;
1011

@@ -32,16 +33,18 @@ public SemanticIdResolverTests()
3233
{
3334
Semantics = new TemplateSemanticsConfig { InternalSemanticId = "InternalSemanticId" }
3435
});
35-
_sut = new SemanticIdResolver(_pluginsConfig, _templateManagementConfig);
36+
var logger = Substitute.For<ILogger<SemanticIdResolver>>();
37+
_sut = new SemanticIdResolver(_pluginsConfig, _templateManagementConfig, logger);
3638
}
3739

3840
[Fact]
3941
public void Constructor_NullOptions_ThrowsException()
4042
{
4143
var options = Options.Create<PluginsConfig>(null!);
4244
var tmConfig = Options.Create(new TemplateManagementConfig());
45+
var logger = Substitute.For<ILogger<SemanticIdResolver>>();
4346

44-
_ = Throws<NullReferenceException>(() => new SemanticIdResolver(options, tmConfig));
47+
_ = Throws<NullReferenceException>(() => new SemanticIdResolver(options, tmConfig, logger));
4548
}
4649

4750
[Fact]
@@ -169,7 +172,6 @@ public void ResolveElementSemanticId_WithTrailingDigits_AppendsIndex()
169172
[InlineData("ZeroToOne", Cardinality.ZeroToOne)]
170173
[InlineData("ZeroToMany", Cardinality.ZeroToMany)]
171174
[InlineData("OneToMany", Cardinality.OneToMany)]
172-
[InlineData("", Cardinality.Unknown)]
173175
public void GetCardinality_VariousQualifierValues_ReturnsExpected(string? qualifierValue, Cardinality expected)
174176
{
175177
var qualifier = Substitute.For<IQualifier>();

0 commit comments

Comments
 (0)