Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -557,6 +557,31 @@ public void FillOutTemplate_SubmodelOfComplexData_ReturnsSubmodelWithValues()
AssertContactInfo(complexData1, 2, "Test1 John Doe");
}

[Fact]
public void FillOutTemplate_SubmodelOfComplexData_ReturnsSubmodelWithValue()
{
var complexData = TestData.ComplexData;
var submodel = TestData.CreateSubmodelWithComplexData();
submodel.SubmodelElements?.Add(complexData);
var values = TestData.CreateSubmodelWithInValidComplexDataTreeNode();

var submodelWithValues = (Submodel)_sut.FillOutTemplate(submodel, values);

Equal(2, submodelWithValues.SubmodelElements!.Count);
Equal("ComplexData0", submodelWithValues.SubmodelElements[0].IdShort);
Equal("ComplexData1", submodelWithValues.SubmodelElements[1].IdShort);
var complexData0 = GetSubmodelElementCollection(submodelWithValues, 0);
var complexData1 = GetSubmodelElementCollection(submodelWithValues, 1);
Equal(3, complexData1.Value!.Count);
AssertMultiLanguageProperty(complexData0, "Test Example Manufacturer", "Test Beispiel Hersteller");
AssertMultiLanguageProperty(complexData1, "Test1 Example Manufacturer", "Test1 Beispiel Hersteller");
AssertModelType(complexData0, 1, "22.47");
AssertModelType(complexData1, 1, "22.47");
AssertContactList(complexData0, 2, "Test John Doe", "Test Example Model");
AssertContactInfo(complexData0, 3, "Test John Doe");
AssertContactInfo(complexData1, 2, "Test1 John Doe");
}

[Fact]
public void FillOutTemplate_ShouldNotChangeAnyThing_WhenReferenceElementHasNullValue()
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -189,22 +189,64 @@ public async Task GetSubmodelTemplateAsync_WithListIndexPath_ReturnsSubmodelWith
}

[Fact]
public async Task GetSubmodelTemplateAsync_ThrowsNotFoundException_WhenListIndexIsOutOfRange()
public async Task GetSubmodelTemplateAsync_Supports_UrlEncoded_ListIndex()
{
var submodel = TestData.CreateSubmodelWithModel3DList();
const string Path = "Model3D[5].ModelFile1";
const string Path = "Model3D%5B0%5D.ModelDataFile";

_mappingProvider.GetTemplateId(SubmodelId).Returns(TemplateId);
_templateProvider.GetSubmodelTemplateAsync(TemplateId, Arg.Any<CancellationToken>())
.Returns(submodel);

await Assert.ThrowsAsync<InternalDataProcessingException>(() => _sut.GetSubmodelTemplateAsync(SubmodelId, Path, CancellationToken.None));
var result = await _sut.GetSubmodelTemplateAsync(SubmodelId, Path, CancellationToken.None);

Assert.NotNull(result);
}

[Fact]
public async Task GetSubmodelTemplateAsync_ThrowsNotFoundException_WhenListElementNotFound()
public async Task GetSubmodelTemplateAsync_Throws_When_ListIndex_IsNegative()
{
var submodel = TestData.CreateSubmodelWithModel3DList();
const string Path = "NonExistentList[0].ModelFile1";
const string Path = "Model3D[-1]";

_mappingProvider.GetTemplateId(SubmodelId).Returns(TemplateId);
_templateProvider.GetSubmodelTemplateAsync(TemplateId, Arg.Any<CancellationToken>())
.Returns(submodel);

await Assert.ThrowsAsync<InternalDataProcessingException>(
() => _sut.GetSubmodelTemplateAsync(SubmodelId, Path, CancellationToken.None));
}

[Fact]
public async Task GetSubmodelTemplateAsync_ReturnsSubmodel_WhenTypeValueListElementIsSubmodelCollection_AndListIndexExceedsAvailableElements()
{
var expectedSubmodel = TestData.CreateSubmodelWithModel3DList();
var submodel = TestData.CreateSubmodelWithModel3DList();
const string Path = "Model3D[5].ModelDataFile";
_mappingProvider.GetTemplateId(SubmodelId).Returns(TemplateId);
_templateProvider.GetSubmodelTemplateAsync(TemplateId, Arg.Any<CancellationToken>())
.Returns(submodel);

var result = await _sut.GetSubmodelTemplateAsync(SubmodelId, Path, CancellationToken.None);

Assert.Equal(GetSemanticId(expectedSubmodel), GetSemanticId(result));

var list = result.SubmodelElements?.FirstOrDefault() as SubmodelElementList;
Assert.NotNull(list);
Assert.Single(list.Value!);
var collection = list.Value![0] as SubmodelElementCollection;
Assert.Single(collection!.Value!);
var file = collection!.Value!.FirstOrDefault() as File;
Assert.NotNull(file);
Assert.Equal("ModelDataFile", file.IdShort);
Assert.Equal("https://localhost/ModelDataFile.glb", file.Value);
}

[Fact]
public async Task GetSubmodelTemplateAsync_ThrowsInternalDataProcessingException_WhenTypeValueListElementIsSubmodelProperty_AndListIndexExceedsAvailableElements()
{
var submodel = TestData.CreateSubmodelWithPropertyInsideList();
const string Path = "listProperty[2]";
_mappingProvider.GetTemplateId(SubmodelId).Returns(TemplateId);
_templateProvider.GetSubmodelTemplateAsync(TemplateId, Arg.Any<CancellationToken>())
.Returns(submodel);
Expand Down Expand Up @@ -280,5 +322,4 @@ public async Task GetSubmodelTemplateAsync_WithIdShortPath_ThrowsSubmodelElement
await Assert.ThrowsAsync<SubmodelElementNotFoundException>(
() => _sut.GetSubmodelTemplateAsync(SubmodelId, "SomePath", CancellationToken.None));
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@

using AasCore.Aas3_0;

using Microsoft.VisualStudio.TestPlatform.ObjectModel;

using File = AasCore.Aas3_0.File;
using Key = AasCore.Aas3_0.Key;
using Range = AasCore.Aas3_0.Range;
Expand Down Expand Up @@ -632,6 +634,41 @@ public static Submodel CreateSubmodelWithoutExtraElementsNested()
);
}

public static SubmodelElementList CreateElementListWithProperty()
{
return new SubmodelElementList(
idShort: "listProperty",
semanticId: new Reference(
ReferenceTypes.ExternalReference,
[
new Key(KeyTypes.SubmodelElementList,
"http://example.com/idta/digital-nameplate/list-property")
]
),
typeValueListElement: AasSubmodelElements.Property,
value: [
CreateContactName()
]
);
}

public static Submodel CreateSubmodelWithPropertyInsideList()
{
return new Submodel(
id: "http://example.com/idta/digital-nameplate",
idShort: "DigitalNameplate",
semanticId: new Reference(
ReferenceTypes.ExternalReference,
[
new Key(KeyTypes.Submodel, "http://example.com/idta/digital-nameplate/semantic-id")
]
),
submodelElements: [
CreateElementListWithProperty()
]
);
}

public static Submodel CreateSubmodelWithoutExtraElements()
{
return new Submodel(
Expand Down Expand Up @@ -720,6 +757,45 @@ public static Submodel CreateSubmodelWithModel3DList()
]
);

public static readonly SubmodelElementCollection InValidComplexData = new(
idShort: "ComplexData",
semanticId: new Reference(
ReferenceTypes.ExternalReference,
[
new Key(KeyTypes.SubmodelElementList, "http://example.com/idta/digital-nameplate/complex-data")
]
),
qualifiers:
[
new Qualifier(
type: "ExternalReference",
valueType: DataTypeDefXsd.String,
value: "OneToMany")
],
value: [
CreateManufacturerName(),
new Property(
idShort: "ModelType",
valueType: DataTypeDefXsd.String,
value: "", // left intentionally empty for FillOut tests
semanticId: new Reference(
ReferenceTypes.ExternalReference,
[
new Key(KeyTypes.Property, "http://example.com/idta/digital-nameplate/model-type")
]
),
qualifiers:
[
new Qualifier(
type: "ExternalReference",
valueType: DataTypeDefXsd.String,
value: "ZeroToOne")
]),
CreateContactList(),
CreateContactInformation(),
]
);

public static readonly SemanticTreeNode SubmodelTreeNode = CreateSubmodelTreeNode();

public static SemanticTreeNode CreateSubmodelTreeNode()
Expand Down Expand Up @@ -778,6 +854,41 @@ public static SemanticTreeNode CreateSubmodelWithComplexDataTreeNode()
return semanticTreeNode;
}

public static SemanticTreeNode CreateSubmodelWithInValidComplexDataTreeNode()
{
var semanticTreeNode = new SemanticBranchNode("http://example.com/idta/digital-nameplate/semantic-id", Cardinality.Unknown);

var complexDataBranchNode1 = new SemanticBranchNode("http://example.com/idta/digital-nameplate/complex-data", Cardinality.ZeroToMany);

var complexDataBranchNode2 = new SemanticBranchNode("http://example.com/idta/digital-nameplate/complex-data", Cardinality.ZeroToMany);

semanticTreeNode.AddChild(complexDataBranchNode1);

semanticTreeNode.AddChild(complexDataBranchNode2);

complexDataBranchNode1.AddChild(CreateManufacturerNameTreeNode());

complexDataBranchNode1.AddChild(CreateModelTypeTreeNode());

complexDataBranchNode1.AddChild(CreateContactListTreeNode());

complexDataBranchNode1.AddChild(CreateContactInformationTreeNode());

complexDataBranchNode2.AddChild(CreateManufacturerNameTreeNode("1"));

complexDataBranchNode2.AddChild(CreateModelTypeTreeNode());

complexDataBranchNode2.AddChild(CreateContactListTreeNode("1"));

complexDataBranchNode2.AddChild(CreateContactListTreeNode("2"));

complexDataBranchNode2.AddChild(new SemanticLeafNode("http://example.com/idta/digital-nameplate/contact-list", $"Test InValid Contact List", DataType.String, Cardinality.One));

complexDataBranchNode2.AddChild(CreateContactInformationTreeNode("1"));

return semanticTreeNode;
}

public static SemanticTreeNode CreateManufacturerNameTreeNode(string testObject = "")
{
var manufacturerName = new SemanticBranchNode("http://example.com/idta/digital-nameplate/manufacturer-name", Cardinality.One);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -526,6 +526,15 @@ private void FillOutSubmodelElementValue(List<ISubmodelElement> elements, Semant
continue;
}

if (!AreAllNodesOfSameType(semanticTreeNodes, out _))
{
logger.LogWarning("Mixed node types found for element '{IdShort}' with SemanticId '{SemanticId}'. Expected all nodes to be either SemanticBranchNode or SemanticLeafNode. Removing element.",
element.IdShort,
ExtractSemanticId(element));
_ = elements.Remove(element);
continue;
}

if (semanticTreeNodes.Count > 1 && element is not Property && element is not ReferenceElement)
{
_ = elements.Remove(element);
Expand All @@ -548,6 +557,25 @@ private void FillOutSubmodelElementValue(List<ISubmodelElement> elements, Semant
}
}

private static bool AreAllNodesOfSameType(List<SemanticTreeNode> nodes, out Type? nodeType)
{
if (nodes.Count == 0)
{
nodeType = null;
return true;
}

var firstNodeType = nodes[0].GetType();
nodeType = firstNodeType;

if (firstNodeType != typeof(SemanticBranchNode) && firstNodeType != typeof(SemanticLeafNode))
{
return false;
}

return nodes.All(node => node.GetType() == firstNodeType);
}

private void HandleSingleSemanticTreeNode(ISubmodelElement element, SemanticTreeNode node) => FillOutTemplate(element, node);

private void FillOutMultiLanguageProperty(MultiLanguageProperty mlp, SemanticTreeNode values)
Expand Down Expand Up @@ -800,7 +828,7 @@ private static IEnumerable<SemanticTreeNode> FindNodeBySemanticId(SemanticTreeNo
/// e.g. "element[3]" -> matches Group1= "element", Group2 = "3"
/// Pattern: ^(.+?)\[(\d+)\]$
/// </summary>
[GeneratedRegex(@"^(.+?)\[(\d+)\]$")]
[GeneratedRegex(@"^(.+?)(?:\[(\d+)\]|%5B(\d+)%5D)$")]
private static partial Regex SubmodelElementListIndex();

/// <summary>
Expand All @@ -821,19 +849,27 @@ private static IEnumerable<SemanticTreeNode> FindNodeBySemanticId(SemanticTreeNo
return submodelElements?.FirstOrDefault(e => e.IdShort == idShort);
}

private static bool TryParseIdShortWithBracketIndex(string segment, out string idShortWithoutIndex, out int index)
private static bool TryParseIdShortWithBracketIndex(string idShort, out string idShortWithoutIndex, out int index)
{
var match = SubmodelElementListIndex().Match(segment);
if (match.Success)
var match = SubmodelElementListIndex().Match(idShort);
if (!match.Success)
{
idShortWithoutIndex = match.Groups[1].Value;
index = int.Parse(match.Groups[2].Value, CultureInfo.InvariantCulture);
return true;
idShortWithoutIndex = string.Empty;
index = -1;
return false;
}

idShortWithoutIndex = match.Groups[1].Value;
var indexGroup = match.Groups[2].Success ? match.Groups[2] : match.Groups[3];
if (!indexGroup.Success)
{
idShortWithoutIndex = string.Empty;
index = -1;
return false;
}

idShortWithoutIndex = string.Empty;
index = -1;
return false;
index = int.Parse(indexGroup.Value, CultureInfo.InvariantCulture);
return true;
}

private ISubmodelElement GetElementFromListByIndex(IEnumerable<ISubmodelElement>? elements, string idShortWithoutIndex, int index)
Expand Down
Loading
Loading