From eb6ba2ef3b6d4bb8b7fee59f4bfea82d45e58eb2 Mon Sep 17 00:00:00 2001 From: Kevalkumar Date: Thu, 5 Mar 2026 14:45:09 +0530 Subject: [PATCH 01/71] refactor: Change public methods to private in SemanticIdHandler for encapsulation --- .../Services/SubmodelRepository/SemanticIdHandler.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/source/AAS.TwinEngine.DataEngine/ApplicationLogic/Services/SubmodelRepository/SemanticIdHandler.cs b/source/AAS.TwinEngine.DataEngine/ApplicationLogic/Services/SubmodelRepository/SemanticIdHandler.cs index aa6df935..72e77e8e 100644 --- a/source/AAS.TwinEngine.DataEngine/ApplicationLogic/Services/SubmodelRepository/SemanticIdHandler.cs +++ b/source/AAS.TwinEngine.DataEngine/ApplicationLogic/Services/SubmodelRepository/SemanticIdHandler.cs @@ -513,7 +513,7 @@ private ISubmodelElement FillOutTemplate(ISubmodelElement submodelElementTemplat return submodelElementTemplate; } - public void FillOutSubmodelElementList(SubmodelElementList list, SemanticTreeNode values) + private void FillOutSubmodelElementList(SubmodelElementList list, SemanticTreeNode values) { if (list?.Value == null || list.Value.Count == 0) { @@ -523,7 +523,7 @@ public void FillOutSubmodelElementList(SubmodelElementList list, SemanticTreeNod FillOutSubmodelElementValue(list.Value, values, false); } - public void FillOutSubmodelElementCollection(SubmodelElementCollection collection, SemanticTreeNode values) + private void FillOutSubmodelElementCollection(SubmodelElementCollection collection, SemanticTreeNode values) { if (collection?.Value == null || collection.Value.Count == 0) { @@ -647,7 +647,7 @@ private void FillOutMultiLanguageProperty(MultiLanguageProperty mlp, SemanticTre } } - public void FillOutEntity(Entity entity, SemanticTreeNode values) + private void FillOutEntity(Entity entity, SemanticTreeNode values) { if (entity.EntityType == EntityType.SelfManagedEntity) { From 21cd26869585e145ee406423861fd067a62a6a4d Mon Sep 17 00:00:00 2001 From: Kevalkumar Date: Fri, 6 Mar 2026 19:17:01 +0530 Subject: [PATCH 02/71] added implementation for refactoring semanticidhandler --- example/docker-compose.yml | 2 +- .../SemanticIdHandlerTests.cs | 69 +- .../Extraction/ISemanticTreeExtractor.cs | 12 + .../Extraction/SemanticTreeExtractor.cs | 244 +++++ .../SemanticId/FillOut/ISubmodelFiller.cs | 10 + .../SemanticId/FillOut/SubmodelFiller.cs | 360 +++++++ .../Helpers/Interfaces/IReferenceHelper.cs | 14 + .../Helpers/Interfaces/ISemanticIdResolver.cs | 24 + .../Interfaces/ISubmodelElementHelper.cs | 16 + .../SemanticId/Helpers/ReferenceHelper.cs | 99 ++ .../SemanticId/Helpers/SemanticIdResolver.cs | 139 +++ .../Helpers/SemanticTreeNavigator.cs | 55 + .../Helpers/SubmodelElementHelper.cs | 119 +++ .../SubmodelRepository/SemanticIdHandler.cs | 946 +----------------- ...pplicationDependencyInjectionExtensions.cs | 9 + 15 files changed, 1150 insertions(+), 968 deletions(-) create mode 100644 source/AAS.TwinEngine.DataEngine/ApplicationLogic/Services/SubmodelRepository/SemanticId/Extraction/ISemanticTreeExtractor.cs create mode 100644 source/AAS.TwinEngine.DataEngine/ApplicationLogic/Services/SubmodelRepository/SemanticId/Extraction/SemanticTreeExtractor.cs create mode 100644 source/AAS.TwinEngine.DataEngine/ApplicationLogic/Services/SubmodelRepository/SemanticId/FillOut/ISubmodelFiller.cs create mode 100644 source/AAS.TwinEngine.DataEngine/ApplicationLogic/Services/SubmodelRepository/SemanticId/FillOut/SubmodelFiller.cs create mode 100644 source/AAS.TwinEngine.DataEngine/ApplicationLogic/Services/SubmodelRepository/SemanticId/Helpers/Interfaces/IReferenceHelper.cs create mode 100644 source/AAS.TwinEngine.DataEngine/ApplicationLogic/Services/SubmodelRepository/SemanticId/Helpers/Interfaces/ISemanticIdResolver.cs create mode 100644 source/AAS.TwinEngine.DataEngine/ApplicationLogic/Services/SubmodelRepository/SemanticId/Helpers/Interfaces/ISubmodelElementHelper.cs create mode 100644 source/AAS.TwinEngine.DataEngine/ApplicationLogic/Services/SubmodelRepository/SemanticId/Helpers/ReferenceHelper.cs create mode 100644 source/AAS.TwinEngine.DataEngine/ApplicationLogic/Services/SubmodelRepository/SemanticId/Helpers/SemanticIdResolver.cs create mode 100644 source/AAS.TwinEngine.DataEngine/ApplicationLogic/Services/SubmodelRepository/SemanticId/Helpers/SemanticTreeNavigator.cs create mode 100644 source/AAS.TwinEngine.DataEngine/ApplicationLogic/Services/SubmodelRepository/SemanticId/Helpers/SubmodelElementHelper.cs diff --git a/example/docker-compose.yml b/example/docker-compose.yml index f7c96541..94208f84 100644 --- a/example/docker-compose.yml +++ b/example/docker-compose.yml @@ -23,7 +23,7 @@ services: - twinengine-network twinengine-dataengine: - image: ghcr.io/aas-twinengine/dataengine:v1.0.0 + image: dataengine:1.0.0 container_name: twinengine-dataengine depends_on: dpp-plugin: diff --git a/source/AAS.TwinEngine.DataEngine.UnitTests/ApplicationLogic/Services/SubmodelRepository/SemanticIdHandlerTests.cs b/source/AAS.TwinEngine.DataEngine.UnitTests/ApplicationLogic/Services/SubmodelRepository/SemanticIdHandlerTests.cs index a7367903..0beea9f6 100644 --- a/source/AAS.TwinEngine.DataEngine.UnitTests/ApplicationLogic/Services/SubmodelRepository/SemanticIdHandlerTests.cs +++ b/source/AAS.TwinEngine.DataEngine.UnitTests/ApplicationLogic/Services/SubmodelRepository/SemanticIdHandlerTests.cs @@ -1,8 +1,12 @@ -using System.Reflection; +/*using System.Reflection; using AAS.TwinEngine.DataEngine.ApplicationLogic.Exceptions.Application; using AAS.TwinEngine.DataEngine.ApplicationLogic.Services.SubmodelRepository; using AAS.TwinEngine.DataEngine.ApplicationLogic.Services.SubmodelRepository.Config; +using AAS.TwinEngine.DataEngine.ApplicationLogic.Services.SubmodelRepository.SemanticId.Extraction; +using AAS.TwinEngine.DataEngine.ApplicationLogic.Services.SubmodelRepository.SemanticId.FillOut; +using AAS.TwinEngine.DataEngine.ApplicationLogic.Services.SubmodelRepository.SemanticId.Helpers; +using AAS.TwinEngine.DataEngine.ApplicationLogic.Services.SubmodelRepository.SemanticId.Helpers.Interfaces; using AAS.TwinEngine.DataEngine.DomainModel.SubmodelRepository; using AasCore.Aas3_0; @@ -23,18 +27,23 @@ namespace AAS.TwinEngine.DataEngine.UnitTests.ApplicationLogic.Services.Submodel public class SemanticIdHandlerTests { private readonly SemanticIdHandler _sut; - private readonly ILogger _logger; + private readonly ILogger _extractorLogger; + private readonly ILogger _fillerLogger; private readonly IOptions _mlpSettings; private readonly IOptions _semantics; + private readonly ISemanticIdResolver _resolver; public SemanticIdHandlerTests() { - _logger = Substitute.For>(); + _extractorLogger = Substitute.For>(); + _fillerLogger = Substitute.For>(); _mlpSettings = Substitute.For>(); _ = _mlpSettings.Value.Returns(new MultiLanguagePropertySettings { DefaultLanguages = null }); _semantics = Substitute.For>(); _ = _semantics.Value.Returns(new Semantics { MultiLanguageSemanticPostfixSeparator = "_", SubmodelElementIndexContextPrefix = "_aastwinengineindex_" }); - _sut = new SemanticIdHandler(_logger, _semantics, _mlpSettings); + + _resolver = new SemanticIdResolver(_semantics); + _sut = CreateSut(_semantics, _mlpSettings); } [Fact] @@ -57,9 +66,8 @@ public void Extract_TemplateNull_ThrowsException() public void SemanticIdHandler_NullSemantics_ThrowsException() { var options = Options.Create(options: null!); - var logger = Substitute.For>(); - _ = Throws(() => new SemanticIdHandler(logger, options, _mlpSettings)); + _ = Throws(() => new SemanticIdResolver(options)); } [Fact] @@ -235,7 +243,7 @@ public void Extract_EmptyMultiLanguageProperty_WithDefaultLanguagesAsNull() public void Extract_EmptyMultiLanguageProperty_WithDefaultLanguagesAs_En_De_Fr() { var mlpSettings = CreateMlpSettings(["de", "en", "fr"]); - var sut = new SemanticIdHandler(_logger, _semantics, mlpSettings); + var sut = CreateSut(_semantics, mlpSettings); var mlp = TestData.CreateSubmodelWithManufacturerNameWithOutElements(); var node = sut.Extract(mlp) as SemanticBranchNode; @@ -253,7 +261,7 @@ public void Extract_EmptyMultiLanguageProperty_WithDefaultLanguagesAs_En_De_Fr() public void Extract_MultiLanguageProperty_WithDefaultLanguagesAs_En_De_Fr() { var mlpSettings = CreateMlpSettings(["de", "en", "fr"]); - var sut = new SemanticIdHandler(_logger, _semantics, mlpSettings); + var sut = CreateSut(_semantics, mlpSettings); var mlp = TestData.CreateSubmodelWithManufacturerNameWithTwoLanguagesInTemplate(); var node = sut.Extract(mlp) as SemanticBranchNode; @@ -279,7 +287,7 @@ public void Extract_EmptySubmodelElementCollection_LogsWarningAndReturnsNode() var contactInformationNode = node.Children[0] as SemanticBranchNode; Equal("http://example.com/idta/digital-nameplate/contact-information", contactInformationNode?.SemanticId); Empty(contactInformationNode!.Children); - _logger.Received(1).Log(LogLevel.Warning, Arg.Any(), + _extractorLogger.Received(1).Log(LogLevel.Warning, Arg.Any(), Arg.Is(state => state.ToString()! .Contains("No elements defined in SubmodelElementCollection ContactInformation")), null, @@ -298,7 +306,7 @@ public void Extract_EmptySubmodelElementList_LogsWarningAndReturnsNode() var contactInformationNode = node.Children[0] as SemanticBranchNode; Equal("http://example.com/idta/digital-nameplate/contact-list", contactInformationNode?.SemanticId); Empty(contactInformationNode!.Children); - _logger.Received(1).Log(LogLevel.Warning, Arg.Any(), + _extractorLogger.Received(1).Log(LogLevel.Warning, Arg.Any(), Arg.Is(state => state.ToString()! .Contains("No elements defined in SubmodelElementList ContactList")), null, @@ -406,9 +414,7 @@ public void GetCardinality_VariousQualifierValues_ReturnsExpected(string? qualif var element = Substitute.For(); element.Qualifiers.Returns([qualifier]); - var actual = (Cardinality)typeof(SemanticIdHandler) - .GetMethod("GetCardinality", BindingFlags.NonPublic | BindingFlags.Static)! - .Invoke(null, [element])!; + var actual = _resolver.GetCardinality(element); Equal(expected, actual); } @@ -419,9 +425,7 @@ public void GetCardinality_QualifiersNull_ReturnsUnknown() var element = Substitute.For(); element.Qualifiers.Returns((List?)null); - var actual = (Cardinality)typeof(SemanticIdHandler) - .GetMethod("GetCardinality", BindingFlags.NonPublic | BindingFlags.Static)! - .Invoke(null, [element])!; + var actual = _resolver.GetCardinality(element); Equal(Cardinality.Unknown, actual); } @@ -432,9 +436,7 @@ public void GetCardinality_EmptyQualifiers_ReturnsUnknown() var element = Substitute.For(); element.Qualifiers.Returns([]); - var actual = (Cardinality)typeof(SemanticIdHandler) - .GetMethod("GetCardinality", BindingFlags.NonPublic | BindingFlags.Static)! - .Invoke(null, [element])!; + var actual = _resolver.GetCardinality(element); Equal(Cardinality.Unknown, actual); } @@ -463,9 +465,7 @@ public void GetValueType_PropertyValueType_ReturnsExpected(DataTypeDefXsd valueT qualifiers: [] ); - var actual = (DataType)typeof(SemanticIdHandler) - .GetMethod("GetValueType", BindingFlags.NonPublic | BindingFlags.Static)! - .Invoke(null, [prop])!; + var actual = _resolver.GetValueType(prop); Equal(expected, actual); } @@ -475,9 +475,7 @@ public void GetValueType_ElementWithoutValueProperty_ReturnsUnknown() { var element = Substitute.For(); - var actual = (DataType)typeof(SemanticIdHandler) - .GetMethod("GetValueType", BindingFlags.NonPublic | BindingFlags.Static)! - .Invoke(null, [element])!; + var actual = _resolver.GetValueType(element); Equal(DataType.Unknown, actual); } @@ -833,7 +831,7 @@ public void FillOutTemplate_MultiLanguageProperty_WithDefaultLanguagesAsNull_Pop public void FillOutTemplate_EmptyMultiLanguageProperty_WithDefaultLanguagesAs_En_De_Fr_AddsAllLanguages() { var mlpSettings = CreateMlpSettings(["de", "en", "fr"]); - var sut = new SemanticIdHandler(_logger, _semantics, mlpSettings); + var sut = CreateSut(_semantics, mlpSettings); var submodel = TestData.CreateSubmodelWithManufacturerNameWithOutElements(); var semanticTree = TestData.CreateSubmodelWithManufacturerName(); @@ -846,7 +844,7 @@ public void FillOutTemplate_EmptyMultiLanguageProperty_WithDefaultLanguagesAs_En Equal(3, mlp.Value!.Count); var languages = mlp.Value.Select(v => v.Language).OrderBy(l => l).ToList(); Equal(["de", "en", "fr"], languages); - _logger.Received(3).Log( + _fillerLogger.Received(3).Log( LogLevel.Information, Arg.Any(), Arg.Is(state => state.ToString()!.Contains("Added language")), @@ -859,7 +857,7 @@ public void FillOutTemplate_EmptyMultiLanguageProperty_WithDefaultLanguagesAs_En public void FillOutTemplate_MultiLanguageProperty_WithDefaultLanguagesAs_En_De_Fr_MergesWithTemplateLanguages() { var mlpSettings = CreateMlpSettings(["de", "en", "fr"]); - var sut = new SemanticIdHandler(_logger, _semantics, mlpSettings); + var sut = CreateSut(_semantics, mlpSettings); var submodel = TestData.CreateSubmodelWithManufacturerNameWithTwoLanguagesInTemplate(); var semanticTree = TestData.CreateSubmodelWithManufacturerName(); @@ -879,7 +877,7 @@ public void FillOutTemplate_MultiLanguageProperty_WithDefaultLanguagesAs_En_De_F var frValue = mlp.Value.FirstOrDefault(v => v.Language == "fr"); NotNull(frValue); Equal("Exemple de test Fabricant", frValue.Text); - _logger.Received(1).Log( + _fillerLogger.Received(1).Log( LogLevel.Information, Arg.Any(), Arg.Is(state => state.ToString()!.Contains("Added language 'fr'")), @@ -969,6 +967,18 @@ private static Submodel CreateSubmodelWithSubmodelElement(ISubmodelElement submo ); } + private SemanticIdHandler CreateSut(IOptions semantics, IOptions mlpSettings) + { + var resolver = new SemanticIdResolver(semantics); + var navigator = new SemanticTreeNavigator(); + var helper = new SubmodelElementHelper(Substitute.For>()); + var multiLanguageHelper = new MultiLanguageHelper(mlpSettings); + var referenceHelper = new ReferenceHelper(resolver, navigator, Substitute.For>()); + var extractor = new SemanticTreeExtractor(resolver, helper, multiLanguageHelper, referenceHelper, _extractorLogger); + var filler = new SubmodelFiller(resolver, navigator, helper, multiLanguageHelper, referenceHelper, _fillerLogger); + return new SemanticIdHandler(extractor, filler); + } + private static string GetSemanticId(IHasSemantics hasSemantics) => hasSemantics.SemanticId?.Keys?.FirstOrDefault()?.Value ?? string.Empty; private static IOptions CreateMlpSettings(List? defaultLanguages) @@ -980,3 +990,4 @@ private static IOptions CreateMlpSettings(List logger) : ISemanticTreeExtractor +{ + public SemanticTreeNode Extract(ISubmodel submodelTemplate) + { + ArgumentNullException.ThrowIfNull(submodelTemplate); + + var rootNode = new SemanticBranchNode(semanticIdResolver.ResolveSemanticId(submodelTemplate, submodelTemplate.IdShort!), Cardinality.Unknown); + var childNodes = submodelTemplate.SubmodelElements! + .Select(ExtractElement) + .Where(childNode => childNode != null) + .ToList(); + + foreach (var childNode in childNodes) + { + rootNode.AddChild(childNode!); + } + + return rootNode; + } + + public ISubmodelElement Extract(ISubmodel submodelTemplate, string idShortPath) + { + ArgumentNullException.ThrowIfNull(submodelTemplate); + ArgumentNullException.ThrowIfNull(idShortPath); + + var currentSubmodelElements = submodelTemplate.SubmodelElements; + var idShortPathSegments = idShortPath.Split('.'); + for (var index = 0; index < idShortPathSegments.Length; index++) + { + var currentIdShort = idShortPathSegments[index]; + var isLastSegment = index == idShortPathSegments.Length - 1; + + var matchedElement = elementHelper.GetElementByIdShort(currentSubmodelElements, currentIdShort) + ?? throw new InternalDataProcessingException(); + if (isLastSegment) + { + return matchedElement; + } + + currentSubmodelElements = elementHelper.GetChildElements(matchedElement) as List + ?? throw new InternalDataProcessingException(); + } + + throw new InternalDataProcessingException(); + } + + private SemanticTreeNode? ExtractElement(ISubmodelElement submodelElementTemplate) + { + ArgumentNullException.ThrowIfNull(submodelElementTemplate); + + return submodelElementTemplate switch + { + SubmodelElementCollection collection => ExtractCollection(collection), + SubmodelElementList list => ExtractList(list), + MultiLanguageProperty mlp => ExtractMultiLanguageProperty(mlp), + Range range => ExtractRange(range), + ReferenceElement re => ExtractReferenceElement(re), + RelationshipElement relationshipElement => ExtractRelationshipElement(relationshipElement), + Entity entity => ExtractEntity(entity), + _ => CreateLeafNode(submodelElementTemplate) + }; + } + + private SemanticBranchNode ExtractList(SubmodelElementList list) + { + var node = new SemanticBranchNode(semanticIdResolver.ResolveElementSemanticId(list, list.IdShort!), semanticIdResolver.GetCardinality(list)); + if (list.Value?.Count > 0) + { + foreach (var element in list.Value) + { + var child = ExtractElement(element); + if (child != null) + { + node.AddChild(child); + } + } + } + else + { + logger.LogWarning("No elements defined in SubmodelElementList {ListIdShort}", list.IdShort); + } + + return node; + } + + private SemanticBranchNode ExtractCollection(SubmodelElementCollection collection) + { + var node = new SemanticBranchNode(semanticIdResolver.ResolveElementSemanticId(collection, collection.IdShort!), semanticIdResolver.GetCardinality(collection)); + if (collection.Value?.Count > 0) + { + foreach (var element in collection.Value.Where(_ => true)) + { + var child = ExtractElement(element); + if (child != null) + { + node.AddChild(child); + } + } + } + else + { + logger.LogWarning("No elements defined in SubmodelElementCollection {CollectionIdShort}", collection.IdShort); + } + + return node; + } + + private SemanticBranchNode? ExtractReferenceElement(ReferenceElement referenceElement) + { + if (referenceElement.Value == null || referenceElement.Value.Type == ReferenceTypes.ExternalReference) + { + return null; + } + + return referenceHelper.ExtractReferenceKeys(referenceElement.Value, semanticIdResolver.ResolveElementSemanticId(referenceElement, referenceElement.IdShort!), semanticIdResolver.GetCardinality(referenceElement)); + } + + private SemanticBranchNode? ExtractRelationshipElement(RelationshipElement relationshipElement) + { + if (relationshipElement.First.Type == ReferenceTypes.ExternalReference && relationshipElement.Second.Type == ReferenceTypes.ExternalReference) + { + return null; + } + + var semanticId = semanticIdResolver.GetSemanticId(relationshipElement); + var cardinality = semanticIdResolver.GetCardinality(relationshipElement); + var relationshipElementNode = new SemanticBranchNode(semanticId, cardinality); + + if (relationshipElement.First.Type == ReferenceTypes.ModelReference) + { + var referenceNode = referenceHelper.ExtractReferenceKeys(relationshipElement.First, $"{semanticId}{SemanticIdResolver.RelationshipElementFirstPostFixSeparator}", cardinality); + if (referenceNode != null) + { + relationshipElementNode.AddChild(referenceNode); + } + } + + if (relationshipElement.Second.Type == ReferenceTypes.ModelReference) + { + var referenceNode = referenceHelper.ExtractReferenceKeys(relationshipElement.Second, $"{semanticId}{SemanticIdResolver.RelationshipElementSecondPostFixSeparator}", cardinality); + if (referenceNode != null) + { + relationshipElementNode.AddChild(referenceNode); + } + } + + return relationshipElementNode; + } + + private SemanticBranchNode ExtractEntity(Entity entity) + { + var semanticId = semanticIdResolver.ResolveElementSemanticId(entity, entity.IdShort!); + var node = new SemanticBranchNode(semanticId, semanticIdResolver.GetCardinality(entity)); + if (entity.EntityType == EntityType.SelfManagedEntity) + { + var globalAssetIdNode = new SemanticLeafNode(semanticId + SemanticIdResolver.EntityGlobalAssetIdPostFix, string.Empty, DataType.String, Cardinality.One); + node.AddChild(globalAssetIdNode); + if (entity.SpecificAssetIds != null) + { + foreach (var specificAssetId in entity.SpecificAssetIds) + { + IHasSemantics specificAsset = specificAssetId; + if (specificAsset.SemanticId == null) + { + continue; + } + + var specificAssetIdNode = new SemanticLeafNode(semanticIdResolver.GetSemanticId(specificAssetId), string.Empty, DataType.String, Cardinality.One); + node.AddChild(specificAssetIdNode); + } + } + } + + if (entity.Statements?.Count > 0) + { + foreach (var child in entity.Statements.Select(ExtractElement).OfType()) + { + node.AddChild(child); + } + } + else + { + logger.LogWarning("No elements defined in Entity {EntityIdShort}", entity.IdShort); + } + + return node; + } + + private SemanticBranchNode? ExtractMultiLanguageProperty(MultiLanguageProperty mlp) + { + var semanticId = semanticIdResolver.ExtractSemanticId(mlp); + var node = new SemanticBranchNode(semanticId, semanticIdResolver.GetCardinality(mlp)); + + var languages = elementHelper.ResolveLanguages(mlp); + + if (mlp.Value is not { Count: > 0 }) + { + logger.LogInformation("No languages defined in template for MultiLanguageProperty {MlpIdShort}", mlp.IdShort); + } + + var mlpSeparator = semanticIdResolver.MlpPostFixSeparator; + foreach (var langSemanticId in languages.Select(language => string.Concat(semanticId, mlpSeparator, language))) + { + node.AddChild(new SemanticLeafNode(langSemanticId, string.Empty, DataType.String, Cardinality.ZeroToOne)); + } + + return node; + } + + private SemanticBranchNode ExtractRange(Range range) + { + var semanticId = semanticIdResolver.ExtractSemanticId(range); + var valueType = semanticIdResolver.GetValueType(range); + var node = new SemanticBranchNode(semanticId, semanticIdResolver.GetCardinality(range)); + + node.AddChild(new SemanticLeafNode(semanticId + SemanticIdResolver.RangeMinimumPostFixSeparator, string.Empty, valueType, Cardinality.ZeroToOne)); + node.AddChild(new SemanticLeafNode(semanticId + SemanticIdResolver.RangeMaximumPostFixSeparator, string.Empty, valueType, Cardinality.ZeroToOne)); + + return node; + } + + private SemanticLeafNode CreateLeafNode(ISubmodelElement element) + { + var semanticId = semanticIdResolver.ResolveElementSemanticId(element, element.IdShort!); + var valueType = semanticIdResolver.GetValueType(element); + var cardinality = semanticIdResolver.GetCardinality(element); + return new SemanticLeafNode(semanticId, string.Empty, valueType, cardinality); + } +} diff --git a/source/AAS.TwinEngine.DataEngine/ApplicationLogic/Services/SubmodelRepository/SemanticId/FillOut/ISubmodelFiller.cs b/source/AAS.TwinEngine.DataEngine/ApplicationLogic/Services/SubmodelRepository/SemanticId/FillOut/ISubmodelFiller.cs new file mode 100644 index 00000000..3a9ddd3c --- /dev/null +++ b/source/AAS.TwinEngine.DataEngine/ApplicationLogic/Services/SubmodelRepository/SemanticId/FillOut/ISubmodelFiller.cs @@ -0,0 +1,10 @@ +using AAS.TwinEngine.DataEngine.DomainModel.SubmodelRepository; + +using AasCore.Aas3_0; + +namespace AAS.TwinEngine.DataEngine.ApplicationLogic.Services.SubmodelRepository.SemanticId.FillOut; + +public interface ISubmodelFiller +{ + ISubmodel FillOutTemplate(ISubmodel submodelTemplate, SemanticTreeNode values); +} diff --git a/source/AAS.TwinEngine.DataEngine/ApplicationLogic/Services/SubmodelRepository/SemanticId/FillOut/SubmodelFiller.cs b/source/AAS.TwinEngine.DataEngine/ApplicationLogic/Services/SubmodelRepository/SemanticId/FillOut/SubmodelFiller.cs new file mode 100644 index 00000000..129a1f12 --- /dev/null +++ b/source/AAS.TwinEngine.DataEngine/ApplicationLogic/Services/SubmodelRepository/SemanticId/FillOut/SubmodelFiller.cs @@ -0,0 +1,360 @@ +using AAS.TwinEngine.DataEngine.ApplicationLogic.Exceptions.Application; +using AAS.TwinEngine.DataEngine.ApplicationLogic.Services.SubmodelRepository.SemanticId.Helpers; +using AAS.TwinEngine.DataEngine.ApplicationLogic.Services.SubmodelRepository.SemanticId.Helpers.Interfaces; +using AAS.TwinEngine.DataEngine.DomainModel.SubmodelRepository; + +using AasCore.Aas3_0; + +using File = AasCore.Aas3_0.File; +using Range = AasCore.Aas3_0.Range; + +namespace AAS.TwinEngine.DataEngine.ApplicationLogic.Services.SubmodelRepository.SemanticId.FillOut; + +public class SubmodelFiller( + ISemanticIdResolver semanticIdResolver, + ISubmodelElementHelper elementHelper, + IReferenceHelper referenceHelper, + ILogger logger) : ISubmodelFiller +{ + + public ISubmodel FillOutTemplate(ISubmodel submodelTemplate, SemanticTreeNode values) + { + ArgumentNullException.ThrowIfNull(submodelTemplate); + ArgumentNullException.ThrowIfNull(submodelTemplate.SubmodelElements); + ArgumentNullException.ThrowIfNull(values); + + var submodelElements = submodelTemplate.SubmodelElements.ToList(); + foreach (var submodelElement in submodelElements) + { + var semanticId = semanticIdResolver.ExtractSemanticId(submodelElement); + + var matchingNodes = SemanticTreeNavigator.FindBranchNodesBySemanticId(values, semanticId)?.ToList(); + + if (matchingNodes == null || matchingNodes.Count == 0) + { + continue; + } + + _ = submodelTemplate.SubmodelElements.Remove(submodelElement); + + if (matchingNodes.Count > 1) + { + HandleMultipleMatchingNodes(matchingNodes, submodelElement, submodelTemplate); + } + else + { + HandleSingleMatchingNode(matchingNodes[0], submodelElement, submodelTemplate); + } + } + + return submodelTemplate; + } + + private void HandleMultipleMatchingNodes( + List matchingNodes, + ISubmodelElement baseElement, + ISubmodel submodelTemplate) + { + for (var i = 0; i < matchingNodes.Count; i++) + { + var node = matchingNodes[i]; + var clonedElement = elementHelper.CloneElement(baseElement); + + if (baseElement is SubmodelElementCollection) + { + clonedElement.IdShort = $"{clonedElement.IdShort}{i}"; + } + + _ = FillOutElement(clonedElement, node); + submodelTemplate.SubmodelElements?.Add(clonedElement); + } + } + + private void HandleSingleMatchingNode( + SemanticTreeNode node, + ISubmodelElement element, + ISubmodel submodelTemplate) + { + _ = FillOutElement(element, node); + submodelTemplate.SubmodelElements?.Add(element); + } + + private ISubmodelElement FillOutElement(ISubmodelElement submodelElementTemplate, SemanticTreeNode values) + { + ArgumentNullException.ThrowIfNull(submodelElementTemplate); + ArgumentNullException.ThrowIfNull(values); + + switch (submodelElementTemplate) + { + case SubmodelElementCollection collection: + FillOutSubmodelElementCollection(collection, values); + break; + + case SubmodelElementList list: + FillOutSubmodelElementList(list, values); + break; + + case MultiLanguageProperty mlp: + FillOutMultiLanguageProperty(mlp, values); + break; + + case Property property: + FillOutProperty(property, values); + break; + + case File file: + FillOutFile(file, values); + break; + + case Blob blob: + FillOutBlob(blob, values); + break; + + case RelationshipElement relationship: + FillOutRelationshipElement(relationship, values); + break; + + case ReferenceElement reference: + FillOutReferenceElement(reference, values); + break; + + case Range range: + FillOutRange(range, values); + break; + + case Entity entity: + FillOutEntity(entity, values); + break; + + default: + logger.LogError("InValid submodelElementTemplate Type. IdShort : {IdShort}", submodelElementTemplate.IdShort); + throw new InternalDataProcessingException(); + } + + return submodelElementTemplate; + } + + private void FillOutSubmodelElementList(SubmodelElementList list, SemanticTreeNode values) + { + if (list?.Value == null || list.Value.Count == 0) + { + return; + } + + FillOutSubmodelElementValue(list.Value, values, false); + } + + private void FillOutSubmodelElementCollection(SubmodelElementCollection collection, SemanticTreeNode values) + { + if (collection?.Value == null || collection.Value.Count == 0) + { + return; + } + + FillOutSubmodelElementValue(collection.Value, values); + } + + private void FillOutSubmodelElementValue(List elements, SemanticTreeNode values, bool updateIdShort = true) + { + var originalElements = elements.ToList(); + foreach (var element in originalElements) + { + var valueNode = SemanticTreeNavigator.FindNodeBySemanticId(values, semanticIdResolver.ExtractSemanticId(element)); + var semanticTreeNodes = valueNode?.ToList(); + + if (semanticTreeNodes == null || semanticTreeNodes.Count == 0) + { + continue; + } + + if (!SemanticTreeNavigator.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, + semanticIdResolver.ExtractSemanticId(element)); + _ = elements.Remove(element); + continue; + } + + if (semanticTreeNodes.Count > 1 && element is not Property && element is not ReferenceElement) + { + _ = elements.Remove(element); + for (var i = 0; i < semanticTreeNodes.Count; i++) + { + var cloned = elementHelper.CloneElement(element); + if (updateIdShort) + { + cloned.IdShort = $"{cloned.IdShort}{i}"; + } + + _ = FillOutElement(cloned, semanticTreeNodes[i]); + elements.Add(cloned); + } + } + else + { + FillOutElement(element, semanticTreeNodes[0]); + } + } + } + + private void FillOutMultiLanguageProperty(MultiLanguageProperty mlp, SemanticTreeNode values) + { + var semanticId = semanticIdResolver.ExtractSemanticId(mlp); + + if (SemanticTreeNavigator.FindNodeBySemanticId(values, semanticId).FirstOrDefault() is not SemanticBranchNode valueNode) + { + logger.LogInformation("No value node found for MultiLanguageProperty {MlpIdShort}", mlp.IdShort); + return; + } + + mlp.Value ??= []; + + var languageValueMap = new Dictionary(StringComparer.OrdinalIgnoreCase); + foreach (var langValue in mlp.Value) + { + languageValueMap[langValue.Language] = (LangStringTextType)langValue; + } + + var languages = elementHelper.ResolveLanguages(mlp); + + var mlpSeparator = semanticIdResolver.MlpPostFixSeparator; + foreach (var language in languages) + { + if (!languageValueMap.TryGetValue(language, out var languageValue)) + { + languageValue = new LangStringTextType(language, string.Empty); + mlp.Value.Add(languageValue); + languageValueMap[language] = languageValue; + + logger.LogInformation("Added language '{Language}' to MultiLanguageProperty {MlpIdShort}", language, mlp.IdShort); + } + + var languageSemanticId = semanticId + mlpSeparator + language; + + var leafNode = valueNode.Children + .OfType() + .FirstOrDefault(child => child.SemanticId.Equals(languageSemanticId, StringComparison.Ordinal)); + + if (leafNode != null) + { + languageValue.Text = leafNode.Value; + } + } + } + + private void FillOutEntity(Entity entity, SemanticTreeNode values) + { + if (entity.EntityType == EntityType.SelfManagedEntity) + { + FillOutSelfManagedEntity(entity, values); + } + + if (entity?.Statements == null || entity.Statements.Count == 0) + { + return; + } + + FillOutSubmodelElementValue(entity.Statements, values); + } + + private void FillOutSelfManagedEntity(Entity entity, SemanticTreeNode values) + { + var semanticId = semanticIdResolver.ResolveElementSemanticId(entity, entity.IdShort!); + + if (SemanticTreeNavigator.FindNodeBySemanticId(values, semanticId).FirstOrDefault() is not SemanticBranchNode valueNode) + { + return; + } + + var globalAssetSemanticId = semanticId + SemanticIdResolver.EntityGlobalAssetIdPostFix; + + var globalAssetNode = valueNode.Children + .OfType() + .FirstOrDefault(c => c.SemanticId == globalAssetSemanticId); + + if (globalAssetNode != null) + { + entity.GlobalAssetId = globalAssetNode.Value; + } + + if (entity.SpecificAssetIds != null) + { + foreach (var specificAssetId in entity.SpecificAssetIds) + { + var specSemanticId = semanticIdResolver.GetSemanticId(specificAssetId); + + var specNode = valueNode.Children + .OfType() + .FirstOrDefault(c => c.SemanticId == specSemanticId); + + if (specNode != null) + { + specificAssetId.Value = specNode.Value; + } + } + } + } + + private static void FillOutProperty(Property valueElement, SemanticTreeNode values) + { + if (values is SemanticLeafNode leafValueNode) + { + valueElement.Value = leafValueNode.Value; + } + } + + private static void FillOutFile(File valueElement, SemanticTreeNode values) + { + if (values is SemanticLeafNode leafValueNode) + { + valueElement.Value = leafValueNode.Value; + } + } + + private static void FillOutBlob(Blob valueElement, SemanticTreeNode values) + { + if (values is SemanticLeafNode leafValueNode) + { + valueElement.Value = Convert.FromBase64String(leafValueNode.Value); + } + } + + private static void FillOutRange(Range valueElement, SemanticTreeNode values) + { + if (values is not SemanticBranchNode branchNode) + { + return; + } + + var leafNodes = branchNode.Children.OfType().ToList(); + + valueElement.Min = leafNodes.FirstOrDefault(n => n.SemanticId + .EndsWith(SemanticIdResolver.RangeMinimumPostFixSeparator, StringComparison.Ordinal))? + .Value; + + valueElement.Max = leafNodes.FirstOrDefault(n => n.SemanticId + .EndsWith(SemanticIdResolver.RangeMaximumPostFixSeparator, StringComparison.Ordinal))? + .Value; + } + + private void FillOutReferenceElement(ReferenceElement referenceElement, SemanticTreeNode semanticNode) + { + if (referenceElement?.Value?.Type != ReferenceTypes.ModelReference) + { + logger.LogInformation("ReferenceElement does not contain a ModelReference for SemanticId '{SemanticId}'. Skipping population.", semanticIdResolver.GetSemanticId(referenceElement!)); + return; + } + + referenceHelper.PopulateReferenceKeys(referenceElement.Value, semanticNode, semanticIdResolver.GetSemanticId(referenceElement)); + } + + private void FillOutRelationshipElement(RelationshipElement relationshipElement, SemanticTreeNode semanticTreeNode) + { + var semanticId = semanticTreeNode.SemanticId; + + referenceHelper.PopulateRelationshipReference(relationshipElement.First, semanticTreeNode, semanticId, SemanticIdResolver.RelationshipElementFirstPostFixSeparator); + + referenceHelper.PopulateRelationshipReference(relationshipElement.Second, semanticTreeNode, semanticId, SemanticIdResolver.RelationshipElementSecondPostFixSeparator); + } +} diff --git a/source/AAS.TwinEngine.DataEngine/ApplicationLogic/Services/SubmodelRepository/SemanticId/Helpers/Interfaces/IReferenceHelper.cs b/source/AAS.TwinEngine.DataEngine/ApplicationLogic/Services/SubmodelRepository/SemanticId/Helpers/Interfaces/IReferenceHelper.cs new file mode 100644 index 00000000..e7c72726 --- /dev/null +++ b/source/AAS.TwinEngine.DataEngine/ApplicationLogic/Services/SubmodelRepository/SemanticId/Helpers/Interfaces/IReferenceHelper.cs @@ -0,0 +1,14 @@ +using AAS.TwinEngine.DataEngine.DomainModel.SubmodelRepository; + +using AasCore.Aas3_0; + +namespace AAS.TwinEngine.DataEngine.ApplicationLogic.Services.SubmodelRepository.SemanticId.Helpers.Interfaces; + +public interface IReferenceHelper +{ + SemanticBranchNode? ExtractReferenceKeys(IReference reference, string semanticId, Cardinality cardinality); + + void PopulateReferenceKeys(IReference reference, SemanticTreeNode semanticNode, string semanticId); + + void PopulateRelationshipReference(IReference reference, SemanticTreeNode semanticTreeNode, string semanticId, string postfixSeparator); +} diff --git a/source/AAS.TwinEngine.DataEngine/ApplicationLogic/Services/SubmodelRepository/SemanticId/Helpers/Interfaces/ISemanticIdResolver.cs b/source/AAS.TwinEngine.DataEngine/ApplicationLogic/Services/SubmodelRepository/SemanticId/Helpers/Interfaces/ISemanticIdResolver.cs new file mode 100644 index 00000000..379365ac --- /dev/null +++ b/source/AAS.TwinEngine.DataEngine/ApplicationLogic/Services/SubmodelRepository/SemanticId/Helpers/Interfaces/ISemanticIdResolver.cs @@ -0,0 +1,24 @@ +using AAS.TwinEngine.DataEngine.DomainModel.SubmodelRepository; + +using AasCore.Aas3_0; + +namespace AAS.TwinEngine.DataEngine.ApplicationLogic.Services.SubmodelRepository.SemanticId.Helpers.Interfaces; + +public interface ISemanticIdResolver +{ + string MlpPostFixSeparator { get; } + + string GetSemanticId(IHasSemantics hasSemantics); + + string ExtractSemanticId(ISubmodelElement element); + + string ResolveSemanticId(IHasSemantics hasSemantics, string idShort); + + string ResolveElementSemanticId(ISubmodelElement element, string idShort); + + Cardinality GetCardinality(ISubmodelElement element); + + DataType GetValueType(ISubmodelElement element); + + string BuildReferenceKeySemanticId(string baseSemanticId, KeyTypes keyType, int index, int totalCount); +} diff --git a/source/AAS.TwinEngine.DataEngine/ApplicationLogic/Services/SubmodelRepository/SemanticId/Helpers/Interfaces/ISubmodelElementHelper.cs b/source/AAS.TwinEngine.DataEngine/ApplicationLogic/Services/SubmodelRepository/SemanticId/Helpers/Interfaces/ISubmodelElementHelper.cs new file mode 100644 index 00000000..5b78c41f --- /dev/null +++ b/source/AAS.TwinEngine.DataEngine/ApplicationLogic/Services/SubmodelRepository/SemanticId/Helpers/Interfaces/ISubmodelElementHelper.cs @@ -0,0 +1,16 @@ +using AasCore.Aas3_0; + +namespace AAS.TwinEngine.DataEngine.ApplicationLogic.Services.SubmodelRepository.SemanticId.Helpers.Interfaces; + +public interface ISubmodelElementHelper +{ + ISubmodelElement CloneElement(ISubmodelElement element); + + ISubmodelElement? GetElementByIdShort(IEnumerable? submodelElements, string idShort); + + ISubmodelElement GetElementFromListByIndex(IEnumerable? elements, string idShortWithoutIndex, int index); + + IList? GetChildElements(ISubmodelElement submodelElement); + + HashSet ResolveLanguages(MultiLanguageProperty mlp); +} diff --git a/source/AAS.TwinEngine.DataEngine/ApplicationLogic/Services/SubmodelRepository/SemanticId/Helpers/ReferenceHelper.cs b/source/AAS.TwinEngine.DataEngine/ApplicationLogic/Services/SubmodelRepository/SemanticId/Helpers/ReferenceHelper.cs new file mode 100644 index 00000000..0359fecb --- /dev/null +++ b/source/AAS.TwinEngine.DataEngine/ApplicationLogic/Services/SubmodelRepository/SemanticId/Helpers/ReferenceHelper.cs @@ -0,0 +1,99 @@ +using AAS.TwinEngine.DataEngine.ApplicationLogic.Services.SubmodelRepository.SemanticId.Helpers.Interfaces; +using AAS.TwinEngine.DataEngine.DomainModel.SubmodelRepository; + +using AasCore.Aas3_0; + +namespace AAS.TwinEngine.DataEngine.ApplicationLogic.Services.SubmodelRepository.SemanticId.Helpers; + +public class ReferenceHelper( + ISemanticIdResolver semanticIdResolver, + ILogger logger) : IReferenceHelper +{ + public SemanticBranchNode? ExtractReferenceKeys(IReference reference, string semanticId, Cardinality cardinality) + { + var keys = reference.Keys; + if (keys.Count <= 0) + { + return null; + } + + var branchNode = new SemanticBranchNode(semanticId, cardinality); + + foreach (var group in keys.GroupBy(k => k.Type)) + { + group.Select((_, index) => new SemanticLeafNode( + semanticIdResolver.BuildReferenceKeySemanticId(semanticId, group.Key, index, group.Count()), + string.Empty, + DataType.String, + Cardinality.ZeroToOne)) + .ToList() + .ForEach(branchNode.AddChild); + } + + return branchNode; + } + + public void PopulateReferenceKeys(IReference reference, SemanticTreeNode semanticNode, string semanticId) + { + if (semanticNode is not SemanticBranchNode branchNode) + { + logger.LogWarning("Expected SemanticBranchNode for SemanticId '{SemanticId}', but got {NodeType}. Skipping population.", semanticId, semanticNode.GetType().Name); + return; + } + + var keys = reference.Keys; + + if (keys.Count <= 0) + { + logger.LogInformation("ReferenceElement has no keys for SemanticId '{SemanticId}'. Nothing to populate.", semanticId); + return; + } + + foreach (var group in keys.GroupBy(k => k.Type)) + { + PopulateReferenceKeyGroup(group, branchNode, semanticId); + } + } + + public void PopulateRelationshipReference(IReference reference, SemanticTreeNode semanticTreeNode, string semanticId, string postfixSeparator) + { + if (reference.Type != ReferenceTypes.ModelReference) + { + return; + } + + var searchPattern = semanticId + postfixSeparator; + var valueNode = SemanticTreeNavigator.FindNodeBySemanticId(semanticTreeNode, searchPattern).FirstOrDefault(); + + if (valueNode != null) + { + PopulateReferenceKeys(reference, valueNode, searchPattern); + } + else + { + logger.LogWarning("No matching node found for reference with pattern: {Pattern}", searchPattern); + } + } + + private void PopulateReferenceKeyGroup(IGrouping group, SemanticBranchNode branchNode, string semanticId) + { + var keyList = group.ToList(); + for (var i = 0; i < keyList.Count; i++) + { + var indexedSemanticId = semanticIdResolver.BuildReferenceKeySemanticId(semanticId, group.Key, i, keyList.Count); + + var leafNode = branchNode.Children + .OfType() + .FirstOrDefault(child => child.SemanticId == indexedSemanticId); + + if (leafNode != null) + { + keyList[i].Value = !string.IsNullOrEmpty(leafNode.Value) ? leafNode.Value : keyList[i].Value; + } + else + { + logger.LogWarning("No matching leaf node found for SemanticId '{IndexedSemanticId}'.", indexedSemanticId); + } + } + } +} diff --git a/source/AAS.TwinEngine.DataEngine/ApplicationLogic/Services/SubmodelRepository/SemanticId/Helpers/SemanticIdResolver.cs b/source/AAS.TwinEngine.DataEngine/ApplicationLogic/Services/SubmodelRepository/SemanticId/Helpers/SemanticIdResolver.cs new file mode 100644 index 00000000..eb9990a6 --- /dev/null +++ b/source/AAS.TwinEngine.DataEngine/ApplicationLogic/Services/SubmodelRepository/SemanticId/Helpers/SemanticIdResolver.cs @@ -0,0 +1,139 @@ +using System.Text.RegularExpressions; + +using AAS.TwinEngine.DataEngine.ApplicationLogic.Services.SubmodelRepository.Config; +using AAS.TwinEngine.DataEngine.ApplicationLogic.Services.SubmodelRepository.SemanticId.Helpers.Interfaces; +using AAS.TwinEngine.DataEngine.DomainModel.SubmodelRepository; + +using AasCore.Aas3_0; + +using Microsoft.Extensions.Options; + +using File = AasCore.Aas3_0.File; +using Range = AasCore.Aas3_0.Range; + +namespace AAS.TwinEngine.DataEngine.ApplicationLogic.Services.SubmodelRepository.SemanticId.Helpers; + +public partial class SemanticIdResolver(IOptions semantics) : ISemanticIdResolver +{ + public const string RangeMinimumPostFixSeparator = "_min"; + public const string RangeMaximumPostFixSeparator = "_max"; + public const string EntityGlobalAssetIdPostFix = "_globalAssetId"; + public const string RelationshipElementFirstPostFixSeparator = "_first"; + public const string RelationshipElementSecondPostFixSeparator = "_second"; + + private readonly string _internalSemanticId = semantics.Value.InternalSemanticId; + private readonly string _submodelElementIndexContextPrefix = semantics.Value.SubmodelElementIndexContextPrefix; + + public string MlpPostFixSeparator { get; } = semantics.Value.MultiLanguageSemanticPostfixSeparator; + + private static readonly HashSet StringTypes = + [ + DataTypeDefXsd.String, DataTypeDefXsd.AnyUri, DataTypeDefXsd.Byte, DataTypeDefXsd.Date, + DataTypeDefXsd.DateTime, DataTypeDefXsd.Duration, DataTypeDefXsd.GDay, DataTypeDefXsd.GYear, + DataTypeDefXsd.GYearMonth, DataTypeDefXsd.HexBinary, DataTypeDefXsd.Time, DataTypeDefXsd.Base64Binary, + DataTypeDefXsd.GMonth, DataTypeDefXsd.GMonthDay + ]; + + private static readonly HashSet IntegerTypes = + [ + DataTypeDefXsd.Int, DataTypeDefXsd.Integer, DataTypeDefXsd.Long, DataTypeDefXsd.NegativeInteger, + DataTypeDefXsd.NonNegativeInteger, DataTypeDefXsd.NonPositiveInteger, DataTypeDefXsd.PositiveInteger, + DataTypeDefXsd.Short, DataTypeDefXsd.UnsignedShort, DataTypeDefXsd.UnsignedLong, + DataTypeDefXsd.UnsignedInt, DataTypeDefXsd.UnsignedByte + ]; + + private static readonly HashSet NumberTypes = + [ + DataTypeDefXsd.Float, DataTypeDefXsd.Double, DataTypeDefXsd.Decimal + ]; + + public string GetSemanticId(IHasSemantics hasSemantics) => hasSemantics.SemanticId?.Keys?.FirstOrDefault()?.Value ?? string.Empty; + + public string ExtractSemanticId(ISubmodelElement element) + { + if (element.Qualifiers == null) + { + return GetSemanticId(element); + } + + var qualifier = element.Qualifiers.FirstOrDefault(q => q.Type == _internalSemanticId); + return qualifier != null ? qualifier.Value! : GetSemanticId(element); + } + + public string ResolveSemanticId(IHasSemantics hasSemantics, string idShort) + { + var baseSemanticId = GetSemanticId(hasSemantics); + return AppendIndex(baseSemanticId, idShort); + } + + public string ResolveElementSemanticId(ISubmodelElement element, string idShort) + { + var baseSemanticId = ExtractSemanticId(element); + return AppendIndex(baseSemanticId, idShort); + } + + public Cardinality GetCardinality(ISubmodelElement element) + { + var qualifierValue = element.Qualifiers?.FirstOrDefault()?.Value; + if (qualifierValue is null) + { + return Cardinality.Unknown; + } + + return Enum.TryParse(qualifierValue, ignoreCase: true, out var result) + ? result + : Cardinality.Unknown; + } + + public DataType GetValueType(ISubmodelElement element) + { + return element switch + { + Property p => GetDataTypeFromValueType(p.ValueType), + Range r => GetDataTypeFromValueType(r.ValueType), + File => DataType.String, + Blob => DataType.String, + _ => DataType.Unknown + }; + } + + private static DataType GetDataTypeFromValueType(DataTypeDefXsd valueType) + { + return valueType switch + { + _ when StringTypes.Contains(valueType) => DataType.String, + _ when IntegerTypes.Contains(valueType) => DataType.Integer, + _ when NumberTypes.Contains(valueType) => DataType.Number, + DataTypeDefXsd.Boolean => DataType.Boolean, + _ => DataType.Unknown + }; + } + + public string BuildReferenceKeySemanticId(string baseSemanticId, KeyTypes keyType, int index, int totalCount) + { + return totalCount > 1 + ? $"{baseSemanticId}{MlpPostFixSeparator}{keyType}{MlpPostFixSeparator}{index}" + : $"{baseSemanticId}{MlpPostFixSeparator}{keyType}"; + } + + private string AppendIndex(string semanticId, string? idShort) + { + var index = string.Empty; + if (idShort != null) + { + index = SubmodelElementCollectionIndex().Match(idShort).Value; + } + + return string.IsNullOrWhiteSpace(index) + ? semanticId + : $"{semanticId}{_submodelElementIndexContextPrefix}{index}"; + } + + /// + /// Matches one or more digits at the end of a string, + /// e.g., "element42" → matches "42" + /// Pattern: \d+$ + /// + [GeneratedRegex(@"\d+$")] + private static partial Regex SubmodelElementCollectionIndex(); +} diff --git a/source/AAS.TwinEngine.DataEngine/ApplicationLogic/Services/SubmodelRepository/SemanticId/Helpers/SemanticTreeNavigator.cs b/source/AAS.TwinEngine.DataEngine/ApplicationLogic/Services/SubmodelRepository/SemanticId/Helpers/SemanticTreeNavigator.cs new file mode 100644 index 00000000..a2a4ab33 --- /dev/null +++ b/source/AAS.TwinEngine.DataEngine/ApplicationLogic/Services/SubmodelRepository/SemanticId/Helpers/SemanticTreeNavigator.cs @@ -0,0 +1,55 @@ +using AAS.TwinEngine.DataEngine.DomainModel.SubmodelRepository; + +namespace AAS.TwinEngine.DataEngine.ApplicationLogic.Services.SubmodelRepository.SemanticId.Helpers; + +public class SemanticTreeNavigator +{ + public static IEnumerable FindBranchNodesBySemanticId(SemanticTreeNode tree, string semanticId) + { + var node = tree as SemanticBranchNode; + + return node?.Children! + .Where(child => child.SemanticId.Equals(semanticId, StringComparison.Ordinal)) + ?? []; + } + + public static IEnumerable FindNodeBySemanticId(SemanticTreeNode tree, string semanticId) + { + if (tree.SemanticId == semanticId) + { + yield return tree; + } + + if (tree is not SemanticBranchNode branchNode) + { + yield break; + } + + foreach (var child in branchNode.Children) + { + foreach (var matchingNode in FindNodeBySemanticId(child, semanticId)) + { + yield return matchingNode; + } + } + } + + public static bool AreAllNodesOfSameType(List 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); + } +} diff --git a/source/AAS.TwinEngine.DataEngine/ApplicationLogic/Services/SubmodelRepository/SemanticId/Helpers/SubmodelElementHelper.cs b/source/AAS.TwinEngine.DataEngine/ApplicationLogic/Services/SubmodelRepository/SemanticId/Helpers/SubmodelElementHelper.cs new file mode 100644 index 00000000..04d4db4c --- /dev/null +++ b/source/AAS.TwinEngine.DataEngine/ApplicationLogic/Services/SubmodelRepository/SemanticId/Helpers/SubmodelElementHelper.cs @@ -0,0 +1,119 @@ +using System.Globalization; +using System.Text.RegularExpressions; + +using AAS.TwinEngine.DataEngine.ApplicationLogic.Exceptions.Application; +using AAS.TwinEngine.DataEngine.ApplicationLogic.Services.SubmodelRepository.Config; +using AAS.TwinEngine.DataEngine.ApplicationLogic.Services.SubmodelRepository.SemanticId.Helpers.Interfaces; + +using AasCore.Aas3_0; + +using Microsoft.Extensions.Options; + +namespace AAS.TwinEngine.DataEngine.ApplicationLogic.Services.SubmodelRepository.SemanticId.Helpers; + +public partial class SubmodelElementHelper(ILogger logger, IOptions mlpSettings) : ISubmodelElementHelper +{ + private readonly HashSet? _defaultLanguagesSet = mlpSettings.Value.DefaultLanguages != null && mlpSettings.Value.DefaultLanguages.Count > 0 + ? new HashSet(mlpSettings.Value.DefaultLanguages, StringComparer.OrdinalIgnoreCase) + : null; + + public ISubmodelElement CloneElement(ISubmodelElement element) + { + var jsonElement = Jsonization.Serialize.ToJsonObject(element); + + return Jsonization.Deserialize.ISubmodelElementFrom(jsonElement); + } + + public ISubmodelElement? GetElementByIdShort(IEnumerable? submodelElements, string idShort) + { + if (TryParseIdShortWithBracketIndex(idShort, out var idShortWithoutIndex, out var index)) + { + return GetElementFromListByIndex(submodelElements, idShortWithoutIndex, index); + } + + return submodelElements?.FirstOrDefault(e => e.IdShort == idShort); + } + + public ISubmodelElement GetElementFromListByIndex(IEnumerable? elements, string idShortWithoutIndex, int index) + { + var baseElement = elements?.FirstOrDefault(e => e.IdShort == idShortWithoutIndex); + + if (baseElement is not ISubmodelElementList list) + { + logger.LogError("Expected list element with IdShort '{IdShortWithoutIndex}' not found or is not a list.", idShortWithoutIndex); + throw new InternalDataProcessingException(); + } + + if (index >= 0 && index < list.Value!.Count) + { + return list.Value[index]; + } + + logger.LogError("Index {Index} is out of bounds for list '{IdShortWithoutIndex}' with count {Count}.", index, idShortWithoutIndex, list.Value!.Count); + throw new InternalDataProcessingException(); + } + + public IList? GetChildElements(ISubmodelElement submodelElement) + { + return submodelElement switch + { + ISubmodelElementCollection c => c.Value, + ISubmodelElementList l => l.Value, + IEntity entity => entity.Statements, + _ => null + }; + } + + public HashSet ResolveLanguages(MultiLanguageProperty mlp) + { + var languages = new HashSet(StringComparer.OrdinalIgnoreCase); + + if (mlp.Value is { Count: > 0 }) + { + foreach (var langValue in mlp.Value) + { + _ = languages.Add(langValue.Language); + } + } + + if (_defaultLanguagesSet != null) + { + languages.UnionWith(_defaultLanguagesSet); + } + + return languages; + } + + private static bool TryParseIdShortWithBracketIndex(string idShort, out string idShortWithoutIndex, out int index) + { + var match = SubmodelElementListIndex().Match(idShort); + if (!match.Success) + { + 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; + } + + index = int.Parse(indexGroup.Value, CultureInfo.InvariantCulture); + return true; + } + + /// + /// Matches strings like "element[3]" and captures: + /// Group 1 → element name (any characters, lazy match) + /// Group 2 → index (digits inside square brackets) + /// e.g. "element[3]" -> matches Group1= "element", Group2 = "3" + /// Pattern: ^(.+?)\[(\d+)\]$ + /// + [GeneratedRegex(@"^(.+?)(?:\[(\d+)\]|%5B(\d+)%5D)$")] + private static partial Regex SubmodelElementListIndex(); +} diff --git a/source/AAS.TwinEngine.DataEngine/ApplicationLogic/Services/SubmodelRepository/SemanticIdHandler.cs b/source/AAS.TwinEngine.DataEngine/ApplicationLogic/Services/SubmodelRepository/SemanticIdHandler.cs index 72e77e8e..8711f051 100644 --- a/source/AAS.TwinEngine.DataEngine/ApplicationLogic/Services/SubmodelRepository/SemanticIdHandler.cs +++ b/source/AAS.TwinEngine.DataEngine/ApplicationLogic/Services/SubmodelRepository/SemanticIdHandler.cs @@ -1,948 +1,18 @@ -using System.Globalization; -using System.Text.RegularExpressions; - -using AAS.TwinEngine.DataEngine.ApplicationLogic.Exceptions.Application; -using AAS.TwinEngine.DataEngine.ApplicationLogic.Services.SubmodelRepository.Config; +using AAS.TwinEngine.DataEngine.ApplicationLogic.Services.SubmodelRepository.SemanticId.Extraction; +using AAS.TwinEngine.DataEngine.ApplicationLogic.Services.SubmodelRepository.SemanticId.FillOut; using AAS.TwinEngine.DataEngine.DomainModel.SubmodelRepository; using AasCore.Aas3_0; -using Microsoft.Extensions.Options; - -using File = AasCore.Aas3_0.File; -using Range = AasCore.Aas3_0.Range; - namespace AAS.TwinEngine.DataEngine.ApplicationLogic.Services.SubmodelRepository; -public partial class SemanticIdHandler( - ILogger logger, - IOptions semantics, - IOptions mlpSettings) : ISemanticIdHandler +public class SemanticIdHandler( + ISemanticTreeExtractor extractor, + ISubmodelFiller filler) : ISemanticIdHandler { - private readonly string _mlpPostFixSeparator = semantics.Value.MultiLanguageSemanticPostfixSeparator; - private readonly string _submodelElementIndexContextPrefix = semantics.Value.SubmodelElementIndexContextPrefix; - private readonly string _internalSemanticId = semantics.Value.InternalSemanticId; - private const string RangeMinimumPostFixSeparator = "_min"; - private const string RangeMaximumPostFixSeparator = "_max"; - private const string EntityGlobalAssetIdPostFix = "_globalAssetId"; - private const string RelationshipElementFirstPostFixSeparator = "_first"; - private const string RelationshipElementSecondPostFixSeparator = "_second"; - - private readonly HashSet? _defaultLanguagesSet = mlpSettings.Value.DefaultLanguages != null && mlpSettings.Value.DefaultLanguages.Count > 0 - ? new HashSet(mlpSettings.Value.DefaultLanguages, StringComparer.OrdinalIgnoreCase) - : null; - - private static readonly HashSet StringTypes = - [ - DataTypeDefXsd.String, DataTypeDefXsd.AnyUri, DataTypeDefXsd.Byte, DataTypeDefXsd.Date, - DataTypeDefXsd.DateTime, DataTypeDefXsd.Duration, DataTypeDefXsd.GDay, DataTypeDefXsd.GYear, - DataTypeDefXsd.GYearMonth, DataTypeDefXsd.HexBinary, DataTypeDefXsd.Time, DataTypeDefXsd.Base64Binary, - DataTypeDefXsd.GMonth, DataTypeDefXsd.GMonthDay - ]; - - private static readonly HashSet IntegerTypes = - [ - DataTypeDefXsd.Int, DataTypeDefXsd.Integer, DataTypeDefXsd.Long, DataTypeDefXsd.NegativeInteger, - DataTypeDefXsd.NonNegativeInteger, DataTypeDefXsd.NonPositiveInteger, DataTypeDefXsd.PositiveInteger, - DataTypeDefXsd.Short, DataTypeDefXsd.UnsignedShort, DataTypeDefXsd.UnsignedLong, - DataTypeDefXsd.UnsignedInt, DataTypeDefXsd.UnsignedByte - ]; - - private static readonly HashSet NumberTypes = - [ - DataTypeDefXsd.Float, DataTypeDefXsd.Double, DataTypeDefXsd.Decimal - ]; - - public SemanticTreeNode Extract(ISubmodel submodelTemplate) - { - ArgumentNullException.ThrowIfNull(submodelTemplate); - - var rootNode = new SemanticBranchNode(GetSemanticId(submodelTemplate, submodelTemplate.IdShort!), Cardinality.Unknown); - var childNodes = submodelTemplate.SubmodelElements! - .Select(Extract) - .Where(childNode => childNode != null) - .ToList(); - - foreach (var childNode in childNodes) - { - rootNode.AddChild(childNode!); - } - - return rootNode; - } - - public ISubmodelElement Extract(ISubmodel submodelTemplate, string idShortPath) - { - ArgumentNullException.ThrowIfNull(submodelTemplate); - ArgumentNullException.ThrowIfNull(idShortPath); - - var currentSubmodelElements = submodelTemplate.SubmodelElements; - var idShortPathSegments = idShortPath.Split('.'); - for (var index = 0; index < idShortPathSegments.Length; index++) - { - var currentIdShort = idShortPathSegments[index]; - var isLastSegment = index == idShortPathSegments.Length - 1; - - var matchedElement = GetElementByIdShort(currentSubmodelElements, currentIdShort) - ?? throw new InternalDataProcessingException(); - if (isLastSegment) - { - return matchedElement; - } - - currentSubmodelElements = GetChildElements(matchedElement) as List - ?? throw new InternalDataProcessingException(); - } - - throw new InternalDataProcessingException(); - } - - private SemanticTreeNode? Extract(ISubmodelElement submodelElementTemplate) - { - ArgumentNullException.ThrowIfNull(submodelElementTemplate); - - return submodelElementTemplate switch - { - SubmodelElementCollection collection => ExtractCollection(collection), - SubmodelElementList list => ExtractList(list), - MultiLanguageProperty mlp => ExtractMultiLanguageProperty(mlp), - Range range => ExtractRange(range), - ReferenceElement re => ExtractReferenceElement(re), - RelationshipElement relationshipElement => ExtractRelationshipElement(relationshipElement), - Entity entity => ExtractEntity(entity), - _ => CreateLeafNode(submodelElementTemplate) - }; - } - - private SemanticBranchNode ExtractList(SubmodelElementList list) - { - var node = new SemanticBranchNode(GetSemanticId(list, list.IdShort!), GetCardinality(list)); - if (list.Value?.Count > 0) - { - foreach (var element in list.Value) - { - var child = Extract(element); - if (child != null) - { - node.AddChild(child); - } - } - } - else - { - logger.LogWarning("No elements defined in SubmodelElementList {ListIdShort}", list.IdShort); - } - - return node; - } - - private SemanticBranchNode ExtractCollection(SubmodelElementCollection collection) - { - var node = new SemanticBranchNode(GetSemanticId(collection, collection.IdShort!), GetCardinality(collection)); - if (collection.Value?.Count > 0) - { - foreach (var element in collection.Value.Where(_ => true)) - { - var child = Extract(element); - if (child != null) - { - node.AddChild(child); - } - } - } - else - { - logger.LogWarning("No elements defined in SubmodelElementCollection {CollectionIdShort}", collection.IdShort); - } - - return node; - } - - private SemanticBranchNode? ExtractReferenceElement(ReferenceElement referenceElement) - { - if (referenceElement.Value == null || referenceElement.Value.Type == ReferenceTypes.ExternalReference) - { - return null; - } - - return ExtractFormReference(referenceElement.Value, GetSemanticId(referenceElement, referenceElement.IdShort!), GetCardinality(referenceElement)); - } - - private SemanticBranchNode? ExtractRelationshipElement(RelationshipElement relationshipElement) - { - if (relationshipElement.First.Type == ReferenceTypes.ExternalReference && relationshipElement.Second.Type == ReferenceTypes.ExternalReference) - { - return null; - } - - var semanticId = GetSemanticId(relationshipElement); - var cardinality = GetCardinality(relationshipElement); - var relationshipElementNode = new SemanticBranchNode(semanticId, cardinality); - - if (relationshipElement.First.Type == ReferenceTypes.ModelReference) - { - var referenceNode = ExtractFormReference(relationshipElement.First, $"{semanticId}{RelationshipElementFirstPostFixSeparator}", cardinality); - if (referenceNode != null) - { - relationshipElementNode.AddChild(referenceNode); - } - } - - if (relationshipElement.Second.Type == ReferenceTypes.ModelReference) - { - var referenceNode = ExtractFormReference(relationshipElement.Second, $"{semanticId}{RelationshipElementSecondPostFixSeparator}", cardinality); - if (referenceNode != null) - { - relationshipElementNode.AddChild(referenceNode); - } - } - - return relationshipElementNode; - } - - private SemanticBranchNode? ExtractFormReference(IReference reference, string semanticId, Cardinality cardinality) - { - var keys = reference.Keys; - if (keys.Count <= 0) - { - return null; - } - - var branchNode = new SemanticBranchNode(semanticId, cardinality); - - foreach (var group in keys.GroupBy(k => k.Type)) - { - group.Select((_, index) => new SemanticLeafNode(group.Count() > 1 - ? $"{semanticId}{_mlpPostFixSeparator}{group.Key}{_mlpPostFixSeparator}{index}" - : $"{semanticId}{_mlpPostFixSeparator}{group.Key}", - string.Empty, - DataType.String, - Cardinality.ZeroToOne)) - .ToList() - .ForEach(branchNode.AddChild); - } - - return branchNode; - } - - private SemanticBranchNode ExtractEntity(Entity entity) - { - var semanticId = GetSemanticId(entity, entity.IdShort!); - var node = new SemanticBranchNode(semanticId, GetCardinality(entity)); - if (entity.EntityType == EntityType.SelfManagedEntity) - { - var globalAssetIdNode = new SemanticLeafNode(semanticId + EntityGlobalAssetIdPostFix, string.Empty, DataType.String, Cardinality.One); - node.AddChild(globalAssetIdNode); - if (entity.SpecificAssetIds != null) - { - foreach (var specificAssetId in entity.SpecificAssetIds) - { - IHasSemantics specificAsset = specificAssetId; - if (specificAsset.SemanticId == null) - { - continue; - } - - var specificAssetIdNode = new SemanticLeafNode(GetSemanticId(specificAssetId), string.Empty, DataType.String, Cardinality.One); - node.AddChild(specificAssetIdNode); - } - } - } - - if (entity.Statements?.Count > 0) - { - foreach (var child in entity.Statements.Select(Extract).OfType()) - { - node.AddChild(child); - } - } - else - { - logger.LogWarning("No elements defined in Entity {EntityIdShort}", entity.IdShort); - } - - return node; - } - - private string ExtractSemanticId(ISubmodelElement element) - { - if (element.Qualifiers == null) - { - return GetSemanticId(element); - } - - var qualifier = element.Qualifiers.FirstOrDefault(q => q.Type == _internalSemanticId); - if (qualifier != null) - { - return qualifier.Value!; - } - - return GetSemanticId(element); - } - - private SemanticBranchNode? ExtractMultiLanguageProperty(MultiLanguageProperty mlp) - { - var semanticId = ExtractSemanticId(mlp); - var node = new SemanticBranchNode(semanticId, GetCardinality(mlp)); - - var languages = new HashSet(StringComparer.OrdinalIgnoreCase); - - if (mlp.Value is { Count: > 0 }) - { - foreach (var langValue in mlp.Value) - { - languages.Add(langValue.Language); - } - } - else - { - logger.LogInformation("No languages defined in template for MultiLanguageProperty {MlpIdShort}", mlp.IdShort); - } - - if (_defaultLanguagesSet != null) - { - languages.UnionWith(_defaultLanguagesSet); - } - - foreach (var langSemanticId in languages.Select(language => string.Concat(semanticId, _mlpPostFixSeparator, language))) - { - node.AddChild(new SemanticLeafNode(langSemanticId, string.Empty, DataType.String, Cardinality.ZeroToOne)); - } - - return node; - } - - private SemanticBranchNode ExtractRange(Range range) - { - var semanticId = ExtractSemanticId(range); - var valueType = GetValueType(range); - var node = new SemanticBranchNode(semanticId, GetCardinality(range)); - - node.AddChild(new SemanticLeafNode(semanticId + RangeMinimumPostFixSeparator, string.Empty, valueType, Cardinality.ZeroToOne)); - node.AddChild(new SemanticLeafNode(semanticId + RangeMaximumPostFixSeparator, string.Empty, valueType, Cardinality.ZeroToOne)); - - return node; - } - - private SemanticLeafNode CreateLeafNode(ISubmodelElement element) - { - var semanticId = GetSemanticId(element, element.IdShort!); - var valueType = GetValueType(element); - var cardinality = GetCardinality(element); - return new SemanticLeafNode(semanticId, string.Empty, valueType, cardinality); - } - - private static Cardinality GetCardinality(ISubmodelElement element) - { - var qualifierValue = element.Qualifiers?.FirstOrDefault()?.Value; - if (qualifierValue is null) - { - return Cardinality.Unknown; - } - - return Enum.TryParse(qualifierValue, ignoreCase: true, out var result) - ? result - : Cardinality.Unknown; - } - - private static DataType GetValueType(ISubmodelElement element) - { - return element switch - { - Property p => GetDataTypeFromValueType(p.ValueType), - Range r => GetDataTypeFromValueType(r.ValueType), - File => DataType.String, - Blob => DataType.String, - _ => DataType.Unknown - }; - } - - private static DataType GetDataTypeFromValueType(DataTypeDefXsd valueType) - { - return valueType switch - { - _ when StringTypes.Contains(valueType) => DataType.String, - _ when IntegerTypes.Contains(valueType) => DataType.Integer, - _ when NumberTypes.Contains(valueType) => DataType.Number, - DataTypeDefXsd.Boolean => DataType.Boolean, - _ => DataType.Unknown - }; - } - - private string GetSemanticId(ISubmodelElement element, string idShort) - { - var baseSemanticId = ExtractSemanticId(element); - return AppendIndex(baseSemanticId, idShort); - } - - private string GetSemanticId(IHasSemantics hasSemantics, string idShort) - { - var baseSemanticId = GetSemanticId(hasSemantics); - return AppendIndex(baseSemanticId, idShort); - } - - private static string GetSemanticId(IHasSemantics hasSemantics) => hasSemantics.SemanticId?.Keys?.FirstOrDefault()?.Value ?? string.Empty; - - private string AppendIndex(string semanticId, string? idShort) - { - var index = string.Empty; - if (idShort != null) - { - index = SubmodelElementCollectionIndex().Match(idShort).Value; - } - - return string.IsNullOrWhiteSpace(index) - ? semanticId - : $"{semanticId}{_submodelElementIndexContextPrefix}{index}"; - } - - public ISubmodel FillOutTemplate(ISubmodel submodelTemplate, SemanticTreeNode values) - { - ArgumentNullException.ThrowIfNull(submodelTemplate); - ArgumentNullException.ThrowIfNull(submodelTemplate.SubmodelElements); - ArgumentNullException.ThrowIfNull(values); - - var submodelElements = submodelTemplate.SubmodelElements.ToList(); - foreach (var submodelElement in submodelElements) - { - var semanticId = ExtractSemanticId(submodelElement); - - var matchingNodes = FindBranchNodesBySemanticId(values, semanticId)?.ToList(); - - if (matchingNodes == null || matchingNodes.Count == 0) - { - continue; - } - - _ = submodelTemplate.SubmodelElements.Remove(submodelElement); - - if (matchingNodes.Count > 1) - { - HandleMultipleMatchingNodes(matchingNodes, submodelElement, submodelTemplate); - } - else - { - HandleSingleMatchingNode(matchingNodes[0], submodelElement, submodelTemplate); - } - } - - return submodelTemplate; - } - - private void HandleMultipleMatchingNodes( - List matchingNodes, - ISubmodelElement baseElement, - ISubmodel submodelTemplate) - { - for (var i = 0; i < matchingNodes.Count; i++) - { - var node = matchingNodes[i]; - var clonedElement = CloneElementJson(baseElement); - - if (baseElement is SubmodelElementCollection) - { - clonedElement.IdShort = $"{clonedElement.IdShort}{i}"; - } - - _ = FillOutTemplate(clonedElement, node); - submodelTemplate.SubmodelElements?.Add(clonedElement); - } - } - - private void HandleSingleMatchingNode( - SemanticTreeNode node, - ISubmodelElement element, - ISubmodel submodelTemplate) - { - _ = FillOutTemplate(element, node); - submodelTemplate.SubmodelElements?.Add(element); - } - - private ISubmodelElement FillOutTemplate(ISubmodelElement submodelElementTemplate, SemanticTreeNode values) - { - ArgumentNullException.ThrowIfNull(submodelElementTemplate); - ArgumentNullException.ThrowIfNull(values); - - switch (submodelElementTemplate) - { - case SubmodelElementCollection collection: - FillOutSubmodelElementCollection(collection, values); - break; - - case SubmodelElementList list: - FillOutSubmodelElementList(list, values); - break; - - case MultiLanguageProperty mlp: - FillOutMultiLanguageProperty(mlp, values); - break; - - case Property property: - FillOutProperty(property, values); - break; - - case File file: - FillOutFile(file, values); - break; - - case Blob blob: - FillOutBlob(blob, values); - break; - - case RelationshipElement relationship: - FillOutRelationshipElement(relationship, values); - break; - - case ReferenceElement reference: - FillOutReferenceElement(reference, values); - break; - - case Range range: - FillOutRange(range, values); - break; - - case Entity entity: - FillOutEntity(entity, values); - break; - - default: - logger.LogError("InValid submodelElementTemplate Type. IdShort : {IdShort}", submodelElementTemplate.IdShort); - throw new InternalDataProcessingException(); - } - - return submodelElementTemplate; - } - - private void FillOutSubmodelElementList(SubmodelElementList list, SemanticTreeNode values) - { - if (list?.Value == null || list.Value.Count == 0) - { - return; - } - - FillOutSubmodelElementValue(list.Value, values, false); - } - - private void FillOutSubmodelElementCollection(SubmodelElementCollection collection, SemanticTreeNode values) - { - if (collection?.Value == null || collection.Value.Count == 0) - { - return; - } - - FillOutSubmodelElementValue(collection.Value, values); - } - - private void FillOutSubmodelElementValue(List elements, SemanticTreeNode values, bool updateIdShort = true) - { - var originalElements = elements.ToList(); - foreach (var element in originalElements) - { - var valueNode = FindNodeBySemanticId(values, ExtractSemanticId(element)); - var semanticTreeNodes = valueNode?.ToList(); - - if (semanticTreeNodes == null || semanticTreeNodes.Count == 0) - { - 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); - for (var i = 0; i < semanticTreeNodes.Count; i++) - { - var cloned = CloneElementJson(element); - if (updateIdShort) - { - cloned.IdShort = $"{cloned.IdShort}{i}"; - } - - _ = FillOutTemplate(cloned, semanticTreeNodes[i]); - elements.Add(cloned); - } - } - else - { - HandleSingleSemanticTreeNode(element, semanticTreeNodes[0]); - } - } - } - - private static bool AreAllNodesOfSameType(List 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) - { - var semanticId = ExtractSemanticId(mlp); - - if (FindNodeBySemanticId(values, semanticId).FirstOrDefault() is not SemanticBranchNode valueNode) - { - logger.LogInformation("No value node found for MultiLanguageProperty {MlpIdShort}", mlp.IdShort); - return; - } - - mlp.Value ??= []; - - var languageValueMap = new Dictionary(StringComparer.OrdinalIgnoreCase); - foreach (var langValue in mlp.Value) - { - languageValueMap[langValue.Language] = (LangStringTextType)langValue; - } - - var languages = new HashSet(languageValueMap.Keys, StringComparer.OrdinalIgnoreCase); - - if (_defaultLanguagesSet != null) - { - languages.UnionWith(_defaultLanguagesSet); - } - - foreach (var language in languages) - { - if (!languageValueMap.TryGetValue(language, out var languageValue)) - { - languageValue = new LangStringTextType(language, string.Empty); - mlp.Value.Add(languageValue); - languageValueMap[language] = languageValue; - - logger.LogInformation("Added language '{Language}' to MultiLanguageProperty {MlpIdShort}", language, mlp.IdShort); - } - - var languageSemanticId = semanticId + _mlpPostFixSeparator + language; - - var leafNode = valueNode.Children - .OfType() - .FirstOrDefault(child => child.SemanticId.Equals(languageSemanticId, StringComparison.Ordinal)); - - if (leafNode != null) - { - languageValue.Text = leafNode.Value; - } - } - } - - private void FillOutEntity(Entity entity, SemanticTreeNode values) - { - if (entity.EntityType == EntityType.SelfManagedEntity) - { - FillOutSelfManagedEntity(entity, values); - } - - if (entity?.Statements == null || entity.Statements.Count == 0) - { - return; - } - - FillOutSubmodelElementValue(entity.Statements, values); - } - - private void FillOutSelfManagedEntity(Entity entity, SemanticTreeNode values) - { - var semanticId = GetSemanticId(entity, entity.IdShort!); - - if (FindNodeBySemanticId(values, semanticId).FirstOrDefault() is not SemanticBranchNode valueNode) - { - return; - } - - var globalAssetSemanticId = semanticId + EntityGlobalAssetIdPostFix; - - var globalAssetNode = valueNode.Children - .OfType() - .FirstOrDefault(c => c.SemanticId == globalAssetSemanticId); - - if (globalAssetNode != null) - { - entity.GlobalAssetId = globalAssetNode.Value; - } - - if (entity.SpecificAssetIds != null) - { - foreach (var specificAssetId in entity.SpecificAssetIds) - { - var specSemanticId = GetSemanticId(specificAssetId); - - var specNode = valueNode.Children - .OfType() - .FirstOrDefault(c => c.SemanticId == specSemanticId); - - if (specNode != null) - { - specificAssetId.Value = specNode.Value; - } - } - } - } - - private static void FillOutProperty(Property valueElement, SemanticTreeNode values) - { - if (values is SemanticLeafNode leafValueNode) - { - valueElement.Value = leafValueNode.Value; - } - } - - private static void FillOutFile(File valueElement, SemanticTreeNode values) - { - if (values is SemanticLeafNode leafValueNode) - { - valueElement.Value = leafValueNode.Value; - } - } - - private static void FillOutBlob(Blob valueElement, SemanticTreeNode values) - { - if (values is SemanticLeafNode leafValueNode) - { - valueElement.Value = Convert.FromBase64String(leafValueNode.Value); - } - } - - private static void FillOutRange(Range valueElement, SemanticTreeNode values) - { - if (values is not SemanticBranchNode branchNode) - { - return; - } - - var leafNodes = branchNode.Children.OfType().ToList(); - - valueElement.Min = leafNodes.FirstOrDefault(n => n.SemanticId - .EndsWith(RangeMinimumPostFixSeparator, StringComparison.Ordinal))? - .Value; - - valueElement.Max = leafNodes.FirstOrDefault(n => n.SemanticId - .EndsWith(RangeMaximumPostFixSeparator, StringComparison.Ordinal))? - .Value; - } - - private void FillOutReferenceElement(ReferenceElement referenceElement, SemanticTreeNode semanticNode) - { - if (referenceElement?.Value?.Type != ReferenceTypes.ModelReference) - { - logger.LogInformation("ReferenceElement does not contain a ModelReference for SemanticId '{SemanticId}'. Skipping population.", GetSemanticId(referenceElement!)); - return; - } - - ProcessReferenceNode(referenceElement.Value, semanticNode, GetSemanticId(referenceElement)); - } - - private void FillOutRelationshipElement(RelationshipElement relationshipElement, SemanticTreeNode semanticTreeNode) - { - var semanticId = semanticTreeNode.SemanticId; - - ProcessRelationshipReference(relationshipElement.First, semanticTreeNode, semanticId, RelationshipElementFirstPostFixSeparator); - - ProcessRelationshipReference(relationshipElement.Second, semanticTreeNode, semanticId, RelationshipElementSecondPostFixSeparator); - } - - private void ProcessReferenceNode(IReference reference, SemanticTreeNode semanticNode, string semanticId) - { - if (semanticNode is not SemanticBranchNode branchNode) - { - logger.LogWarning("Expected SemanticBranchNode for SemanticId '{SemanticId}', but got {NodeType}. Skipping population.", semanticId, semanticNode.GetType().Name); - return; - } - - var keys = reference.Keys; - - if (keys.Count <= 0) - { - logger.LogInformation("ReferenceElement has no keys for SemanticId '{SemanticId}'. Nothing to populate.", semanticId); - return; - } - - foreach (var group in keys.GroupBy(k => k.Type)) - { - ProcessReferenceKeyGroup(group, branchNode, semanticId); - } - } - - private void ProcessReferenceKeyGroup(IGrouping group, SemanticBranchNode branchNode, string semanticId) - { - var keyList = group.ToList(); - for (var i = 0; i < keyList.Count; i++) - { - var indexedSemanticId = keyList.Count > 1 - ? $"{semanticId}{_mlpPostFixSeparator}{group.Key}{_mlpPostFixSeparator}{i}" - : $"{semanticId}{_mlpPostFixSeparator}{group.Key}"; - - var leafNode = branchNode.Children - .OfType() - .FirstOrDefault(child => child.SemanticId == indexedSemanticId); - - if (leafNode != null) - { - keyList[i].Value = !string.IsNullOrEmpty(leafNode.Value) ? leafNode.Value : keyList[i].Value; - } - else - { - logger.LogWarning("No matching leaf node found for SemanticId '{IndexedSemanticId}'.", indexedSemanticId); - } - } - } - - private void ProcessRelationshipReference(IReference reference, SemanticTreeNode semanticTreeNode, string semanticId, string postfixSeparator) - { - if (reference.Type != ReferenceTypes.ModelReference) - { - return; - } - - var searchPattern = semanticId + postfixSeparator; - var valueNode = FindNodeBySemanticId(semanticTreeNode, searchPattern).FirstOrDefault(); - - if (valueNode != null) - { - ProcessReferenceNode(reference, valueNode, searchPattern); - } - else - { - logger.LogWarning("No matching node found for reference with pattern: {Pattern}", searchPattern); - } - } - - private static ISubmodelElement CloneElementJson(ISubmodelElement element) - { - var jsonElement = Jsonization.Serialize.ToJsonObject(element); - - return Jsonization.Deserialize.ISubmodelElementFrom(jsonElement); - } - - private static IEnumerable FindBranchNodesBySemanticId(SemanticTreeNode tree, string semanticId) - { - var node = tree as SemanticBranchNode; - - return node?.Children! - .Where(child => child.SemanticId.Equals(semanticId, StringComparison.Ordinal)) - ?? []; - } - - private static IEnumerable FindNodeBySemanticId(SemanticTreeNode tree, string semanticId) - { - if (tree.SemanticId == semanticId) - { - yield return tree; - } - - if (tree is not SemanticBranchNode branchNode) - { - yield break; - } - - foreach (var child in branchNode.Children) - { - foreach (var matchingNode in FindNodeBySemanticId(child, semanticId)) - { - yield return matchingNode; - } - } - } - - /// - /// Matches strings like "element[3]" and captures: - /// Group 1 → element name (any characters, lazy match) - /// Group 2 → index (digits inside square brackets) - /// e.g. "element[3]" -> matches Group1= "element", Group2 = "3" - /// Pattern: ^(.+?)\[(\d+)\]$ - /// - [GeneratedRegex(@"^(.+?)(?:\[(\d+)\]|%5B(\d+)%5D)$")] - private static partial Regex SubmodelElementListIndex(); - - /// - /// Matches one or more digits at the end of a string, - /// e.g., "element42" → matches "42" - /// Pattern: \d+$ - /// - [GeneratedRegex(@"\d+$")] - private static partial Regex SubmodelElementCollectionIndex(); - - private ISubmodelElement? GetElementByIdShort(IEnumerable? submodelElements, string idShort) - { - if (TryParseIdShortWithBracketIndex(idShort, out var idShortWithoutIndex, out var index)) - { - return GetElementFromListByIndex(submodelElements, idShortWithoutIndex, index); - } - - return submodelElements?.FirstOrDefault(e => e.IdShort == idShort); - } - - private static bool TryParseIdShortWithBracketIndex(string idShort, out string idShortWithoutIndex, out int index) - { - var match = SubmodelElementListIndex().Match(idShort); - if (!match.Success) - { - 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; - } - - index = int.Parse(indexGroup.Value, CultureInfo.InvariantCulture); - return true; - } - - private ISubmodelElement GetElementFromListByIndex(IEnumerable? elements, string idShortWithoutIndex, int index) - { - var baseElement = elements?.FirstOrDefault(e => e.IdShort == idShortWithoutIndex); - - if (baseElement is not ISubmodelElementList list) - { - logger.LogError("Expected list element with IdShort '{IdShortWithoutIndex}' not found or is not a list.", idShortWithoutIndex); - throw new InternalDataProcessingException(); - } - - if (index >= 0 && index < list.Value!.Count) - { - return list.Value[index]; - } + public SemanticTreeNode Extract(ISubmodel submodelTemplate) => extractor.Extract(submodelTemplate); - logger.LogError("Index {Index} is out of bounds for list '{IdShortWithoutIndex}' with count {Count}.", index, idShortWithoutIndex, list.Value!.Count); - throw new InternalDataProcessingException(); - } + public ISubmodelElement Extract(ISubmodel submodelTemplate, string idShortPath) => extractor.Extract(submodelTemplate, idShortPath); - private static List? GetChildElements(ISubmodelElement submodelElement) - { - return submodelElement switch - { - ISubmodelElementCollection c => c.Value, - ISubmodelElementList l => l.Value, - IEntity entity => entity.Statements, - _ => null - }; - } + public ISubmodel FillOutTemplate(ISubmodel submodelTemplate, SemanticTreeNode values) => filler.FillOutTemplate(submodelTemplate, values); } diff --git a/source/AAS.TwinEngine.DataEngine/ServiceConfiguration/ApplicationDependencyInjectionExtensions.cs b/source/AAS.TwinEngine.DataEngine/ServiceConfiguration/ApplicationDependencyInjectionExtensions.cs index 419656e0..40fe3bda 100644 --- a/source/AAS.TwinEngine.DataEngine/ServiceConfiguration/ApplicationDependencyInjectionExtensions.cs +++ b/source/AAS.TwinEngine.DataEngine/ServiceConfiguration/ApplicationDependencyInjectionExtensions.cs @@ -9,6 +9,10 @@ using AAS.TwinEngine.DataEngine.ApplicationLogic.Services.Plugin; using AAS.TwinEngine.DataEngine.ApplicationLogic.Services.SubmodelRegistry; using AAS.TwinEngine.DataEngine.ApplicationLogic.Services.SubmodelRepository; +using AAS.TwinEngine.DataEngine.ApplicationLogic.Services.SubmodelRepository.SemanticId.Extraction; +using AAS.TwinEngine.DataEngine.ApplicationLogic.Services.SubmodelRepository.SemanticId.FillOut; +using AAS.TwinEngine.DataEngine.ApplicationLogic.Services.SubmodelRepository.SemanticId.Helpers; +using AAS.TwinEngine.DataEngine.ApplicationLogic.Services.SubmodelRepository.SemanticId.Helpers.Interfaces; using AAS.TwinEngine.DataEngine.Infrastructure.Providers.AasRegistryProvider.Services; using AAS.TwinEngine.DataEngine.Infrastructure.Providers.PluginDataProvider.Services; @@ -31,6 +35,11 @@ public static void ConfigureApplication(this IServiceCollection services, IConfi _ = services.AddScoped(); _ = services.AddScoped(); _ = services.AddScoped(); + _ = services.AddScoped(); + _ = services.AddScoped(); + _ = services.AddScoped(); + _ = services.AddScoped(); + _ = services.AddScoped(); _ = services.AddScoped(); _ = services.AddScoped(); _ = services.AddScoped(); From 332a6ddc6b196cd5ad56891baa0398b88ce526bc Mon Sep 17 00:00:00 2001 From: Kevalkumar Date: Thu, 12 Mar 2026 11:54:48 +0530 Subject: [PATCH 03/71] Added Refactored SemanticIdHandler --- .../SubmodelRepositoryControllerTests.cs | 4 +- .../ElementHandlers/BlobHandlerTests.cs | 79 +++++ .../ElementHandlers/CollectionHandlerTests.cs | 138 ++++++++ .../ElementHandlers/EntityHandlerTests.cs | 185 +++++++++++ .../ElementHandlers/FileHandlerTests.cs | 78 +++++ .../ElementHandlers/ListHandlerTests.cs | 122 +++++++ .../MultiLanguagePropertyHandlerTests.cs | 156 +++++++++ .../ElementHandlers/PropertyHandlerTests.cs | 77 +++++ .../ElementHandlers/RangeHandlerTests.cs | 102 ++++++ .../ReferenceElementHandlerTests.cs | 138 ++++++++ .../RelationshipElementHandlerTests.cs | 131 ++++++++ .../Extraction/SemanticTreeExtractorTests.cs | 186 +++++++++++ .../SemanticId/FillOut/SubmodelFillerTests.cs | 131 ++++++++ .../Helpers/ReferenceHelperTests.cs | 221 +++++++++++++ .../Helpers/SemanticIdResolverTests.cs | 303 ++++++++++++++++++ .../Helpers/SemanticTreeNavigatorTests.cs | 154 +++++++++ .../Helpers/SubmodelElementHelperTests.cs | 290 +++++++++++++++++ .../SemanticIdHandlerTests.cs | 154 ++------- .../Services/SubmodelRepository/TestData.cs | 8 +- .../SemanticId/ElementHandlers/BlobHandler.cs | 25 ++ .../ElementHandlers/CollectionHandler.cs | 49 +++ .../ElementHandlers/EntityHandler.cs | 111 +++++++ .../SemanticId/ElementHandlers/FileHandler.cs | 27 ++ .../ISubmodelElementTypeHandler.cs | 14 + .../SemanticId/ElementHandlers/ListHandler.cs | 45 +++ .../MultiLanguagePropertyHandler.cs | 83 +++++ .../ElementHandlers/PropertyHandler.cs | 25 ++ .../ElementHandlers/RangeHandler.cs | 47 +++ .../ReferenceElementHandler.cs | 42 +++ .../RelationshipElementHandler.cs | 58 ++++ .../Extraction/SemanticTreeExtractor.cs | 184 +---------- .../SemanticId/FillOut/SubmodelFiller.cs | 246 +------------- .../Helpers/SemanticTreeNavigator.cs | 4 +- .../Helpers/SubmodelElementHelper.cs | 2 +- .../SubmodelRepositoryService.cs | 23 +- ...pplicationDependencyInjectionExtensions.cs | 11 + ...astructureDependencyInjectionExtensions.cs | 3 - 37 files changed, 3104 insertions(+), 552 deletions(-) create mode 100644 source/AAS.TwinEngine.DataEngine.UnitTests/ApplicationLogic/Services/SubmodelRepository/SemanticId/ElementHandlers/BlobHandlerTests.cs create mode 100644 source/AAS.TwinEngine.DataEngine.UnitTests/ApplicationLogic/Services/SubmodelRepository/SemanticId/ElementHandlers/CollectionHandlerTests.cs create mode 100644 source/AAS.TwinEngine.DataEngine.UnitTests/ApplicationLogic/Services/SubmodelRepository/SemanticId/ElementHandlers/EntityHandlerTests.cs create mode 100644 source/AAS.TwinEngine.DataEngine.UnitTests/ApplicationLogic/Services/SubmodelRepository/SemanticId/ElementHandlers/FileHandlerTests.cs create mode 100644 source/AAS.TwinEngine.DataEngine.UnitTests/ApplicationLogic/Services/SubmodelRepository/SemanticId/ElementHandlers/ListHandlerTests.cs create mode 100644 source/AAS.TwinEngine.DataEngine.UnitTests/ApplicationLogic/Services/SubmodelRepository/SemanticId/ElementHandlers/MultiLanguagePropertyHandlerTests.cs create mode 100644 source/AAS.TwinEngine.DataEngine.UnitTests/ApplicationLogic/Services/SubmodelRepository/SemanticId/ElementHandlers/PropertyHandlerTests.cs create mode 100644 source/AAS.TwinEngine.DataEngine.UnitTests/ApplicationLogic/Services/SubmodelRepository/SemanticId/ElementHandlers/RangeHandlerTests.cs create mode 100644 source/AAS.TwinEngine.DataEngine.UnitTests/ApplicationLogic/Services/SubmodelRepository/SemanticId/ElementHandlers/ReferenceElementHandlerTests.cs create mode 100644 source/AAS.TwinEngine.DataEngine.UnitTests/ApplicationLogic/Services/SubmodelRepository/SemanticId/ElementHandlers/RelationshipElementHandlerTests.cs create mode 100644 source/AAS.TwinEngine.DataEngine.UnitTests/ApplicationLogic/Services/SubmodelRepository/SemanticId/Extraction/SemanticTreeExtractorTests.cs create mode 100644 source/AAS.TwinEngine.DataEngine.UnitTests/ApplicationLogic/Services/SubmodelRepository/SemanticId/FillOut/SubmodelFillerTests.cs create mode 100644 source/AAS.TwinEngine.DataEngine.UnitTests/ApplicationLogic/Services/SubmodelRepository/SemanticId/Helpers/ReferenceHelperTests.cs create mode 100644 source/AAS.TwinEngine.DataEngine.UnitTests/ApplicationLogic/Services/SubmodelRepository/SemanticId/Helpers/SemanticIdResolverTests.cs create mode 100644 source/AAS.TwinEngine.DataEngine.UnitTests/ApplicationLogic/Services/SubmodelRepository/SemanticId/Helpers/SemanticTreeNavigatorTests.cs create mode 100644 source/AAS.TwinEngine.DataEngine.UnitTests/ApplicationLogic/Services/SubmodelRepository/SemanticId/Helpers/SubmodelElementHelperTests.cs create mode 100644 source/AAS.TwinEngine.DataEngine/ApplicationLogic/Services/SubmodelRepository/SemanticId/ElementHandlers/BlobHandler.cs create mode 100644 source/AAS.TwinEngine.DataEngine/ApplicationLogic/Services/SubmodelRepository/SemanticId/ElementHandlers/CollectionHandler.cs create mode 100644 source/AAS.TwinEngine.DataEngine/ApplicationLogic/Services/SubmodelRepository/SemanticId/ElementHandlers/EntityHandler.cs create mode 100644 source/AAS.TwinEngine.DataEngine/ApplicationLogic/Services/SubmodelRepository/SemanticId/ElementHandlers/FileHandler.cs create mode 100644 source/AAS.TwinEngine.DataEngine/ApplicationLogic/Services/SubmodelRepository/SemanticId/ElementHandlers/ISubmodelElementTypeHandler.cs create mode 100644 source/AAS.TwinEngine.DataEngine/ApplicationLogic/Services/SubmodelRepository/SemanticId/ElementHandlers/ListHandler.cs create mode 100644 source/AAS.TwinEngine.DataEngine/ApplicationLogic/Services/SubmodelRepository/SemanticId/ElementHandlers/MultiLanguagePropertyHandler.cs create mode 100644 source/AAS.TwinEngine.DataEngine/ApplicationLogic/Services/SubmodelRepository/SemanticId/ElementHandlers/PropertyHandler.cs create mode 100644 source/AAS.TwinEngine.DataEngine/ApplicationLogic/Services/SubmodelRepository/SemanticId/ElementHandlers/RangeHandler.cs create mode 100644 source/AAS.TwinEngine.DataEngine/ApplicationLogic/Services/SubmodelRepository/SemanticId/ElementHandlers/ReferenceElementHandler.cs create mode 100644 source/AAS.TwinEngine.DataEngine/ApplicationLogic/Services/SubmodelRepository/SemanticId/ElementHandlers/RelationshipElementHandler.cs diff --git a/source/AAS.TwinEngine.DataEngine.ModuleTests/Api/Services/SubmodelRepository/SubmodelRepositoryControllerTests.cs b/source/AAS.TwinEngine.DataEngine.ModuleTests/Api/Services/SubmodelRepository/SubmodelRepositoryControllerTests.cs index 3c5ee1cc..474fd6cb 100644 --- a/source/AAS.TwinEngine.DataEngine.ModuleTests/Api/Services/SubmodelRepository/SubmodelRepositoryControllerTests.cs +++ b/source/AAS.TwinEngine.DataEngine.ModuleTests/Api/Services/SubmodelRepository/SubmodelRepositoryControllerTests.cs @@ -71,7 +71,7 @@ public async Task GetSubmodelAsync_WithValidIdentifier_ReturnsOkAsync() _ = _httpClientFactory.CreateClient(HttpClientNamePlugin1).Returns(httpClientPlugin1); const string HttpClientNamePlugin2 = $"{PluginConfig.HttpClientNamePrefix}TestPlugin2"; - _httpClientFactory.CreateClient(HttpClientNamePlugin2).Returns(httpClientPlugin2); + _ = _httpClientFactory.CreateClient(HttpClientNamePlugin2).Returns(httpClientPlugin2); var submodelId = "Q29udGFjdEluZm9ybWF0aW9u"; var mockSubmodel = TestData.CreateSubmodel(); @@ -131,7 +131,7 @@ public async Task GetSubmodelElementAsync_ReturnsOkAsync() const string SubmodelId = "Q29udGFjdEluZm9ybWF0aW9u"; const string IdShortPath = "ContactName"; var mockSubmodel = TestData.CreateSubmodel(); - TestData.CreatePluginResponseForSubmodelElement(); + _ = TestData.CreatePluginResponseForSubmodelElement(); using var messageHandler = new FakeHttpMessageHandler((_, _) => Task.FromResult(new HttpResponseMessage { diff --git a/source/AAS.TwinEngine.DataEngine.UnitTests/ApplicationLogic/Services/SubmodelRepository/SemanticId/ElementHandlers/BlobHandlerTests.cs b/source/AAS.TwinEngine.DataEngine.UnitTests/ApplicationLogic/Services/SubmodelRepository/SemanticId/ElementHandlers/BlobHandlerTests.cs new file mode 100644 index 00000000..1471f0a9 --- /dev/null +++ b/source/AAS.TwinEngine.DataEngine.UnitTests/ApplicationLogic/Services/SubmodelRepository/SemanticId/ElementHandlers/BlobHandlerTests.cs @@ -0,0 +1,79 @@ +using AAS.TwinEngine.DataEngine.ApplicationLogic.Services.SubmodelRepository.SemanticId.ElementHandlers; +using AAS.TwinEngine.DataEngine.ApplicationLogic.Services.SubmodelRepository.SemanticId.Helpers.Interfaces; +using AAS.TwinEngine.DataEngine.DomainModel.SubmodelRepository; + +using AasCore.Aas3_0; + +using NSubstitute; + +using static Xunit.Assert; + +namespace AAS.TwinEngine.DataEngine.UnitTests.ApplicationLogic.Services.SubmodelRepository.SemanticId.ElementHandlers; + +public class BlobHandlerTests +{ + private readonly BlobHandler _sut; + private readonly ISemanticIdResolver _resolver; + + public BlobHandlerTests() + { + _resolver = Substitute.For(); + _sut = new BlobHandler(_resolver); + } + + [Fact] + public void CanHandle_Blob_ReturnsTrue() + { + var blob = new Blob(contentType: "application/octet-stream", idShort: "Test"); + + True(_sut.CanHandle(blob)); + } + + [Fact] + public void CanHandle_NonBlob_ReturnsFalse() + { + var property = new Property(idShort: "Test", valueType: DataTypeDefXsd.String); + + False(_sut.CanHandle(property)); + } + + [Fact] + public void Extract_ReturnsLeafNode() + { + var blob = new Blob(contentType: "image/png", idShort: "MyBlob"); + _resolver.ResolveElementSemanticId(blob, "MyBlob").Returns("http://test/blob"); + _resolver.GetValueType(blob).Returns(DataType.String); + _resolver.GetCardinality(blob).Returns(Cardinality.One); + + var result = _sut.Extract(blob, _ => null); + + var leaf = IsType(result); + Equal("http://test/blob", leaf.SemanticId); + Equal(DataType.String, leaf.DataType); + } + + [Fact] + public void FillOut_WithLeafNode_SetsBase64Value() + { + var blob = new Blob(contentType: "image/png", idShort: "MyBlob"); + var base64 = Convert.ToBase64String(new byte[] { 1, 2, 3 }); + var values = new SemanticLeafNode("http://test/blob", base64, DataType.String, Cardinality.One); + + _sut.FillOut(blob, values, (_, _, _) => { }); + + NotNull(blob.Value); + Equal([1, 2, 3], blob.Value); + } + + [Fact] + public void FillOut_WithBranchNode_DoesNotModifyValue() + { + var originalBytes = new byte[] { 10, 20, 30 }; + var blob = new Blob(contentType: "image/png", idShort: "MyBlob", value: originalBytes); + var values = new SemanticBranchNode("http://test/blob", Cardinality.One); + + _sut.FillOut(blob, values, (_, _, _) => { }); + + Equal(originalBytes, blob.Value); + } +} diff --git a/source/AAS.TwinEngine.DataEngine.UnitTests/ApplicationLogic/Services/SubmodelRepository/SemanticId/ElementHandlers/CollectionHandlerTests.cs b/source/AAS.TwinEngine.DataEngine.UnitTests/ApplicationLogic/Services/SubmodelRepository/SemanticId/ElementHandlers/CollectionHandlerTests.cs new file mode 100644 index 00000000..fca9abc1 --- /dev/null +++ b/source/AAS.TwinEngine.DataEngine.UnitTests/ApplicationLogic/Services/SubmodelRepository/SemanticId/ElementHandlers/CollectionHandlerTests.cs @@ -0,0 +1,138 @@ +using AAS.TwinEngine.DataEngine.ApplicationLogic.Services.SubmodelRepository.SemanticId.ElementHandlers; +using AAS.TwinEngine.DataEngine.ApplicationLogic.Services.SubmodelRepository.SemanticId.Helpers.Interfaces; +using AAS.TwinEngine.DataEngine.DomainModel.SubmodelRepository; + +using AasCore.Aas3_0; + +using Microsoft.Extensions.Logging; + +using NSubstitute; + +using static Xunit.Assert; + +namespace AAS.TwinEngine.DataEngine.UnitTests.ApplicationLogic.Services.SubmodelRepository.SemanticId.ElementHandlers; + +public class CollectionHandlerTests +{ + private readonly CollectionHandler _sut; + private readonly ISemanticIdResolver _resolver; + private readonly ILogger _logger; + + public CollectionHandlerTests() + { + _resolver = Substitute.For(); + _logger = Substitute.For>(); + _sut = new CollectionHandler(_resolver, _logger); + } + + [Fact] + public void CanHandle_Collection_ReturnsTrue() + { + var collection = new SubmodelElementCollection(idShort: "Test"); + + True(_sut.CanHandle(collection)); + } + + [Fact] + public void CanHandle_NonCollection_ReturnsFalse() + { + var property = new Property(idShort: "Test", valueType: DataTypeDefXsd.String); + + False(_sut.CanHandle(property)); + } + + [Fact] + public void Extract_WithChildren_ReturnsBranchNodeWithChildren() + { + var child = new Property(idShort: "Child", valueType: DataTypeDefXsd.String); + var collection = new SubmodelElementCollection(idShort: "MyCollection", value: [child]); + _resolver.ResolveElementSemanticId(collection, "MyCollection").Returns("http://test/collection"); + _resolver.GetCardinality(collection).Returns(Cardinality.ZeroToMany); + + var childNode = new SemanticLeafNode("http://test/child", "", DataType.String, Cardinality.One); + SemanticTreeNode? extractChild(ISubmodelElement _) => childNode; + + var result = _sut.Extract(collection, extractChild); + + var branch = IsType(result); + Equal("http://test/collection", branch.SemanticId); + Equal(Cardinality.ZeroToMany, branch.Cardinality); + Single(branch.Children); + } + + [Fact] + public void Extract_WithNullValue_ReturnsBranchNodeAndLogsWarning() + { + var collection = new SubmodelElementCollection(idShort: "EmptyCollection", value: null); + _resolver.ResolveElementSemanticId(collection, "EmptyCollection").Returns("http://test/empty"); + _resolver.GetCardinality(collection).Returns(Cardinality.Unknown); + + var result = _sut.Extract(collection, _ => null); + + var branch = IsType(result); + Empty(branch.Children); + _logger.Received(1).Log( + LogLevel.Warning, + Arg.Any(), + Arg.Is(state => state.ToString()!.Contains("No elements defined in SubmodelElementCollection EmptyCollection")), + null, + Arg.Any>()! + ); + } + + [Fact] + public void Extract_WithEmptyValue_ReturnsBranchNodeAndLogsWarning() + { + var collection = new SubmodelElementCollection(idShort: "EmptyCollection", value: []); + _resolver.ResolveElementSemanticId(collection, "EmptyCollection").Returns("http://test/empty"); + _resolver.GetCardinality(collection).Returns(Cardinality.Unknown); + + var result = _sut.Extract(collection, _ => null); + + var branch = IsType(result); + Empty(branch.Children); + _logger.Received(1).Log( + LogLevel.Warning, + Arg.Any(), + Arg.Is(state => state.ToString()!.Contains("No elements defined in SubmodelElementCollection EmptyCollection")), + null, + Arg.Any>()! + ); + } + + [Fact] + public void FillOut_WithChildren_DelegatesToFillOutChildren() + { + var child = new Property(idShort: "Child", valueType: DataTypeDefXsd.String); + var collection = new SubmodelElementCollection(idShort: "Col", value: [child]); + var values = new SemanticBranchNode("http://test/col", Cardinality.One); + var fillOutCalled = false; + + _sut.FillOut(collection, values, (elements, node, updateIdShort) => + { + fillOutCalled = true; + True(updateIdShort); + Same(collection.Value, elements); + }); + + True(fillOutCalled); + } + + [Fact] + public void FillOut_WithNullValue_DoesNotCallFillOutChildren() + { + var collection = new SubmodelElementCollection(idShort: "Col", value: null); + var values = new SemanticBranchNode("http://test/col", Cardinality.One); + + _sut.FillOut(collection, values, (_, _, _) => Fail("Should not be called")); + } + + [Fact] + public void FillOut_WithEmptyValue_DoesNotCallFillOutChildren() + { + var collection = new SubmodelElementCollection(idShort: "Col", value: []); + var values = new SemanticBranchNode("http://test/col", Cardinality.One); + + _sut.FillOut(collection, values, (_, _, _) => Fail("Should not be called")); + } +} diff --git a/source/AAS.TwinEngine.DataEngine.UnitTests/ApplicationLogic/Services/SubmodelRepository/SemanticId/ElementHandlers/EntityHandlerTests.cs b/source/AAS.TwinEngine.DataEngine.UnitTests/ApplicationLogic/Services/SubmodelRepository/SemanticId/ElementHandlers/EntityHandlerTests.cs new file mode 100644 index 00000000..c444370a --- /dev/null +++ b/source/AAS.TwinEngine.DataEngine.UnitTests/ApplicationLogic/Services/SubmodelRepository/SemanticId/ElementHandlers/EntityHandlerTests.cs @@ -0,0 +1,185 @@ +using AAS.TwinEngine.DataEngine.ApplicationLogic.Services.SubmodelRepository.SemanticId.ElementHandlers; +using AAS.TwinEngine.DataEngine.ApplicationLogic.Services.SubmodelRepository.SemanticId.Helpers; +using AAS.TwinEngine.DataEngine.ApplicationLogic.Services.SubmodelRepository.SemanticId.Helpers.Interfaces; +using AAS.TwinEngine.DataEngine.DomainModel.SubmodelRepository; + +using AasCore.Aas3_0; + +using Microsoft.Extensions.Logging; + +using NSubstitute; + +using static Xunit.Assert; + +namespace AAS.TwinEngine.DataEngine.UnitTests.ApplicationLogic.Services.SubmodelRepository.SemanticId.ElementHandlers; + +public class EntityHandlerTests +{ + private readonly EntityHandler _sut; + private readonly ISemanticIdResolver _resolver; + private readonly ILogger _logger; + + public EntityHandlerTests() + { + _resolver = Substitute.For(); + _logger = Substitute.For>(); + _sut = new EntityHandler(_resolver, _logger); + } + + [Fact] + public void CanHandle_Entity_ReturnsTrue() + { + var entity = new Entity(idShort: "Test", entityType: EntityType.SelfManagedEntity); + + True(_sut.CanHandle(entity)); + } + + [Fact] + public void CanHandle_NonEntity_ReturnsFalse() + { + var property = new Property(idShort: "Test", valueType: DataTypeDefXsd.String); + + False(_sut.CanHandle(property)); + } + + [Fact] + public void Extract_SelfManagedEntity_ReturnsBranchWithGlobalAssetIdAndSpecificAssetIds() + { + var specificAssetId = new SpecificAssetId(name: "Manufacturer", value: "Corp") + { + SemanticId = new Reference(ReferenceTypes.ModelReference, + [new Key(KeyTypes.ConceptDescription, "https://example.com/cd/manufacturer")]) + }; + + var entity = new Entity( + idShort: "MyEntity", + entityType: EntityType.SelfManagedEntity, + globalAssetId: "", + specificAssetIds: [specificAssetId], + statements: [new Property(idShort: "Stmt", valueType: DataTypeDefXsd.String)] + ); + + _resolver.ResolveElementSemanticId(entity, "MyEntity").Returns("http://test/entity"); + _resolver.GetCardinality(entity).Returns(Cardinality.ZeroToMany); + _resolver.GetSemanticId(specificAssetId).Returns("https://example.com/cd/manufacturer"); + + var stmtNode = new SemanticLeafNode("http://test/stmt", "", DataType.String, Cardinality.One); + + var result = _sut.Extract(entity, _ => stmtNode); + + var branch = IsType(result); + Equal("http://test/entity", branch.SemanticId); + Equal(3, branch.Children.Count); + var globalAssetLeaf = IsType(branch.Children[0]); + Equal("http://test/entity" + SemanticIdResolver.EntityGlobalAssetIdPostFix, globalAssetLeaf.SemanticId); + var specificLeaf = IsType(branch.Children[1]); + Equal("https://example.com/cd/manufacturer", specificLeaf.SemanticId); + } + + [Fact] + public void Extract_CoManagedEntity_DoesNotAddGlobalAssetId() + { + var entity = new Entity( + idShort: "MyEntity", + entityType: EntityType.CoManagedEntity, + statements: [new Property(idShort: "Stmt", valueType: DataTypeDefXsd.String)] + ); + + _resolver.ResolveElementSemanticId(entity, "MyEntity").Returns("http://test/entity"); + _resolver.GetCardinality(entity).Returns(Cardinality.One); + + var stmtNode = new SemanticLeafNode("http://test/stmt", "", DataType.String, Cardinality.One); + + var result = _sut.Extract(entity, _ => stmtNode); + + var branch = IsType(result); + Single(branch.Children); + } + + [Fact] + public void Extract_EntityWithNoStatements_LogsWarning() + { + var entity = new Entity( + idShort: "MyEntity", + entityType: EntityType.CoManagedEntity, + statements: null + ); + + _resolver.ResolveElementSemanticId(entity, "MyEntity").Returns("http://test/entity"); + _resolver.GetCardinality(entity).Returns(Cardinality.One); + + _sut.Extract(entity, _ => null); + + _logger.Received(1).Log( + LogLevel.Warning, + Arg.Any(), + Arg.Is(state => state.ToString()!.Contains("No elements defined in Entity MyEntity")), + null, + Arg.Any>()! + ); + } + + [Fact] + public void FillOut_SelfManagedEntity_SetsGlobalAssetIdAndSpecificAssetIds() + { + var specificAssetId = new SpecificAssetId(name: "Manufacturer", value: "") + { + SemanticId = new Reference(ReferenceTypes.ModelReference, + [new Key(KeyTypes.ConceptDescription, "https://example.com/cd/manufacturer")]) + }; + + var entity = new Entity( + idShort: "MyEntity", + entityType: EntityType.SelfManagedEntity, + globalAssetId: "", + specificAssetIds: [specificAssetId], + statements: [new Property(idShort: "Stmt", valueType: DataTypeDefXsd.String)] + ); + + _resolver.ResolveElementSemanticId(entity, "MyEntity").Returns("http://test/entity"); + _resolver.GetSemanticId(specificAssetId).Returns("https://example.com/cd/manufacturer"); + + var valueNode = new SemanticBranchNode("http://test/entity", Cardinality.One); + valueNode.AddChild(new SemanticLeafNode("http://test/entity_globalAssetId", "urn:uuid:12345", DataType.String, Cardinality.One)); + valueNode.AddChild(new SemanticLeafNode("https://example.com/cd/manufacturer", "NewCorp", DataType.String, Cardinality.One)); + + _sut.FillOut(entity, valueNode, (_, _, _) => { }); + + Equal("urn:uuid:12345", entity.GlobalAssetId); + Equal("NewCorp", specificAssetId.Value); + } + + [Fact] + public void FillOut_WithStatements_DelegatesToFillOutChildren() + { + var stmt = new Property(idShort: "Stmt", valueType: DataTypeDefXsd.String); + var entity = new Entity( + idShort: "MyEntity", + entityType: EntityType.CoManagedEntity, + statements: [stmt] + ); + var values = new SemanticBranchNode("http://test/entity", Cardinality.One); + var fillOutCalled = false; + + _sut.FillOut(entity, values, (elements, node, updateIdShort) => + { + fillOutCalled = true; + True(updateIdShort); + }); + + True(fillOutCalled); + } + + [Fact] + public void FillOut_EntityWithNullStatements_DoesNotCallFillOutChildren() + { + var entity = new Entity( + idShort: "MyEntity", + entityType: EntityType.CoManagedEntity, + statements: null + ); + var values = new SemanticBranchNode("http://test/entity", Cardinality.One); + + _sut.FillOut(entity, values, (_, _, _) => Fail("Should not be called")); + } +} diff --git a/source/AAS.TwinEngine.DataEngine.UnitTests/ApplicationLogic/Services/SubmodelRepository/SemanticId/ElementHandlers/FileHandlerTests.cs b/source/AAS.TwinEngine.DataEngine.UnitTests/ApplicationLogic/Services/SubmodelRepository/SemanticId/ElementHandlers/FileHandlerTests.cs new file mode 100644 index 00000000..b9a38447 --- /dev/null +++ b/source/AAS.TwinEngine.DataEngine.UnitTests/ApplicationLogic/Services/SubmodelRepository/SemanticId/ElementHandlers/FileHandlerTests.cs @@ -0,0 +1,78 @@ +using AAS.TwinEngine.DataEngine.ApplicationLogic.Services.SubmodelRepository.SemanticId.ElementHandlers; +using AAS.TwinEngine.DataEngine.ApplicationLogic.Services.SubmodelRepository.SemanticId.Helpers.Interfaces; +using AAS.TwinEngine.DataEngine.DomainModel.SubmodelRepository; + +using AasCore.Aas3_0; + +using NSubstitute; + +using static Xunit.Assert; + +using File = AasCore.Aas3_0.File; + +namespace AAS.TwinEngine.DataEngine.UnitTests.ApplicationLogic.Services.SubmodelRepository.SemanticId.ElementHandlers; + +public class FileHandlerTests +{ + private readonly FileHandler _sut; + private readonly ISemanticIdResolver _resolver; + + public FileHandlerTests() + { + _resolver = Substitute.For(); + _sut = new FileHandler(_resolver); + } + + [Fact] + public void CanHandle_File_ReturnsTrue() + { + var file = new File(contentType: "image/png", idShort: "Test"); + + True(_sut.CanHandle(file)); + } + + [Fact] + public void CanHandle_NonFile_ReturnsFalse() + { + var property = new Property(idShort: "Test", valueType: DataTypeDefXsd.String); + + False(_sut.CanHandle(property)); + } + + [Fact] + public void Extract_ReturnsLeafNode() + { + var file = new File(contentType: "image/png", idShort: "Thumbnail"); + _resolver.ResolveElementSemanticId(file, "Thumbnail").Returns("http://test/thumbnail"); + _resolver.GetValueType(file).Returns(DataType.String); + _resolver.GetCardinality(file).Returns(Cardinality.ZeroToOne); + + var result = _sut.Extract(file, _ => null); + + var leaf = IsType(result); + Equal("http://test/thumbnail", leaf.SemanticId); + Equal(DataType.String, leaf.DataType); + } + + [Fact] + public void FillOut_WithLeafNode_SetsFileValue() + { + var file = new File(contentType: "image/png", idShort: "Thumbnail", value: ""); + var values = new SemanticLeafNode("http://test/thumbnail", "https://localhost/image.png", DataType.String, Cardinality.One); + + _sut.FillOut(file, values, (_, _, _) => { }); + + Equal("https://localhost/image.png", file.Value); + } + + [Fact] + public void FillOut_WithBranchNode_DoesNotModifyValue() + { + var file = new File(contentType: "image/png", idShort: "Thumbnail", value: "original"); + var values = new SemanticBranchNode("http://test/thumbnail", Cardinality.One); + + _sut.FillOut(file, values, (_, _, _) => { }); + + Equal("original", file.Value); + } +} diff --git a/source/AAS.TwinEngine.DataEngine.UnitTests/ApplicationLogic/Services/SubmodelRepository/SemanticId/ElementHandlers/ListHandlerTests.cs b/source/AAS.TwinEngine.DataEngine.UnitTests/ApplicationLogic/Services/SubmodelRepository/SemanticId/ElementHandlers/ListHandlerTests.cs new file mode 100644 index 00000000..4fca5e9a --- /dev/null +++ b/source/AAS.TwinEngine.DataEngine.UnitTests/ApplicationLogic/Services/SubmodelRepository/SemanticId/ElementHandlers/ListHandlerTests.cs @@ -0,0 +1,122 @@ +using AAS.TwinEngine.DataEngine.ApplicationLogic.Services.SubmodelRepository.SemanticId.ElementHandlers; +using AAS.TwinEngine.DataEngine.ApplicationLogic.Services.SubmodelRepository.SemanticId.Helpers.Interfaces; +using AAS.TwinEngine.DataEngine.DomainModel.SubmodelRepository; + +using AasCore.Aas3_0; + +using Microsoft.Extensions.Logging; + +using NSubstitute; + +using static Xunit.Assert; + +namespace AAS.TwinEngine.DataEngine.UnitTests.ApplicationLogic.Services.SubmodelRepository.SemanticId.ElementHandlers; + +public class ListHandlerTests +{ + private readonly ListHandler _sut; + private readonly ISemanticIdResolver _resolver; + private readonly ILogger _logger; + + public ListHandlerTests() + { + _resolver = Substitute.For(); + _logger = Substitute.For>(); + _sut = new ListHandler(_resolver, _logger); + } + + [Fact] + public void CanHandle_SubmodelElementList_ReturnsTrue() + { + var list = new SubmodelElementList(idShort: "Test", typeValueListElement: AasSubmodelElements.Property); + + True(_sut.CanHandle(list)); + } + + [Fact] + public void CanHandle_NonList_ReturnsFalse() + { + var property = new Property(idShort: "Test", valueType: DataTypeDefXsd.String); + + False(_sut.CanHandle(property)); + } + + [Fact] + public void Extract_WithChildren_ReturnsBranchNodeWithChildren() + { + var child = new Property(idShort: "Item", valueType: DataTypeDefXsd.String); + var list = new SubmodelElementList( + idShort: "MyList", + typeValueListElement: AasSubmodelElements.Property, + value: [child] + ); + _resolver.ResolveElementSemanticId(list, "MyList").Returns("http://test/list"); + _resolver.GetCardinality(list).Returns(Cardinality.ZeroToMany); + + var childNode = new SemanticLeafNode("http://test/item", "", DataType.String, Cardinality.One); + + var result = _sut.Extract(list, _ => childNode); + + var branch = IsType(result); + Equal("http://test/list", branch.SemanticId); + Single(branch.Children); + } + + [Fact] + public void Extract_WithNullValue_LogsWarningAndReturnsEmptyBranch() + { + var list = new SubmodelElementList( + idShort: "EmptyList", + typeValueListElement: AasSubmodelElements.Property, + value: null + ); + _resolver.ResolveElementSemanticId(list, "EmptyList").Returns("http://test/empty"); + _resolver.GetCardinality(list).Returns(Cardinality.Unknown); + + var result = _sut.Extract(list, _ => null); + + var branch = IsType(result); + Empty(branch.Children); + _logger.Received(1).Log( + LogLevel.Warning, + Arg.Any(), + Arg.Is(state => state.ToString()!.Contains("No elements defined in SubmodelElementList EmptyList")), + null, + Arg.Any>()! + ); + } + + [Fact] + public void FillOut_WithChildren_DelegatesToFillOutChildren() + { + var child = new Property(idShort: "Item", valueType: DataTypeDefXsd.String); + var list = new SubmodelElementList( + idShort: "List", + typeValueListElement: AasSubmodelElements.Property, + value: [child] + ); + var values = new SemanticBranchNode("http://test/list", Cardinality.One); + var fillOutCalled = false; + + _sut.FillOut(list, values, (elements, node, updateIdShort) => + { + fillOutCalled = true; + False(updateIdShort); + }); + + True(fillOutCalled); + } + + [Fact] + public void FillOut_WithNullValue_DoesNotCallFillOutChildren() + { + var list = new SubmodelElementList( + idShort: "List", + typeValueListElement: AasSubmodelElements.Property, + value: null + ); + var values = new SemanticBranchNode("http://test/list", Cardinality.One); + + _sut.FillOut(list, values, (_, _, _) => Fail("Should not be called")); + } +} diff --git a/source/AAS.TwinEngine.DataEngine.UnitTests/ApplicationLogic/Services/SubmodelRepository/SemanticId/ElementHandlers/MultiLanguagePropertyHandlerTests.cs b/source/AAS.TwinEngine.DataEngine.UnitTests/ApplicationLogic/Services/SubmodelRepository/SemanticId/ElementHandlers/MultiLanguagePropertyHandlerTests.cs new file mode 100644 index 00000000..70fddbc4 --- /dev/null +++ b/source/AAS.TwinEngine.DataEngine.UnitTests/ApplicationLogic/Services/SubmodelRepository/SemanticId/ElementHandlers/MultiLanguagePropertyHandlerTests.cs @@ -0,0 +1,156 @@ +using AAS.TwinEngine.DataEngine.ApplicationLogic.Services.SubmodelRepository.SemanticId.ElementHandlers; +using AAS.TwinEngine.DataEngine.ApplicationLogic.Services.SubmodelRepository.SemanticId.Helpers.Interfaces; +using AAS.TwinEngine.DataEngine.DomainModel.SubmodelRepository; + +using AasCore.Aas3_0; + +using Microsoft.Extensions.Logging; + +using NSubstitute; + +using static Xunit.Assert; + +namespace AAS.TwinEngine.DataEngine.UnitTests.ApplicationLogic.Services.SubmodelRepository.SemanticId.ElementHandlers; + +public class MultiLanguagePropertyHandlerTests +{ + private readonly MultiLanguagePropertyHandler _sut; + private readonly ISemanticIdResolver _resolver; + private readonly ISubmodelElementHelper _elementHelper; + private readonly ILogger _logger; + + public MultiLanguagePropertyHandlerTests() + { + _resolver = Substitute.For(); + _elementHelper = Substitute.For(); + _logger = Substitute.For>(); + _sut = new MultiLanguagePropertyHandler(_resolver, _elementHelper, _logger); + } + + [Fact] + public void CanHandle_MultiLanguageProperty_ReturnsTrue() + { + var mlp = new MultiLanguageProperty(idShort: "Test"); + + True(_sut.CanHandle(mlp)); + } + + [Fact] + public void CanHandle_NonMlp_ReturnsFalse() + { + var property = new Property(idShort: "Test", valueType: DataTypeDefXsd.String); + + False(_sut.CanHandle(property)); + } + + [Fact] + public void Extract_WithLanguages_ReturnsBranchWithLanguageLeaves() + { + var mlp = new MultiLanguageProperty( + idShort: "ManufacturerName", + value: [new LangStringTextType("en", ""), new LangStringTextType("de", "")] + ); + _resolver.ExtractSemanticId(mlp).Returns("http://test/manufacturer-name"); + _resolver.GetCardinality(mlp).Returns(Cardinality.One); + _resolver.MlpPostFixSeparator.Returns("_"); + _elementHelper.ResolveLanguages(mlp).Returns(["en", "de"]); + + var result = _sut.Extract(mlp, _ => null); + + var branch = IsType(result); + Equal("http://test/manufacturer-name", branch.SemanticId); + Equal(2, branch.Children.Count); + var semanticIds = branch.Children.Select(c => c.SemanticId).OrderBy(s => s).ToList(); + Contains("http://test/manufacturer-name_de", semanticIds); + Contains("http://test/manufacturer-name_en", semanticIds); + } + + [Fact] + public void Extract_WithNoLanguages_ReturnsEmptyBranchAndLogsInfo() + { + var mlp = new MultiLanguageProperty(idShort: "EmptyMlp", value: null); + _resolver.ExtractSemanticId(mlp).Returns("http://test/empty"); + _resolver.GetCardinality(mlp).Returns(Cardinality.Unknown); + _resolver.MlpPostFixSeparator.Returns("_"); + _elementHelper.ResolveLanguages(mlp).Returns([]); + + var result = _sut.Extract(mlp, _ => null); + + var branch = IsType(result); + Empty(branch.Children); + _logger.Received(1).Log( + LogLevel.Information, + Arg.Any(), + Arg.Is(state => state.ToString()!.Contains("No languages defined")), + null, + Arg.Any>()! + ); + } + + [Fact] + public void FillOut_WithMatchingLeafNodes_SetsLanguageValues() + { + var mlp = new MultiLanguageProperty( + idShort: "MfName", + value: [new LangStringTextType("en", ""), new LangStringTextType("de", "")] + ); + _resolver.ExtractSemanticId(mlp).Returns("http://test/mfname"); + _resolver.MlpPostFixSeparator.Returns("_"); + _elementHelper.ResolveLanguages(mlp).Returns(["en", "de"]); + + var valueNode = new SemanticBranchNode("http://test/mfname", Cardinality.One); + valueNode.AddChild(new SemanticLeafNode("http://test/mfname_en", "English Value", DataType.String, Cardinality.One)); + valueNode.AddChild(new SemanticLeafNode("http://test/mfname_de", "German Value", DataType.String, Cardinality.One)); + + _sut.FillOut(mlp, valueNode, (_, _, _) => { }); + + Equal("English Value", mlp.Value!.First(v => v.Language == "en").Text); + Equal("German Value", mlp.Value!.First(v => v.Language == "de").Text); + } + + [Fact] + public void FillOut_WithNoMatchingValueNode_LogsInfo() + { + var mlp = new MultiLanguageProperty(idShort: "MfName"); + _resolver.ExtractSemanticId(mlp).Returns("http://test/mfname"); + var nonMatchingNode = new SemanticBranchNode("http://test/other", Cardinality.One); + + _sut.FillOut(mlp, nonMatchingNode, (_, _, _) => { }); + + _logger.Received(1).Log( + LogLevel.Information, + Arg.Any(), + Arg.Is(state => state.ToString()!.Contains("No value node found")), + null, + Arg.Any>()! + ); + } + + [Fact] + public void FillOut_WithNewDefaultLanguage_AddsLanguageAndLogsInfo() + { + var mlp = new MultiLanguageProperty( + idShort: "MfName", + value: [new LangStringTextType("en", "")] + ); + _resolver.ExtractSemanticId(mlp).Returns("http://test/mfname"); + _resolver.MlpPostFixSeparator.Returns("_"); + _elementHelper.ResolveLanguages(mlp).Returns(["en", "fr"]); + + var valueNode = new SemanticBranchNode("http://test/mfname", Cardinality.One); + valueNode.AddChild(new SemanticLeafNode("http://test/mfname_en", "English", DataType.String, Cardinality.One)); + valueNode.AddChild(new SemanticLeafNode("http://test/mfname_fr", "French", DataType.String, Cardinality.One)); + + _sut.FillOut(mlp, valueNode, (_, _, _) => { }); + + Equal(2, mlp.Value!.Count); + Equal("French", mlp.Value.First(v => v.Language == "fr").Text); + _logger.Received(1).Log( + LogLevel.Information, + Arg.Any(), + Arg.Is(state => state.ToString()!.Contains("Added language 'fr'")), + null, + Arg.Any>()! + ); + } +} diff --git a/source/AAS.TwinEngine.DataEngine.UnitTests/ApplicationLogic/Services/SubmodelRepository/SemanticId/ElementHandlers/PropertyHandlerTests.cs b/source/AAS.TwinEngine.DataEngine.UnitTests/ApplicationLogic/Services/SubmodelRepository/SemanticId/ElementHandlers/PropertyHandlerTests.cs new file mode 100644 index 00000000..f19467ba --- /dev/null +++ b/source/AAS.TwinEngine.DataEngine.UnitTests/ApplicationLogic/Services/SubmodelRepository/SemanticId/ElementHandlers/PropertyHandlerTests.cs @@ -0,0 +1,77 @@ +using AAS.TwinEngine.DataEngine.ApplicationLogic.Services.SubmodelRepository.SemanticId.ElementHandlers; +using AAS.TwinEngine.DataEngine.ApplicationLogic.Services.SubmodelRepository.SemanticId.Helpers.Interfaces; +using AAS.TwinEngine.DataEngine.DomainModel.SubmodelRepository; + +using AasCore.Aas3_0; + +using NSubstitute; + +using static Xunit.Assert; + +namespace AAS.TwinEngine.DataEngine.UnitTests.ApplicationLogic.Services.SubmodelRepository.SemanticId.ElementHandlers; + +public class PropertyHandlerTests +{ + private readonly PropertyHandler _sut; + private readonly ISemanticIdResolver _resolver; + + public PropertyHandlerTests() + { + _resolver = Substitute.For(); + _sut = new PropertyHandler(_resolver); + } + + [Fact] + public void CanHandle_Property_ReturnsTrue() + { + var property = new Property(idShort: "Test", valueType: DataTypeDefXsd.String); + + True(_sut.CanHandle(property)); + } + + [Fact] + public void CanHandle_NonProperty_ReturnsFalse() + { + var collection = new SubmodelElementCollection(idShort: "Test"); + + False(_sut.CanHandle(collection)); + } + + [Fact] + public void Extract_ReturnsLeafNodeWithSemanticIdAndType() + { + var property = new Property(idShort: "MyProp", valueType: DataTypeDefXsd.String, value: "test"); + _resolver.ResolveElementSemanticId(property, "MyProp").Returns("http://test/my-prop"); + _resolver.GetValueType(property).Returns(DataType.String); + _resolver.GetCardinality(property).Returns(Cardinality.One); + + var result = _sut.Extract(property, _ => null); + + var leaf = IsType(result); + Equal("http://test/my-prop", leaf.SemanticId); + Equal(DataType.String, leaf.DataType); + Equal(Cardinality.One, leaf.Cardinality); + } + + [Fact] + public void FillOut_WithLeafNode_SetsPropertyValue() + { + var property = new Property(idShort: "MyProp", valueType: DataTypeDefXsd.String, value: ""); + var values = new SemanticLeafNode("http://test/my-prop", "NewValue", DataType.String, Cardinality.One); + + _sut.FillOut(property, values, (_, _, _) => { }); + + Equal("NewValue", property.Value); + } + + [Fact] + public void FillOut_WithBranchNode_DoesNotModifyPropertyValue() + { + var property = new Property(idShort: "MyProp", valueType: DataTypeDefXsd.String, value: "original"); + var values = new SemanticBranchNode("http://test/my-prop", Cardinality.One); + + _sut.FillOut(property, values, (_, _, _) => { }); + + Equal("original", property.Value); + } +} diff --git a/source/AAS.TwinEngine.DataEngine.UnitTests/ApplicationLogic/Services/SubmodelRepository/SemanticId/ElementHandlers/RangeHandlerTests.cs b/source/AAS.TwinEngine.DataEngine.UnitTests/ApplicationLogic/Services/SubmodelRepository/SemanticId/ElementHandlers/RangeHandlerTests.cs new file mode 100644 index 00000000..bf9ebb88 --- /dev/null +++ b/source/AAS.TwinEngine.DataEngine.UnitTests/ApplicationLogic/Services/SubmodelRepository/SemanticId/ElementHandlers/RangeHandlerTests.cs @@ -0,0 +1,102 @@ +using AAS.TwinEngine.DataEngine.ApplicationLogic.Services.SubmodelRepository.SemanticId.ElementHandlers; +using AAS.TwinEngine.DataEngine.ApplicationLogic.Services.SubmodelRepository.SemanticId.Helpers; +using AAS.TwinEngine.DataEngine.ApplicationLogic.Services.SubmodelRepository.SemanticId.Helpers.Interfaces; +using AAS.TwinEngine.DataEngine.DomainModel.SubmodelRepository; + +using AasCore.Aas3_0; + +using NSubstitute; + +using static Xunit.Assert; + +using Range = AasCore.Aas3_0.Range; + +namespace AAS.TwinEngine.DataEngine.UnitTests.ApplicationLogic.Services.SubmodelRepository.SemanticId.ElementHandlers; + +public class RangeHandlerTests +{ + private readonly RangeHandler _sut; + private readonly ISemanticIdResolver _resolver; + + public RangeHandlerTests() + { + _resolver = Substitute.For(); + _sut = new RangeHandler(_resolver); + } + + [Fact] + public void CanHandle_Range_ReturnsTrue() + { + var range = new Range(valueType: DataTypeDefXsd.Double, idShort: "Test"); + + True(_sut.CanHandle(range)); + } + + [Fact] + public void CanHandle_NonRange_ReturnsFalse() + { + var property = new Property(idShort: "Test", valueType: DataTypeDefXsd.String); + + False(_sut.CanHandle(property)); + } + + [Fact] + public void Extract_ReturnsBranchWithMinAndMaxLeaves() + { + var range = new Range(valueType: DataTypeDefXsd.Double, idShort: "TestRange"); + _resolver.ExtractSemanticId(range).Returns("http://test/range"); + _resolver.GetValueType(range).Returns(DataType.Number); + _resolver.GetCardinality(range).Returns(Cardinality.One); + + var result = _sut.Extract(range, _ => null); + + var branch = IsType(result); + Equal("http://test/range", branch.SemanticId); + Equal(2, branch.Children.Count); + var minLeaf = IsType(branch.Children[0]); + Equal("http://test/range" + SemanticIdResolver.RangeMinimumPostFixSeparator, minLeaf.SemanticId); + Equal(DataType.Number, minLeaf.DataType); + var maxLeaf = IsType(branch.Children[1]); + Equal("http://test/range" + SemanticIdResolver.RangeMaximumPostFixSeparator, maxLeaf.SemanticId); + Equal(DataType.Number, maxLeaf.DataType); + } + + [Fact] + public void FillOut_WithBranchNode_SetsMinAndMax() + { + var range = new Range(valueType: DataTypeDefXsd.Double, idShort: "TestRange"); + var branchNode = new SemanticBranchNode("http://test/range", Cardinality.One); + branchNode.AddChild(new SemanticLeafNode("http://test/range_min", "10.5", DataType.Number, Cardinality.One)); + branchNode.AddChild(new SemanticLeafNode("http://test/range_max", "99.9", DataType.Number, Cardinality.One)); + + _sut.FillOut(range, branchNode, (_, _, _) => { }); + + Equal("10.5", range.Min); + Equal("99.9", range.Max); + } + + [Fact] + public void FillOut_WithLeafNode_DoesNotSetMinMax() + { + var range = new Range(valueType: DataTypeDefXsd.Double, idShort: "TestRange", min: "0", max: "100"); + var leafNode = new SemanticLeafNode("http://test/range", "val", DataType.Number, Cardinality.One); + + _sut.FillOut(range, leafNode, (_, _, _) => { }); + + Equal("0", range.Min); + Equal("100", range.Max); + } + + [Fact] + public void FillOut_WithMissingMinLeaf_SetsMinToNull() + { + var range = new Range(valueType: DataTypeDefXsd.Double, idShort: "TestRange"); + var branchNode = new SemanticBranchNode("http://test/range", Cardinality.One); + branchNode.AddChild(new SemanticLeafNode("http://test/range_max", "99.9", DataType.Number, Cardinality.One)); + + _sut.FillOut(range, branchNode, (_, _, _) => { }); + + Null(range.Min); + Equal("99.9", range.Max); + } +} diff --git a/source/AAS.TwinEngine.DataEngine.UnitTests/ApplicationLogic/Services/SubmodelRepository/SemanticId/ElementHandlers/ReferenceElementHandlerTests.cs b/source/AAS.TwinEngine.DataEngine.UnitTests/ApplicationLogic/Services/SubmodelRepository/SemanticId/ElementHandlers/ReferenceElementHandlerTests.cs new file mode 100644 index 00000000..e56a58d0 --- /dev/null +++ b/source/AAS.TwinEngine.DataEngine.UnitTests/ApplicationLogic/Services/SubmodelRepository/SemanticId/ElementHandlers/ReferenceElementHandlerTests.cs @@ -0,0 +1,138 @@ +using AAS.TwinEngine.DataEngine.ApplicationLogic.Services.SubmodelRepository.SemanticId.ElementHandlers; +using AAS.TwinEngine.DataEngine.ApplicationLogic.Services.SubmodelRepository.SemanticId.Helpers.Interfaces; +using AAS.TwinEngine.DataEngine.DomainModel.SubmodelRepository; + +using AasCore.Aas3_0; + +using Microsoft.Extensions.Logging; + +using NSubstitute; + +using static Xunit.Assert; + +namespace AAS.TwinEngine.DataEngine.UnitTests.ApplicationLogic.Services.SubmodelRepository.SemanticId.ElementHandlers; + +public class ReferenceElementHandlerTests +{ + private readonly ReferenceElementHandler _sut; + private readonly ISemanticIdResolver _resolver; + private readonly IReferenceHelper _referenceHelper; + private readonly ILogger _logger; + + public ReferenceElementHandlerTests() + { + _resolver = Substitute.For(); + _referenceHelper = Substitute.For(); + _logger = Substitute.For>(); + _sut = new ReferenceElementHandler(_resolver, _referenceHelper, _logger); + } + + [Fact] + public void CanHandle_ReferenceElement_ReturnsTrue() + { + var refElement = new ReferenceElement(idShort: "Test"); + + True(_sut.CanHandle(refElement)); + } + + [Fact] + public void CanHandle_NonReferenceElement_ReturnsFalse() + { + var property = new Property(idShort: "Test", valueType: DataTypeDefXsd.String); + + False(_sut.CanHandle(property)); + } + + [Fact] + public void Extract_WithNullValue_ReturnsNull() + { + var refElement = new ReferenceElement(idShort: "Test", value: null); + + var result = _sut.Extract(refElement, _ => null); + + Null(result); + } + + [Fact] + public void Extract_WithExternalReference_ReturnsNull() + { + var refElement = new ReferenceElement( + idShort: "Test", + value: new Reference(ReferenceTypes.ExternalReference, + [new Key(KeyTypes.GlobalReference, "http://external")]) + ); + + var result = _sut.Extract(refElement, _ => null); + + Null(result); + } + + [Fact] + public void Extract_WithModelReference_DelegatesToReferenceHelper() + { + var modelRef = new Reference(ReferenceTypes.ModelReference, + [new Key(KeyTypes.Submodel, "http://submodel")]); + var refElement = new ReferenceElement(idShort: "Test", value: modelRef); + _resolver.ResolveElementSemanticId(refElement, "Test").Returns("http://test/ref"); + _resolver.GetCardinality(refElement).Returns(Cardinality.One); + + var expectedNode = new SemanticBranchNode("http://test/ref", Cardinality.One); + _referenceHelper.ExtractReferenceKeys(modelRef, "http://test/ref", Cardinality.One).Returns(expectedNode); + + var result = _sut.Extract(refElement, _ => null); + + Same(expectedNode, result); + } + + [Fact] + public void FillOut_WithModelReference_DelegatesToReferenceHelper() + { + var modelRef = new Reference(ReferenceTypes.ModelReference, + [new Key(KeyTypes.Submodel, "")]); + var refElement = new ReferenceElement(idShort: "Test", value: modelRef); + _resolver.GetSemanticId(refElement).Returns("http://test/ref"); + + var values = new SemanticBranchNode("http://test/ref", Cardinality.One); + + _sut.FillOut(refElement, values, (_, _, _) => { }); + + _referenceHelper.Received(1).PopulateReferenceKeys(modelRef, values, "http://test/ref"); + } + + [Fact] + public void FillOut_WithNullValue_LogsInfoAndSkips() + { + var refElement = new ReferenceElement(idShort: "Test", value: null); + _resolver.GetSemanticId(refElement).Returns("http://test/ref"); + + var values = new SemanticBranchNode("http://test/ref", Cardinality.One); + + _sut.FillOut(refElement, values, (_, _, _) => { }); + + _referenceHelper.DidNotReceive().PopulateReferenceKeys( + Arg.Any(), Arg.Any(), Arg.Any()); + } + + [Fact] + public void FillOut_WithExternalReference_LogsInfoAndSkips() + { + var externalRef = new Reference(ReferenceTypes.ExternalReference, + [new Key(KeyTypes.GlobalReference, "http://external")]); + var refElement = new ReferenceElement(idShort: "Test", value: externalRef); + _resolver.GetSemanticId(refElement).Returns("http://test/ref"); + + var values = new SemanticBranchNode("http://test/ref", Cardinality.One); + + _sut.FillOut(refElement, values, (_, _, _) => { }); + + _referenceHelper.DidNotReceive().PopulateReferenceKeys( + Arg.Any(), Arg.Any(), Arg.Any()); + _logger.Received(1).Log( + LogLevel.Information, + Arg.Any(), + Arg.Is(state => state.ToString()!.Contains("does not contain a ModelReference")), + null, + Arg.Any>()! + ); + } +} diff --git a/source/AAS.TwinEngine.DataEngine.UnitTests/ApplicationLogic/Services/SubmodelRepository/SemanticId/ElementHandlers/RelationshipElementHandlerTests.cs b/source/AAS.TwinEngine.DataEngine.UnitTests/ApplicationLogic/Services/SubmodelRepository/SemanticId/ElementHandlers/RelationshipElementHandlerTests.cs new file mode 100644 index 00000000..27f40c61 --- /dev/null +++ b/source/AAS.TwinEngine.DataEngine.UnitTests/ApplicationLogic/Services/SubmodelRepository/SemanticId/ElementHandlers/RelationshipElementHandlerTests.cs @@ -0,0 +1,131 @@ +using AAS.TwinEngine.DataEngine.ApplicationLogic.Services.SubmodelRepository.SemanticId.ElementHandlers; +using AAS.TwinEngine.DataEngine.ApplicationLogic.Services.SubmodelRepository.SemanticId.Helpers; +using AAS.TwinEngine.DataEngine.ApplicationLogic.Services.SubmodelRepository.SemanticId.Helpers.Interfaces; +using AAS.TwinEngine.DataEngine.DomainModel.SubmodelRepository; + +using AasCore.Aas3_0; + +using NSubstitute; + +using static Xunit.Assert; + +namespace AAS.TwinEngine.DataEngine.UnitTests.ApplicationLogic.Services.SubmodelRepository.SemanticId.ElementHandlers; + +public class RelationshipElementHandlerTests +{ + private readonly RelationshipElementHandler _sut; + private readonly ISemanticIdResolver _resolver; + private readonly IReferenceHelper _referenceHelper; + + public RelationshipElementHandlerTests() + { + _resolver = Substitute.For(); + _referenceHelper = Substitute.For(); + _sut = new RelationshipElementHandler(_resolver, _referenceHelper); + } + + [Fact] + public void CanHandle_RelationshipElement_ReturnsTrue() + { + var rel = new RelationshipElement( + first: new Reference(ReferenceTypes.ExternalReference, [new Key(KeyTypes.GlobalReference, "a")]), + second: new Reference(ReferenceTypes.ExternalReference, [new Key(KeyTypes.GlobalReference, "b")]), + idShort: "Test" + ); + + True(_sut.CanHandle(rel)); + } + + [Fact] + public void CanHandle_NonRelationshipElement_ReturnsFalse() + { + var property = new Property(idShort: "Test", valueType: DataTypeDefXsd.String); + + False(_sut.CanHandle(property)); + } + + [Fact] + public void Extract_BothExternalReferences_ReturnsNull() + { + var rel = new RelationshipElement( + first: new Reference(ReferenceTypes.ExternalReference, [new Key(KeyTypes.GlobalReference, "a")]), + second: new Reference(ReferenceTypes.ExternalReference, [new Key(KeyTypes.GlobalReference, "b")]), + idShort: "Test" + ); + + var result = _sut.Extract(rel, _ => null); + + Null(result); + } + + [Fact] + public void Extract_FirstModelReference_ExtractsFirstAndDelegatesToReferenceHelper() + { + var firstRef = new Reference(ReferenceTypes.ModelReference, [new Key(KeyTypes.Submodel, "sub")]); + var secondRef = new Reference(ReferenceTypes.ExternalReference, [new Key(KeyTypes.GlobalReference, "ext")]); + var rel = new RelationshipElement(first: firstRef, second: secondRef, idShort: "Test"); + _resolver.GetSemanticId(rel).Returns("http://test/rel"); + _resolver.GetCardinality(rel).Returns(Cardinality.One); + + var firstNode = new SemanticBranchNode("http://test/rel_first", Cardinality.One); + _referenceHelper.ExtractReferenceKeys( + firstRef, + "http://test/rel" + SemanticIdResolver.RelationshipElementFirstPostFixSeparator, + Cardinality.One + ).Returns(firstNode); + + var result = _sut.Extract(rel, _ => null); + + var branch = IsType(result); + Equal("http://test/rel", branch.SemanticId); + Single(branch.Children); + Same(firstNode, branch.Children[0]); + } + + [Fact] + public void Extract_BothModelReferences_ExtractsBoth() + { + var firstRef = new Reference(ReferenceTypes.ModelReference, [new Key(KeyTypes.Submodel, "sub1")]); + var secondRef = new Reference(ReferenceTypes.ModelReference, [new Key(KeyTypes.Submodel, "sub2")]); + var rel = new RelationshipElement(first: firstRef, second: secondRef, idShort: "Test"); + _resolver.GetSemanticId(rel).Returns("http://test/rel"); + _resolver.GetCardinality(rel).Returns(Cardinality.One); + + var firstNode = new SemanticBranchNode("http://test/rel_first", Cardinality.One); + var secondNode = new SemanticBranchNode("http://test/rel_second", Cardinality.One); + _referenceHelper.ExtractReferenceKeys( + firstRef, + "http://test/rel" + SemanticIdResolver.RelationshipElementFirstPostFixSeparator, + Cardinality.One + ).Returns(firstNode); + _referenceHelper.ExtractReferenceKeys( + secondRef, + "http://test/rel" + SemanticIdResolver.RelationshipElementSecondPostFixSeparator, + Cardinality.One + ).Returns(secondNode); + + var result = _sut.Extract(rel, _ => null); + + var branch = IsType(result); + Equal(2, branch.Children.Count); + } + + [Fact] + public void FillOut_DelegatesToReferenceHelperForBothReferences() + { + var firstRef = new Reference(ReferenceTypes.ModelReference, [new Key(KeyTypes.Submodel, "")]); + var secondRef = new Reference(ReferenceTypes.ModelReference, [new Key(KeyTypes.Submodel, "")]); + var rel = new RelationshipElement(first: firstRef, second: secondRef, idShort: "Test"); + + var values = new SemanticBranchNode("http://test/rel", Cardinality.One); + + _sut.FillOut(rel, values, (_, _, _) => { }); + + _referenceHelper.Received(1).PopulateRelationshipReference( + firstRef, values, "http://test/rel", + SemanticIdResolver.RelationshipElementFirstPostFixSeparator); + _referenceHelper.Received(1).PopulateRelationshipReference( + secondRef, values, "http://test/rel", + SemanticIdResolver.RelationshipElementSecondPostFixSeparator); + } +} diff --git a/source/AAS.TwinEngine.DataEngine.UnitTests/ApplicationLogic/Services/SubmodelRepository/SemanticId/Extraction/SemanticTreeExtractorTests.cs b/source/AAS.TwinEngine.DataEngine.UnitTests/ApplicationLogic/Services/SubmodelRepository/SemanticId/Extraction/SemanticTreeExtractorTests.cs new file mode 100644 index 00000000..b81680ef --- /dev/null +++ b/source/AAS.TwinEngine.DataEngine.UnitTests/ApplicationLogic/Services/SubmodelRepository/SemanticId/Extraction/SemanticTreeExtractorTests.cs @@ -0,0 +1,186 @@ +using AAS.TwinEngine.DataEngine.ApplicationLogic.Exceptions.Application; +using AAS.TwinEngine.DataEngine.ApplicationLogic.Services.SubmodelRepository.SemanticId.ElementHandlers; +using AAS.TwinEngine.DataEngine.ApplicationLogic.Services.SubmodelRepository.SemanticId.Extraction; +using AAS.TwinEngine.DataEngine.ApplicationLogic.Services.SubmodelRepository.SemanticId.Helpers.Interfaces; +using AAS.TwinEngine.DataEngine.DomainModel.SubmodelRepository; + +using AasCore.Aas3_0; + +using Microsoft.Extensions.Logging; + +using NSubstitute; + +using static Xunit.Assert; + +namespace AAS.TwinEngine.DataEngine.UnitTests.ApplicationLogic.Services.SubmodelRepository.SemanticId.Extraction; + +public class SemanticTreeExtractorTests +{ + private readonly SemanticTreeExtractor _sut; + private readonly ISemanticIdResolver _resolver; + private readonly ISubmodelElementHelper _elementHelper; + private readonly ILogger _logger; + private readonly List _handlers; + + public SemanticTreeExtractorTests() + { + _resolver = Substitute.For(); + _elementHelper = Substitute.For(); + _logger = Substitute.For>(); + _handlers = []; + _sut = new SemanticTreeExtractor(_resolver, _elementHelper, _handlers, _logger); + } + + [Fact] + public void Extract_NullSubmodel_ThrowsArgumentNullException() + { + Throws(() => _sut.Extract(null!)); + } + + [Fact] + public void Extract_SubmodelWithNoElements_ReturnsRootNodeWithNoChildren() + { + var submodel = Substitute.For(); + submodel.IdShort.Returns("TestSubmodel"); + submodel.SubmodelElements.Returns(new List()); + _resolver.ResolveSemanticId(submodel, "TestSubmodel").Returns("http://test/root"); + + var result = _sut.Extract(submodel) as SemanticBranchNode; + + NotNull(result); + Equal("http://test/root", result!.SemanticId); + Empty(result.Children); + } + + [Fact] + public void Extract_SubmodelWithElements_DelegatesToHandlers() + { + var property = new Property(idShort: "Prop", valueType: DataTypeDefXsd.String); + var submodel = Substitute.For(); + submodel.IdShort.Returns("Test"); + submodel.SubmodelElements.Returns(new List { property }); + _resolver.ResolveSemanticId(submodel, "Test").Returns("http://test/root"); + + var handler = Substitute.For(); + handler.CanHandle(property).Returns(true); + var expectedNode = new SemanticLeafNode("http://test/prop", "", DataType.String, Cardinality.One); + handler.Extract(property, Arg.Any>()).Returns(expectedNode); + _handlers.Add(handler); + + var result = _sut.Extract(submodel) as SemanticBranchNode; + + NotNull(result); + Single(result!.Children); + Same(expectedNode, result.Children[0]); + } + + [Fact] + public void Extract_ElementWithNoHandler_CreatesLeafNodeFallback() + { + var element = Substitute.For(); + element.IdShort.Returns("UnknownElement"); + var submodel = Substitute.For(); + submodel.IdShort.Returns("Test"); + submodel.SubmodelElements.Returns(new List { element }); + _resolver.ResolveSemanticId(submodel, "Test").Returns("http://test/root"); + _resolver.ResolveElementSemanticId(element, "UnknownElement").Returns("http://test/unknown"); + _resolver.GetValueType(element).Returns(DataType.Unknown); + _resolver.GetCardinality(element).Returns(Cardinality.Unknown); + + var result = _sut.Extract(submodel) as SemanticBranchNode; + + NotNull(result); + Single(result!.Children); + var leaf = IsType(result.Children[0]); + Equal("http://test/unknown", leaf.SemanticId); + Equal(DataType.Unknown, leaf.DataType); + } + + [Fact] + public void Extract_ByIdShortPath_NullSubmodel_ThrowsArgumentNullException() + { + Throws(() => _sut.Extract(null!, "path")); + } + + [Fact] + public void Extract_ByIdShortPath_NullPath_ThrowsArgumentNullException() + { + var submodel = Substitute.For(); + Throws(() => _sut.Extract(submodel, null!)); + } + + [Fact] + public void Extract_ByIdShortPath_SingleSegment_ReturnsMatchingElement() + { + var property = new Property(idShort: "MyProp", valueType: DataTypeDefXsd.String, value: "test"); + var submodel = Substitute.For(); + submodel.SubmodelElements.Returns(new List { property }); + _elementHelper.GetElementByIdShort(Arg.Any>(), "MyProp").Returns(property); + + var result = _sut.Extract(submodel, "MyProp"); + + Same(property, result); + } + + [Fact] + public void Extract_ByIdShortPath_NestedPath_ReturnsNestedElement() + { + var childProp = new Property(idShort: "ChildProp", valueType: DataTypeDefXsd.String); + var collection = new SubmodelElementCollection(idShort: "Parent", value: [childProp]); + var submodel = Substitute.For(); + submodel.SubmodelElements.Returns(new List { collection }); + _elementHelper.GetElementByIdShort(Arg.Any>(), "Parent").Returns(collection); + _elementHelper.GetChildElements(collection).Returns(collection.Value); + _elementHelper.GetElementByIdShort(collection.Value, "ChildProp").Returns(childProp); + + var result = _sut.Extract(submodel, "Parent.ChildProp"); + + Same(childProp, result); + } + + [Fact] + public void Extract_ByIdShortPath_ElementNotFound_ThrowsException() + { + var submodel = Substitute.For(); + submodel.SubmodelElements.Returns(new List()); + _elementHelper.GetElementByIdShort(Arg.Any>(), "NonExistent").Returns((ISubmodelElement?)null); + + Throws(() => _sut.Extract(submodel, "NonExistent")); + } + + [Fact] + public void Extract_ByIdShortPath_ChildElementsNull_ThrowsException() + { + var property = new Property(idShort: "Prop", valueType: DataTypeDefXsd.String); + var submodel = Substitute.For(); + submodel.SubmodelElements.Returns(new List { property }); + _elementHelper.GetElementByIdShort(Arg.Any>(), "Prop").Returns(property); + _elementHelper.GetChildElements(property).Returns((IList?)null); + + Throws(() => _sut.Extract(submodel, "Prop.Child")); + } + + [Fact] + public void ExtractElement_NullElement_ThrowsArgumentNullException() + { + Throws(() => _sut.ExtractElement(null!)); + } + + [Fact] + public void ExtractElement_HandlerReturnsNull_CreatesFallbackLeaf() + { + var element = Substitute.For(); + element.IdShort.Returns("Test"); + _resolver.ResolveElementSemanticId(element, "Test").Returns("http://test/element"); + _resolver.GetValueType(element).Returns(DataType.String); + _resolver.GetCardinality(element).Returns(Cardinality.ZeroToOne); + + var result = _sut.ExtractElement(element); + + NotNull(result); + var leaf = IsType(result); + Equal("http://test/element", leaf.SemanticId); + Equal(DataType.String, leaf.DataType); + Equal(Cardinality.ZeroToOne, leaf.Cardinality); + } +} diff --git a/source/AAS.TwinEngine.DataEngine.UnitTests/ApplicationLogic/Services/SubmodelRepository/SemanticId/FillOut/SubmodelFillerTests.cs b/source/AAS.TwinEngine.DataEngine.UnitTests/ApplicationLogic/Services/SubmodelRepository/SemanticId/FillOut/SubmodelFillerTests.cs new file mode 100644 index 00000000..b42dd023 --- /dev/null +++ b/source/AAS.TwinEngine.DataEngine.UnitTests/ApplicationLogic/Services/SubmodelRepository/SemanticId/FillOut/SubmodelFillerTests.cs @@ -0,0 +1,131 @@ +using AAS.TwinEngine.DataEngine.ApplicationLogic.Exceptions.Application; +using AAS.TwinEngine.DataEngine.ApplicationLogic.Services.SubmodelRepository.SemanticId.ElementHandlers; +using AAS.TwinEngine.DataEngine.ApplicationLogic.Services.SubmodelRepository.SemanticId.FillOut; +using AAS.TwinEngine.DataEngine.ApplicationLogic.Services.SubmodelRepository.SemanticId.Helpers.Interfaces; +using AAS.TwinEngine.DataEngine.DomainModel.SubmodelRepository; + +using AasCore.Aas3_0; + +using Microsoft.Extensions.Logging; + +using NSubstitute; + +using static Xunit.Assert; + +namespace AAS.TwinEngine.DataEngine.UnitTests.ApplicationLogic.Services.SubmodelRepository.SemanticId.FillOut; + +public class SubmodelFillerTests +{ + private readonly SubmodelFiller _sut; + private readonly ISemanticIdResolver _resolver; + private readonly ISubmodelElementHelper _elementHelper; + private readonly ILogger _logger; + private readonly List _handlers; + + public SubmodelFillerTests() + { + _resolver = Substitute.For(); + _elementHelper = Substitute.For(); + _logger = Substitute.For>(); + _handlers = []; + _sut = new SubmodelFiller(_resolver, _elementHelper, _handlers, _logger); + } + + [Fact] + public void FillOutTemplate_NullSubmodel_ThrowsArgumentNullException() + { + var values = new SemanticBranchNode("root", Cardinality.Unknown); + + Throws(() => _sut.FillOutTemplate(null!, values)); + } + + [Fact] + public void FillOutTemplate_NullValues_ThrowsArgumentNullException() + { + var submodel = Substitute.For(); + submodel.SubmodelElements.Returns(new List()); + + Throws(() => _sut.FillOutTemplate(submodel, null!)); + } + + [Fact] + public void FillOutTemplate_NullSubmodelElements_ThrowsArgumentNullException() + { + var submodel = Substitute.For(); + submodel.SubmodelElements.Returns((List?)null); + var values = new SemanticBranchNode("root", Cardinality.Unknown); + + Throws(() => _sut.FillOutTemplate(submodel, values)); + } + + [Fact] + public void FillOutTemplate_NoMatchingNodes_PreservesElements() + { + var property = new Property(idShort: "Prop", valueType: DataTypeDefXsd.String); + var submodel = Substitute.For(); + var elements = new List { property }; + submodel.SubmodelElements.Returns(elements); + _resolver.ExtractSemanticId(property).Returns("http://test/prop"); + + var values = new SemanticBranchNode("root", Cardinality.Unknown); + + _sut.FillOutTemplate(submodel, values); + + Single(elements); + } + + [Fact] + public void FillOutElement_NullElement_ThrowsArgumentNullException() + { + var values = new SemanticLeafNode("test", "val", DataType.String, Cardinality.One); + + Throws(() => _sut.FillOutElement(null!, values)); + } + + [Fact] + public void FillOutElement_NullValues_ThrowsArgumentNullException() + { + var element = new Property(idShort: "Prop", valueType: DataTypeDefXsd.String); + + Throws(() => _sut.FillOutElement(element, null!)); + } + + [Fact] + public void FillOutElement_NoMatchingHandler_ThrowsException() + { + var element = new Property(idShort: "Prop", valueType: DataTypeDefXsd.String); + var values = new SemanticLeafNode("test", "val", DataType.String, Cardinality.One); + + var ex = Throws(() => _sut.FillOutElement(element, values)); + Equal("Internal Server Error.", ex.Message); + } + + [Fact] + public void FillOutElement_WithMatchingHandler_DelegatesToHandler() + { + var element = new Property(idShort: "Prop", valueType: DataTypeDefXsd.String); + var values = new SemanticLeafNode("test", "val", DataType.String, Cardinality.One); + + var handler = Substitute.For(); + handler.CanHandle(element).Returns(true); + _handlers.Add(handler); + + _sut.FillOutElement(element, values); + + handler.Received(1).FillOut(element, values, Arg.Any, SemanticTreeNode, bool>>()); + } + + [Fact] + public void FillOutSubmodelElementValue_NoMatchingValueNode_PreservesElements() + { + var property = new Property(idShort: "Prop", valueType: DataTypeDefXsd.String, value: "original"); + var elements = new List { property }; + var values = new SemanticBranchNode("root", Cardinality.Unknown); + _resolver.ExtractSemanticId(property).Returns("http://test/prop"); + + _sut.FillOutSubmodelElementValue(elements, values, false); + + Single(elements); + Equal("original", ((Property)elements[0]).Value); + } +} diff --git a/source/AAS.TwinEngine.DataEngine.UnitTests/ApplicationLogic/Services/SubmodelRepository/SemanticId/Helpers/ReferenceHelperTests.cs b/source/AAS.TwinEngine.DataEngine.UnitTests/ApplicationLogic/Services/SubmodelRepository/SemanticId/Helpers/ReferenceHelperTests.cs new file mode 100644 index 00000000..b8d214ab --- /dev/null +++ b/source/AAS.TwinEngine.DataEngine.UnitTests/ApplicationLogic/Services/SubmodelRepository/SemanticId/Helpers/ReferenceHelperTests.cs @@ -0,0 +1,221 @@ +using AAS.TwinEngine.DataEngine.ApplicationLogic.Services.SubmodelRepository.Config; +using AAS.TwinEngine.DataEngine.ApplicationLogic.Services.SubmodelRepository.SemanticId.Helpers; +using AAS.TwinEngine.DataEngine.ApplicationLogic.Services.SubmodelRepository.SemanticId.Helpers.Interfaces; +using AAS.TwinEngine.DataEngine.DomainModel.SubmodelRepository; + +using AasCore.Aas3_0; + +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; + +using NSubstitute; + +using static Xunit.Assert; + +namespace AAS.TwinEngine.DataEngine.UnitTests.ApplicationLogic.Services.SubmodelRepository.SemanticId.Helpers; + +public class ReferenceHelperTests +{ + private readonly ReferenceHelper _sut; + private readonly ISemanticIdResolver _resolver; + private readonly ILogger _logger; + + public ReferenceHelperTests() + { + var semantics = Options.Create(new Semantics + { + MultiLanguageSemanticPostfixSeparator = "_", + SubmodelElementIndexContextPrefix = "_aastwinengineindex_" + }); + _resolver = new SemanticIdResolver(semantics); + _logger = Substitute.For>(); + _sut = new ReferenceHelper(_resolver, _logger); + } + + [Fact] + public void ExtractReferenceKeys_WithKeys_ReturnsBranchNode() + { + var reference = new Reference( + ReferenceTypes.ModelReference, + [ + new Key(KeyTypes.Submodel, "submodel-value"), + new Key(KeyTypes.Property, "prop-value"), + ] + ); + + var result = _sut.ExtractReferenceKeys(reference, "http://test/ref", Cardinality.One); + + NotNull(result); + Equal("http://test/ref", result!.SemanticId); + Equal(2, result.Children.Count); + var submodelLeaf = IsType(result.Children[0]); + Equal("http://test/ref_Submodel", submodelLeaf.SemanticId); + var propLeaf = IsType(result.Children[1]); + Equal("http://test/ref_Property", propLeaf.SemanticId); + } + + [Fact] + public void ExtractReferenceKeys_WithMultipleSameType_IncludesIndex() + { + var reference = new Reference( + ReferenceTypes.ModelReference, + [ + new Key(KeyTypes.SubmodelElementCollection, "col0"), + new Key(KeyTypes.SubmodelElementCollection, "col1"), + ] + ); + + var result = _sut.ExtractReferenceKeys(reference, "http://test/ref", Cardinality.One); + + NotNull(result); + Equal(2, result!.Children.Count); + var leaf0 = IsType(result.Children[0]); + Equal("http://test/ref_SubmodelElementCollection_0", leaf0.SemanticId); + var leaf1 = IsType(result.Children[1]); + Equal("http://test/ref_SubmodelElementCollection_1", leaf1.SemanticId); + } + + [Fact] + public void ExtractReferenceKeys_EmptyKeys_ReturnsNull() + { + var reference = new Reference(ReferenceTypes.ModelReference, []); + + var result = _sut.ExtractReferenceKeys(reference, "http://test/ref", Cardinality.One); + + Null(result); + } + + [Fact] + public void PopulateReferenceKeys_WithMatchingLeafNodes_UpdatesKeyValues() + { + var reference = new Reference( + ReferenceTypes.ModelReference, + [ + new Key(KeyTypes.Submodel, ""), + new Key(KeyTypes.Property, ""), + ] + ); + + var branchNode = new SemanticBranchNode("http://test/ref", Cardinality.One); + branchNode.AddChild(new SemanticLeafNode("http://test/ref_Submodel", "NewSubmodelValue", DataType.String, Cardinality.One)); + branchNode.AddChild(new SemanticLeafNode("http://test/ref_Property", "NewPropValue", DataType.String, Cardinality.One)); + + _sut.PopulateReferenceKeys(reference, branchNode, "http://test/ref"); + + Equal("NewSubmodelValue", reference.Keys[0].Value); + Equal("NewPropValue", reference.Keys[1].Value); + } + + [Fact] + public void PopulateReferenceKeys_WithNonBranchNode_LogsWarning() + { + var reference = new Reference( + ReferenceTypes.ModelReference, + [new Key(KeyTypes.Submodel, "original")] + ); + var leafNode = new SemanticLeafNode("http://test/ref", "val", DataType.String, Cardinality.One); + + _sut.PopulateReferenceKeys(reference, leafNode, "http://test/ref"); + + Equal("original", reference.Keys[0].Value); + _logger.Received(1).Log( + LogLevel.Warning, + Arg.Any(), + Arg.Is(state => state.ToString()!.Contains("Expected SemanticBranchNode")), + null, + Arg.Any>()! + ); + } + + [Fact] + public void PopulateReferenceKeys_EmptyKeys_LogsInfo() + { + var reference = new Reference(ReferenceTypes.ModelReference, []); + var branchNode = new SemanticBranchNode("http://test/ref", Cardinality.One); + + _sut.PopulateReferenceKeys(reference, branchNode, "http://test/ref"); + + _logger.Received(1).Log( + LogLevel.Information, + Arg.Any(), + Arg.Is(state => state.ToString()!.Contains("has no keys")), + null, + Arg.Any>()! + ); + } + + [Fact] + public void PopulateReferenceKeys_MissingLeafNode_LogsWarning() + { + var reference = new Reference( + ReferenceTypes.ModelReference, + [new Key(KeyTypes.Submodel, "original")] + ); + var branchNode = new SemanticBranchNode("http://test/ref", Cardinality.One); + + _sut.PopulateReferenceKeys(reference, branchNode, "http://test/ref"); + + Equal("original", reference.Keys[0].Value); + _logger.Received(1).Log( + LogLevel.Warning, + Arg.Any(), + Arg.Is(state => state.ToString()!.Contains("No matching leaf node")), + null, + Arg.Any>()! + ); + } + + [Fact] + public void PopulateRelationshipReference_ExternalReference_DoesNotModify() + { + var reference = new Reference( + ReferenceTypes.ExternalReference, + [new Key(KeyTypes.GlobalReference, "original")] + ); + var tree = new SemanticBranchNode("http://test", Cardinality.One); + + _sut.PopulateRelationshipReference(reference, tree, "http://test", "_first"); + + Equal("original", reference.Keys[0].Value); + } + + [Fact] + public void PopulateRelationshipReference_ModelReference_WithMatchingNode_PopulatesKeys() + { + var reference = new Reference( + ReferenceTypes.ModelReference, + [new Key(KeyTypes.Submodel, "")] + ); + + var firstBranch = new SemanticBranchNode("http://test_first", Cardinality.One); + firstBranch.AddChild(new SemanticLeafNode("http://test_first_Submodel", "NewValue", DataType.String, Cardinality.One)); + + var tree = new SemanticBranchNode("http://test", Cardinality.One); + tree.AddChild(firstBranch); + + _sut.PopulateRelationshipReference(reference, tree, "http://test", "_first"); + + Equal("NewValue", reference.Keys[0].Value); + } + + [Fact] + public void PopulateRelationshipReference_ModelReference_NoMatchingNode_LogsWarning() + { + var reference = new Reference( + ReferenceTypes.ModelReference, + [new Key(KeyTypes.Submodel, "original")] + ); + var tree = new SemanticBranchNode("http://test", Cardinality.One); + + _sut.PopulateRelationshipReference(reference, tree, "http://test", "_first"); + + Equal("original", reference.Keys[0].Value); + _logger.Received(1).Log( + LogLevel.Warning, + Arg.Any(), + Arg.Is(state => state.ToString()!.Contains("No matching node")), + null, + Arg.Any>()! + ); + } +} diff --git a/source/AAS.TwinEngine.DataEngine.UnitTests/ApplicationLogic/Services/SubmodelRepository/SemanticId/Helpers/SemanticIdResolverTests.cs b/source/AAS.TwinEngine.DataEngine.UnitTests/ApplicationLogic/Services/SubmodelRepository/SemanticId/Helpers/SemanticIdResolverTests.cs new file mode 100644 index 00000000..8b6acd7b --- /dev/null +++ b/source/AAS.TwinEngine.DataEngine.UnitTests/ApplicationLogic/Services/SubmodelRepository/SemanticId/Helpers/SemanticIdResolverTests.cs @@ -0,0 +1,303 @@ +using AAS.TwinEngine.DataEngine.ApplicationLogic.Services.SubmodelRepository.Config; +using AAS.TwinEngine.DataEngine.ApplicationLogic.Services.SubmodelRepository.SemanticId.Helpers; +using AAS.TwinEngine.DataEngine.DomainModel.SubmodelRepository; + +using AasCore.Aas3_0; + +using Microsoft.Extensions.Options; + +using NSubstitute; + +using static Xunit.Assert; + +using File = AasCore.Aas3_0.File; +using Range = AasCore.Aas3_0.Range; + +namespace AAS.TwinEngine.DataEngine.UnitTests.ApplicationLogic.Services.SubmodelRepository.SemanticId.Helpers; + +public class SemanticIdResolverTests +{ + private readonly SemanticIdResolver _sut; + private readonly IOptions _semantics; + + public SemanticIdResolverTests() + { + _semantics = Options.Create(new Semantics + { + MultiLanguageSemanticPostfixSeparator = "_", + SubmodelElementIndexContextPrefix = "_aastwinengineindex_", + InternalSemanticId = "InternalSemanticId" + }); + _sut = new SemanticIdResolver(_semantics); + } + + [Fact] + public void Constructor_NullOptions_ThrowsException() + { + var options = Options.Create(null!); + + _ = Throws(() => new SemanticIdResolver(options)); + } + + [Fact] + public void GetSemanticId_WithSemanticId_ReturnsValue() + { + var element = CreateElementWithSemanticId("http://example.com/semantic-id"); + + var result = _sut.GetSemanticId(element); + + Equal("http://example.com/semantic-id", result); + } + + [Fact] + public void GetSemanticId_WithNullSemanticId_ReturnsEmpty() + { + var element = Substitute.For(); + element.SemanticId.Returns((Reference)null!); + + var result = _sut.GetSemanticId(element); + + Equal(string.Empty, result); + } + + [Fact] + public void GetSemanticId_WithEmptyKeys_ReturnsEmpty() + { + var reference = Substitute.For(); + reference.Keys.Returns(new List()); + var element = Substitute.For(); + element.SemanticId.Returns(reference); + + var result = _sut.GetSemanticId(element); + + Equal(string.Empty, result); + } + + [Fact] + public void ExtractSemanticId_WithInternalSemanticIdQualifier_ReturnsQualifierValue() + { + var element = Substitute.For(); + element.SemanticId.Returns(CreateReference("http://original-semantic-id")); + var qualifier = Substitute.For(); + qualifier.Type.Returns("InternalSemanticId"); + qualifier.Value.Returns("http://internal-semantic-id"); + element.Qualifiers.Returns(new List { qualifier }); + + var result = _sut.ExtractSemanticId(element); + + Equal("http://internal-semantic-id", result); + } + + [Fact] + public void ExtractSemanticId_WithoutInternalSemanticIdQualifier_ReturnsSemantId() + { + var element = Substitute.For(); + element.SemanticId.Returns(CreateReference("http://original-semantic-id")); + var qualifier = Substitute.For(); + qualifier.Type.Returns("SomeOtherQualifier"); + qualifier.Value.Returns("http://other-value"); + element.Qualifiers.Returns(new List { qualifier }); + + var result = _sut.ExtractSemanticId(element); + + Equal("http://original-semantic-id", result); + } + + [Fact] + public void ExtractSemanticId_WithNullQualifiers_ReturnsSemanticId() + { + var element = Substitute.For(); + element.SemanticId.Returns(CreateReference("http://original-semantic-id")); + element.Qualifiers.Returns((List?)null); + + var result = _sut.ExtractSemanticId(element); + + Equal("http://original-semantic-id", result); + } + + [Fact] + public void ResolveSemanticId_WithoutIndex_ReturnsBaseSemanticId() + { + var element = CreateElementWithSemanticId("http://example.com/semantic-id"); + + var result = _sut.ResolveSemanticId(element, "MyElement"); + + Equal("http://example.com/semantic-id", result); + } + + [Fact] + public void ResolveSemanticId_WithTrailingIndex_AppendsIndex() + { + var element = CreateElementWithSemanticId("http://example.com/semantic-id"); + + var result = _sut.ResolveSemanticId(element, "MyElement42"); + + Equal("http://example.com/semantic-id_aastwinengineindex_42", result); + } + + [Fact] + public void ResolveElementSemanticId_WithoutIndex_ReturnsBaseSemanticId() + { + var element = Substitute.For(); + element.SemanticId.Returns(CreateReference("http://example.com/semantic-id")); + element.Qualifiers.Returns(new List()); + + var result = _sut.ResolveElementSemanticId(element, "ContactList"); + + Equal("http://example.com/semantic-id", result); + } + + [Fact] + public void ResolveElementSemanticId_WithTrailingDigits_AppendsIndex() + { + var element = Substitute.For(); + element.SemanticId.Returns(CreateReference("http://example.com/semantic-id")); + element.Qualifiers.Returns(new List()); + + var result = _sut.ResolveElementSemanticId(element, "ContactList01"); + + Equal("http://example.com/semantic-id_aastwinengineindex_01", result); + } + + [Theory] + [InlineData("One", Cardinality.One)] + [InlineData("ZeroToOne", Cardinality.ZeroToOne)] + [InlineData("ZeroToMany", Cardinality.ZeroToMany)] + [InlineData("OneToMany", Cardinality.OneToMany)] + [InlineData("", Cardinality.Unknown)] + public void GetCardinality_VariousQualifierValues_ReturnsExpected(string? qualifierValue, Cardinality expected) + { + var qualifier = Substitute.For(); + qualifier.Value.Returns(qualifierValue); + var element = Substitute.For(); + element.Qualifiers.Returns(new List { qualifier }); + + var actual = _sut.GetCardinality(element); + + Equal(expected, actual); + } + + [Fact] + public void GetCardinality_QualifiersNull_ReturnsUnknown() + { + var element = Substitute.For(); + element.Qualifiers.Returns((List?)null); + + var actual = _sut.GetCardinality(element); + + Equal(Cardinality.Unknown, actual); + } + + [Fact] + public void GetCardinality_EmptyQualifiers_ReturnsUnknown() + { + var element = Substitute.For(); + element.Qualifiers.Returns(new List()); + + var actual = _sut.GetCardinality(element); + + Equal(Cardinality.Unknown, actual); + } + + [Theory] + [InlineData(DataTypeDefXsd.DateTime, DataType.String)] + [InlineData(DataTypeDefXsd.UnsignedShort, DataType.Integer)] + [InlineData(DataTypeDefXsd.Double, DataType.Number)] + [InlineData(DataTypeDefXsd.Boolean, DataType.Boolean)] + [InlineData((DataTypeDefXsd)999, DataType.Unknown)] + [InlineData(DataTypeDefXsd.AnyUri, DataType.String)] + [InlineData(DataTypeDefXsd.Duration, DataType.String)] + [InlineData(DataTypeDefXsd.NonNegativeInteger, DataType.Integer)] + [InlineData(DataTypeDefXsd.GYearMonth, DataType.String)] + [InlineData(DataTypeDefXsd.Float, DataType.Number)] + [InlineData(DataTypeDefXsd.HexBinary, DataType.String)] + [InlineData(DataTypeDefXsd.PositiveInteger, DataType.Integer)] + [InlineData(DataTypeDefXsd.Decimal, DataType.Number)] + public void GetValueType_PropertyValueType_ReturnsExpected(DataTypeDefXsd valueType, DataType expected) + { + var prop = new Property( + idShort: "MyProp", + valueType: valueType, + value: "", + semanticId: new Reference(ReferenceTypes.ExternalReference, + [new Key(KeyTypes.Property, "http://example.com/test")]), + qualifiers: [] + ); + + var actual = _sut.GetValueType(prop); + + Equal(expected, actual); + } + + [Fact] + public void GetValueType_RangeElement_ReturnsExpectedType() + { + var range = new Range(valueType: DataTypeDefXsd.Double, idShort: "TestRange"); + + var actual = _sut.GetValueType(range); + + Equal(DataType.Number, actual); + } + + [Fact] + public void GetValueType_FileElement_ReturnsString() + { + var file = new File(contentType: "image/png", idShort: "TestFile"); + + var actual = _sut.GetValueType(file); + + Equal(DataType.String, actual); + } + + [Fact] + public void GetValueType_BlobElement_ReturnsString() + { + var blob = new Blob(contentType: "application/octet-stream", idShort: "TestBlob"); + + var actual = _sut.GetValueType(blob); + + Equal(DataType.String, actual); + } + + [Fact] + public void GetValueType_UnsupportedElement_ReturnsUnknown() + { + var element = Substitute.For(); + + var actual = _sut.GetValueType(element); + + Equal(DataType.Unknown, actual); + } + + [Fact] + public void BuildReferenceKeySemanticId_SingleKey_OmitsIndex() + { + var result = _sut.BuildReferenceKeySemanticId("http://base", KeyTypes.Submodel, 0, 1); + + Equal("http://base_Submodel", result); + } + + [Fact] + public void BuildReferenceKeySemanticId_MultipleKeys_IncludesIndex() + { + var result = _sut.BuildReferenceKeySemanticId("http://base", KeyTypes.SubmodelElementCollection, 1, 3); + + Equal("http://base_SubmodelElementCollection_1", result); + } + + [Fact] + public void MlpPostFixSeparator_ReturnsConfiguredValue() + { + Equal("_", _sut.MlpPostFixSeparator); + } + + private static IHasSemantics CreateElementWithSemanticId(string semanticId) + { + var element = Substitute.For(); + element.SemanticId.Returns(CreateReference(semanticId)); + return element; + } + + private static Reference CreateReference(string value) => + new(ReferenceTypes.ExternalReference, [new Key(KeyTypes.GlobalReference, value)]); +} diff --git a/source/AAS.TwinEngine.DataEngine.UnitTests/ApplicationLogic/Services/SubmodelRepository/SemanticId/Helpers/SemanticTreeNavigatorTests.cs b/source/AAS.TwinEngine.DataEngine.UnitTests/ApplicationLogic/Services/SubmodelRepository/SemanticId/Helpers/SemanticTreeNavigatorTests.cs new file mode 100644 index 00000000..d6f07f71 --- /dev/null +++ b/source/AAS.TwinEngine.DataEngine.UnitTests/ApplicationLogic/Services/SubmodelRepository/SemanticId/Helpers/SemanticTreeNavigatorTests.cs @@ -0,0 +1,154 @@ +using AAS.TwinEngine.DataEngine.ApplicationLogic.Services.SubmodelRepository.SemanticId.Helpers; +using AAS.TwinEngine.DataEngine.DomainModel.SubmodelRepository; + +using static Xunit.Assert; + +namespace AAS.TwinEngine.DataEngine.UnitTests.ApplicationLogic.Services.SubmodelRepository.SemanticId.Helpers; + +public class SemanticTreeNavigatorTests +{ + [Fact] + public void FindBranchNodesBySemanticId_ReturnsMatchingChildren() + { + var root = new SemanticBranchNode("root", Cardinality.Unknown); + var child1 = new SemanticBranchNode("target", Cardinality.One); + var child2 = new SemanticBranchNode("target", Cardinality.ZeroToOne); + var child3 = new SemanticBranchNode("other", Cardinality.One); + root.AddChild(child1); + root.AddChild(child2); + root.AddChild(child3); + + var result = SemanticTreeNavigator.FindBranchNodesBySemanticId(root, "target").ToList(); + + Equal(2, result.Count); + Contains(child1, result); + Contains(child2, result); + } + + [Fact] + public void FindBranchNodesBySemanticId_NoMatch_ReturnsEmpty() + { + var root = new SemanticBranchNode("root", Cardinality.Unknown); + root.AddChild(new SemanticBranchNode("other", Cardinality.One)); + + var result = SemanticTreeNavigator.FindBranchNodesBySemanticId(root, "nonexistent").ToList(); + + Empty(result); + } + + [Fact] + public void FindBranchNodesBySemanticId_LeafNode_ReturnsEmpty() + { + var leaf = new SemanticLeafNode("leaf", "value", DataType.String, Cardinality.One); + + var result = SemanticTreeNavigator.FindBranchNodesBySemanticId(leaf, "leaf").ToList(); + + Empty(result); + } + + [Fact] + public void FindNodeBySemanticId_ReturnsMatchingNode_AtRoot() + { + var root = new SemanticBranchNode("target", Cardinality.Unknown); + + var result = SemanticTreeNavigator.FindNodeBySemanticId(root, "target").ToList(); + + Single(result); + Same(root, result[0]); + } + + [Fact] + public void FindNodeBySemanticId_ReturnsMatchingNodes_InNestedTree() + { + var root = new SemanticBranchNode("root", Cardinality.Unknown); + var child = new SemanticBranchNode("branch", Cardinality.One); + var grandchild = new SemanticLeafNode("target", "val", DataType.String, Cardinality.One); + child.AddChild(grandchild); + root.AddChild(child); + + var result = SemanticTreeNavigator.FindNodeBySemanticId(root, "target").ToList(); + + Single(result); + Same(grandchild, result[0]); + } + + [Fact] + public void FindNodeBySemanticId_ReturnsMultipleMatches_AcrossTree() + { + var root = new SemanticBranchNode("root", Cardinality.Unknown); + var leaf1 = new SemanticLeafNode("target", "v1", DataType.String, Cardinality.One); + var branch = new SemanticBranchNode("branch", Cardinality.One); + var leaf2 = new SemanticLeafNode("target", "v2", DataType.String, Cardinality.One); + branch.AddChild(leaf2); + root.AddChild(leaf1); + root.AddChild(branch); + + var result = SemanticTreeNavigator.FindNodeBySemanticId(root, "target").ToList(); + + Equal(2, result.Count); + } + + [Fact] + public void FindNodeBySemanticId_NoMatch_ReturnsEmpty() + { + var root = new SemanticBranchNode("root", Cardinality.Unknown); + root.AddChild(new SemanticLeafNode("other", "val", DataType.String, Cardinality.One)); + + var result = SemanticTreeNavigator.FindNodeBySemanticId(root, "nonexistent").ToList(); + + Empty(result); + } + + [Fact] + public void AreAllNodesOfSameType_EmptyList_ReturnsTrueWithNullType() + { + var result = SemanticTreeNavigator.AreAllNodesOfSameType([], out var nodeType); + + True(result); + Null(nodeType); + } + + [Fact] + public void AreAllNodesOfSameType_AllBranchNodes_ReturnsTrue() + { + var nodes = new List + { + new SemanticBranchNode("a", Cardinality.One), + new SemanticBranchNode("b", Cardinality.ZeroToOne), + }; + + var result = SemanticTreeNavigator.AreAllNodesOfSameType(nodes, out var nodeType); + + True(result); + Equal(typeof(SemanticBranchNode), nodeType); + } + + [Fact] + public void AreAllNodesOfSameType_AllLeafNodes_ReturnsTrue() + { + var nodes = new List + { + new SemanticLeafNode("a", "v1", DataType.String, Cardinality.One), + new SemanticLeafNode("b", "v2", DataType.Integer, Cardinality.ZeroToOne), + }; + + var result = SemanticTreeNavigator.AreAllNodesOfSameType(nodes, out var nodeType); + + True(result); + Equal(typeof(SemanticLeafNode), nodeType); + } + + [Fact] + public void AreAllNodesOfSameType_MixedNodes_ReturnsFalse() + { + var nodes = new List + { + new SemanticBranchNode("a", Cardinality.One), + new SemanticLeafNode("b", "v2", DataType.String, Cardinality.One), + }; + + var result = SemanticTreeNavigator.AreAllNodesOfSameType(nodes, out _); + + False(result); + } +} diff --git a/source/AAS.TwinEngine.DataEngine.UnitTests/ApplicationLogic/Services/SubmodelRepository/SemanticId/Helpers/SubmodelElementHelperTests.cs b/source/AAS.TwinEngine.DataEngine.UnitTests/ApplicationLogic/Services/SubmodelRepository/SemanticId/Helpers/SubmodelElementHelperTests.cs new file mode 100644 index 00000000..652da14d --- /dev/null +++ b/source/AAS.TwinEngine.DataEngine.UnitTests/ApplicationLogic/Services/SubmodelRepository/SemanticId/Helpers/SubmodelElementHelperTests.cs @@ -0,0 +1,290 @@ +using AAS.TwinEngine.DataEngine.ApplicationLogic.Exceptions.Application; +using AAS.TwinEngine.DataEngine.ApplicationLogic.Services.SubmodelRepository.Config; +using AAS.TwinEngine.DataEngine.ApplicationLogic.Services.SubmodelRepository.SemanticId.Helpers; + +using AasCore.Aas3_0; + +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; + +using NSubstitute; + +using static Xunit.Assert; + +using File = AasCore.Aas3_0.File; + +namespace AAS.TwinEngine.DataEngine.UnitTests.ApplicationLogic.Services.SubmodelRepository.SemanticId.Helpers; + +public class SubmodelElementHelperTests +{ + private readonly SubmodelElementHelper _sut; + private readonly ILogger _logger; + + public SubmodelElementHelperTests() + { + _logger = Substitute.For>(); + var mlpSettings = Options.Create(new MultiLanguagePropertySettings { DefaultLanguages = null }); + _sut = new SubmodelElementHelper(_logger, mlpSettings); + } + + [Fact] + public void CloneElement_Property_ReturnsDeepCopy() + { + var original = new Property( + idShort: "TestProp", + valueType: DataTypeDefXsd.String, + value: "original" + ); + + var cloned = _sut.CloneElement(original); + + NotSame(original, cloned); + Equal("TestProp", cloned.IdShort); + var clonedProp = IsType(cloned); + Equal("original", clonedProp.Value); + } + + [Fact] + public void CloneElement_Collection_ReturnsDeepCopyWithChildren() + { + var original = new SubmodelElementCollection( + idShort: "TestCollection", + value: [new Property(idShort: "Child", valueType: DataTypeDefXsd.String, value: "childVal")] + ); + + var cloned = _sut.CloneElement(original); + + NotSame(original, cloned); + var clonedCollection = IsType(cloned); + Single(clonedCollection.Value!); + Equal("Child", clonedCollection.Value![0].IdShort); + } + + [Fact] + public void GetElementByIdShort_MatchingElement_ReturnsElement() + { + var elements = new List + { + new Property(idShort: "First", valueType: DataTypeDefXsd.String), + new Property(idShort: "Second", valueType: DataTypeDefXsd.String), + }; + + var result = _sut.GetElementByIdShort(elements, "Second"); + + NotNull(result); + Equal("Second", result!.IdShort); + } + + [Fact] + public void GetElementByIdShort_NoMatch_ReturnsNull() + { + var elements = new List + { + new Property(idShort: "First", valueType: DataTypeDefXsd.String), + }; + + var result = _sut.GetElementByIdShort(elements, "NonExistent"); + + Null(result); + } + + [Fact] + public void GetElementByIdShort_NullCollection_ReturnsNull() + { + var result = _sut.GetElementByIdShort(null, "Any"); + + Null(result); + } + + [Fact] + public void GetElementByIdShort_WithBracketIndex_ReturnsListElement() + { + var listElement = new SubmodelElementList( + idShort: "MyList", + typeValueListElement: AasSubmodelElements.Property, + value: [ + new Property(idShort: "Item0", valueType: DataTypeDefXsd.String, value: "zero"), + new Property(idShort: "Item1", valueType: DataTypeDefXsd.String, value: "one"), + ] + ); + var elements = new List { listElement }; + + var result = _sut.GetElementByIdShort(elements, "MyList[1]"); + + NotNull(result); + Equal("Item1", result!.IdShort); + } + + [Fact] + public void GetElementByIdShort_WithEncodedBracketIndex_ReturnsListElement() + { + var listElement = new SubmodelElementList( + idShort: "MyList", + typeValueListElement: AasSubmodelElements.Property, + value: [ + new Property(idShort: "Item0", valueType: DataTypeDefXsd.String, value: "zero"), + ] + ); + var elements = new List { listElement }; + + var result = _sut.GetElementByIdShort(elements, "MyList%5B0%5D"); + + NotNull(result); + Equal("Item0", result!.IdShort); + } + + [Fact] + public void GetElementFromListByIndex_ValidIndex_ReturnsElement() + { + var list = new SubmodelElementList( + idShort: "TestList", + typeValueListElement: AasSubmodelElements.Property, + value: [ + new Property(idShort: "Item0", valueType: DataTypeDefXsd.String), + new Property(idShort: "Item1", valueType: DataTypeDefXsd.String), + ] + ); + var elements = new List { list }; + + var result = _sut.GetElementFromListByIndex(elements, "TestList", 1); + + Equal("Item1", result.IdShort); + } + + [Fact] + public void GetElementFromListByIndex_OutOfBounds_ThrowsException() + { + var list = new SubmodelElementList( + idShort: "TestList", + typeValueListElement: AasSubmodelElements.Property, + value: [new Property(idShort: "Item0", valueType: DataTypeDefXsd.String)] + ); + var elements = new List { list }; + + Throws(() => _sut.GetElementFromListByIndex(elements, "TestList", 5)); + } + + [Fact] + public void GetElementFromListByIndex_NotAList_ThrowsException() + { + var elements = new List + { + new Property(idShort: "NotAList", valueType: DataTypeDefXsd.String), + }; + + Throws(() => _sut.GetElementFromListByIndex(elements, "NotAList", 0)); + } + + [Fact] + public void GetChildElements_Collection_ReturnsValue() + { + var child = new Property(idShort: "Child", valueType: DataTypeDefXsd.String); + var collection = new SubmodelElementCollection(idShort: "Col", value: [child]); + + var result = _sut.GetChildElements(collection); + + NotNull(result); + Single(result!); + Same(child, result[0]); + } + + [Fact] + public void GetChildElements_List_ReturnsValue() + { + var child = new Property(idShort: "Child", valueType: DataTypeDefXsd.String); + var list = new SubmodelElementList( + idShort: "List", + typeValueListElement: AasSubmodelElements.Property, + value: [child] + ); + + var result = _sut.GetChildElements(list); + + NotNull(result); + Single(result!); + } + + [Fact] + public void GetChildElements_Entity_ReturnsStatements() + { + var statement = new Property(idShort: "Statement", valueType: DataTypeDefXsd.String); + var entity = new Entity(idShort: "Ent", entityType: EntityType.SelfManagedEntity, statements: [statement]); + + var result = _sut.GetChildElements(entity); + + NotNull(result); + Single(result!); + } + + [Fact] + public void GetChildElements_Property_ReturnsNull() + { + var property = new Property(idShort: "Prop", valueType: DataTypeDefXsd.String); + + var result = _sut.GetChildElements(property); + + Null(result); + } + + [Fact] + public void ResolveLanguages_WithValues_ReturnsLanguagesFromValues() + { + var mlp = new MultiLanguageProperty( + idShort: "TestMlp", + value: [ + new LangStringTextType("en", "English"), + new LangStringTextType("de", "German"), + ] + ); + + var result = _sut.ResolveLanguages(mlp); + + Equal(2, result.Count); + Contains("en", result); + Contains("de", result); + } + + [Fact] + public void ResolveLanguages_WithNullValue_ReturnsEmpty() + { + var mlp = new MultiLanguageProperty(idShort: "TestMlp", value: null); + + var result = _sut.ResolveLanguages(mlp); + + Empty(result); + } + + [Fact] + public void ResolveLanguages_WithDefaultLanguages_MergesWithDefaults() + { + var mlpSettings = Options.Create(new MultiLanguagePropertySettings { DefaultLanguages = ["en", "fr"] }); + var sut = new SubmodelElementHelper(Substitute.For>(), mlpSettings); + + var mlp = new MultiLanguageProperty( + idShort: "TestMlp", + value: [new LangStringTextType("en", "English"), new LangStringTextType("de", "German")] + ); + + var result = sut.ResolveLanguages(mlp); + + Equal(3, result.Count); + Contains("en", result); + Contains("de", result); + Contains("fr", result); + } + + [Fact] + public void ResolveLanguages_WithOnlyDefaultLanguages_ReturnsDefaults() + { + var mlpSettings = Options.Create(new MultiLanguagePropertySettings { DefaultLanguages = ["en", "fr"] }); + var sut = new SubmodelElementHelper(Substitute.For>(), mlpSettings); + + var mlp = new MultiLanguageProperty(idShort: "TestMlp", value: null); + + var result = sut.ResolveLanguages(mlp); + + Equal(2, result.Count); + Contains("en", result); + Contains("fr", result); + } +} diff --git a/source/AAS.TwinEngine.DataEngine.UnitTests/ApplicationLogic/Services/SubmodelRepository/SemanticIdHandlerTests.cs b/source/AAS.TwinEngine.DataEngine.UnitTests/ApplicationLogic/Services/SubmodelRepository/SemanticIdHandlerTests.cs index 0beea9f6..e032483b 100644 --- a/source/AAS.TwinEngine.DataEngine.UnitTests/ApplicationLogic/Services/SubmodelRepository/SemanticIdHandlerTests.cs +++ b/source/AAS.TwinEngine.DataEngine.UnitTests/ApplicationLogic/Services/SubmodelRepository/SemanticIdHandlerTests.cs @@ -1,21 +1,19 @@ -/*using System.Reflection; - -using AAS.TwinEngine.DataEngine.ApplicationLogic.Exceptions.Application; +using AAS.TwinEngine.DataEngine.ApplicationLogic.Exceptions.Application; using AAS.TwinEngine.DataEngine.ApplicationLogic.Services.SubmodelRepository; using AAS.TwinEngine.DataEngine.ApplicationLogic.Services.SubmodelRepository.Config; +using AAS.TwinEngine.DataEngine.ApplicationLogic.Services.SubmodelRepository.SemanticId.ElementHandlers; using AAS.TwinEngine.DataEngine.ApplicationLogic.Services.SubmodelRepository.SemanticId.Extraction; using AAS.TwinEngine.DataEngine.ApplicationLogic.Services.SubmodelRepository.SemanticId.FillOut; using AAS.TwinEngine.DataEngine.ApplicationLogic.Services.SubmodelRepository.SemanticId.Helpers; -using AAS.TwinEngine.DataEngine.ApplicationLogic.Services.SubmodelRepository.SemanticId.Helpers.Interfaces; using AAS.TwinEngine.DataEngine.DomainModel.SubmodelRepository; +using MongoDB.Bson; + using AasCore.Aas3_0; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; -using MongoDB.Bson; - using NSubstitute; using static Xunit.Assert; @@ -31,7 +29,6 @@ public class SemanticIdHandlerTests private readonly ILogger _fillerLogger; private readonly IOptions _mlpSettings; private readonly IOptions _semantics; - private readonly ISemanticIdResolver _resolver; public SemanticIdHandlerTests() { @@ -42,7 +39,6 @@ public SemanticIdHandlerTests() _semantics = Substitute.For>(); _ = _semantics.Value.Returns(new Semantics { MultiLanguageSemanticPostfixSeparator = "_", SubmodelElementIndexContextPrefix = "_aastwinengineindex_" }); - _resolver = new SemanticIdResolver(_semantics); _sut = CreateSut(_semantics, _mlpSettings); } @@ -62,14 +58,6 @@ public void Extract_TemplateNull_ThrowsException() [Fact] public void FillOutTemplate_TemplateNull_ThrowsException() => _ = Throws(() => _sut.FillOutTemplate(submodelTemplate: null!, TestData.SubmodelTreeNode)); - [Fact] - public void SemanticIdHandler_NullSemantics_ThrowsException() - { - var options = Options.Create(options: null!); - - _ = Throws(() => new SemanticIdResolver(options)); - } - [Fact] public void Extract_Submodel_ReturnsSemanticTreeNode() { @@ -276,7 +264,7 @@ public void Extract_MultiLanguageProperty_WithDefaultLanguagesAs_En_De_Fr() } [Fact] - public void Extract_EmptySubmodelElementCollection_LogsWarningAndReturnsNode() + public void Extract_EmptySubmodelElementCollection_ReturnsNodeWithEmptyChildren() { var mlp = TestData.CreateSubmodelWithContactInformationWithOutElements(); @@ -287,15 +275,10 @@ public void Extract_EmptySubmodelElementCollection_LogsWarningAndReturnsNode() var contactInformationNode = node.Children[0] as SemanticBranchNode; Equal("http://example.com/idta/digital-nameplate/contact-information", contactInformationNode?.SemanticId); Empty(contactInformationNode!.Children); - _extractorLogger.Received(1).Log(LogLevel.Warning, Arg.Any(), - Arg.Is(state => state.ToString()! - .Contains("No elements defined in SubmodelElementCollection ContactInformation")), - null, - Arg.Any>()!); } [Fact] - public void Extract_EmptySubmodelElementList_LogsWarningAndReturnsNode() + public void Extract_EmptySubmodelElementList_ReturnsNodeWithEmptyChildren() { var mlp = TestData.CreateSubmodelWithContactListWithOutElements(); @@ -306,11 +289,6 @@ public void Extract_EmptySubmodelElementList_LogsWarningAndReturnsNode() var contactInformationNode = node.Children[0] as SemanticBranchNode; Equal("http://example.com/idta/digital-nameplate/contact-list", contactInformationNode?.SemanticId); Empty(contactInformationNode!.Children); - _extractorLogger.Received(1).Log(LogLevel.Warning, Arg.Any(), - Arg.Is(state => state.ToString()! - .Contains("No elements defined in SubmodelElementList ContactList")), - null, - Arg.Any>()!); } [Fact] @@ -401,85 +379,6 @@ public void Extract_ThrowsNotFoundException_WhenElementNotFound() Throws(() => _sut.Extract(submodel, Path)); } - [Theory] - [InlineData("One", Cardinality.One)] - [InlineData("ZeroToOne", Cardinality.ZeroToOne)] - [InlineData("ZeroToMany", Cardinality.ZeroToMany)] - [InlineData("OneToMany", Cardinality.OneToMany)] - [InlineData("", Cardinality.Unknown)] - public void GetCardinality_VariousQualifierValues_ReturnsExpected(string? qualifierValue, Cardinality expected) - { - var qualifier = Substitute.For(); - qualifier.Value.Returns(qualifierValue); - var element = Substitute.For(); - element.Qualifiers.Returns([qualifier]); - - var actual = _resolver.GetCardinality(element); - - Equal(expected, actual); - } - - [Fact] - public void GetCardinality_QualifiersNull_ReturnsUnknown() - { - var element = Substitute.For(); - element.Qualifiers.Returns((List?)null); - - var actual = _resolver.GetCardinality(element); - - Equal(Cardinality.Unknown, actual); - } - - [Fact] - public void GetCardinality_EmptyQualifiers_ReturnsUnknown() - { - var element = Substitute.For(); - element.Qualifiers.Returns([]); - - var actual = _resolver.GetCardinality(element); - - Equal(Cardinality.Unknown, actual); - } - - [Theory] - [InlineData(DataTypeDefXsd.DateTime, DataType.String)] - [InlineData(DataTypeDefXsd.UnsignedShort, DataType.Integer)] - [InlineData(DataTypeDefXsd.Double, DataType.Number)] - [InlineData(DataTypeDefXsd.Boolean, DataType.Boolean)] - [InlineData((DataTypeDefXsd)999, DataType.Unknown)] - [InlineData(DataTypeDefXsd.AnyUri, DataType.String)] - [InlineData(DataTypeDefXsd.Duration, DataType.String)] - [InlineData(DataTypeDefXsd.NonNegativeInteger, DataType.Integer)] - [InlineData(DataTypeDefXsd.GYearMonth, DataType.String)] - [InlineData(DataTypeDefXsd.Float, DataType.Number)] - [InlineData(DataTypeDefXsd.HexBinary, DataType.String)] - [InlineData(DataTypeDefXsd.PositiveInteger, DataType.Integer)] - [InlineData(DataTypeDefXsd.Decimal, DataType.Number)] - public void GetValueType_PropertyValueType_ReturnsExpected(DataTypeDefXsd valueType, DataType expected) - { - var prop = new Property( - idShort: "MyProp", - valueType: valueType, - value: "", - semanticId: TestData.CreateContactName().SemanticId, - qualifiers: [] - ); - - var actual = _resolver.GetValueType(prop); - - Equal(expected, actual); - } - - [Fact] - public void GetValueType_ElementWithoutValueProperty_ReturnsUnknown() - { - var element = Substitute.For(); - - var actual = _resolver.GetValueType(element); - - Equal(DataType.Unknown, actual); - } - [Theory] [InlineData("ContactList01", "http://example.com/idta/digital-nameplate/contact-list_aastwinengineindex_01")] [InlineData("ContactList42", "http://example.com/idta/digital-nameplate/contact-list_aastwinengineindex_42")] @@ -844,13 +743,6 @@ public void FillOutTemplate_EmptyMultiLanguageProperty_WithDefaultLanguagesAs_En Equal(3, mlp.Value!.Count); var languages = mlp.Value.Select(v => v.Language).OrderBy(l => l).ToList(); Equal(["de", "en", "fr"], languages); - _fillerLogger.Received(3).Log( - LogLevel.Information, - Arg.Any(), - Arg.Is(state => state.ToString()!.Contains("Added language")), - null, - Arg.Any>()! - ); } [Fact] @@ -877,13 +769,6 @@ public void FillOutTemplate_MultiLanguageProperty_WithDefaultLanguagesAs_En_De_F var frValue = mlp.Value.FirstOrDefault(v => v.Language == "fr"); NotNull(frValue); Equal("Exemple de test Fabricant", frValue.Text); - _fillerLogger.Received(1).Log( - LogLevel.Information, - Arg.Any(), - Arg.Is(state => state.ToString()!.Contains("Added language 'fr'")), - null, - Arg.Any>()! - ); } [Fact] @@ -970,12 +855,25 @@ private static Submodel CreateSubmodelWithSubmodelElement(ISubmodelElement submo private SemanticIdHandler CreateSut(IOptions semantics, IOptions mlpSettings) { var resolver = new SemanticIdResolver(semantics); - var navigator = new SemanticTreeNavigator(); - var helper = new SubmodelElementHelper(Substitute.For>()); - var multiLanguageHelper = new MultiLanguageHelper(mlpSettings); - var referenceHelper = new ReferenceHelper(resolver, navigator, Substitute.For>()); - var extractor = new SemanticTreeExtractor(resolver, helper, multiLanguageHelper, referenceHelper, _extractorLogger); - var filler = new SubmodelFiller(resolver, navigator, helper, multiLanguageHelper, referenceHelper, _fillerLogger); + var helper = new SubmodelElementHelper(Substitute.For>(), mlpSettings); + var referenceHelper = new ReferenceHelper(resolver, Substitute.For>()); + + var handlers = new List + { + new PropertyHandler(resolver), + new CollectionHandler(resolver, Substitute.For>()), + new ListHandler(resolver, Substitute.For>()), + new MultiLanguagePropertyHandler(resolver, helper, Substitute.For>()), + new RangeHandler(resolver), + new FileHandler(resolver), + new BlobHandler(resolver), + new EntityHandler(resolver, Substitute.For>()), + new ReferenceElementHandler(resolver, referenceHelper, Substitute.For>()), + new RelationshipElementHandler(resolver, referenceHelper), + }; + + var extractor = new SemanticTreeExtractor(resolver, helper, handlers, _extractorLogger); + var filler = new SubmodelFiller(resolver, helper, handlers, _fillerLogger); return new SemanticIdHandler(extractor, filler); } @@ -990,4 +888,4 @@ private static IOptions CreateMlpSettings(List specificAssetIds = new List -{ + public static List _specificAssetIds = +[ new SpecificAssetId( name: "Manufacturer", value: "ExampleCorp", @@ -1339,7 +1339,7 @@ public static RelationshipElement CreateFilledRelationshipElementWithBothModelRe ] ) } -}; +]; public static Submodel CreateFilledSubmodel() => new( id: "http://example.com/idta/digital-nameplate", diff --git a/source/AAS.TwinEngine.DataEngine/ApplicationLogic/Services/SubmodelRepository/SemanticId/ElementHandlers/BlobHandler.cs b/source/AAS.TwinEngine.DataEngine/ApplicationLogic/Services/SubmodelRepository/SemanticId/ElementHandlers/BlobHandler.cs new file mode 100644 index 00000000..e4d4a1cb --- /dev/null +++ b/source/AAS.TwinEngine.DataEngine/ApplicationLogic/Services/SubmodelRepository/SemanticId/ElementHandlers/BlobHandler.cs @@ -0,0 +1,25 @@ +using AAS.TwinEngine.DataEngine.ApplicationLogic.Services.SubmodelRepository.SemanticId.Helpers.Interfaces; +using AAS.TwinEngine.DataEngine.DomainModel.SubmodelRepository; + +using AasCore.Aas3_0; + +namespace AAS.TwinEngine.DataEngine.ApplicationLogic.Services.SubmodelRepository.SemanticId.ElementHandlers; + +public class BlobHandler(ISemanticIdResolver semanticIdResolver) : ISubmodelElementTypeHandler +{ + public bool CanHandle(ISubmodelElement element) => element is Blob; + + public SemanticTreeNode? Extract(ISubmodelElement element, Func extractChild) + { + var semanticId = semanticIdResolver.ResolveElementSemanticId(element, element.IdShort!); + return new SemanticLeafNode(semanticId, string.Empty, semanticIdResolver.GetValueType(element), semanticIdResolver.GetCardinality(element)); + } + + public void FillOut(ISubmodelElement element, SemanticTreeNode values, Action, SemanticTreeNode, bool> fillOutChildren) + { + if (values is SemanticLeafNode leafValueNode) + { + ((Blob)element).Value = Convert.FromBase64String(leafValueNode.Value); + } + } +} diff --git a/source/AAS.TwinEngine.DataEngine/ApplicationLogic/Services/SubmodelRepository/SemanticId/ElementHandlers/CollectionHandler.cs b/source/AAS.TwinEngine.DataEngine/ApplicationLogic/Services/SubmodelRepository/SemanticId/ElementHandlers/CollectionHandler.cs new file mode 100644 index 00000000..7d36bcbb --- /dev/null +++ b/source/AAS.TwinEngine.DataEngine/ApplicationLogic/Services/SubmodelRepository/SemanticId/ElementHandlers/CollectionHandler.cs @@ -0,0 +1,49 @@ +using AAS.TwinEngine.DataEngine.ApplicationLogic.Services.SubmodelRepository.SemanticId.Helpers.Interfaces; +using AAS.TwinEngine.DataEngine.DomainModel.SubmodelRepository; + +using AasCore.Aas3_0; + +namespace AAS.TwinEngine.DataEngine.ApplicationLogic.Services.SubmodelRepository.SemanticId.ElementHandlers; + +public class CollectionHandler( + ISemanticIdResolver semanticIdResolver, + ILogger logger) : ISubmodelElementTypeHandler +{ + public bool CanHandle(ISubmodelElement element) => element is SubmodelElementCollection; + + public SemanticTreeNode? Extract(ISubmodelElement element, Func extractChild) + { + var collection = (SubmodelElementCollection)element; + var node = new SemanticBranchNode(semanticIdResolver.ResolveElementSemanticId(collection, collection.IdShort!), semanticIdResolver.GetCardinality(collection)); + + if (collection.Value?.Count > 0) + { + foreach (var child in collection.Value.Where(_ => true)) + { + var childNode = extractChild(child); + if (childNode != null) + { + node.AddChild(childNode); + } + } + } + else + { + logger.LogWarning("No elements defined in SubmodelElementCollection {CollectionIdShort}", collection.IdShort); + } + + return node; + } + + public void FillOut(ISubmodelElement element, SemanticTreeNode values, Action, SemanticTreeNode, bool> fillOutChildren) + { + var collection = (SubmodelElementCollection)element; + + if (collection?.Value == null || collection.Value.Count == 0) + { + return; + } + + fillOutChildren(collection.Value, values, true); + } +} diff --git a/source/AAS.TwinEngine.DataEngine/ApplicationLogic/Services/SubmodelRepository/SemanticId/ElementHandlers/EntityHandler.cs b/source/AAS.TwinEngine.DataEngine/ApplicationLogic/Services/SubmodelRepository/SemanticId/ElementHandlers/EntityHandler.cs new file mode 100644 index 00000000..2e2ed78f --- /dev/null +++ b/source/AAS.TwinEngine.DataEngine/ApplicationLogic/Services/SubmodelRepository/SemanticId/ElementHandlers/EntityHandler.cs @@ -0,0 +1,111 @@ +using AAS.TwinEngine.DataEngine.ApplicationLogic.Services.SubmodelRepository.SemanticId.Helpers; +using AAS.TwinEngine.DataEngine.ApplicationLogic.Services.SubmodelRepository.SemanticId.Helpers.Interfaces; +using AAS.TwinEngine.DataEngine.DomainModel.SubmodelRepository; + +using AasCore.Aas3_0; + +namespace AAS.TwinEngine.DataEngine.ApplicationLogic.Services.SubmodelRepository.SemanticId.ElementHandlers; + +public class EntityHandler( + ISemanticIdResolver semanticIdResolver, + ILogger logger) : ISubmodelElementTypeHandler +{ + public bool CanHandle(ISubmodelElement element) => element is Entity; + + public SemanticTreeNode? Extract(ISubmodelElement element, Func extractChild) + { + var entity = (Entity)element; + var semanticId = semanticIdResolver.ResolveElementSemanticId(entity, entity.IdShort!); + var node = new SemanticBranchNode(semanticId, semanticIdResolver.GetCardinality(entity)); + + if (entity.EntityType == EntityType.SelfManagedEntity) + { + var globalAssetIdNode = new SemanticLeafNode(semanticId + SemanticIdResolver.EntityGlobalAssetIdPostFix, string.Empty, DataType.String, Cardinality.One); + node.AddChild(globalAssetIdNode); + + if (entity.SpecificAssetIds != null) + { + foreach (var specificAssetId in entity.SpecificAssetIds) + { + IHasSemantics specificAsset = specificAssetId; + if (specificAsset.SemanticId == null) + { + continue; + } + + var specificAssetIdNode = new SemanticLeafNode(semanticIdResolver.GetSemanticId(specificAssetId), string.Empty, DataType.String, Cardinality.One); + node.AddChild(specificAssetIdNode); + } + } + } + + if (entity.Statements?.Count > 0) + { + foreach (var child in entity.Statements.Select(extractChild).OfType()) + { + node.AddChild(child); + } + } + else + { + logger.LogWarning("No elements defined in Entity {EntityIdShort}", entity.IdShort); + } + + return node; + } + + public void FillOut(ISubmodelElement element, SemanticTreeNode values, Action, SemanticTreeNode, bool> fillOutChildren) + { + var entity = (Entity)element; + + if (entity.EntityType == EntityType.SelfManagedEntity) + { + FillOutSelfManagedEntity(entity, values); + } + + if (entity?.Statements == null || entity.Statements.Count == 0) + { + return; + } + + fillOutChildren(entity.Statements, values, true); + } + + private void FillOutSelfManagedEntity(Entity entity, SemanticTreeNode values) + { + var semanticId = semanticIdResolver.ResolveElementSemanticId(entity, entity.IdShort!); + + if (SemanticTreeNavigator.FindNodeBySemanticId(values, semanticId).FirstOrDefault() is not SemanticBranchNode valueNode) + { + return; + } + + var globalAssetSemanticId = semanticId + SemanticIdResolver.EntityGlobalAssetIdPostFix; + + var globalAssetNode = valueNode.Children + .OfType() + .FirstOrDefault(c => c.SemanticId == globalAssetSemanticId); + + if (globalAssetNode != null) + { + entity.GlobalAssetId = globalAssetNode.Value; + } + + if (entity.SpecificAssetIds != null) + { + foreach (var specificAssetId in entity.SpecificAssetIds) + { + var specSemanticId = semanticIdResolver.GetSemanticId(specificAssetId); + + var specNode = valueNode.Children + .OfType() + .FirstOrDefault(c => c.SemanticId == specSemanticId); + + if (specNode != null) + { + specificAssetId.Value = specNode.Value; + } + } + } + } +} diff --git a/source/AAS.TwinEngine.DataEngine/ApplicationLogic/Services/SubmodelRepository/SemanticId/ElementHandlers/FileHandler.cs b/source/AAS.TwinEngine.DataEngine/ApplicationLogic/Services/SubmodelRepository/SemanticId/ElementHandlers/FileHandler.cs new file mode 100644 index 00000000..8dab50d1 --- /dev/null +++ b/source/AAS.TwinEngine.DataEngine/ApplicationLogic/Services/SubmodelRepository/SemanticId/ElementHandlers/FileHandler.cs @@ -0,0 +1,27 @@ +using AAS.TwinEngine.DataEngine.ApplicationLogic.Services.SubmodelRepository.SemanticId.Helpers.Interfaces; +using AAS.TwinEngine.DataEngine.DomainModel.SubmodelRepository; + +using AasCore.Aas3_0; + +using File = AasCore.Aas3_0.File; + +namespace AAS.TwinEngine.DataEngine.ApplicationLogic.Services.SubmodelRepository.SemanticId.ElementHandlers; + +public class FileHandler(ISemanticIdResolver semanticIdResolver) : ISubmodelElementTypeHandler +{ + public bool CanHandle(ISubmodelElement element) => element is File; + + public SemanticTreeNode? Extract(ISubmodelElement element, Func extractChild) + { + var semanticId = semanticIdResolver.ResolveElementSemanticId(element, element.IdShort!); + return new SemanticLeafNode(semanticId, string.Empty, semanticIdResolver.GetValueType(element), semanticIdResolver.GetCardinality(element)); + } + + public void FillOut(ISubmodelElement element, SemanticTreeNode values, Action, SemanticTreeNode, bool> fillOutChildren) + { + if (values is SemanticLeafNode leafValueNode) + { + ((File)element).Value = leafValueNode.Value; + } + } +} diff --git a/source/AAS.TwinEngine.DataEngine/ApplicationLogic/Services/SubmodelRepository/SemanticId/ElementHandlers/ISubmodelElementTypeHandler.cs b/source/AAS.TwinEngine.DataEngine/ApplicationLogic/Services/SubmodelRepository/SemanticId/ElementHandlers/ISubmodelElementTypeHandler.cs new file mode 100644 index 00000000..c47663ae --- /dev/null +++ b/source/AAS.TwinEngine.DataEngine/ApplicationLogic/Services/SubmodelRepository/SemanticId/ElementHandlers/ISubmodelElementTypeHandler.cs @@ -0,0 +1,14 @@ +using AAS.TwinEngine.DataEngine.DomainModel.SubmodelRepository; + +using AasCore.Aas3_0; + +namespace AAS.TwinEngine.DataEngine.ApplicationLogic.Services.SubmodelRepository.SemanticId.ElementHandlers; + +public interface ISubmodelElementTypeHandler +{ + bool CanHandle(ISubmodelElement element); + + SemanticTreeNode? Extract(ISubmodelElement element, Func extractChild); + + void FillOut(ISubmodelElement element, SemanticTreeNode values, Action, SemanticTreeNode, bool> fillOutChildren); +} diff --git a/source/AAS.TwinEngine.DataEngine/ApplicationLogic/Services/SubmodelRepository/SemanticId/ElementHandlers/ListHandler.cs b/source/AAS.TwinEngine.DataEngine/ApplicationLogic/Services/SubmodelRepository/SemanticId/ElementHandlers/ListHandler.cs new file mode 100644 index 00000000..9141b916 --- /dev/null +++ b/source/AAS.TwinEngine.DataEngine/ApplicationLogic/Services/SubmodelRepository/SemanticId/ElementHandlers/ListHandler.cs @@ -0,0 +1,45 @@ +using AAS.TwinEngine.DataEngine.ApplicationLogic.Services.SubmodelRepository.SemanticId.Helpers.Interfaces; +using AAS.TwinEngine.DataEngine.DomainModel.SubmodelRepository; + +using AasCore.Aas3_0; + +namespace AAS.TwinEngine.DataEngine.ApplicationLogic.Services.SubmodelRepository.SemanticId.ElementHandlers; + +public class ListHandler( + ISemanticIdResolver semanticIdResolver, + ILogger logger) : ISubmodelElementTypeHandler +{ + public bool CanHandle(ISubmodelElement element) => element is SubmodelElementList; + + public SemanticTreeNode? Extract(ISubmodelElement element, Func extractChild) + { + var list = (SubmodelElementList)element; + var node = new SemanticBranchNode(semanticIdResolver.ResolveElementSemanticId(list, list.IdShort!), semanticIdResolver.GetCardinality(list)); + + if (list.Value?.Count > 0) + { + foreach (var childNode in list.Value.Select(extractChild).OfType()) + { + node.AddChild(childNode); + } + } + else + { + logger.LogWarning("No elements defined in SubmodelElementList {ListIdShort}", list.IdShort); + } + + return node; + } + + public void FillOut(ISubmodelElement element, SemanticTreeNode values, Action, SemanticTreeNode, bool> fillOutChildren) + { + var list = (SubmodelElementList)element; + + if (list?.Value == null || list.Value.Count == 0) + { + return; + } + + fillOutChildren(list.Value, values, false); + } +} diff --git a/source/AAS.TwinEngine.DataEngine/ApplicationLogic/Services/SubmodelRepository/SemanticId/ElementHandlers/MultiLanguagePropertyHandler.cs b/source/AAS.TwinEngine.DataEngine/ApplicationLogic/Services/SubmodelRepository/SemanticId/ElementHandlers/MultiLanguagePropertyHandler.cs new file mode 100644 index 00000000..c628c5d8 --- /dev/null +++ b/source/AAS.TwinEngine.DataEngine/ApplicationLogic/Services/SubmodelRepository/SemanticId/ElementHandlers/MultiLanguagePropertyHandler.cs @@ -0,0 +1,83 @@ +using AAS.TwinEngine.DataEngine.ApplicationLogic.Services.SubmodelRepository.SemanticId.Helpers; +using AAS.TwinEngine.DataEngine.ApplicationLogic.Services.SubmodelRepository.SemanticId.Helpers.Interfaces; +using AAS.TwinEngine.DataEngine.DomainModel.SubmodelRepository; + +using AasCore.Aas3_0; + +namespace AAS.TwinEngine.DataEngine.ApplicationLogic.Services.SubmodelRepository.SemanticId.ElementHandlers; + +public class MultiLanguagePropertyHandler( + ISemanticIdResolver semanticIdResolver, + ISubmodelElementHelper elementHelper, + ILogger logger) : ISubmodelElementTypeHandler +{ + public bool CanHandle(ISubmodelElement element) => element is MultiLanguageProperty; + + public SemanticTreeNode? Extract(ISubmodelElement element, Func extractChild) + { + var mlp = (MultiLanguageProperty)element; + var semanticId = semanticIdResolver.ExtractSemanticId(mlp); + var node = new SemanticBranchNode(semanticId, semanticIdResolver.GetCardinality(mlp)); + + var languages = elementHelper.ResolveLanguages(mlp); + + if (mlp.Value is not { Count: > 0 }) + { + logger.LogInformation("No languages defined in template for MultiLanguageProperty {MlpIdShort}", mlp.IdShort); + } + + var mlpSeparator = semanticIdResolver.MlpPostFixSeparator; + foreach (var langSemanticId in languages.Select(language => string.Concat(semanticId, mlpSeparator, language))) + { + node.AddChild(new SemanticLeafNode(langSemanticId, string.Empty, DataType.String, Cardinality.ZeroToOne)); + } + + return node; + } + + public void FillOut(ISubmodelElement element, SemanticTreeNode values, Action, SemanticTreeNode, bool> fillOutChildren) + { + var mlp = (MultiLanguageProperty)element; + var semanticId = semanticIdResolver.ExtractSemanticId(mlp); + + if (SemanticTreeNavigator.FindNodeBySemanticId(values, semanticId).FirstOrDefault() is not SemanticBranchNode valueNode) + { + logger.LogInformation("No value node found for MultiLanguageProperty {MlpIdShort}", mlp.IdShort); + return; + } + + mlp.Value ??= []; + + var languageValueMap = new Dictionary(StringComparer.OrdinalIgnoreCase); + foreach (var langValue in mlp.Value) + { + languageValueMap[langValue.Language] = (LangStringTextType)langValue; + } + + var languages = elementHelper.ResolveLanguages(mlp); + + var mlpSeparator = semanticIdResolver.MlpPostFixSeparator; + foreach (var language in languages) + { + if (!languageValueMap.TryGetValue(language, out var languageValue)) + { + languageValue = new LangStringTextType(language, string.Empty); + mlp.Value.Add(languageValue); + languageValueMap[language] = languageValue; + + logger.LogInformation("Added language '{Language}' to MultiLanguageProperty {MlpIdShort}", language, mlp.IdShort); + } + + var languageSemanticId = semanticId + mlpSeparator + language; + + var leafNode = valueNode.Children + .OfType() + .FirstOrDefault(child => child.SemanticId.Equals(languageSemanticId, StringComparison.Ordinal)); + + if (leafNode != null) + { + languageValue.Text = leafNode.Value; + } + } + } +} diff --git a/source/AAS.TwinEngine.DataEngine/ApplicationLogic/Services/SubmodelRepository/SemanticId/ElementHandlers/PropertyHandler.cs b/source/AAS.TwinEngine.DataEngine/ApplicationLogic/Services/SubmodelRepository/SemanticId/ElementHandlers/PropertyHandler.cs new file mode 100644 index 00000000..cfe3b423 --- /dev/null +++ b/source/AAS.TwinEngine.DataEngine/ApplicationLogic/Services/SubmodelRepository/SemanticId/ElementHandlers/PropertyHandler.cs @@ -0,0 +1,25 @@ +using AAS.TwinEngine.DataEngine.ApplicationLogic.Services.SubmodelRepository.SemanticId.Helpers.Interfaces; +using AAS.TwinEngine.DataEngine.DomainModel.SubmodelRepository; + +using AasCore.Aas3_0; + +namespace AAS.TwinEngine.DataEngine.ApplicationLogic.Services.SubmodelRepository.SemanticId.ElementHandlers; + +public class PropertyHandler(ISemanticIdResolver semanticIdResolver) : ISubmodelElementTypeHandler +{ + public bool CanHandle(ISubmodelElement element) => element is Property; + + public SemanticTreeNode? Extract(ISubmodelElement element, Func extractChild) + { + var semanticId = semanticIdResolver.ResolveElementSemanticId(element, element.IdShort!); + return new SemanticLeafNode(semanticId, string.Empty, semanticIdResolver.GetValueType(element), semanticIdResolver.GetCardinality(element)); + } + + public void FillOut(ISubmodelElement element, SemanticTreeNode values, Action, SemanticTreeNode, bool> fillOutChildren) + { + if (values is SemanticLeafNode leafValueNode) + { + ((Property)element).Value = leafValueNode.Value; + } + } +} diff --git a/source/AAS.TwinEngine.DataEngine/ApplicationLogic/Services/SubmodelRepository/SemanticId/ElementHandlers/RangeHandler.cs b/source/AAS.TwinEngine.DataEngine/ApplicationLogic/Services/SubmodelRepository/SemanticId/ElementHandlers/RangeHandler.cs new file mode 100644 index 00000000..1ee208f0 --- /dev/null +++ b/source/AAS.TwinEngine.DataEngine/ApplicationLogic/Services/SubmodelRepository/SemanticId/ElementHandlers/RangeHandler.cs @@ -0,0 +1,47 @@ +using AAS.TwinEngine.DataEngine.ApplicationLogic.Services.SubmodelRepository.SemanticId.Helpers; +using AAS.TwinEngine.DataEngine.ApplicationLogic.Services.SubmodelRepository.SemanticId.Helpers.Interfaces; +using AAS.TwinEngine.DataEngine.DomainModel.SubmodelRepository; + +using AasCore.Aas3_0; + +using Range = AasCore.Aas3_0.Range; + +namespace AAS.TwinEngine.DataEngine.ApplicationLogic.Services.SubmodelRepository.SemanticId.ElementHandlers; + +public class RangeHandler(ISemanticIdResolver semanticIdResolver) : ISubmodelElementTypeHandler +{ + public bool CanHandle(ISubmodelElement element) => element is Range; + + public SemanticTreeNode? Extract(ISubmodelElement element, Func extractChild) + { + var range = (Range)element; + var semanticId = semanticIdResolver.ExtractSemanticId(range); + var valueType = semanticIdResolver.GetValueType(range); + var node = new SemanticBranchNode(semanticId, semanticIdResolver.GetCardinality(range)); + + node.AddChild(new SemanticLeafNode(semanticId + SemanticIdResolver.RangeMinimumPostFixSeparator, string.Empty, valueType, Cardinality.ZeroToOne)); + node.AddChild(new SemanticLeafNode(semanticId + SemanticIdResolver.RangeMaximumPostFixSeparator, string.Empty, valueType, Cardinality.ZeroToOne)); + + return node; + } + + public void FillOut(ISubmodelElement element, SemanticTreeNode values, Action, SemanticTreeNode, bool> fillOutChildren) + { + var range = (Range)element; + + if (values is not SemanticBranchNode branchNode) + { + return; + } + + var leafNodes = branchNode.Children.OfType().ToList(); + + range.Min = leafNodes.FirstOrDefault(n => n.SemanticId + .EndsWith(SemanticIdResolver.RangeMinimumPostFixSeparator, StringComparison.Ordinal))? + .Value; + + range.Max = leafNodes.FirstOrDefault(n => n.SemanticId + .EndsWith(SemanticIdResolver.RangeMaximumPostFixSeparator, StringComparison.Ordinal))? + .Value; + } +} diff --git a/source/AAS.TwinEngine.DataEngine/ApplicationLogic/Services/SubmodelRepository/SemanticId/ElementHandlers/ReferenceElementHandler.cs b/source/AAS.TwinEngine.DataEngine/ApplicationLogic/Services/SubmodelRepository/SemanticId/ElementHandlers/ReferenceElementHandler.cs new file mode 100644 index 00000000..cd195553 --- /dev/null +++ b/source/AAS.TwinEngine.DataEngine/ApplicationLogic/Services/SubmodelRepository/SemanticId/ElementHandlers/ReferenceElementHandler.cs @@ -0,0 +1,42 @@ +using AAS.TwinEngine.DataEngine.ApplicationLogic.Services.SubmodelRepository.SemanticId.Helpers.Interfaces; +using AAS.TwinEngine.DataEngine.DomainModel.SubmodelRepository; + +using AasCore.Aas3_0; + +namespace AAS.TwinEngine.DataEngine.ApplicationLogic.Services.SubmodelRepository.SemanticId.ElementHandlers; + +public class ReferenceElementHandler( + ISemanticIdResolver semanticIdResolver, + IReferenceHelper referenceHelper, + ILogger logger) : ISubmodelElementTypeHandler +{ + public bool CanHandle(ISubmodelElement element) => element is ReferenceElement; + + public SemanticTreeNode? Extract(ISubmodelElement element, Func extractChild) + { + var referenceElement = (ReferenceElement)element; + + if (referenceElement.Value == null || referenceElement.Value.Type == ReferenceTypes.ExternalReference) + { + return null; + } + + return referenceHelper.ExtractReferenceKeys( + referenceElement.Value, + semanticIdResolver.ResolveElementSemanticId(referenceElement, referenceElement.IdShort!), + semanticIdResolver.GetCardinality(referenceElement)); + } + + public void FillOut(ISubmodelElement element, SemanticTreeNode values, Action, SemanticTreeNode, bool> fillOutChildren) + { + var referenceElement = (ReferenceElement)element; + + if (referenceElement?.Value?.Type != ReferenceTypes.ModelReference) + { + logger.LogInformation("ReferenceElement does not contain a ModelReference for SemanticId '{SemanticId}'. Skipping population.", semanticIdResolver.GetSemanticId(referenceElement!)); + return; + } + + referenceHelper.PopulateReferenceKeys(referenceElement.Value, values, semanticIdResolver.GetSemanticId(referenceElement)); + } +} diff --git a/source/AAS.TwinEngine.DataEngine/ApplicationLogic/Services/SubmodelRepository/SemanticId/ElementHandlers/RelationshipElementHandler.cs b/source/AAS.TwinEngine.DataEngine/ApplicationLogic/Services/SubmodelRepository/SemanticId/ElementHandlers/RelationshipElementHandler.cs new file mode 100644 index 00000000..71c7b295 --- /dev/null +++ b/source/AAS.TwinEngine.DataEngine/ApplicationLogic/Services/SubmodelRepository/SemanticId/ElementHandlers/RelationshipElementHandler.cs @@ -0,0 +1,58 @@ +using AAS.TwinEngine.DataEngine.ApplicationLogic.Services.SubmodelRepository.SemanticId.Helpers; +using AAS.TwinEngine.DataEngine.ApplicationLogic.Services.SubmodelRepository.SemanticId.Helpers.Interfaces; +using AAS.TwinEngine.DataEngine.DomainModel.SubmodelRepository; + +using AasCore.Aas3_0; + +namespace AAS.TwinEngine.DataEngine.ApplicationLogic.Services.SubmodelRepository.SemanticId.ElementHandlers; + +public class RelationshipElementHandler( + ISemanticIdResolver semanticIdResolver, + IReferenceHelper referenceHelper) : ISubmodelElementTypeHandler +{ + public bool CanHandle(ISubmodelElement element) => element is RelationshipElement; + + public SemanticTreeNode? Extract(ISubmodelElement element, Func extractChild) + { + var relationshipElement = (RelationshipElement)element; + + if (relationshipElement.First.Type == ReferenceTypes.ExternalReference && relationshipElement.Second.Type == ReferenceTypes.ExternalReference) + { + return null; + } + + var semanticId = semanticIdResolver.GetSemanticId(relationshipElement); + var cardinality = semanticIdResolver.GetCardinality(relationshipElement); + var relationshipElementNode = new SemanticBranchNode(semanticId, cardinality); + + if (relationshipElement.First.Type == ReferenceTypes.ModelReference) + { + var referenceNode = referenceHelper.ExtractReferenceKeys(relationshipElement.First, $"{semanticId}{SemanticIdResolver.RelationshipElementFirstPostFixSeparator}", cardinality); + if (referenceNode != null) + { + relationshipElementNode.AddChild(referenceNode); + } + } + + if (relationshipElement.Second.Type == ReferenceTypes.ModelReference) + { + var referenceNode = referenceHelper.ExtractReferenceKeys(relationshipElement.Second, $"{semanticId}{SemanticIdResolver.RelationshipElementSecondPostFixSeparator}", cardinality); + if (referenceNode != null) + { + relationshipElementNode.AddChild(referenceNode); + } + } + + return relationshipElementNode; + } + + public void FillOut(ISubmodelElement element, SemanticTreeNode values, Action, SemanticTreeNode, bool> fillOutChildren) + { + var relationshipElement = (RelationshipElement)element; + var semanticId = values.SemanticId; + + referenceHelper.PopulateRelationshipReference(relationshipElement.First, values, semanticId, SemanticIdResolver.RelationshipElementFirstPostFixSeparator); + + referenceHelper.PopulateRelationshipReference(relationshipElement.Second, values, semanticId, SemanticIdResolver.RelationshipElementSecondPostFixSeparator); + } +} diff --git a/source/AAS.TwinEngine.DataEngine/ApplicationLogic/Services/SubmodelRepository/SemanticId/Extraction/SemanticTreeExtractor.cs b/source/AAS.TwinEngine.DataEngine/ApplicationLogic/Services/SubmodelRepository/SemanticId/Extraction/SemanticTreeExtractor.cs index 2c314dca..0b542eed 100644 --- a/source/AAS.TwinEngine.DataEngine/ApplicationLogic/Services/SubmodelRepository/SemanticId/Extraction/SemanticTreeExtractor.cs +++ b/source/AAS.TwinEngine.DataEngine/ApplicationLogic/Services/SubmodelRepository/SemanticId/Extraction/SemanticTreeExtractor.cs @@ -1,19 +1,16 @@ using AAS.TwinEngine.DataEngine.ApplicationLogic.Exceptions.Application; -using AAS.TwinEngine.DataEngine.ApplicationLogic.Services.SubmodelRepository.SemanticId.Helpers; +using AAS.TwinEngine.DataEngine.ApplicationLogic.Services.SubmodelRepository.SemanticId.ElementHandlers; using AAS.TwinEngine.DataEngine.ApplicationLogic.Services.SubmodelRepository.SemanticId.Helpers.Interfaces; using AAS.TwinEngine.DataEngine.DomainModel.SubmodelRepository; using AasCore.Aas3_0; -using Range = AasCore.Aas3_0.Range; - namespace AAS.TwinEngine.DataEngine.ApplicationLogic.Services.SubmodelRepository.SemanticId.Extraction; public class SemanticTreeExtractor( ISemanticIdResolver semanticIdResolver, ISubmodelElementHelper elementHelper, - IReferenceHelper referenceHelper, - ILogger logger) : ISemanticTreeExtractor + IEnumerable handlers) : ISemanticTreeExtractor { public SemanticTreeNode Extract(ISubmodel submodelTemplate) { @@ -59,179 +56,14 @@ public ISubmodelElement Extract(ISubmodel submodelTemplate, string idShortPath) throw new InternalDataProcessingException(); } - private SemanticTreeNode? ExtractElement(ISubmodelElement submodelElementTemplate) - { - ArgumentNullException.ThrowIfNull(submodelElementTemplate); - - return submodelElementTemplate switch - { - SubmodelElementCollection collection => ExtractCollection(collection), - SubmodelElementList list => ExtractList(list), - MultiLanguageProperty mlp => ExtractMultiLanguageProperty(mlp), - Range range => ExtractRange(range), - ReferenceElement re => ExtractReferenceElement(re), - RelationshipElement relationshipElement => ExtractRelationshipElement(relationshipElement), - Entity entity => ExtractEntity(entity), - _ => CreateLeafNode(submodelElementTemplate) - }; - } - - private SemanticBranchNode ExtractList(SubmodelElementList list) - { - var node = new SemanticBranchNode(semanticIdResolver.ResolveElementSemanticId(list, list.IdShort!), semanticIdResolver.GetCardinality(list)); - if (list.Value?.Count > 0) - { - foreach (var element in list.Value) - { - var child = ExtractElement(element); - if (child != null) - { - node.AddChild(child); - } - } - } - else - { - logger.LogWarning("No elements defined in SubmodelElementList {ListIdShort}", list.IdShort); - } - - return node; - } - - private SemanticBranchNode ExtractCollection(SubmodelElementCollection collection) + internal SemanticTreeNode? ExtractElement(ISubmodelElement element) { - var node = new SemanticBranchNode(semanticIdResolver.ResolveElementSemanticId(collection, collection.IdShort!), semanticIdResolver.GetCardinality(collection)); - if (collection.Value?.Count > 0) - { - foreach (var element in collection.Value.Where(_ => true)) - { - var child = ExtractElement(element); - if (child != null) - { - node.AddChild(child); - } - } - } - else - { - logger.LogWarning("No elements defined in SubmodelElementCollection {CollectionIdShort}", collection.IdShort); - } - - return node; - } - - private SemanticBranchNode? ExtractReferenceElement(ReferenceElement referenceElement) - { - if (referenceElement.Value == null || referenceElement.Value.Type == ReferenceTypes.ExternalReference) - { - return null; - } - - return referenceHelper.ExtractReferenceKeys(referenceElement.Value, semanticIdResolver.ResolveElementSemanticId(referenceElement, referenceElement.IdShort!), semanticIdResolver.GetCardinality(referenceElement)); - } - - private SemanticBranchNode? ExtractRelationshipElement(RelationshipElement relationshipElement) - { - if (relationshipElement.First.Type == ReferenceTypes.ExternalReference && relationshipElement.Second.Type == ReferenceTypes.ExternalReference) - { - return null; - } - - var semanticId = semanticIdResolver.GetSemanticId(relationshipElement); - var cardinality = semanticIdResolver.GetCardinality(relationshipElement); - var relationshipElementNode = new SemanticBranchNode(semanticId, cardinality); - - if (relationshipElement.First.Type == ReferenceTypes.ModelReference) - { - var referenceNode = referenceHelper.ExtractReferenceKeys(relationshipElement.First, $"{semanticId}{SemanticIdResolver.RelationshipElementFirstPostFixSeparator}", cardinality); - if (referenceNode != null) - { - relationshipElementNode.AddChild(referenceNode); - } - } - - if (relationshipElement.Second.Type == ReferenceTypes.ModelReference) - { - var referenceNode = referenceHelper.ExtractReferenceKeys(relationshipElement.Second, $"{semanticId}{SemanticIdResolver.RelationshipElementSecondPostFixSeparator}", cardinality); - if (referenceNode != null) - { - relationshipElementNode.AddChild(referenceNode); - } - } - - return relationshipElementNode; - } - - private SemanticBranchNode ExtractEntity(Entity entity) - { - var semanticId = semanticIdResolver.ResolveElementSemanticId(entity, entity.IdShort!); - var node = new SemanticBranchNode(semanticId, semanticIdResolver.GetCardinality(entity)); - if (entity.EntityType == EntityType.SelfManagedEntity) - { - var globalAssetIdNode = new SemanticLeafNode(semanticId + SemanticIdResolver.EntityGlobalAssetIdPostFix, string.Empty, DataType.String, Cardinality.One); - node.AddChild(globalAssetIdNode); - if (entity.SpecificAssetIds != null) - { - foreach (var specificAssetId in entity.SpecificAssetIds) - { - IHasSemantics specificAsset = specificAssetId; - if (specificAsset.SemanticId == null) - { - continue; - } - - var specificAssetIdNode = new SemanticLeafNode(semanticIdResolver.GetSemanticId(specificAssetId), string.Empty, DataType.String, Cardinality.One); - node.AddChild(specificAssetIdNode); - } - } - } - - if (entity.Statements?.Count > 0) - { - foreach (var child in entity.Statements.Select(ExtractElement).OfType()) - { - node.AddChild(child); - } - } - else - { - logger.LogWarning("No elements defined in Entity {EntityIdShort}", entity.IdShort); - } - - return node; - } - - private SemanticBranchNode? ExtractMultiLanguageProperty(MultiLanguageProperty mlp) - { - var semanticId = semanticIdResolver.ExtractSemanticId(mlp); - var node = new SemanticBranchNode(semanticId, semanticIdResolver.GetCardinality(mlp)); - - var languages = elementHelper.ResolveLanguages(mlp); - - if (mlp.Value is not { Count: > 0 }) - { - logger.LogInformation("No languages defined in template for MultiLanguageProperty {MlpIdShort}", mlp.IdShort); - } - - var mlpSeparator = semanticIdResolver.MlpPostFixSeparator; - foreach (var langSemanticId in languages.Select(language => string.Concat(semanticId, mlpSeparator, language))) - { - node.AddChild(new SemanticLeafNode(langSemanticId, string.Empty, DataType.String, Cardinality.ZeroToOne)); - } - - return node; - } - - private SemanticBranchNode ExtractRange(Range range) - { - var semanticId = semanticIdResolver.ExtractSemanticId(range); - var valueType = semanticIdResolver.GetValueType(range); - var node = new SemanticBranchNode(semanticId, semanticIdResolver.GetCardinality(range)); - - node.AddChild(new SemanticLeafNode(semanticId + SemanticIdResolver.RangeMinimumPostFixSeparator, string.Empty, valueType, Cardinality.ZeroToOne)); - node.AddChild(new SemanticLeafNode(semanticId + SemanticIdResolver.RangeMaximumPostFixSeparator, string.Empty, valueType, Cardinality.ZeroToOne)); + ArgumentNullException.ThrowIfNull(element); - return node; + var handler = handlers.FirstOrDefault(h => h.CanHandle(element)); + return handler != null + ? handler.Extract(element, ExtractElement) + : CreateLeafNode(element); } private SemanticLeafNode CreateLeafNode(ISubmodelElement element) diff --git a/source/AAS.TwinEngine.DataEngine/ApplicationLogic/Services/SubmodelRepository/SemanticId/FillOut/SubmodelFiller.cs b/source/AAS.TwinEngine.DataEngine/ApplicationLogic/Services/SubmodelRepository/SemanticId/FillOut/SubmodelFiller.cs index 129a1f12..6173551d 100644 --- a/source/AAS.TwinEngine.DataEngine/ApplicationLogic/Services/SubmodelRepository/SemanticId/FillOut/SubmodelFiller.cs +++ b/source/AAS.TwinEngine.DataEngine/ApplicationLogic/Services/SubmodelRepository/SemanticId/FillOut/SubmodelFiller.cs @@ -1,22 +1,19 @@ using AAS.TwinEngine.DataEngine.ApplicationLogic.Exceptions.Application; +using AAS.TwinEngine.DataEngine.ApplicationLogic.Services.SubmodelRepository.SemanticId.ElementHandlers; using AAS.TwinEngine.DataEngine.ApplicationLogic.Services.SubmodelRepository.SemanticId.Helpers; using AAS.TwinEngine.DataEngine.ApplicationLogic.Services.SubmodelRepository.SemanticId.Helpers.Interfaces; using AAS.TwinEngine.DataEngine.DomainModel.SubmodelRepository; using AasCore.Aas3_0; -using File = AasCore.Aas3_0.File; -using Range = AasCore.Aas3_0.Range; - namespace AAS.TwinEngine.DataEngine.ApplicationLogic.Services.SubmodelRepository.SemanticId.FillOut; public class SubmodelFiller( ISemanticIdResolver semanticIdResolver, ISubmodelElementHelper elementHelper, - IReferenceHelper referenceHelper, + IEnumerable handlers, ILogger logger) : ISubmodelFiller { - public ISubmodel FillOutTemplate(ISubmodel submodelTemplate, SemanticTreeNode values) { ArgumentNullException.ThrowIfNull(submodelTemplate); @@ -79,82 +76,23 @@ private void HandleSingleMatchingNode( submodelTemplate.SubmodelElements?.Add(element); } - private ISubmodelElement FillOutElement(ISubmodelElement submodelElementTemplate, SemanticTreeNode values) + internal ISubmodelElement FillOutElement(ISubmodelElement element, SemanticTreeNode values) { - ArgumentNullException.ThrowIfNull(submodelElementTemplate); + ArgumentNullException.ThrowIfNull(element); ArgumentNullException.ThrowIfNull(values); - switch (submodelElementTemplate) - { - case SubmodelElementCollection collection: - FillOutSubmodelElementCollection(collection, values); - break; - - case SubmodelElementList list: - FillOutSubmodelElementList(list, values); - break; - - case MultiLanguageProperty mlp: - FillOutMultiLanguageProperty(mlp, values); - break; - - case Property property: - FillOutProperty(property, values); - break; - - case File file: - FillOutFile(file, values); - break; - - case Blob blob: - FillOutBlob(blob, values); - break; - - case RelationshipElement relationship: - FillOutRelationshipElement(relationship, values); - break; - - case ReferenceElement reference: - FillOutReferenceElement(reference, values); - break; - - case Range range: - FillOutRange(range, values); - break; - - case Entity entity: - FillOutEntity(entity, values); - break; - - default: - logger.LogError("InValid submodelElementTemplate Type. IdShort : {IdShort}", submodelElementTemplate.IdShort); - throw new InternalDataProcessingException(); - } - - return submodelElementTemplate; - } - - private void FillOutSubmodelElementList(SubmodelElementList list, SemanticTreeNode values) - { - if (list?.Value == null || list.Value.Count == 0) - { - return; - } - - FillOutSubmodelElementValue(list.Value, values, false); - } - - private void FillOutSubmodelElementCollection(SubmodelElementCollection collection, SemanticTreeNode values) - { - if (collection?.Value == null || collection.Value.Count == 0) + var handler = handlers.FirstOrDefault(h => h.CanHandle(element)); + if (handler == null) { - return; + logger.LogError("InValid submodelElementTemplate Type. IdShort : {IdShort}", element.IdShort); + throw new InternalDataProcessingException(); } - FillOutSubmodelElementValue(collection.Value, values); + handler.FillOut(element, values, FillOutSubmodelElementValue); + return element; } - private void FillOutSubmodelElementValue(List elements, SemanticTreeNode values, bool updateIdShort = true) + internal void FillOutSubmodelElementValue(List elements, SemanticTreeNode values, bool updateIdShort) { var originalElements = elements.ToList(); foreach (var element in originalElements) @@ -193,168 +131,8 @@ private void FillOutSubmodelElementValue(List elements, Semant } else { - FillOutElement(element, semanticTreeNodes[0]); + _ = FillOutElement(element, semanticTreeNodes[0]); } } } - - private void FillOutMultiLanguageProperty(MultiLanguageProperty mlp, SemanticTreeNode values) - { - var semanticId = semanticIdResolver.ExtractSemanticId(mlp); - - if (SemanticTreeNavigator.FindNodeBySemanticId(values, semanticId).FirstOrDefault() is not SemanticBranchNode valueNode) - { - logger.LogInformation("No value node found for MultiLanguageProperty {MlpIdShort}", mlp.IdShort); - return; - } - - mlp.Value ??= []; - - var languageValueMap = new Dictionary(StringComparer.OrdinalIgnoreCase); - foreach (var langValue in mlp.Value) - { - languageValueMap[langValue.Language] = (LangStringTextType)langValue; - } - - var languages = elementHelper.ResolveLanguages(mlp); - - var mlpSeparator = semanticIdResolver.MlpPostFixSeparator; - foreach (var language in languages) - { - if (!languageValueMap.TryGetValue(language, out var languageValue)) - { - languageValue = new LangStringTextType(language, string.Empty); - mlp.Value.Add(languageValue); - languageValueMap[language] = languageValue; - - logger.LogInformation("Added language '{Language}' to MultiLanguageProperty {MlpIdShort}", language, mlp.IdShort); - } - - var languageSemanticId = semanticId + mlpSeparator + language; - - var leafNode = valueNode.Children - .OfType() - .FirstOrDefault(child => child.SemanticId.Equals(languageSemanticId, StringComparison.Ordinal)); - - if (leafNode != null) - { - languageValue.Text = leafNode.Value; - } - } - } - - private void FillOutEntity(Entity entity, SemanticTreeNode values) - { - if (entity.EntityType == EntityType.SelfManagedEntity) - { - FillOutSelfManagedEntity(entity, values); - } - - if (entity?.Statements == null || entity.Statements.Count == 0) - { - return; - } - - FillOutSubmodelElementValue(entity.Statements, values); - } - - private void FillOutSelfManagedEntity(Entity entity, SemanticTreeNode values) - { - var semanticId = semanticIdResolver.ResolveElementSemanticId(entity, entity.IdShort!); - - if (SemanticTreeNavigator.FindNodeBySemanticId(values, semanticId).FirstOrDefault() is not SemanticBranchNode valueNode) - { - return; - } - - var globalAssetSemanticId = semanticId + SemanticIdResolver.EntityGlobalAssetIdPostFix; - - var globalAssetNode = valueNode.Children - .OfType() - .FirstOrDefault(c => c.SemanticId == globalAssetSemanticId); - - if (globalAssetNode != null) - { - entity.GlobalAssetId = globalAssetNode.Value; - } - - if (entity.SpecificAssetIds != null) - { - foreach (var specificAssetId in entity.SpecificAssetIds) - { - var specSemanticId = semanticIdResolver.GetSemanticId(specificAssetId); - - var specNode = valueNode.Children - .OfType() - .FirstOrDefault(c => c.SemanticId == specSemanticId); - - if (specNode != null) - { - specificAssetId.Value = specNode.Value; - } - } - } - } - - private static void FillOutProperty(Property valueElement, SemanticTreeNode values) - { - if (values is SemanticLeafNode leafValueNode) - { - valueElement.Value = leafValueNode.Value; - } - } - - private static void FillOutFile(File valueElement, SemanticTreeNode values) - { - if (values is SemanticLeafNode leafValueNode) - { - valueElement.Value = leafValueNode.Value; - } - } - - private static void FillOutBlob(Blob valueElement, SemanticTreeNode values) - { - if (values is SemanticLeafNode leafValueNode) - { - valueElement.Value = Convert.FromBase64String(leafValueNode.Value); - } - } - - private static void FillOutRange(Range valueElement, SemanticTreeNode values) - { - if (values is not SemanticBranchNode branchNode) - { - return; - } - - var leafNodes = branchNode.Children.OfType().ToList(); - - valueElement.Min = leafNodes.FirstOrDefault(n => n.SemanticId - .EndsWith(SemanticIdResolver.RangeMinimumPostFixSeparator, StringComparison.Ordinal))? - .Value; - - valueElement.Max = leafNodes.FirstOrDefault(n => n.SemanticId - .EndsWith(SemanticIdResolver.RangeMaximumPostFixSeparator, StringComparison.Ordinal))? - .Value; - } - - private void FillOutReferenceElement(ReferenceElement referenceElement, SemanticTreeNode semanticNode) - { - if (referenceElement?.Value?.Type != ReferenceTypes.ModelReference) - { - logger.LogInformation("ReferenceElement does not contain a ModelReference for SemanticId '{SemanticId}'. Skipping population.", semanticIdResolver.GetSemanticId(referenceElement!)); - return; - } - - referenceHelper.PopulateReferenceKeys(referenceElement.Value, semanticNode, semanticIdResolver.GetSemanticId(referenceElement)); - } - - private void FillOutRelationshipElement(RelationshipElement relationshipElement, SemanticTreeNode semanticTreeNode) - { - var semanticId = semanticTreeNode.SemanticId; - - referenceHelper.PopulateRelationshipReference(relationshipElement.First, semanticTreeNode, semanticId, SemanticIdResolver.RelationshipElementFirstPostFixSeparator); - - referenceHelper.PopulateRelationshipReference(relationshipElement.Second, semanticTreeNode, semanticId, SemanticIdResolver.RelationshipElementSecondPostFixSeparator); - } } diff --git a/source/AAS.TwinEngine.DataEngine/ApplicationLogic/Services/SubmodelRepository/SemanticId/Helpers/SemanticTreeNavigator.cs b/source/AAS.TwinEngine.DataEngine/ApplicationLogic/Services/SubmodelRepository/SemanticId/Helpers/SemanticTreeNavigator.cs index a2a4ab33..69c03914 100644 --- a/source/AAS.TwinEngine.DataEngine/ApplicationLogic/Services/SubmodelRepository/SemanticId/Helpers/SemanticTreeNavigator.cs +++ b/source/AAS.TwinEngine.DataEngine/ApplicationLogic/Services/SubmodelRepository/SemanticId/Helpers/SemanticTreeNavigator.cs @@ -2,7 +2,7 @@ namespace AAS.TwinEngine.DataEngine.ApplicationLogic.Services.SubmodelRepository.SemanticId.Helpers; -public class SemanticTreeNavigator +public static class SemanticTreeNavigator { public static IEnumerable FindBranchNodesBySemanticId(SemanticTreeNode tree, string semanticId) { @@ -34,7 +34,7 @@ public static IEnumerable FindNodeBySemanticId(SemanticTreeNod } } - public static bool AreAllNodesOfSameType(List nodes, out Type? nodeType) + public static bool AreAllNodesOfSameType(IList nodes, out Type? nodeType) { if (nodes.Count == 0) { diff --git a/source/AAS.TwinEngine.DataEngine/ApplicationLogic/Services/SubmodelRepository/SemanticId/Helpers/SubmodelElementHelper.cs b/source/AAS.TwinEngine.DataEngine/ApplicationLogic/Services/SubmodelRepository/SemanticId/Helpers/SubmodelElementHelper.cs index 04d4db4c..069e8b25 100644 --- a/source/AAS.TwinEngine.DataEngine/ApplicationLogic/Services/SubmodelRepository/SemanticId/Helpers/SubmodelElementHelper.cs +++ b/source/AAS.TwinEngine.DataEngine/ApplicationLogic/Services/SubmodelRepository/SemanticId/Helpers/SubmodelElementHelper.cs @@ -13,7 +13,7 @@ namespace AAS.TwinEngine.DataEngine.ApplicationLogic.Services.SubmodelRepository public partial class SubmodelElementHelper(ILogger logger, IOptions mlpSettings) : ISubmodelElementHelper { - private readonly HashSet? _defaultLanguagesSet = mlpSettings.Value.DefaultLanguages != null && mlpSettings.Value.DefaultLanguages.Count > 0 + private readonly HashSet? _defaultLanguagesSet = mlpSettings.Value.DefaultLanguages is { Count: > 0 } ? new HashSet(mlpSettings.Value.DefaultLanguages, StringComparer.OrdinalIgnoreCase) : null; diff --git a/source/AAS.TwinEngine.DataEngine/ApplicationLogic/Services/SubmodelRepository/SubmodelRepositoryService.cs b/source/AAS.TwinEngine.DataEngine/ApplicationLogic/Services/SubmodelRepository/SubmodelRepositoryService.cs index 093e9161..d2a3384b 100644 --- a/source/AAS.TwinEngine.DataEngine/ApplicationLogic/Services/SubmodelRepository/SubmodelRepositoryService.cs +++ b/source/AAS.TwinEngine.DataEngine/ApplicationLogic/Services/SubmodelRepository/SubmodelRepositoryService.cs @@ -1,4 +1,6 @@ -using AAS.TwinEngine.DataEngine.ApplicationLogic.Exceptions.Application; +using System.Diagnostics; + +using AAS.TwinEngine.DataEngine.ApplicationLogic.Exceptions.Application; using AAS.TwinEngine.DataEngine.ApplicationLogic.Exceptions.Infrastructure; using AAS.TwinEngine.DataEngine.ApplicationLogic.Services.Plugin; @@ -40,13 +42,30 @@ public async Task GetSubmodelElementAsync(string submodelId, s private async Task BuildSubmodelWithValuesAsync(ISubmodel template, string submodelId, CancellationToken cancellationToken) { + var stopwatch1 = new Stopwatch(); + var stopwatch2 = new Stopwatch(); + + GC.Collect(); + GC.WaitForPendingFinalizers(); + GC.Collect(); + stopwatch1.Start(); var semanticIds = semanticIdHandler.Extract(template); + stopwatch1.Stop(); var pluginManifests = pluginManifestConflictHandler.Manifests; var values = await pluginDataHandler.TryGetValuesAsync(pluginManifests, semanticIds, submodelId, cancellationToken).ConfigureAwait(false); - return semanticIdHandler.FillOutTemplate(template, values); + GC.Collect(); + GC.WaitForPendingFinalizers(); + GC.Collect(); + stopwatch2.Start(); + var result = semanticIdHandler.FillOutTemplate(template, values); + stopwatch2.Stop(); + + var time = stopwatch1.ElapsedMilliseconds + stopwatch2.ElapsedMilliseconds; + result.Kind = ModellingKind.Instance; + return result; } private static async Task ExecuteWithExceptionHandlingAsync(Func> action) diff --git a/source/AAS.TwinEngine.DataEngine/ServiceConfiguration/ApplicationDependencyInjectionExtensions.cs b/source/AAS.TwinEngine.DataEngine/ServiceConfiguration/ApplicationDependencyInjectionExtensions.cs index 40fe3bda..1c8e58b4 100644 --- a/source/AAS.TwinEngine.DataEngine/ServiceConfiguration/ApplicationDependencyInjectionExtensions.cs +++ b/source/AAS.TwinEngine.DataEngine/ServiceConfiguration/ApplicationDependencyInjectionExtensions.cs @@ -9,6 +9,7 @@ using AAS.TwinEngine.DataEngine.ApplicationLogic.Services.Plugin; using AAS.TwinEngine.DataEngine.ApplicationLogic.Services.SubmodelRegistry; using AAS.TwinEngine.DataEngine.ApplicationLogic.Services.SubmodelRepository; +using AAS.TwinEngine.DataEngine.ApplicationLogic.Services.SubmodelRepository.SemanticId.ElementHandlers; using AAS.TwinEngine.DataEngine.ApplicationLogic.Services.SubmodelRepository.SemanticId.Extraction; using AAS.TwinEngine.DataEngine.ApplicationLogic.Services.SubmodelRepository.SemanticId.FillOut; using AAS.TwinEngine.DataEngine.ApplicationLogic.Services.SubmodelRepository.SemanticId.Helpers; @@ -38,6 +39,16 @@ public static void ConfigureApplication(this IServiceCollection services, IConfi _ = services.AddScoped(); _ = services.AddScoped(); _ = services.AddScoped(); + _ = services.AddScoped(); + _ = services.AddScoped(); + _ = services.AddScoped(); + _ = services.AddScoped(); + _ = services.AddScoped(); + _ = services.AddScoped(); + _ = services.AddScoped(); + _ = services.AddScoped(); + _ = services.AddScoped(); + _ = services.AddScoped(); _ = services.AddScoped(); _ = services.AddScoped(); _ = services.AddScoped(); diff --git a/source/AAS.TwinEngine.DataEngine/ServiceConfiguration/InfrastructureDependencyInjectionExtensions.cs b/source/AAS.TwinEngine.DataEngine/ServiceConfiguration/InfrastructureDependencyInjectionExtensions.cs index e132051e..17533a6d 100644 --- a/source/AAS.TwinEngine.DataEngine/ServiceConfiguration/InfrastructureDependencyInjectionExtensions.cs +++ b/source/AAS.TwinEngine.DataEngine/ServiceConfiguration/InfrastructureDependencyInjectionExtensions.cs @@ -42,16 +42,13 @@ public static void ConfigureInfrastructure(this IServiceCollection services, ICo var aasEnvironment = configuration.GetSection(AasEnvironmentConfig.Section).Get(); var plugins = configuration.GetSection(PluginConfig.Section).Get(); - _ = services.AddHttpClientWithResilience(configuration, AasEnvironmentConfig.AasEnvironmentRepoHttpClientName, HttpRetryPolicyOptions.TemplateProvider, aasEnvironment?.AasEnvironmentRepositoryBaseUrl!); _ = services.AddHttpClientWithResilience(configuration, AasEnvironmentConfig.AasRegistryHttpClientName, HttpRetryPolicyOptions.TemplateProvider, aasEnvironment?.AasRegistryBaseUrl!); _ = services.AddHttpClientWithResilience(configuration, AasEnvironmentConfig.SubmodelRegistryHttpClientName, HttpRetryPolicyOptions.SubmodelDescriptorProvider, aasEnvironment?.SubModelRegistryBaseUrl!); - _ = services.AddOptions() .Bind(configuration.GetSection(MultiLanguagePropertySettings.Section)) .ValidateOnStart(); _ = services.AddSingleton, MultiLanguagePropertySettingsValidator>(); - foreach (var plugin in plugins.Plugins) { _ = services.AddHttpClientWithResilience(configuration, PluginConfig.HttpClientNamePrefix + plugin.PluginName, HttpRetryPolicyOptions.PluginDataProvider, plugin?.PluginUrl); From 4ad124197915029f48a69f93e5e4984c0d3011a7 Mon Sep 17 00:00:00 2001 From: Kevalkumar Date: Wed, 18 Mar 2026 23:52:11 +0530 Subject: [PATCH 04/71] Refactor the code to remove warnings --- .../Extraction/SemanticTreeExtractorTests.cs | 35 +++++++------------ .../SemanticId/FillOut/SubmodelFillerTests.cs | 18 ++-------- .../SemanticIdHandlerTests.cs | 4 +-- .../AasRegistry/ShellDescriptorService.cs | 2 +- .../Extraction/SemanticTreeExtractor.cs | 2 +- .../SemanticId/FillOut/SubmodelFiller.cs | 2 +- .../SubmodelRepositoryService.cs | 15 -------- 7 files changed, 18 insertions(+), 60 deletions(-) diff --git a/source/AAS.TwinEngine.DataEngine.UnitTests/ApplicationLogic/Services/SubmodelRepository/SemanticId/Extraction/SemanticTreeExtractorTests.cs b/source/AAS.TwinEngine.DataEngine.UnitTests/ApplicationLogic/Services/SubmodelRepository/SemanticId/Extraction/SemanticTreeExtractorTests.cs index b81680ef..57bc2546 100644 --- a/source/AAS.TwinEngine.DataEngine.UnitTests/ApplicationLogic/Services/SubmodelRepository/SemanticId/Extraction/SemanticTreeExtractorTests.cs +++ b/source/AAS.TwinEngine.DataEngine.UnitTests/ApplicationLogic/Services/SubmodelRepository/SemanticId/Extraction/SemanticTreeExtractorTests.cs @@ -1,4 +1,4 @@ -using AAS.TwinEngine.DataEngine.ApplicationLogic.Exceptions.Application; +using AAS.TwinEngine.DataEngine.ApplicationLogic.Exceptions.Application; using AAS.TwinEngine.DataEngine.ApplicationLogic.Services.SubmodelRepository.SemanticId.ElementHandlers; using AAS.TwinEngine.DataEngine.ApplicationLogic.Services.SubmodelRepository.SemanticId.Extraction; using AAS.TwinEngine.DataEngine.ApplicationLogic.Services.SubmodelRepository.SemanticId.Helpers.Interfaces; @@ -19,30 +19,25 @@ public class SemanticTreeExtractorTests private readonly SemanticTreeExtractor _sut; private readonly ISemanticIdResolver _resolver; private readonly ISubmodelElementHelper _elementHelper; - private readonly ILogger _logger; private readonly List _handlers; public SemanticTreeExtractorTests() { _resolver = Substitute.For(); _elementHelper = Substitute.For(); - _logger = Substitute.For>(); _handlers = []; - _sut = new SemanticTreeExtractor(_resolver, _elementHelper, _handlers, _logger); + _sut = new SemanticTreeExtractor(_resolver, _elementHelper, _handlers); } [Fact] - public void Extract_NullSubmodel_ThrowsArgumentNullException() - { - Throws(() => _sut.Extract(null!)); - } + public void Extract_NullSubmodel_ThrowsArgumentNullException() => Throws(() => _sut.Extract(null!)); [Fact] public void Extract_SubmodelWithNoElements_ReturnsRootNodeWithNoChildren() { var submodel = Substitute.For(); submodel.IdShort.Returns("TestSubmodel"); - submodel.SubmodelElements.Returns(new List()); + submodel.SubmodelElements.Returns([]); _resolver.ResolveSemanticId(submodel, "TestSubmodel").Returns("http://test/root"); var result = _sut.Extract(submodel) as SemanticBranchNode; @@ -58,7 +53,7 @@ public void Extract_SubmodelWithElements_DelegatesToHandlers() var property = new Property(idShort: "Prop", valueType: DataTypeDefXsd.String); var submodel = Substitute.For(); submodel.IdShort.Returns("Test"); - submodel.SubmodelElements.Returns(new List { property }); + submodel.SubmodelElements.Returns([property]); _resolver.ResolveSemanticId(submodel, "Test").Returns("http://test/root"); var handler = Substitute.For(); @@ -81,7 +76,7 @@ public void Extract_ElementWithNoHandler_CreatesLeafNodeFallback() element.IdShort.Returns("UnknownElement"); var submodel = Substitute.For(); submodel.IdShort.Returns("Test"); - submodel.SubmodelElements.Returns(new List { element }); + submodel.SubmodelElements.Returns([element]); _resolver.ResolveSemanticId(submodel, "Test").Returns("http://test/root"); _resolver.ResolveElementSemanticId(element, "UnknownElement").Returns("http://test/unknown"); _resolver.GetValueType(element).Returns(DataType.Unknown); @@ -97,10 +92,7 @@ public void Extract_ElementWithNoHandler_CreatesLeafNodeFallback() } [Fact] - public void Extract_ByIdShortPath_NullSubmodel_ThrowsArgumentNullException() - { - Throws(() => _sut.Extract(null!, "path")); - } + public void Extract_ByIdShortPath_NullSubmodel_ThrowsArgumentNullException() => Throws(() => _sut.Extract(null!, "path")); [Fact] public void Extract_ByIdShortPath_NullPath_ThrowsArgumentNullException() @@ -114,7 +106,7 @@ public void Extract_ByIdShortPath_SingleSegment_ReturnsMatchingElement() { var property = new Property(idShort: "MyProp", valueType: DataTypeDefXsd.String, value: "test"); var submodel = Substitute.For(); - submodel.SubmodelElements.Returns(new List { property }); + submodel.SubmodelElements.Returns([property]); _elementHelper.GetElementByIdShort(Arg.Any>(), "MyProp").Returns(property); var result = _sut.Extract(submodel, "MyProp"); @@ -128,7 +120,7 @@ public void Extract_ByIdShortPath_NestedPath_ReturnsNestedElement() var childProp = new Property(idShort: "ChildProp", valueType: DataTypeDefXsd.String); var collection = new SubmodelElementCollection(idShort: "Parent", value: [childProp]); var submodel = Substitute.For(); - submodel.SubmodelElements.Returns(new List { collection }); + submodel.SubmodelElements.Returns([collection]); _elementHelper.GetElementByIdShort(Arg.Any>(), "Parent").Returns(collection); _elementHelper.GetChildElements(collection).Returns(collection.Value); _elementHelper.GetElementByIdShort(collection.Value, "ChildProp").Returns(childProp); @@ -142,7 +134,7 @@ public void Extract_ByIdShortPath_NestedPath_ReturnsNestedElement() public void Extract_ByIdShortPath_ElementNotFound_ThrowsException() { var submodel = Substitute.For(); - submodel.SubmodelElements.Returns(new List()); + submodel.SubmodelElements.Returns([]); _elementHelper.GetElementByIdShort(Arg.Any>(), "NonExistent").Returns((ISubmodelElement?)null); Throws(() => _sut.Extract(submodel, "NonExistent")); @@ -153,7 +145,7 @@ public void Extract_ByIdShortPath_ChildElementsNull_ThrowsException() { var property = new Property(idShort: "Prop", valueType: DataTypeDefXsd.String); var submodel = Substitute.For(); - submodel.SubmodelElements.Returns(new List { property }); + submodel.SubmodelElements.Returns([property]); _elementHelper.GetElementByIdShort(Arg.Any>(), "Prop").Returns(property); _elementHelper.GetChildElements(property).Returns((IList?)null); @@ -161,10 +153,7 @@ public void Extract_ByIdShortPath_ChildElementsNull_ThrowsException() } [Fact] - public void ExtractElement_NullElement_ThrowsArgumentNullException() - { - Throws(() => _sut.ExtractElement(null!)); - } + public void ExtractElement_NullElement_ThrowsArgumentNullException() => Throws(() => _sut.ExtractElement(null!)); [Fact] public void ExtractElement_HandlerReturnsNull_CreatesFallbackLeaf() diff --git a/source/AAS.TwinEngine.DataEngine.UnitTests/ApplicationLogic/Services/SubmodelRepository/SemanticId/FillOut/SubmodelFillerTests.cs b/source/AAS.TwinEngine.DataEngine.UnitTests/ApplicationLogic/Services/SubmodelRepository/SemanticId/FillOut/SubmodelFillerTests.cs index b42dd023..bf407fd0 100644 --- a/source/AAS.TwinEngine.DataEngine.UnitTests/ApplicationLogic/Services/SubmodelRepository/SemanticId/FillOut/SubmodelFillerTests.cs +++ b/source/AAS.TwinEngine.DataEngine.UnitTests/ApplicationLogic/Services/SubmodelRepository/SemanticId/FillOut/SubmodelFillerTests.cs @@ -1,4 +1,4 @@ -using AAS.TwinEngine.DataEngine.ApplicationLogic.Exceptions.Application; +using AAS.TwinEngine.DataEngine.ApplicationLogic.Exceptions.Application; using AAS.TwinEngine.DataEngine.ApplicationLogic.Services.SubmodelRepository.SemanticId.ElementHandlers; using AAS.TwinEngine.DataEngine.ApplicationLogic.Services.SubmodelRepository.SemanticId.FillOut; using AAS.TwinEngine.DataEngine.ApplicationLogic.Services.SubmodelRepository.SemanticId.Helpers.Interfaces; @@ -43,7 +43,7 @@ public void FillOutTemplate_NullSubmodel_ThrowsArgumentNullException() public void FillOutTemplate_NullValues_ThrowsArgumentNullException() { var submodel = Substitute.For(); - submodel.SubmodelElements.Returns(new List()); + submodel.SubmodelElements.Returns([]); Throws(() => _sut.FillOutTemplate(submodel, null!)); } @@ -114,18 +114,4 @@ public void FillOutElement_WithMatchingHandler_DelegatesToHandler() handler.Received(1).FillOut(element, values, Arg.Any, SemanticTreeNode, bool>>()); } - - [Fact] - public void FillOutSubmodelElementValue_NoMatchingValueNode_PreservesElements() - { - var property = new Property(idShort: "Prop", valueType: DataTypeDefXsd.String, value: "original"); - var elements = new List { property }; - var values = new SemanticBranchNode("root", Cardinality.Unknown); - _resolver.ExtractSemanticId(property).Returns("http://test/prop"); - - _sut.FillOutSubmodelElementValue(elements, values, false); - - Single(elements); - Equal("original", ((Property)elements[0]).Value); - } } diff --git a/source/AAS.TwinEngine.DataEngine.UnitTests/ApplicationLogic/Services/SubmodelRepository/SemanticIdHandlerTests.cs b/source/AAS.TwinEngine.DataEngine.UnitTests/ApplicationLogic/Services/SubmodelRepository/SemanticIdHandlerTests.cs index e032483b..250b98f1 100644 --- a/source/AAS.TwinEngine.DataEngine.UnitTests/ApplicationLogic/Services/SubmodelRepository/SemanticIdHandlerTests.cs +++ b/source/AAS.TwinEngine.DataEngine.UnitTests/ApplicationLogic/Services/SubmodelRepository/SemanticIdHandlerTests.cs @@ -25,14 +25,12 @@ namespace AAS.TwinEngine.DataEngine.UnitTests.ApplicationLogic.Services.Submodel public class SemanticIdHandlerTests { private readonly SemanticIdHandler _sut; - private readonly ILogger _extractorLogger; private readonly ILogger _fillerLogger; private readonly IOptions _mlpSettings; private readonly IOptions _semantics; public SemanticIdHandlerTests() { - _extractorLogger = Substitute.For>(); _fillerLogger = Substitute.For>(); _mlpSettings = Substitute.For>(); _ = _mlpSettings.Value.Returns(new MultiLanguagePropertySettings { DefaultLanguages = null }); @@ -872,7 +870,7 @@ private SemanticIdHandler CreateSut(IOptions semantics, IOptions GetSubmodelElementAsync(string submodelId, s private async Task BuildSubmodelWithValuesAsync(ISubmodel template, string submodelId, CancellationToken cancellationToken) { - var stopwatch1 = new Stopwatch(); - var stopwatch2 = new Stopwatch(); - - GC.Collect(); - GC.WaitForPendingFinalizers(); - GC.Collect(); - stopwatch1.Start(); var semanticIds = semanticIdHandler.Extract(template); - stopwatch1.Stop(); var pluginManifests = pluginManifestConflictHandler.Manifests; var values = await pluginDataHandler.TryGetValuesAsync(pluginManifests, semanticIds, submodelId, cancellationToken).ConfigureAwait(false); - GC.Collect(); - GC.WaitForPendingFinalizers(); - GC.Collect(); - stopwatch2.Start(); var result = semanticIdHandler.FillOutTemplate(template, values); - stopwatch2.Stop(); - var time = stopwatch1.ElapsedMilliseconds + stopwatch2.ElapsedMilliseconds; - result.Kind = ModellingKind.Instance; return result; } From 040b9ca4b2e806345a9ca2df229602dc4b0fcf61 Mon Sep 17 00:00:00 2001 From: Kevalkumar Date: Thu, 19 Mar 2026 00:17:33 +0530 Subject: [PATCH 05/71] refactor to remove warnings --- .../Extraction/SemanticTreeExtractorTests.cs | 2 -- .../ElementHandlers/EntityHandler.cs | 4 +-- .../SemanticId/FillOut/SubmodelFiller.cs | 29 ++++++++++++++----- 3 files changed, 24 insertions(+), 11 deletions(-) diff --git a/source/AAS.TwinEngine.DataEngine.UnitTests/ApplicationLogic/Services/SubmodelRepository/SemanticId/Extraction/SemanticTreeExtractorTests.cs b/source/AAS.TwinEngine.DataEngine.UnitTests/ApplicationLogic/Services/SubmodelRepository/SemanticId/Extraction/SemanticTreeExtractorTests.cs index 57bc2546..1fc5ae0b 100644 --- a/source/AAS.TwinEngine.DataEngine.UnitTests/ApplicationLogic/Services/SubmodelRepository/SemanticId/Extraction/SemanticTreeExtractorTests.cs +++ b/source/AAS.TwinEngine.DataEngine.UnitTests/ApplicationLogic/Services/SubmodelRepository/SemanticId/Extraction/SemanticTreeExtractorTests.cs @@ -6,8 +6,6 @@ using AasCore.Aas3_0; -using Microsoft.Extensions.Logging; - using NSubstitute; using static Xunit.Assert; diff --git a/source/AAS.TwinEngine.DataEngine/ApplicationLogic/Services/SubmodelRepository/SemanticId/ElementHandlers/EntityHandler.cs b/source/AAS.TwinEngine.DataEngine/ApplicationLogic/Services/SubmodelRepository/SemanticId/ElementHandlers/EntityHandler.cs index 2e2ed78f..787624ce 100644 --- a/source/AAS.TwinEngine.DataEngine/ApplicationLogic/Services/SubmodelRepository/SemanticId/ElementHandlers/EntityHandler.cs +++ b/source/AAS.TwinEngine.DataEngine/ApplicationLogic/Services/SubmodelRepository/SemanticId/ElementHandlers/EntityHandler.cs @@ -1,4 +1,4 @@ -using AAS.TwinEngine.DataEngine.ApplicationLogic.Services.SubmodelRepository.SemanticId.Helpers; +using AAS.TwinEngine.DataEngine.ApplicationLogic.Services.SubmodelRepository.SemanticId.Helpers; using AAS.TwinEngine.DataEngine.ApplicationLogic.Services.SubmodelRepository.SemanticId.Helpers.Interfaces; using AAS.TwinEngine.DataEngine.DomainModel.SubmodelRepository; @@ -63,7 +63,7 @@ public void FillOut(ISubmodelElement element, SemanticTreeNode values, Action
  • elements, Seman var originalElements = elements.ToList(); foreach (var element in originalElements) { - var valueNode = SemanticTreeNavigator.FindNodeBySemanticId(values, semanticIdResolver.ExtractSemanticId(element)); - var semanticTreeNodes = valueNode?.ToList(); + var semanticTreeNodes = GetSemanticNodes(element, values); if (semanticTreeNodes == null || semanticTreeNodes.Count == 0) { continue; } - if (!SemanticTreeNavigator.AreAllNodesOfSameType(semanticTreeNodes, out _)) + if (HasMixedNodeTypes(semanticTreeNodes, element, elements)) { - 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, - semanticIdResolver.ExtractSemanticId(element)); - _ = elements.Remove(element); continue; } @@ -135,4 +130,24 @@ internal void FillOutSubmodelElementValue(List elements, Seman } } } + + private List? GetSemanticNodes( ISubmodelElement element, SemanticTreeNode values) + { + var valueNode = SemanticTreeNavigator.FindNodeBySemanticId(values, semanticIdResolver.ExtractSemanticId(element)); + + return valueNode?.ToList(); + } + + private bool HasMixedNodeTypes(List nodes, ISubmodelElement element, List elements) + { + if (SemanticTreeNavigator.AreAllNodesOfSameType(nodes, out _)) + { + return false; + } + + logger.LogWarning("Mixed node types found for element '{IdShort}' with SemanticId '{SemanticId}'. Removing element.", element.IdShort, semanticIdResolver.ExtractSemanticId(element)); + + _ = elements.Remove(element); + return true; + } } From c75f9d62b76664047427fc614de4a68c1e74ea90 Mon Sep 17 00:00:00 2001 From: Kevalkumar Date: Thu, 19 Mar 2026 00:18:16 +0530 Subject: [PATCH 06/71] remove internal methods test cases --- .../Extraction/SemanticTreeExtractorTests.cs | 21 ------------------- 1 file changed, 21 deletions(-) diff --git a/source/AAS.TwinEngine.DataEngine.UnitTests/ApplicationLogic/Services/SubmodelRepository/SemanticId/Extraction/SemanticTreeExtractorTests.cs b/source/AAS.TwinEngine.DataEngine.UnitTests/ApplicationLogic/Services/SubmodelRepository/SemanticId/Extraction/SemanticTreeExtractorTests.cs index 1fc5ae0b..1e732063 100644 --- a/source/AAS.TwinEngine.DataEngine.UnitTests/ApplicationLogic/Services/SubmodelRepository/SemanticId/Extraction/SemanticTreeExtractorTests.cs +++ b/source/AAS.TwinEngine.DataEngine.UnitTests/ApplicationLogic/Services/SubmodelRepository/SemanticId/Extraction/SemanticTreeExtractorTests.cs @@ -149,25 +149,4 @@ public void Extract_ByIdShortPath_ChildElementsNull_ThrowsException() Throws(() => _sut.Extract(submodel, "Prop.Child")); } - - [Fact] - public void ExtractElement_NullElement_ThrowsArgumentNullException() => Throws(() => _sut.ExtractElement(null!)); - - [Fact] - public void ExtractElement_HandlerReturnsNull_CreatesFallbackLeaf() - { - var element = Substitute.For(); - element.IdShort.Returns("Test"); - _resolver.ResolveElementSemanticId(element, "Test").Returns("http://test/element"); - _resolver.GetValueType(element).Returns(DataType.String); - _resolver.GetCardinality(element).Returns(Cardinality.ZeroToOne); - - var result = _sut.ExtractElement(element); - - NotNull(result); - var leaf = IsType(result); - Equal("http://test/element", leaf.SemanticId); - Equal(DataType.String, leaf.DataType); - Equal(Cardinality.ZeroToOne, leaf.Cardinality); - } } From 91a1d0e781b51a8ca28c8119daef444d3738ca4a Mon Sep 17 00:00:00 2001 From: Kevalkumar Date: Thu, 19 Mar 2026 00:24:26 +0530 Subject: [PATCH 07/71] change dataengine image --- example/docker-compose.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/example/docker-compose.yml b/example/docker-compose.yml index a7343777..196ce776 100644 --- a/example/docker-compose.yml +++ b/example/docker-compose.yml @@ -23,7 +23,7 @@ services: - twinengine-network twinengine-dataengine: - image: dataengine:1.0.0 + image: ghcr.io/aas-twinengine/dataengine:v1.0.0 container_name: twinengine-dataengine depends_on: dpp-plugin: From 1501c54a9178dba423d28336e13ac06c2afa3fec Mon Sep 17 00:00:00 2001 From: Kevalkumar Date: Thu, 19 Mar 2026 00:30:53 +0530 Subject: [PATCH 08/71] remove cognitive complexity inside submodel filler --- example/docker-compose.yml | 2 +- .../SemanticId/FillOut/SubmodelFiller.cs | 49 ++++++++++--------- 2 files changed, 28 insertions(+), 23 deletions(-) diff --git a/example/docker-compose.yml b/example/docker-compose.yml index 196ce776..1c02d944 100644 --- a/example/docker-compose.yml +++ b/example/docker-compose.yml @@ -23,7 +23,7 @@ services: - twinengine-network twinengine-dataengine: - image: ghcr.io/aas-twinengine/dataengine:v1.0.0 + image: ghcr.io/aas-twinengine/dataengine:v1.0.0 container_name: twinengine-dataengine depends_on: dpp-plugin: diff --git a/source/AAS.TwinEngine.DataEngine/ApplicationLogic/Services/SubmodelRepository/SemanticId/FillOut/SubmodelFiller.cs b/source/AAS.TwinEngine.DataEngine/ApplicationLogic/Services/SubmodelRepository/SemanticId/FillOut/SubmodelFiller.cs index d16b2b12..1cbb9b21 100644 --- a/source/AAS.TwinEngine.DataEngine/ApplicationLogic/Services/SubmodelRepository/SemanticId/FillOut/SubmodelFiller.cs +++ b/source/AAS.TwinEngine.DataEngine/ApplicationLogic/Services/SubmodelRepository/SemanticId/FillOut/SubmodelFiller.cs @@ -99,39 +99,26 @@ internal void FillOutSubmodelElementValue(List elements, Seman { var semanticTreeNodes = GetSemanticNodes(element, values); - if (semanticTreeNodes == null || semanticTreeNodes.Count == 0) + if (ShouldSkipElement(semanticTreeNodes, element, elements)) { continue; } - if (HasMixedNodeTypes(semanticTreeNodes, element, elements)) + if (ShouldCloneElements(semanticTreeNodes, element)) { + ReplaceWithClones(elements, element, semanticTreeNodes, updateIdShort); continue; } - if (semanticTreeNodes.Count > 1 && element is not Property && element is not ReferenceElement) - { - _ = elements.Remove(element); - for (var i = 0; i < semanticTreeNodes.Count; i++) - { - var cloned = elementHelper.CloneElement(element); - if (updateIdShort) - { - cloned.IdShort = $"{cloned.IdShort}{i}"; - } - - _ = FillOutElement(cloned, semanticTreeNodes[i]); - elements.Add(cloned); - } - } - else - { - _ = FillOutElement(element, semanticTreeNodes[0]); - } + _ = FillOutElement(element, semanticTreeNodes[0]); } } - private List? GetSemanticNodes( ISubmodelElement element, SemanticTreeNode values) + private bool ShouldSkipElement(List? nodes, ISubmodelElement element, List elements) => nodes == null || nodes.Count == 0 || HasMixedNodeTypes(nodes, element, elements); + + private static bool ShouldCloneElements(List nodes, ISubmodelElement element) => nodes.Count > 1 && element is not Property && element is not ReferenceElement; + + private List? GetSemanticNodes(ISubmodelElement element, SemanticTreeNode values) { var valueNode = SemanticTreeNavigator.FindNodeBySemanticId(values, semanticIdResolver.ExtractSemanticId(element)); @@ -150,4 +137,22 @@ private bool HasMixedNodeTypes(List nodes, ISubmodelElement el _ = elements.Remove(element); return true; } + + private void ReplaceWithClones(List elements, ISubmodelElement element, List nodes, bool updateIdShort) + { + _ = elements.Remove(element); + + for (var i = 0; i < nodes.Count; i++) + { + var cloned = elementHelper.CloneElement(element); + + if (updateIdShort) + { + cloned.IdShort = $"{cloned.IdShort}{i}"; + } + + _ = FillOutElement(cloned, nodes[i]); + elements.Add(cloned); + } + } } From 30fc78ebbd45cc6df7c32f5d76c27cc257694726 Mon Sep 17 00:00:00 2001 From: Kevalkumar Date: Thu, 19 Mar 2026 00:32:27 +0530 Subject: [PATCH 09/71] remove unused using --- .../Services/SubmodelRepository/SubmodelRepositoryService.cs | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/source/AAS.TwinEngine.DataEngine/ApplicationLogic/Services/SubmodelRepository/SubmodelRepositoryService.cs b/source/AAS.TwinEngine.DataEngine/ApplicationLogic/Services/SubmodelRepository/SubmodelRepositoryService.cs index f4e77d61..875b86b2 100644 --- a/source/AAS.TwinEngine.DataEngine/ApplicationLogic/Services/SubmodelRepository/SubmodelRepositoryService.cs +++ b/source/AAS.TwinEngine.DataEngine/ApplicationLogic/Services/SubmodelRepository/SubmodelRepositoryService.cs @@ -1,6 +1,4 @@ -using System.Diagnostics; - -using AAS.TwinEngine.DataEngine.ApplicationLogic.Exceptions.Application; +using AAS.TwinEngine.DataEngine.ApplicationLogic.Exceptions.Application; using AAS.TwinEngine.DataEngine.ApplicationLogic.Exceptions.Infrastructure; using AAS.TwinEngine.DataEngine.ApplicationLogic.Services.Plugin; From b7ee7c5b72e405943bb2628b76b09e84ac71e059 Mon Sep 17 00:00:00 2001 From: Kevalkumar Date: Fri, 20 Mar 2026 13:27:55 +0530 Subject: [PATCH 10/71] Added internal semantic id removal logic from submodel qualifiers --- .../ReferenceElementHandlerTests.cs | 6 +- .../RelationshipElementHandlerTests.cs | 4 +- .../SemanticId/FillOut/SubmodelFillerTests.cs | 333 ++++++++++++++++++ .../ReferenceElementHandler.cs | 4 +- .../RelationshipElementHandler.cs | 2 +- .../SemanticId/FillOut/SubmodelFiller.cs | 48 ++- .../Helpers/Interfaces/ISemanticIdResolver.cs | 2 + .../SemanticId/Helpers/SemanticIdResolver.cs | 6 +- .../Helpers/SemanticTreeNavigator.cs | 4 +- .../SubmodelRepository/SemanticIdHandler.cs | 2 +- 10 files changed, 388 insertions(+), 23 deletions(-) diff --git a/source/AAS.TwinEngine.DataEngine.UnitTests/ApplicationLogic/Services/SubmodelRepository/SemanticId/ElementHandlers/ReferenceElementHandlerTests.cs b/source/AAS.TwinEngine.DataEngine.UnitTests/ApplicationLogic/Services/SubmodelRepository/SemanticId/ElementHandlers/ReferenceElementHandlerTests.cs index e56a58d0..5fb61b36 100644 --- a/source/AAS.TwinEngine.DataEngine.UnitTests/ApplicationLogic/Services/SubmodelRepository/SemanticId/ElementHandlers/ReferenceElementHandlerTests.cs +++ b/source/AAS.TwinEngine.DataEngine.UnitTests/ApplicationLogic/Services/SubmodelRepository/SemanticId/ElementHandlers/ReferenceElementHandlerTests.cs @@ -90,7 +90,7 @@ public void FillOut_WithModelReference_DelegatesToReferenceHelper() var modelRef = new Reference(ReferenceTypes.ModelReference, [new Key(KeyTypes.Submodel, "")]); var refElement = new ReferenceElement(idShort: "Test", value: modelRef); - _resolver.GetSemanticId(refElement).Returns("http://test/ref"); + _resolver.ExtractSemanticId(refElement).Returns("http://test/ref"); var values = new SemanticBranchNode("http://test/ref", Cardinality.One); @@ -103,7 +103,7 @@ public void FillOut_WithModelReference_DelegatesToReferenceHelper() public void FillOut_WithNullValue_LogsInfoAndSkips() { var refElement = new ReferenceElement(idShort: "Test", value: null); - _resolver.GetSemanticId(refElement).Returns("http://test/ref"); + _resolver.ExtractSemanticId(refElement).Returns("http://test/ref"); var values = new SemanticBranchNode("http://test/ref", Cardinality.One); @@ -119,7 +119,7 @@ public void FillOut_WithExternalReference_LogsInfoAndSkips() var externalRef = new Reference(ReferenceTypes.ExternalReference, [new Key(KeyTypes.GlobalReference, "http://external")]); var refElement = new ReferenceElement(idShort: "Test", value: externalRef); - _resolver.GetSemanticId(refElement).Returns("http://test/ref"); + _resolver.ExtractSemanticId(refElement).Returns("http://test/ref"); var values = new SemanticBranchNode("http://test/ref", Cardinality.One); diff --git a/source/AAS.TwinEngine.DataEngine.UnitTests/ApplicationLogic/Services/SubmodelRepository/SemanticId/ElementHandlers/RelationshipElementHandlerTests.cs b/source/AAS.TwinEngine.DataEngine.UnitTests/ApplicationLogic/Services/SubmodelRepository/SemanticId/ElementHandlers/RelationshipElementHandlerTests.cs index 27f40c61..655fda68 100644 --- a/source/AAS.TwinEngine.DataEngine.UnitTests/ApplicationLogic/Services/SubmodelRepository/SemanticId/ElementHandlers/RelationshipElementHandlerTests.cs +++ b/source/AAS.TwinEngine.DataEngine.UnitTests/ApplicationLogic/Services/SubmodelRepository/SemanticId/ElementHandlers/RelationshipElementHandlerTests.cs @@ -64,7 +64,7 @@ public void Extract_FirstModelReference_ExtractsFirstAndDelegatesToReferenceHelp var firstRef = new Reference(ReferenceTypes.ModelReference, [new Key(KeyTypes.Submodel, "sub")]); var secondRef = new Reference(ReferenceTypes.ExternalReference, [new Key(KeyTypes.GlobalReference, "ext")]); var rel = new RelationshipElement(first: firstRef, second: secondRef, idShort: "Test"); - _resolver.GetSemanticId(rel).Returns("http://test/rel"); + _resolver.ExtractSemanticId(rel).Returns("http://test/rel"); _resolver.GetCardinality(rel).Returns(Cardinality.One); var firstNode = new SemanticBranchNode("http://test/rel_first", Cardinality.One); @@ -88,7 +88,7 @@ public void Extract_BothModelReferences_ExtractsBoth() var firstRef = new Reference(ReferenceTypes.ModelReference, [new Key(KeyTypes.Submodel, "sub1")]); var secondRef = new Reference(ReferenceTypes.ModelReference, [new Key(KeyTypes.Submodel, "sub2")]); var rel = new RelationshipElement(first: firstRef, second: secondRef, idShort: "Test"); - _resolver.GetSemanticId(rel).Returns("http://test/rel"); + _resolver.ExtractSemanticId(rel).Returns("http://test/rel"); _resolver.GetCardinality(rel).Returns(Cardinality.One); var firstNode = new SemanticBranchNode("http://test/rel_first", Cardinality.One); diff --git a/source/AAS.TwinEngine.DataEngine.UnitTests/ApplicationLogic/Services/SubmodelRepository/SemanticId/FillOut/SubmodelFillerTests.cs b/source/AAS.TwinEngine.DataEngine.UnitTests/ApplicationLogic/Services/SubmodelRepository/SemanticId/FillOut/SubmodelFillerTests.cs index bf407fd0..f6741025 100644 --- a/source/AAS.TwinEngine.DataEngine.UnitTests/ApplicationLogic/Services/SubmodelRepository/SemanticId/FillOut/SubmodelFillerTests.cs +++ b/source/AAS.TwinEngine.DataEngine.UnitTests/ApplicationLogic/Services/SubmodelRepository/SemanticId/FillOut/SubmodelFillerTests.cs @@ -114,4 +114,337 @@ public void FillOutElement_WithMatchingHandler_DelegatesToHandler() handler.Received(1).FillOut(element, values, Arg.Any, SemanticTreeNode, bool>>()); } + + [Fact] + public void FillOutTemplate_RemovesInternalSemanticIdQualifier_FromProperty() + { + var property = new Property( + idShort: "Prop", + valueType: DataTypeDefXsd.String, + qualifiers: [ + new Qualifier(type: "InternalSemanticId", valueType: DataTypeDefXsd.String, value: "http://test/internal") + ]); + var submodel = Substitute.For(); + var elements = new List { property }; + submodel.SubmodelElements.Returns(elements); + _resolver.ExtractSemanticId(property).Returns("http://test/prop"); + _resolver.InternalSemanticIdType.Returns("InternalSemanticId"); + + var values = new SemanticBranchNode("root", Cardinality.Unknown); + + _sut.FillOutTemplate(submodel, values); + + Empty(property.Qualifiers!); + } + + [Fact] + public void FillOutTemplate_PreservesNonInternalQualifiers_WhenRemovingInternalSemanticId() + { + var property = new Property( + idShort: "Prop", + valueType: DataTypeDefXsd.String, + qualifiers: [ + new Qualifier(type: "ExternalReference", valueType: DataTypeDefXsd.String, value: "ZeroToOne"), + new Qualifier(type: "InternalSemanticId", valueType: DataTypeDefXsd.String, value: "http://test/internal") + ]); + var submodel = Substitute.For(); + var elements = new List { property }; + submodel.SubmodelElements.Returns(elements); + _resolver.ExtractSemanticId(property).Returns("http://test/prop"); + _resolver.InternalSemanticIdType.Returns("InternalSemanticId"); + + var values = new SemanticBranchNode("root", Cardinality.Unknown); + + _sut.FillOutTemplate(submodel, values); + + Single(property.Qualifiers!); + Equal("ExternalReference", property.Qualifiers[0].Type); + } + + [Fact] + public void FillOutTemplate_RemovesInternalSemanticIdQualifier_FromNestedCollection() + { + var innerProperty = new Property( + idShort: "InnerProp", + valueType: DataTypeDefXsd.String, + qualifiers: [ + new Qualifier(type: "InternalSemanticId", valueType: DataTypeDefXsd.String, value: "http://test/inner") + ]); + var collection = new SubmodelElementCollection( + idShort: "Collection", + qualifiers: [ + new Qualifier(type: "InternalSemanticId", valueType: DataTypeDefXsd.String, value: "http://test/collection") + ], + value: [innerProperty]); + + var submodel = Substitute.For(); + var elements = new List { collection }; + submodel.SubmodelElements.Returns(elements); + _resolver.ExtractSemanticId(collection).Returns("http://test/collection"); + _resolver.InternalSemanticIdType.Returns("InternalSemanticId"); + + var values = new SemanticBranchNode("root", Cardinality.Unknown); + + _sut.FillOutTemplate(submodel, values); + + Empty(collection.Qualifiers!); + Empty(innerProperty.Qualifiers!); + } + + [Fact] + public void FillOutTemplate_RemovesInternalSemanticIdQualifier_FromNestedSubmodelElementList() + { + var innerProperty = new Property( + idShort: "ListItem", + valueType: DataTypeDefXsd.String, + qualifiers: [ + new Qualifier(type: "InternalSemanticId", valueType: DataTypeDefXsd.String, value: "http://test/item") + ]); + var list = new SubmodelElementList( + AasSubmodelElements.Property, + idShort: "List", + qualifiers: [ + new Qualifier(type: "InternalSemanticId", valueType: DataTypeDefXsd.String, value: "http://test/list") + ], + value: [innerProperty]); + + var submodel = Substitute.For(); + var elements = new List { list }; + submodel.SubmodelElements.Returns(elements); + _resolver.ExtractSemanticId(list).Returns("http://test/list"); + _resolver.InternalSemanticIdType.Returns("InternalSemanticId"); + + var values = new SemanticBranchNode("root", Cardinality.Unknown); + + _sut.FillOutTemplate(submodel, values); + + Empty(list.Qualifiers!); + Empty(innerProperty.Qualifiers!); + } + + [Fact] + public void FillOutTemplate_RemovesInternalSemanticIdQualifier_FromNestedEntity() + { + var statement = new Property( + idShort: "Statement", + valueType: DataTypeDefXsd.String, + qualifiers: [ + new Qualifier(type: "InternalSemanticId", valueType: DataTypeDefXsd.String, value: "http://test/statement") + ]); + var entity = new Entity( + EntityType.CoManagedEntity, + idShort: "Entity", + qualifiers: [ + new Qualifier(type: "InternalSemanticId", valueType: DataTypeDefXsd.String, value: "http://test/entity") + ], + statements: [statement]); + + var submodel = Substitute.For(); + var elements = new List { entity }; + submodel.SubmodelElements.Returns(elements); + _resolver.ExtractSemanticId(entity).Returns("http://test/entity"); + _resolver.InternalSemanticIdType.Returns("InternalSemanticId"); + + var values = new SemanticBranchNode("root", Cardinality.Unknown); + + _sut.FillOutTemplate(submodel, values); + + Empty(entity.Qualifiers!); + Empty(statement.Qualifiers!); + } + + [Fact] + public void FillOutTemplate_ElementWithNoQualifiers_DoesNotThrow() + { + var property = new Property(idShort: "Prop", valueType: DataTypeDefXsd.String); + var submodel = Substitute.For(); + var elements = new List { property }; + submodel.SubmodelElements.Returns(elements); + _resolver.ExtractSemanticId(property).Returns("http://test/prop"); + _resolver.InternalSemanticIdType.Returns("InternalSemanticId"); + + var values = new SemanticBranchNode("root", Cardinality.Unknown); + + var result = _sut.FillOutTemplate(submodel, values); + + NotNull(result); + Null(property.Qualifiers); + } + + [Fact] + public void FillOutTemplate_ElementWithOnlyNonInternalQualifiers_PreservesAll() + { + var property = new Property( + idShort: "Prop", + valueType: DataTypeDefXsd.String, + qualifiers: [ + new Qualifier(type: "ExternalReference", valueType: DataTypeDefXsd.String, value: "ZeroToOne"), + new Qualifier(type: "OtherQualifier", valueType: DataTypeDefXsd.String, value: "SomeValue") + ]); + var submodel = Substitute.For(); + var elements = new List { property }; + submodel.SubmodelElements.Returns(elements); + _resolver.ExtractSemanticId(property).Returns("http://test/prop"); + _resolver.InternalSemanticIdType.Returns("InternalSemanticId"); + + var values = new SemanticBranchNode("root", Cardinality.Unknown); + + _sut.FillOutTemplate(submodel, values); + + Equal(2, property.Qualifiers!.Count); + } + + [Fact] + public void FillOutTemplate_DeeplyNestedElements_RemovesInternalSemanticIdAtAllLevels() + { + var deepProperty = new Property( + idShort: "DeepProp", + valueType: DataTypeDefXsd.String, + qualifiers: [ + new Qualifier(type: "InternalSemanticId", valueType: DataTypeDefXsd.String, value: "http://test/deep") + ]); + var innerCollection = new SubmodelElementCollection( + idShort: "InnerCollection", + qualifiers: [ + new Qualifier(type: "InternalSemanticId", valueType: DataTypeDefXsd.String, value: "http://test/inner") + ], + value: [deepProperty]); + var outerCollection = new SubmodelElementCollection( + idShort: "OuterCollection", + qualifiers: [ + new Qualifier(type: "InternalSemanticId", valueType: DataTypeDefXsd.String, value: "http://test/outer") + ], + value: [innerCollection]); + + var submodel = Substitute.For(); + var elements = new List { outerCollection }; + submodel.SubmodelElements.Returns(elements); + _resolver.ExtractSemanticId(outerCollection).Returns("http://test/outer"); + _resolver.InternalSemanticIdType.Returns("InternalSemanticId"); + + var values = new SemanticBranchNode("root", Cardinality.Unknown); + + _sut.FillOutTemplate(submodel, values); + + Empty(outerCollection.Qualifiers!); + Empty(innerCollection.Qualifiers!); + Empty(deepProperty.Qualifiers!); + } + + [Fact] + public void FillOutTemplate_MultipleElementsWithInternalSemanticId_RemovesFromAll() + { + var prop1 = new Property( + idShort: "Prop1", + valueType: DataTypeDefXsd.String, + qualifiers: [ + new Qualifier(type: "InternalSemanticId", valueType: DataTypeDefXsd.String, value: "http://test/1") + ]); + var prop2 = new Property( + idShort: "Prop2", + valueType: DataTypeDefXsd.String, + qualifiers: [ + new Qualifier(type: "InternalSemanticId", valueType: DataTypeDefXsd.String, value: "http://test/2") + ]); + var submodel = Substitute.For(); + var elements = new List { prop1, prop2 }; + submodel.SubmodelElements.Returns(elements); + _resolver.ExtractSemanticId(prop1).Returns("http://test/1"); + _resolver.ExtractSemanticId(prop2).Returns("http://test/2"); + _resolver.InternalSemanticIdType.Returns("InternalSemanticId"); + + var values = new SemanticBranchNode("root", Cardinality.Unknown); + + _sut.FillOutTemplate(submodel, values); + + Empty(prop1.Qualifiers!); + Empty(prop2.Qualifiers!); + } + + [Fact] + public void FillOutTemplate_TwoQualifiers_RemovesOnlyInternalSemanticId() + { + var property = new Property( + idShort: "Prop", + valueType: DataTypeDefXsd.String, + qualifiers: [ + new Qualifier(type: "InternalSemanticId", valueType: DataTypeDefXsd.String, value: "http://test/internal"), + new Qualifier(type: "ExternalReference", valueType: DataTypeDefXsd.String, value: "ZeroToOne") + ]); + var submodel = Substitute.For(); + var elements = new List { property }; + submodel.SubmodelElements.Returns(elements); + _resolver.ExtractSemanticId(property).Returns("http://test/prop"); + _resolver.InternalSemanticIdType.Returns("InternalSemanticId"); + + var values = new SemanticBranchNode("root", Cardinality.Unknown); + + _sut.FillOutTemplate(submodel, values); + + Single(property.Qualifiers!); + Equal("ExternalReference", property.Qualifiers[0].Type); + Equal("ZeroToOne", property.Qualifiers[0].Value); + DoesNotContain(property.Qualifiers, q => q.Type == "InternalSemanticId"); + } + + [Fact] + public void FillOutTemplate_ReferenceElementWithTwoQualifiers_RemovesOnlyInternalSemanticId() + { + var refElement = new ReferenceElement( + idShort: "RefElement", + qualifiers: [ + new Qualifier(type: "InternalSemanticId", valueType: DataTypeDefXsd.String, value: "http://test/ref-internal"), + new Qualifier(type: "ExternalReference", valueType: DataTypeDefXsd.String, value: "ZeroToOne") + ], + value: new Reference( + ReferenceTypes.ExternalReference, + [new Key(KeyTypes.GlobalReference, "http://example.com/ref")])); + + var submodel = Substitute.For(); + var elements = new List { refElement }; + submodel.SubmodelElements.Returns(elements); + _resolver.ExtractSemanticId(refElement).Returns("http://test/ref"); + _resolver.InternalSemanticIdType.Returns("InternalSemanticId"); + + var values = new SemanticBranchNode("root", Cardinality.Unknown); + + _sut.FillOutTemplate(submodel, values); + + Single(refElement.Qualifiers!); + Equal("ExternalReference", refElement.Qualifiers![0].Type); + Equal("ZeroToOne", refElement.Qualifiers[0].Value); + DoesNotContain(refElement.Qualifiers, q => q.Type == "InternalSemanticId"); + } + + [Fact] + public void FillOutTemplate_RelationshipElementWithTwoQualifiers_RemovesOnlyInternalSemanticId() + { + var relationship = new RelationshipElement( + first: new Reference( + ReferenceTypes.ExternalReference, + [new Key(KeyTypes.GlobalReference, "http://example.com/first")]), + second: new Reference( + ReferenceTypes.ExternalReference, + [new Key(KeyTypes.GlobalReference, "http://example.com/second")]), + idShort: "RelElement", + qualifiers: [ + new Qualifier(type: "InternalSemanticId", valueType: DataTypeDefXsd.String, value: "http://test/rel-internal"), + new Qualifier(type: "ExternalReference", valueType: DataTypeDefXsd.String, value: "One") + ]); + + var submodel = Substitute.For(); + var elements = new List { relationship }; + submodel.SubmodelElements.Returns(elements); + _resolver.ExtractSemanticId(relationship).Returns("http://test/rel"); + _resolver.InternalSemanticIdType.Returns("InternalSemanticId"); + + var values = new SemanticBranchNode("root", Cardinality.Unknown); + + _sut.FillOutTemplate(submodel, values); + + Single(relationship.Qualifiers!); + Equal("ExternalReference", relationship.Qualifiers![0].Type); + Equal("One", relationship.Qualifiers[0].Value); + DoesNotContain(relationship.Qualifiers, q => q.Type == "InternalSemanticId"); + } } diff --git a/source/AAS.TwinEngine.DataEngine/ApplicationLogic/Services/SubmodelRepository/SemanticId/ElementHandlers/ReferenceElementHandler.cs b/source/AAS.TwinEngine.DataEngine/ApplicationLogic/Services/SubmodelRepository/SemanticId/ElementHandlers/ReferenceElementHandler.cs index cd195553..5491caf9 100644 --- a/source/AAS.TwinEngine.DataEngine/ApplicationLogic/Services/SubmodelRepository/SemanticId/ElementHandlers/ReferenceElementHandler.cs +++ b/source/AAS.TwinEngine.DataEngine/ApplicationLogic/Services/SubmodelRepository/SemanticId/ElementHandlers/ReferenceElementHandler.cs @@ -33,10 +33,10 @@ public void FillOut(ISubmodelElement element, SemanticTreeNode values, Action
  • matchingNodes, - ISubmodelElement baseElement, - ISubmodel submodelTemplate) + private void RemoveInternalSemanticIdQualifiers(IEnumerable? elements) + { + if (elements == null) + { + return; + } + + foreach (var element in elements) + { + if (element.Qualifiers != null) + { + var internalQualifiers = element.Qualifiers + .Where(q => q.Type == semanticIdResolver.InternalSemanticIdType) + .ToList(); + + foreach (var qualifier in internalQualifiers) + { + _ = element.Qualifiers.Remove(qualifier); + } + } + + switch (element) + { + case SubmodelElementCollection collection: + RemoveInternalSemanticIdQualifiers(collection.Value); + break; + case SubmodelElementList list: + RemoveInternalSemanticIdQualifiers(list.Value); + break; + case Entity entity: + RemoveInternalSemanticIdQualifiers(entity.Statements); + break; + } + } + } + + private void HandleMultipleMatchingNodes(List matchingNodes, ISubmodelElement baseElement, ISubmodel submodelTemplate) { for (var i = 0; i < matchingNodes.Count; i++) { @@ -67,10 +102,7 @@ private void HandleMultipleMatchingNodes( } } - private void HandleSingleMatchingNode( - SemanticTreeNode node, - ISubmodelElement element, - ISubmodel submodelTemplate) + private void HandleSingleMatchingNode(SemanticTreeNode node, ISubmodelElement element, ISubmodel submodelTemplate) { _ = FillOutElement(element, node); submodelTemplate.SubmodelElements?.Add(element); diff --git a/source/AAS.TwinEngine.DataEngine/ApplicationLogic/Services/SubmodelRepository/SemanticId/Helpers/Interfaces/ISemanticIdResolver.cs b/source/AAS.TwinEngine.DataEngine/ApplicationLogic/Services/SubmodelRepository/SemanticId/Helpers/Interfaces/ISemanticIdResolver.cs index 379365ac..b664d33c 100644 --- a/source/AAS.TwinEngine.DataEngine/ApplicationLogic/Services/SubmodelRepository/SemanticId/Helpers/Interfaces/ISemanticIdResolver.cs +++ b/source/AAS.TwinEngine.DataEngine/ApplicationLogic/Services/SubmodelRepository/SemanticId/Helpers/Interfaces/ISemanticIdResolver.cs @@ -8,6 +8,8 @@ public interface ISemanticIdResolver { string MlpPostFixSeparator { get; } + string InternalSemanticIdType { get; } + string GetSemanticId(IHasSemantics hasSemantics); string ExtractSemanticId(ISubmodelElement element); diff --git a/source/AAS.TwinEngine.DataEngine/ApplicationLogic/Services/SubmodelRepository/SemanticId/Helpers/SemanticIdResolver.cs b/source/AAS.TwinEngine.DataEngine/ApplicationLogic/Services/SubmodelRepository/SemanticId/Helpers/SemanticIdResolver.cs index eb9990a6..df91ff9e 100644 --- a/source/AAS.TwinEngine.DataEngine/ApplicationLogic/Services/SubmodelRepository/SemanticId/Helpers/SemanticIdResolver.cs +++ b/source/AAS.TwinEngine.DataEngine/ApplicationLogic/Services/SubmodelRepository/SemanticId/Helpers/SemanticIdResolver.cs @@ -20,12 +20,12 @@ public partial class SemanticIdResolver(IOptions semantics) : ISemant public const string EntityGlobalAssetIdPostFix = "_globalAssetId"; public const string RelationshipElementFirstPostFixSeparator = "_first"; public const string RelationshipElementSecondPostFixSeparator = "_second"; - - private readonly string _internalSemanticId = semantics.Value.InternalSemanticId; private readonly string _submodelElementIndexContextPrefix = semantics.Value.SubmodelElementIndexContextPrefix; public string MlpPostFixSeparator { get; } = semantics.Value.MultiLanguageSemanticPostfixSeparator; + public string InternalSemanticIdType { get; } = semantics.Value.InternalSemanticId; + private static readonly HashSet StringTypes = [ DataTypeDefXsd.String, DataTypeDefXsd.AnyUri, DataTypeDefXsd.Byte, DataTypeDefXsd.Date, @@ -56,7 +56,7 @@ public string ExtractSemanticId(ISubmodelElement element) return GetSemanticId(element); } - var qualifier = element.Qualifiers.FirstOrDefault(q => q.Type == _internalSemanticId); + var qualifier = element.Qualifiers.FirstOrDefault(q => q.Type == InternalSemanticIdType); return qualifier != null ? qualifier.Value! : GetSemanticId(element); } diff --git a/source/AAS.TwinEngine.DataEngine/ApplicationLogic/Services/SubmodelRepository/SemanticId/Helpers/SemanticTreeNavigator.cs b/source/AAS.TwinEngine.DataEngine/ApplicationLogic/Services/SubmodelRepository/SemanticId/Helpers/SemanticTreeNavigator.cs index 69c03914..aa20ac5f 100644 --- a/source/AAS.TwinEngine.DataEngine/ApplicationLogic/Services/SubmodelRepository/SemanticId/Helpers/SemanticTreeNavigator.cs +++ b/source/AAS.TwinEngine.DataEngine/ApplicationLogic/Services/SubmodelRepository/SemanticId/Helpers/SemanticTreeNavigator.cs @@ -8,9 +8,7 @@ public static IEnumerable FindBranchNodesBySemanticId(Semantic { var node = tree as SemanticBranchNode; - return node?.Children! - .Where(child => child.SemanticId.Equals(semanticId, StringComparison.Ordinal)) - ?? []; + return node?.Children!.Where(child => child.SemanticId.Equals(semanticId, StringComparison.Ordinal)) ?? []; } public static IEnumerable FindNodeBySemanticId(SemanticTreeNode tree, string semanticId) diff --git a/source/AAS.TwinEngine.DataEngine/ApplicationLogic/Services/SubmodelRepository/SemanticIdHandler.cs b/source/AAS.TwinEngine.DataEngine/ApplicationLogic/Services/SubmodelRepository/SemanticIdHandler.cs index 8711f051..50f41348 100644 --- a/source/AAS.TwinEngine.DataEngine/ApplicationLogic/Services/SubmodelRepository/SemanticIdHandler.cs +++ b/source/AAS.TwinEngine.DataEngine/ApplicationLogic/Services/SubmodelRepository/SemanticIdHandler.cs @@ -15,4 +15,4 @@ public class SemanticIdHandler( public ISubmodelElement Extract(ISubmodel submodelTemplate, string idShortPath) => extractor.Extract(submodelTemplate, idShortPath); public ISubmodel FillOutTemplate(ISubmodel submodelTemplate, SemanticTreeNode values) => filler.FillOutTemplate(submodelTemplate, values); -} +} \ No newline at end of file From c8df6feccde61232d5ddfefa05706fda5a4cdd38 Mon Sep 17 00:00:00 2001 From: Kevalkumar Date: Fri, 20 Mar 2026 13:59:18 +0530 Subject: [PATCH 11/71] remove unused var --- .../Services/SubmodelRepository/SubmodelRepositoryService.cs | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/source/AAS.TwinEngine.DataEngine/ApplicationLogic/Services/SubmodelRepository/SubmodelRepositoryService.cs b/source/AAS.TwinEngine.DataEngine/ApplicationLogic/Services/SubmodelRepository/SubmodelRepositoryService.cs index 875b86b2..5a69ef25 100644 --- a/source/AAS.TwinEngine.DataEngine/ApplicationLogic/Services/SubmodelRepository/SubmodelRepositoryService.cs +++ b/source/AAS.TwinEngine.DataEngine/ApplicationLogic/Services/SubmodelRepository/SubmodelRepositoryService.cs @@ -48,9 +48,7 @@ private async Task BuildSubmodelWithValuesAsync(ISubmodel template, s var values = await pluginDataHandler.TryGetValuesAsync(pluginManifests, semanticIds, submodelId, cancellationToken).ConfigureAwait(false); - var result = semanticIdHandler.FillOutTemplate(template, values); - - return result; + return semanticIdHandler.FillOutTemplate(template, values); } private static async Task ExecuteWithExceptionHandlingAsync(Func> action) From dd01f18b4de3166c9a94c4d6619097c8fe33a7f4 Mon Sep 17 00:00:00 2001 From: Kevalkumar Date: Fri, 20 Mar 2026 14:45:03 +0530 Subject: [PATCH 12/71] remove internal semantic from test data --- ...ntactInfo_ContactInformation_Expected.json | 18 +----------------- .../GetSubmodel_ContactInfo_Expected.json | 19 ++----------------- .../GetSubmodel_Nameplate_Expected.json | 18 +----------------- 3 files changed, 4 insertions(+), 51 deletions(-) diff --git a/source/AAS.TwinEngine.Plugin.TestPlugin.PlaywrightTests/SubmodelRepository/TestData/GetSubmodelElement_ContactInfo_ContactInformation_Expected.json b/source/AAS.TwinEngine.Plugin.TestPlugin.PlaywrightTests/SubmodelRepository/TestData/GetSubmodelElement_ContactInfo_ContactInformation_Expected.json index b55a8eeb..b481d47c 100644 --- a/source/AAS.TwinEngine.Plugin.TestPlugin.PlaywrightTests/SubmodelRepository/TestData/GetSubmodelElement_ContactInfo_ContactInformation_Expected.json +++ b/source/AAS.TwinEngine.Plugin.TestPlugin.PlaywrightTests/SubmodelRepository/TestData/GetSubmodelElement_ContactInfo_ContactInformation_Expected.json @@ -64,29 +64,13 @@ } ] }, - "qualifiers": [ + "qualifiers": { "kind": "ConceptQualifier", "type": "Multiplicity", "valueType": "xs:string", "value": "ZeroToOne" }, - { - "semanticId": { - "type": "ExternalReference", - "keys": [ - { - "type": "GlobalReference", - "value": "https://mm-software.com/twinengine/qualifier" - } - ] - }, - "kind": "TemplateQualifier", - "type": "InternalSemanticId", - "valueType": "xs:string", - "value": "https://mm-software.com/AddressOfAdditionalLink" - } - ], "valueType": "xs:string", "value": "https://www.mm-software.com/more-the-newsroom/", "modelType": "Property" diff --git a/source/AAS.TwinEngine.Plugin.TestPlugin.PlaywrightTests/SubmodelRepository/TestData/GetSubmodel_ContactInfo_Expected.json b/source/AAS.TwinEngine.Plugin.TestPlugin.PlaywrightTests/SubmodelRepository/TestData/GetSubmodel_ContactInfo_Expected.json index 656b7c2a..6f55a3dc 100644 --- a/source/AAS.TwinEngine.Plugin.TestPlugin.PlaywrightTests/SubmodelRepository/TestData/GetSubmodel_ContactInfo_Expected.json +++ b/source/AAS.TwinEngine.Plugin.TestPlugin.PlaywrightTests/SubmodelRepository/TestData/GetSubmodel_ContactInfo_Expected.json @@ -78,29 +78,14 @@ } ] }, - "qualifiers": [ + "qualifiers": { "kind": "ConceptQualifier", "type": "Multiplicity", "valueType": "xs:string", "value": "ZeroToOne" - }, - { - "semanticId": { - "type": "ExternalReference", - "keys": [ - { - "type": "GlobalReference", - "value": "https://mm-software.com/twinengine/qualifier" - } - ] - }, - "kind": "TemplateQualifier", - "type": "InternalSemanticId", - "valueType": "xs:string", - "value": "https://mm-software.com/AddressOfAdditionalLink" } - ], + , "valueType": "xs:string", "value": "https://www.mm-software.com/mobile-arbeitsmaschinen/", "modelType": "Property" diff --git a/source/AAS.TwinEngine.Plugin.TestPlugin.PlaywrightTests/SubmodelRepository/TestData/GetSubmodel_Nameplate_Expected.json b/source/AAS.TwinEngine.Plugin.TestPlugin.PlaywrightTests/SubmodelRepository/TestData/GetSubmodel_Nameplate_Expected.json index 4a5ed6c2..824cf788 100644 --- a/source/AAS.TwinEngine.Plugin.TestPlugin.PlaywrightTests/SubmodelRepository/TestData/GetSubmodel_Nameplate_Expected.json +++ b/source/AAS.TwinEngine.Plugin.TestPlugin.PlaywrightTests/SubmodelRepository/TestData/GetSubmodel_Nameplate_Expected.json @@ -1312,7 +1312,7 @@ } ] }, - "qualifiers": [ + "qualifiers": { "semanticId": { "type": "ExternalReference", @@ -1328,22 +1328,6 @@ "valueType": "xs:string", "value": "ZeroToMany" }, - { - "semanticId": { - "type": "ExternalReference", - "keys": [ - { - "type": "GlobalReference", - "value": "https://mm-software.com/twinengine/qualifier" - } - ] - }, - "kind": "TemplateQualifier", - "type": "InternalSemanticId", - "valueType": "xs:string", - "value": "https://mm-software.com/AssetSpecificProperties/ArbitraryMLP" - } - ], "value": [ { "language": "en", From f2b936214757853082f3fc3d72fe68f0699fd622 Mon Sep 17 00:00:00 2001 From: Kevalkumar Date: Fri, 20 Mar 2026 14:49:07 +0530 Subject: [PATCH 13/71] Revert "remove internal semantic from test data" This reverts commit dd01f18b4de3166c9a94c4d6619097c8fe33a7f4. --- ...ntactInfo_ContactInformation_Expected.json | 18 +++++++++++++++++- .../GetSubmodel_ContactInfo_Expected.json | 19 +++++++++++++++++-- .../GetSubmodel_Nameplate_Expected.json | 18 +++++++++++++++++- 3 files changed, 51 insertions(+), 4 deletions(-) diff --git a/source/AAS.TwinEngine.Plugin.TestPlugin.PlaywrightTests/SubmodelRepository/TestData/GetSubmodelElement_ContactInfo_ContactInformation_Expected.json b/source/AAS.TwinEngine.Plugin.TestPlugin.PlaywrightTests/SubmodelRepository/TestData/GetSubmodelElement_ContactInfo_ContactInformation_Expected.json index b481d47c..b55a8eeb 100644 --- a/source/AAS.TwinEngine.Plugin.TestPlugin.PlaywrightTests/SubmodelRepository/TestData/GetSubmodelElement_ContactInfo_ContactInformation_Expected.json +++ b/source/AAS.TwinEngine.Plugin.TestPlugin.PlaywrightTests/SubmodelRepository/TestData/GetSubmodelElement_ContactInfo_ContactInformation_Expected.json @@ -64,13 +64,29 @@ } ] }, - "qualifiers": + "qualifiers": [ { "kind": "ConceptQualifier", "type": "Multiplicity", "valueType": "xs:string", "value": "ZeroToOne" }, + { + "semanticId": { + "type": "ExternalReference", + "keys": [ + { + "type": "GlobalReference", + "value": "https://mm-software.com/twinengine/qualifier" + } + ] + }, + "kind": "TemplateQualifier", + "type": "InternalSemanticId", + "valueType": "xs:string", + "value": "https://mm-software.com/AddressOfAdditionalLink" + } + ], "valueType": "xs:string", "value": "https://www.mm-software.com/more-the-newsroom/", "modelType": "Property" diff --git a/source/AAS.TwinEngine.Plugin.TestPlugin.PlaywrightTests/SubmodelRepository/TestData/GetSubmodel_ContactInfo_Expected.json b/source/AAS.TwinEngine.Plugin.TestPlugin.PlaywrightTests/SubmodelRepository/TestData/GetSubmodel_ContactInfo_Expected.json index 6f55a3dc..656b7c2a 100644 --- a/source/AAS.TwinEngine.Plugin.TestPlugin.PlaywrightTests/SubmodelRepository/TestData/GetSubmodel_ContactInfo_Expected.json +++ b/source/AAS.TwinEngine.Plugin.TestPlugin.PlaywrightTests/SubmodelRepository/TestData/GetSubmodel_ContactInfo_Expected.json @@ -78,14 +78,29 @@ } ] }, - "qualifiers": + "qualifiers": [ { "kind": "ConceptQualifier", "type": "Multiplicity", "valueType": "xs:string", "value": "ZeroToOne" + }, + { + "semanticId": { + "type": "ExternalReference", + "keys": [ + { + "type": "GlobalReference", + "value": "https://mm-software.com/twinengine/qualifier" + } + ] + }, + "kind": "TemplateQualifier", + "type": "InternalSemanticId", + "valueType": "xs:string", + "value": "https://mm-software.com/AddressOfAdditionalLink" } - , + ], "valueType": "xs:string", "value": "https://www.mm-software.com/mobile-arbeitsmaschinen/", "modelType": "Property" diff --git a/source/AAS.TwinEngine.Plugin.TestPlugin.PlaywrightTests/SubmodelRepository/TestData/GetSubmodel_Nameplate_Expected.json b/source/AAS.TwinEngine.Plugin.TestPlugin.PlaywrightTests/SubmodelRepository/TestData/GetSubmodel_Nameplate_Expected.json index 824cf788..4a5ed6c2 100644 --- a/source/AAS.TwinEngine.Plugin.TestPlugin.PlaywrightTests/SubmodelRepository/TestData/GetSubmodel_Nameplate_Expected.json +++ b/source/AAS.TwinEngine.Plugin.TestPlugin.PlaywrightTests/SubmodelRepository/TestData/GetSubmodel_Nameplate_Expected.json @@ -1312,7 +1312,7 @@ } ] }, - "qualifiers": + "qualifiers": [ { "semanticId": { "type": "ExternalReference", @@ -1328,6 +1328,22 @@ "valueType": "xs:string", "value": "ZeroToMany" }, + { + "semanticId": { + "type": "ExternalReference", + "keys": [ + { + "type": "GlobalReference", + "value": "https://mm-software.com/twinengine/qualifier" + } + ] + }, + "kind": "TemplateQualifier", + "type": "InternalSemanticId", + "valueType": "xs:string", + "value": "https://mm-software.com/AssetSpecificProperties/ArbitraryMLP" + } + ], "value": [ { "language": "en", From 60432c8749345d2fa7986a140a9f61a6ea92869d Mon Sep 17 00:00:00 2001 From: Kevalkumar Date: Fri, 20 Mar 2026 14:50:26 +0530 Subject: [PATCH 14/71] remove internal semantic id from qualifires in testdata --- ..._ContactInfo_ContactInformation_Expected.json | 15 --------------- .../GetSubmodel_ContactInfo_Expected.json | 15 --------------- .../TestData/GetSubmodel_Nameplate_Expected.json | 16 +--------------- 3 files changed, 1 insertion(+), 45 deletions(-) diff --git a/source/AAS.TwinEngine.Plugin.TestPlugin.PlaywrightTests/SubmodelRepository/TestData/GetSubmodelElement_ContactInfo_ContactInformation_Expected.json b/source/AAS.TwinEngine.Plugin.TestPlugin.PlaywrightTests/SubmodelRepository/TestData/GetSubmodelElement_ContactInfo_ContactInformation_Expected.json index b55a8eeb..c77d421e 100644 --- a/source/AAS.TwinEngine.Plugin.TestPlugin.PlaywrightTests/SubmodelRepository/TestData/GetSubmodelElement_ContactInfo_ContactInformation_Expected.json +++ b/source/AAS.TwinEngine.Plugin.TestPlugin.PlaywrightTests/SubmodelRepository/TestData/GetSubmodelElement_ContactInfo_ContactInformation_Expected.json @@ -70,21 +70,6 @@ "type": "Multiplicity", "valueType": "xs:string", "value": "ZeroToOne" - }, - { - "semanticId": { - "type": "ExternalReference", - "keys": [ - { - "type": "GlobalReference", - "value": "https://mm-software.com/twinengine/qualifier" - } - ] - }, - "kind": "TemplateQualifier", - "type": "InternalSemanticId", - "valueType": "xs:string", - "value": "https://mm-software.com/AddressOfAdditionalLink" } ], "valueType": "xs:string", diff --git a/source/AAS.TwinEngine.Plugin.TestPlugin.PlaywrightTests/SubmodelRepository/TestData/GetSubmodel_ContactInfo_Expected.json b/source/AAS.TwinEngine.Plugin.TestPlugin.PlaywrightTests/SubmodelRepository/TestData/GetSubmodel_ContactInfo_Expected.json index 656b7c2a..225cb784 100644 --- a/source/AAS.TwinEngine.Plugin.TestPlugin.PlaywrightTests/SubmodelRepository/TestData/GetSubmodel_ContactInfo_Expected.json +++ b/source/AAS.TwinEngine.Plugin.TestPlugin.PlaywrightTests/SubmodelRepository/TestData/GetSubmodel_ContactInfo_Expected.json @@ -84,21 +84,6 @@ "type": "Multiplicity", "valueType": "xs:string", "value": "ZeroToOne" - }, - { - "semanticId": { - "type": "ExternalReference", - "keys": [ - { - "type": "GlobalReference", - "value": "https://mm-software.com/twinengine/qualifier" - } - ] - }, - "kind": "TemplateQualifier", - "type": "InternalSemanticId", - "valueType": "xs:string", - "value": "https://mm-software.com/AddressOfAdditionalLink" } ], "valueType": "xs:string", diff --git a/source/AAS.TwinEngine.Plugin.TestPlugin.PlaywrightTests/SubmodelRepository/TestData/GetSubmodel_Nameplate_Expected.json b/source/AAS.TwinEngine.Plugin.TestPlugin.PlaywrightTests/SubmodelRepository/TestData/GetSubmodel_Nameplate_Expected.json index 4a5ed6c2..9bd42c47 100644 --- a/source/AAS.TwinEngine.Plugin.TestPlugin.PlaywrightTests/SubmodelRepository/TestData/GetSubmodel_Nameplate_Expected.json +++ b/source/AAS.TwinEngine.Plugin.TestPlugin.PlaywrightTests/SubmodelRepository/TestData/GetSubmodel_Nameplate_Expected.json @@ -6,6 +6,7 @@ "text": "Contains the nameplate information attached to the product" } ], + "administration": { "administration": { "version": "3", "revision": "0" @@ -1327,21 +1328,6 @@ "type": "SMT/Cardinality", "valueType": "xs:string", "value": "ZeroToMany" - }, - { - "semanticId": { - "type": "ExternalReference", - "keys": [ - { - "type": "GlobalReference", - "value": "https://mm-software.com/twinengine/qualifier" - } - ] - }, - "kind": "TemplateQualifier", - "type": "InternalSemanticId", - "valueType": "xs:string", - "value": "https://mm-software.com/AssetSpecificProperties/ArbitraryMLP" } ], "value": [ From 54fe06ddca23adb70f8913c592c7828c91d2fa36 Mon Sep 17 00:00:00 2001 From: Kevalkumar Date: Fri, 20 Mar 2026 15:32:42 +0530 Subject: [PATCH 15/71] remove internal semantic ids from testdata of playwrite tests --- ...ntactInfo_ContactInformation_Expected.json | 60 -------- .../GetSubmodel_ContactInfo_Expected.json | 135 ------------------ .../GetSubmodel_Nameplate_Expected.json | 45 ------ 3 files changed, 240 deletions(-) diff --git a/source/AAS.TwinEngine.Plugin.TestPlugin.PlaywrightTests/SubmodelRepository/TestData/GetSubmodelElement_ContactInfo_ContactInformation_Expected.json b/source/AAS.TwinEngine.Plugin.TestPlugin.PlaywrightTests/SubmodelRepository/TestData/GetSubmodelElement_ContactInfo_ContactInformation_Expected.json index c77d421e..2e628d84 100644 --- a/source/AAS.TwinEngine.Plugin.TestPlugin.PlaywrightTests/SubmodelRepository/TestData/GetSubmodelElement_ContactInfo_ContactInformation_Expected.json +++ b/source/AAS.TwinEngine.Plugin.TestPlugin.PlaywrightTests/SubmodelRepository/TestData/GetSubmodelElement_ContactInfo_ContactInformation_Expected.json @@ -356,21 +356,6 @@ "type": "Multiplicity", "valueType": "xs:string", "value": "One" - }, - { - "semanticId": { - "type": "ExternalReference", - "keys": [ - { - "type": "GlobalReference", - "value": "https://mm-software.com/twinengine/qualifier" - } - ] - }, - "kind": "TemplateQualifier", - "type": "InternalSemanticId", - "valueType": "xs:string", - "value": "https://mm-software.com/ipCommunication/AddressOfAdditionalLink" } ], "valueType": "xs:string", @@ -417,21 +402,6 @@ "type": "Multiplicity", "valueType": "xs:string", "value": "ZeroToOne" - }, - { - "semanticId": { - "type": "ExternalReference", - "keys": [ - { - "type": "GlobalReference", - "value": "https://mm-software.com/twinengine/qualifier" - } - ] - }, - "kind": "TemplateQualifier", - "type": "InternalSemanticId", - "valueType": "xs:string", - "value": "https://mm-software.com/IPCommunication/AvailableTime" } ], "value": [ @@ -515,21 +485,6 @@ "type": "Multiplicity", "valueType": "xs:string", "value": "ZeroToOne" - }, - { - "semanticId": { - "type": "ExternalReference", - "keys": [ - { - "type": "GlobalReference", - "value": "https://mm-software.com/twinengine/qualifier" - } - ] - }, - "kind": "TemplateQualifier", - "type": "InternalSemanticId", - "valueType": "xs:string", - "value": "https://mm-software.com/Phone/AvailableTime" } ], "value": [ @@ -642,21 +597,6 @@ "type": "Multiplicity", "valueType": "xs:string", "value": "ZeroToOne" - }, - { - "semanticId": { - "type": "ExternalReference", - "keys": [ - { - "type": "GlobalReference", - "value": "https://mm-software.com/twinengine/qualifier" - } - ] - }, - "kind": "TemplateQualifier", - "type": "InternalSemanticId", - "valueType": "xs:string", - "value": "https://mm-software.com/Phone/AvailableTime" } ], "value": [ diff --git a/source/AAS.TwinEngine.Plugin.TestPlugin.PlaywrightTests/SubmodelRepository/TestData/GetSubmodel_ContactInfo_Expected.json b/source/AAS.TwinEngine.Plugin.TestPlugin.PlaywrightTests/SubmodelRepository/TestData/GetSubmodel_ContactInfo_Expected.json index 225cb784..5a66e771 100644 --- a/source/AAS.TwinEngine.Plugin.TestPlugin.PlaywrightTests/SubmodelRepository/TestData/GetSubmodel_ContactInfo_Expected.json +++ b/source/AAS.TwinEngine.Plugin.TestPlugin.PlaywrightTests/SubmodelRepository/TestData/GetSubmodel_ContactInfo_Expected.json @@ -370,21 +370,6 @@ "type": "Multiplicity", "valueType": "xs:string", "value": "One" - }, - { - "semanticId": { - "type": "ExternalReference", - "keys": [ - { - "type": "GlobalReference", - "value": "https://mm-software.com/twinengine/qualifier" - } - ] - }, - "kind": "TemplateQualifier", - "type": "InternalSemanticId", - "valueType": "xs:string", - "value": "https://mm-software.com/ipCommunication/AddressOfAdditionalLink" } ], "valueType": "xs:string", @@ -431,21 +416,6 @@ "type": "Multiplicity", "valueType": "xs:string", "value": "ZeroToOne" - }, - { - "semanticId": { - "type": "ExternalReference", - "keys": [ - { - "type": "GlobalReference", - "value": "https://mm-software.com/twinengine/qualifier" - } - ] - }, - "kind": "TemplateQualifier", - "type": "InternalSemanticId", - "valueType": "xs:string", - "value": "https://mm-software.com/IPCommunication/AvailableTime" } ], "value": [ @@ -529,21 +499,6 @@ "type": "Multiplicity", "valueType": "xs:string", "value": "ZeroToOne" - }, - { - "semanticId": { - "type": "ExternalReference", - "keys": [ - { - "type": "GlobalReference", - "value": "https://mm-software.com/twinengine/qualifier" - } - ] - }, - "kind": "TemplateQualifier", - "type": "InternalSemanticId", - "valueType": "xs:string", - "value": "https://mm-software.com/Phone/AvailableTime" } ], "value": [ @@ -656,21 +611,6 @@ "type": "Multiplicity", "valueType": "xs:string", "value": "ZeroToOne" - }, - { - "semanticId": { - "type": "ExternalReference", - "keys": [ - { - "type": "GlobalReference", - "value": "https://mm-software.com/twinengine/qualifier" - } - ] - }, - "kind": "TemplateQualifier", - "type": "InternalSemanticId", - "valueType": "xs:string", - "value": "https://mm-software.com/Phone/AvailableTime" } ], "value": [ @@ -788,21 +728,6 @@ "type": "Multiplicity", "valueType": "xs:string", "value": "ZeroToOne" - }, - { - "semanticId": { - "type": "ExternalReference", - "keys": [ - { - "type": "GlobalReference", - "value": "https://mm-software.com/twinengine/qualifier" - } - ] - }, - "kind": "TemplateQualifier", - "type": "InternalSemanticId", - "valueType": "xs:string", - "value": "https://mm-software.com/AddressOfAdditionalLink" } ], "valueType": "xs:string", @@ -1089,21 +1014,6 @@ "type": "Multiplicity", "valueType": "xs:string", "value": "One" - }, - { - "semanticId": { - "type": "ExternalReference", - "keys": [ - { - "type": "GlobalReference", - "value": "https://mm-software.com/twinengine/qualifier" - } - ] - }, - "kind": "TemplateQualifier", - "type": "InternalSemanticId", - "valueType": "xs:string", - "value": "https://mm-software.com/ipCommunication/AddressOfAdditionalLink" } ], "valueType": "xs:string", @@ -1150,21 +1060,6 @@ "type": "Multiplicity", "valueType": "xs:string", "value": "ZeroToOne" - }, - { - "semanticId": { - "type": "ExternalReference", - "keys": [ - { - "type": "GlobalReference", - "value": "https://mm-software.com/twinengine/qualifier" - } - ] - }, - "kind": "TemplateQualifier", - "type": "InternalSemanticId", - "valueType": "xs:string", - "value": "https://mm-software.com/IPCommunication/AvailableTime" } ], "value": [ @@ -1248,21 +1143,6 @@ "type": "Multiplicity", "valueType": "xs:string", "value": "ZeroToOne" - }, - { - "semanticId": { - "type": "ExternalReference", - "keys": [ - { - "type": "GlobalReference", - "value": "https://mm-software.com/twinengine/qualifier" - } - ] - }, - "kind": "TemplateQualifier", - "type": "InternalSemanticId", - "valueType": "xs:string", - "value": "https://mm-software.com/Phone/AvailableTime" } ], "value": [ @@ -1375,21 +1255,6 @@ "type": "Multiplicity", "valueType": "xs:string", "value": "ZeroToOne" - }, - { - "semanticId": { - "type": "ExternalReference", - "keys": [ - { - "type": "GlobalReference", - "value": "https://mm-software.com/twinengine/qualifier" - } - ] - }, - "kind": "TemplateQualifier", - "type": "InternalSemanticId", - "valueType": "xs:string", - "value": "https://mm-software.com/Phone/AvailableTime" } ], "value": [ diff --git a/source/AAS.TwinEngine.Plugin.TestPlugin.PlaywrightTests/SubmodelRepository/TestData/GetSubmodel_Nameplate_Expected.json b/source/AAS.TwinEngine.Plugin.TestPlugin.PlaywrightTests/SubmodelRepository/TestData/GetSubmodel_Nameplate_Expected.json index 9bd42c47..ca8527d6 100644 --- a/source/AAS.TwinEngine.Plugin.TestPlugin.PlaywrightTests/SubmodelRepository/TestData/GetSubmodel_Nameplate_Expected.json +++ b/source/AAS.TwinEngine.Plugin.TestPlugin.PlaywrightTests/SubmodelRepository/TestData/GetSubmodel_Nameplate_Expected.json @@ -1378,21 +1378,6 @@ "type": "SMT/Cardinality", "valueType": "xs:string", "value": "ZeroToMany" - }, - { - "semanticId": { - "type": "ExternalReference", - "keys": [ - { - "type": "GlobalReference", - "value": "https://mm-software.com/twinengine/qualifier" - } - ] - }, - "kind": "ConceptQualifier", - "type": "InternalSemanticId", - "valueType": "xs:string", - "value": "https://mm-software.com/AssetSpecificProperties/ArbitraryFile" } ], "value": "https://raw.githubusercontent.com/AAS-TwinEngine/AAS.TwinEngine.DataEngine/refs/heads/main/example/data/dummy_document.pdf", @@ -1576,21 +1561,6 @@ "type": "SMT/Cardinality", "valueType": "xs:string", "value": "ZeroToMany" - }, - { - "semanticId": { - "type": "ExternalReference", - "keys": [ - { - "type": "GlobalReference", - "value": "https://mm-software.com/twinengine/qualifier" - } - ] - }, - "kind": "ConceptQualifier", - "type": "InternalSemanticId", - "valueType": "xs:string", - "value": "https://mm-software.com/GuidelineSpecificProperties/ArbitraryFile" } ], "value": "https://raw.githubusercontent.com/AAS-TwinEngine/AAS.TwinEngine.DataEngine/refs/heads/main/example/data/checkmark.png", @@ -1637,21 +1607,6 @@ "type": "SMT/Cardinality", "valueType": "xs:string", "value": "ZeroToMany" - }, - { - "semanticId": { - "type": "ExternalReference", - "keys": [ - { - "type": "GlobalReference", - "value": "https://mm-software.com/twinengine/qualifier" - } - ] - }, - "kind": "TemplateQualifier", - "type": "InternalSemanticId", - "valueType": "xs:string", - "value": "https://mm-software.com/GuidelineSpecificProperties/ArbitraryMLP" } ], "value": [ From d4149721cf3bebdd4d07f76abfa003cd049d5a51 Mon Sep 17 00:00:00 2001 From: Kevalkumar Date: Fri, 20 Mar 2026 15:42:20 +0530 Subject: [PATCH 16/71] remove extra property added in testdata json --- .../TestData/GetSubmodel_Nameplate_Expected.json | 1 - 1 file changed, 1 deletion(-) diff --git a/source/AAS.TwinEngine.Plugin.TestPlugin.PlaywrightTests/SubmodelRepository/TestData/GetSubmodel_Nameplate_Expected.json b/source/AAS.TwinEngine.Plugin.TestPlugin.PlaywrightTests/SubmodelRepository/TestData/GetSubmodel_Nameplate_Expected.json index ca8527d6..3b37fde8 100644 --- a/source/AAS.TwinEngine.Plugin.TestPlugin.PlaywrightTests/SubmodelRepository/TestData/GetSubmodel_Nameplate_Expected.json +++ b/source/AAS.TwinEngine.Plugin.TestPlugin.PlaywrightTests/SubmodelRepository/TestData/GetSubmodel_Nameplate_Expected.json @@ -6,7 +6,6 @@ "text": "Contains the nameplate information attached to the product" } ], - "administration": { "administration": { "version": "3", "revision": "0" From 6461331083f7669bfb13771824bde7f37f63bb9a Mon Sep 17 00:00:00 2001 From: Kevalkumar Date: Wed, 8 Apr 2026 02:27:08 +0530 Subject: [PATCH 17/71] Added v2 config file with backward compatibility --- example/docker-compose.yml | 82 +-- ...S.TwinEngine.DataEngine.ModuleTests.csproj | 5 + .../ShellDescriptorControllerTests.cs | 65 ++- .../Api/Services/AasRegistry/TestData.cs | 6 +- .../AasRepositoryControllerTests.cs | 51 +- ...urationBackwardCompatibilityModuleTests.cs | 397 +++++++++++++++ .../SubmodelDescriptorControllerTests.cs | 47 +- .../SerializationControllerTests.cs | 53 +- .../SubmodelRepositoryControllerTests.cs | 55 +- .../Common/ConfigTestFactory.cs | 39 ++ .../TestData/v1-config/appsettings.json | 164 ++++++ .../TestData/v2-config/appsettings.json | 176 +++++++ .../AasRepositoryTemplateServiceTests.cs | 6 +- .../SubmodelDescriptorServiceTests.cs | 58 +-- ...iLanguagePropertySettingsValidatorTests.cs | 3 +- .../Helpers/ReferenceHelperTests.cs | 9 +- .../Helpers/SemanticIdResolverTests.cs | 23 +- .../Helpers/SubmodelElementHelperTests.cs | 23 +- .../SemanticIdHandlerTests.cs | 57 ++- .../SerializationServiceTests.cs | 8 +- .../HeaderForwardingHandlerTests.cs | 31 +- .../Headers/RequestHeaderMapperTests.cs | 106 ++-- .../HttpClientRegistrationExtensionsTests.cs | 50 +- .../ResilienceHandlerExtensionsTests.cs | 16 +- .../PluginAvailabilityHealthCheckTests.cs | 55 +- .../TemplateRegistryHealthCheckTests.cs | 127 +---- .../TemplateRepositoryHealthCheckTests.cs | 129 +---- .../AasRegistryProviderTests.cs | 10 +- .../ShellDescriptorSyncHostedTests.cs | 15 +- .../Helper/JsonSchemaValidatorTests.cs | 8 +- .../Services/MultiPluginDataHandlerTests.cs | 8 +- .../Services/PluginDataHandlerTests.cs | 16 +- .../PluginManifestConflictHandlerTests.cs | 91 ++-- .../Services/PluginManifestProviderTests.cs | 17 +- .../SubmodelDescriptorProviderTests.cs | 27 +- .../ShellTemplateMappingProviderTests.cs | 47 +- .../SubmodelTemplateMappingProviderTests.cs | 36 +- .../Services/TemplateProviderTests.cs | 33 +- ...figurationBackwardCompatibilityE2ETests.cs | 476 ++++++++++++++++++ .../LegacyConfigurationDetectorTests.cs | 123 +++++ .../LegacyGeneralConfigAdapterTests.cs | 130 +++++ .../LegacyPluginsConfigAdapterTests.cs | 209 ++++++++ ...egacyRegistrySettingsConfigAdapterTests.cs | 141 ++++++ ...acyTemplateManagementConfigAdapterTests.cs | 217 ++++++++ .../V2DirectBindingTests.cs | 262 ++++++++++ .../AasRepositoryTemplateService.cs | 7 +- .../Plugin/Config/AasEnvironmentConfig.cs | 28 +- .../Services/Shared/IBaseUrlProvider.cs | 11 + .../SubmodelDescriptorService.cs | 43 +- .../Config/AasxExportOptions.cs | 4 +- .../Config/Helper/PluginsConfigValidator.cs | 54 ++ .../SemanticId/Helpers/SemanticIdResolver.cs | 10 +- .../Helpers/SubmodelElementHelper.cs | 8 +- .../SerializationService.cs | 9 +- .../ConfigV1}/AasRegistryPreComputed.cs | 2 +- .../ConfigV1}/HttpRetryPolicyOptions.cs | 2 +- .../MultiLanguagePropertySettings.cs | 2 +- .../MultiLanguagePropertySettingsValidator.cs | 2 +- .../LegacyV1/ConfigV1}/Semantics.cs | 2 +- .../LegacyV1/LegacyConfigurationDetector.cs | 33 ++ .../LegacyV1/LegacyGeneralConfigAdapter.cs | 59 +++ .../LegacyV1/LegacyPluginsConfigAdapter.cs | 77 +++ .../LegacyRegistrySettingsConfigAdapter.cs | 43 ++ .../LegacyTemplateManagementConfigAdapter.cs | 87 ++++ .../LegacyV1ConfigurationExtensions.cs | 31 ++ .../Config/HeaderForwardingOptions.cs | 8 +- .../Headers/RequestHeaderMapper.cs | 60 +-- .../HttpClientRegistrationExtensions.cs | 9 +- .../Policies/ResilienceHandlerExtensions.cs | 12 +- .../PluginAvailabilityHealthCheck.cs | 21 +- .../Monitoring/TemplateRegistryHealthCheck.cs | 10 +- .../TemplateRepositoryHealthCheck.cs | 11 +- .../Services/AasRegistryProvider.cs | 26 +- .../Services/ShellDescriptorSyncHosted.cs | 8 +- .../Helper/JsonSchemaValidator.cs | 6 +- .../Helper/RegisterPluginHttpClients.cs | 7 +- .../Services/MultiPluginDataHandler.cs | 6 +- .../Services/PluginDataHandler.cs | 9 +- .../Services/PluginManifestConflictHandler.cs | 6 +- .../Services/PluginManifestProvider.cs | 11 +- .../Services/SubmodelDescriptorProvider.cs | 41 +- .../Services/ShellTemplateMappingProvider.cs | 7 +- .../SubmodelTemplateMappingProvider.cs | 5 +- .../Services/TemplateProvider.cs | 26 +- .../Shared/HttpRequestBaseUrlProvider.cs | 38 ++ ...pplicationDependencyInjectionExtensions.cs | 5 +- .../Config/GeneralConfig.cs | 32 ++ .../Config/PluginsConfig.cs | 54 ++ .../Config/RegistrySettingsConfig.cs | 23 + .../Config/TemplateManagementConfig.cs | 40 ++ ...astructureDependencyInjectionExtensions.cs | 76 +-- .../LoggingConfigurationExtension.cs | 3 +- .../appsettings.development.json | 350 +++++++------ .../appsettings.json | 302 +++++------ .../ApiTestBase.cs | 1 - .../SubmodelRegistry/SubmodelRegistryTests.cs | 6 +- .../SubmodelRepository/SubmodelTests.cs | 6 +- .../Example/basyx/aas-env.properties | 2 +- .../Example/docker-compose.yml | 86 ++-- .../Example/nginx/default.conf.template | 2 +- 100 files changed, 4118 insertions(+), 1418 deletions(-) create mode 100644 source/AAS.TwinEngine.DataEngine.ModuleTests/Api/Services/ConfigurationMigration/ConfigurationBackwardCompatibilityModuleTests.cs create mode 100644 source/AAS.TwinEngine.DataEngine.ModuleTests/Common/ConfigTestFactory.cs create mode 100644 source/AAS.TwinEngine.DataEngine.ModuleTests/TestData/v1-config/appsettings.json create mode 100644 source/AAS.TwinEngine.DataEngine.ModuleTests/TestData/v2-config/appsettings.json create mode 100644 source/AAS.TwinEngine.DataEngine.UnitTests/ServiceConfiguration/ConfigurationMigration/ConfigurationBackwardCompatibilityE2ETests.cs create mode 100644 source/AAS.TwinEngine.DataEngine.UnitTests/ServiceConfiguration/ConfigurationMigration/LegacyConfigurationDetectorTests.cs create mode 100644 source/AAS.TwinEngine.DataEngine.UnitTests/ServiceConfiguration/ConfigurationMigration/LegacyGeneralConfigAdapterTests.cs create mode 100644 source/AAS.TwinEngine.DataEngine.UnitTests/ServiceConfiguration/ConfigurationMigration/LegacyPluginsConfigAdapterTests.cs create mode 100644 source/AAS.TwinEngine.DataEngine.UnitTests/ServiceConfiguration/ConfigurationMigration/LegacyRegistrySettingsConfigAdapterTests.cs create mode 100644 source/AAS.TwinEngine.DataEngine.UnitTests/ServiceConfiguration/ConfigurationMigration/LegacyTemplateManagementConfigAdapterTests.cs create mode 100644 source/AAS.TwinEngine.DataEngine.UnitTests/ServiceConfiguration/ConfigurationMigration/V2DirectBindingTests.cs create mode 100644 source/AAS.TwinEngine.DataEngine/ApplicationLogic/Services/Shared/IBaseUrlProvider.cs create mode 100644 source/AAS.TwinEngine.DataEngine/ApplicationLogic/Services/SubmodelRepository/Config/Helper/PluginsConfigValidator.cs rename source/AAS.TwinEngine.DataEngine/Infrastructure/{Providers/AasRegistryProvider/Config => Configuration/LegacyV1/ConfigV1}/AasRegistryPreComputed.cs (71%) rename source/AAS.TwinEngine.DataEngine/Infrastructure/{Http/Config => Configuration/LegacyV1/ConfigV1}/HttpRetryPolicyOptions.cs (86%) rename source/AAS.TwinEngine.DataEngine/{ApplicationLogic/Services/SubmodelRepository/Config => Infrastructure/Configuration/LegacyV1/ConfigV1}/MultiLanguagePropertySettings.cs (64%) rename source/AAS.TwinEngine.DataEngine/{ApplicationLogic/Services/SubmodelRepository/Config/Helper => Infrastructure/Configuration/LegacyV1/ConfigV1}/MultiLanguagePropertySettingsValidator.cs (94%) rename source/AAS.TwinEngine.DataEngine/{ApplicationLogic/Services/SubmodelRepository/Config => Infrastructure/Configuration/LegacyV1/ConfigV1}/Semantics.cs (77%) create mode 100644 source/AAS.TwinEngine.DataEngine/Infrastructure/Configuration/LegacyV1/LegacyConfigurationDetector.cs create mode 100644 source/AAS.TwinEngine.DataEngine/Infrastructure/Configuration/LegacyV1/LegacyGeneralConfigAdapter.cs create mode 100644 source/AAS.TwinEngine.DataEngine/Infrastructure/Configuration/LegacyV1/LegacyPluginsConfigAdapter.cs create mode 100644 source/AAS.TwinEngine.DataEngine/Infrastructure/Configuration/LegacyV1/LegacyRegistrySettingsConfigAdapter.cs create mode 100644 source/AAS.TwinEngine.DataEngine/Infrastructure/Configuration/LegacyV1/LegacyTemplateManagementConfigAdapter.cs create mode 100644 source/AAS.TwinEngine.DataEngine/Infrastructure/Configuration/LegacyV1/LegacyV1ConfigurationExtensions.cs create mode 100644 source/AAS.TwinEngine.DataEngine/Infrastructure/Shared/HttpRequestBaseUrlProvider.cs create mode 100644 source/AAS.TwinEngine.DataEngine/ServiceConfiguration/Config/GeneralConfig.cs create mode 100644 source/AAS.TwinEngine.DataEngine/ServiceConfiguration/Config/PluginsConfig.cs create mode 100644 source/AAS.TwinEngine.DataEngine/ServiceConfiguration/Config/RegistrySettingsConfig.cs create mode 100644 source/AAS.TwinEngine.DataEngine/ServiceConfiguration/Config/TemplateManagementConfig.cs diff --git a/example/docker-compose.yml b/example/docker-compose.yml index 1c02d944..1e33e2b3 100644 --- a/example/docker-compose.yml +++ b/example/docker-compose.yml @@ -30,43 +30,51 @@ services: condition: service_started restart: always environment: - - AasEnvironment__DataEngineRepositoryBaseUrl=http://localhost:8080 - - AasEnvironment__AasEnvironmentRepositoryBaseUrl=http://template-repository:8081 - - AasEnvironment__AasRegistryBaseUrl=http://aas-template-registry:8080 - - AasEnvironment__SubModelRegistryBaseUrl=http://sm-template-registry:8080 - - AasEnvironment__CustomerDomainUrl=https://mm-software.com - - TemplateMappingRules__SubmodelTemplateMappings__0__templateId=https://admin-shell.io/ZVEI/TechnicalData/Submodel/1/2 - - TemplateMappingRules__SubmodelTemplateMappings__0__pattern__0=TechnicalData - - TemplateMappingRules__SubmodelTemplateMappings__1__templateId=https://admin-shell.io/idta/SubmodelTemplate/DigitalNameplate/3/0 - - TemplateMappingRules__SubmodelTemplateMappings__1__pattern__0=Nameplate - - TemplateMappingRules__SubmodelTemplateMappings__2__templateId=https://admin-shell.io/idta/SubmodelTemplate/ContactInformation/1/0 - - TemplateMappingRules__SubmodelTemplateMappings__2__pattern__0=ContactInformation - - TemplateMappingRules__SubmodelTemplateMappings__3__templateId=https://admin-shell.io/idta/SubmodelTemplate/CarbonFootprint/1/0 - - TemplateMappingRules__SubmodelTemplateMappings__3__pattern__0=CarbonFootprint - - TemplateMappingRules__SubmodelTemplateMappings__4__templateId=https://admin-shell.io/idta/SubmodelTemplate/HandoverDocumentation/2/0 - - TemplateMappingRules__SubmodelTemplateMappings__4__pattern__0=HandoverDocumentation - - TemplateMappingRules__ShellTemplateMappings__0__templateId=https://mm-software.com/aas/aasTemplate - - TemplateMappingRules__ShellTemplateMappings__0__pattern__0= - - TemplateMappingRules__AasIdExtractionRules__0__pattern=Regex - - TemplateMappingRules__AasIdExtractionRules__0__index=6 - - TemplateMappingRules__AasIdExtractionRules__0__separator=/ - - PluginConfig__Plugins__0__PluginName=Plugin1 - - PluginConfig__Plugins__0__PluginUrl=http://dpp-plugin:8080 - - HeaderForwarding__HeaderMappings__TemplateRepository__0__Source=Authorization - - HeaderForwarding__HeaderMappings__TemplateRepository__0__Target=Authorization - - HeaderForwarding__HeaderMappings__TemplateRepository__0__Required=false - - HeaderForwarding__HeaderMappings__TemplateRepository__1__Source=X-User-Roles - - HeaderForwarding__HeaderMappings__TemplateRepository__1__Target=X-Template-Access-Roles - - HeaderForwarding__HeaderMappings__TemplateRepository__1__Required=false - - HeaderForwarding__HeaderMappings__TemplateRegistry__0__Source=Authorization - - HeaderForwarding__HeaderMappings__TemplateRegistry__0__Target=Authorization - - HeaderForwarding__HeaderMappings__TemplateRegistry__0__Required=false - - HeaderForwarding__HeaderMappings__Plugins__Plugin1__0__Source=Authorization - - HeaderForwarding__HeaderMappings__Plugins__Plugin1__0__Target=X-Auth-Token - - HeaderForwarding__HeaderMappings__Plugins__Plugin1__0__Required=false - - HeaderForwarding__HeaderMappings__Plugins__Plugin1__1__Source=X-Organization-Id - - HeaderForwarding__HeaderMappings__Plugins__Plugin1__1__Target=X-Tenant-Context - - HeaderForwarding__HeaderMappings__Plugins__Plugin1__1__Required=false + - General__DataEngineRepositoryBaseUrl=http://localhost:8080 + - General__CustomerDomainUrl=https://mm-software.com + - Plugins__Instances__0__Name=RelationalDatabasePlugin + - Plugins__Instances__0__baseUrl=http://dpp-plugin:8080 + - Plugins__Instances__0__headerMappings__0__source=Authorization + - Plugins__Instances__0__headerMappings__0__target=X-Auth-Token + - Plugins__Instances__0__headerMappings__0__required=false + - Plugins__Instances__0__headerMappings__1__source=X-Organization-Id + - Plugins__Instances__0__headerMappings__1__target=X-Tenant-Context + - Plugins__Instances__0__headerMappings__1__required=false + - TemplateManagement__TemplateMappingRules__SubmodelTemplateMappings__0__templateId=https://admin-shell.io/ZVEI/TechnicalData/Submodel/1/2 + - TemplateManagement__TemplateMappingRules__SubmodelTemplateMappings__0__pattern__0=TechnicalData + - TemplateManagement__TemplateMappingRules__SubmodelTemplateMappings__1__templateId=https://admin-shell.io/idta/SubmodelTemplate/DigitalNameplate/3/0 + - TemplateManagement__TemplateMappingRules__SubmodelTemplateMappings__1__pattern__0=Nameplate + - TemplateManagement__TemplateMappingRules__SubmodelTemplateMappings__2__templateId=https://admin-shell.io/idta/SubmodelTemplate/ContactInformation/1/0 + - TemplateManagement__TemplateMappingRules__SubmodelTemplateMappings__2__pattern__0=ContactInformation + - TemplateManagement__TemplateMappingRules__SubmodelTemplateMappings__3__templateId=https://admin-shell.io/idta/SubmodelTemplate/CarbonFootprint/1/0 + - TemplateManagement__TemplateMappingRules__SubmodelTemplateMappings__3__pattern__0=CarbonFootprint + - TemplateManagement__TemplateMappingRules__SubmodelTemplateMappings__4__templateId=https://admin-shell.io/idta/SubmodelTemplate/HandoverDocumentation/2/0 + - TemplateManagement__TemplateMappingRules__SubmodelTemplateMappings__4__pattern__0=HandoverDocumentation + - TemplateManagement__TemplateMappingRules__ShellTemplateMappings__0__templateId=https://mm-software.com/aas/aasTemplate + - TemplateManagement__TemplateMappingRules__ShellTemplateMappings__0__pattern__0= + - TemplateManagement__TemplateMappingRules__AasIdExtractionRules__1__Pattern=Regex + - TemplateManagement__TemplateMappingRules__AasIdExtractionRules__1__Index=6 + - TemplateManagement__TemplateMappingRules__AasIdExtractionRules__1__Separator=/ + - TemplateManagement__AasTemplateRepository__Name=AasTemplateRepository + - TemplateManagement__AasTemplateRepository__baseUrl=http://template-repository:8081 + - TemplateManagement__AasTemplateRepository__headerMappings__0__source=Authorization + - TemplateManagement__AasTemplateRepository__headerMappings__0__target=Authorization + - TemplateManagement__AasTemplateRepository__headerMappings__0__required=false + - TemplateManagement__SubmodelTemplateRepository__Name=SubmodelTemplateRepository + - TemplateManagement__SubmodelTemplateRepository__baseUrl=http://template-repository:8081 + - TemplateManagement__SubmodelTemplateRepository__headerMappings__0__source=Authorization + - TemplateManagement__SubmodelTemplateRepository__headerMappings__0__target=Authorization + - TemplateManagement__SubmodelTemplateRepository__headerMappings__0__required=false + - TemplateManagement__AasTemplateRegistry__Name=AasTemplateRegistry + - TemplateManagement__AasTemplateRegistry__baseUrl=http://aas-template-registry:8080 + - TemplateManagement__AasTemplateRegistry__headerMappings__0__source=Authorization + - TemplateManagement__AasTemplateRegistry__headerMappings__0__target=Authorization + - TemplateManagement__AasTemplateRegistry__headerMappings__0__required=false + - TemplateManagement__SubmodelTemplateRegistry__Name=SubmodelTemplateRegistry + - TemplateManagement__SubmodelTemplateRegistry__baseUrl=http://sm-template-registry:8080 + - TemplateManagement__SubmodelTemplateRegistry__headerMappings__0__source=Authorization + - TemplateManagement__SubmodelTemplateRegistry__headerMappings__0__target=Authorization + - TemplateManagement__SubmodelTemplateRegistry__headerMappings__0__required=false networks: - twinengine-network diff --git a/source/AAS.TwinEngine.DataEngine.ModuleTests/AAS.TwinEngine.DataEngine.ModuleTests.csproj b/source/AAS.TwinEngine.DataEngine.ModuleTests/AAS.TwinEngine.DataEngine.ModuleTests.csproj index 51fb8a1b..291f284e 100644 --- a/source/AAS.TwinEngine.DataEngine.ModuleTests/AAS.TwinEngine.DataEngine.ModuleTests.csproj +++ b/source/AAS.TwinEngine.DataEngine.ModuleTests/AAS.TwinEngine.DataEngine.ModuleTests.csproj @@ -26,4 +26,9 @@ + + + + + diff --git a/source/AAS.TwinEngine.DataEngine.ModuleTests/Api/Services/AasRegistry/ShellDescriptorControllerTests.cs b/source/AAS.TwinEngine.DataEngine.ModuleTests/Api/Services/AasRegistry/ShellDescriptorControllerTests.cs index fefd9dbe..6e2cb509 100644 --- a/source/AAS.TwinEngine.DataEngine.ModuleTests/Api/Services/AasRegistry/ShellDescriptorControllerTests.cs +++ b/source/AAS.TwinEngine.DataEngine.ModuleTests/Api/Services/AasRegistry/ShellDescriptorControllerTests.cs @@ -9,8 +9,8 @@ using AAS.TwinEngine.DataEngine.ApplicationLogic.Services.Plugin.Providers; using AAS.TwinEngine.DataEngine.Infrastructure.Http.Clients; using AAS.TwinEngine.DataEngine.Infrastructure.Providers.PluginDataProvider.Config; +using AAS.TwinEngine.DataEngine.ModuleTests.Common; -using Microsoft.AspNetCore.Mvc.Testing; using Microsoft.AspNetCore.WebUtilities; using Microsoft.Extensions.DependencyInjection; @@ -19,34 +19,39 @@ namespace AAS.TwinEngine.DataEngine.ModuleTests.Api.Services.AasRegistry; -public class ShellDescriptorControllerTests : IClassFixture> +public abstract class ShellDescriptorControllerTestsBase : IDisposable { + private readonly ConfigTestFactory _factory; private readonly ITemplateProvider _mockTemplateProvider; private readonly HttpClient _client; private readonly ICreateClient _httpClientFactory; - public ShellDescriptorControllerTests(WebApplicationFactory factory) + protected ShellDescriptorControllerTestsBase(string configDir) { _mockTemplateProvider = Substitute.For(); var mockPluginManifestProvider = Substitute.For(); var mockPluginManifestConflictHandler = Substitute.For(); _httpClientFactory = Substitute.For(); - var factory1 = factory.WithWebHostBuilder(builder => + _factory = new ConfigTestFactory(configDir, services => { - _ = builder.ConfigureServices(services => - { - _ = services.AddSingleton(_httpClientFactory); - _ = services.AddSingleton(mockPluginManifestProvider); - _ = services.AddSingleton(_mockTemplateProvider); - _ = services.AddSingleton(mockPluginManifestConflictHandler); - }); + _ = services.AddSingleton(_httpClientFactory); + _ = services.AddSingleton(mockPluginManifestProvider); + _ = services.AddSingleton(_mockTemplateProvider); + _ = services.AddSingleton(mockPluginManifestConflictHandler); }); - _client = factory1.CreateClient(); + _client = _factory.CreateClient(); _ = mockPluginManifestConflictHandler.Manifests.Returns(TestData.CreatePluginManifests()); } + public void Dispose() + { + _client.Dispose(); + _factory.Dispose(); + GC.SuppressFinalize(this); + } + [Fact] public async Task GetAllShellDescriptorsAsync_ReturnsOkAsync() { @@ -71,10 +76,10 @@ public async Task GetAllShellDescriptorsAsync_ReturnsOkAsync() httpClientPlugin2.BaseAddress = new Uri("https://testendpoint2.com"); const string HttpClientNamePlugin1 = $"{PluginConfig.HttpClientNamePrefix}TestPlugin1"; - _httpClientFactory.CreateClient(HttpClientNamePlugin1).Returns(httpClientPlugin1); + _ = _httpClientFactory.CreateClient(HttpClientNamePlugin1).Returns(httpClientPlugin1); const string HttpClientNamePlugin2 = $"{PluginConfig.HttpClientNamePrefix}TestPlugin2"; - _httpClientFactory.CreateClient(HttpClientNamePlugin2).Returns(httpClientPlugin2); + _ = _httpClientFactory.CreateClient(HttpClientNamePlugin2).Returns(httpClientPlugin2); _ = _mockTemplateProvider.GetShellDescriptorsTemplateAsync(Arg.Any()).Returns(template); @@ -155,10 +160,10 @@ public async Task GetShellDescriptorByIdAsync_ReturnsOkAsync() httpClient2.BaseAddress = new Uri("https://testendpoint2.com"); const string HttpClientName1 = $"{PluginConfig.HttpClientNamePrefix}TestPlugin1"; - _httpClientFactory.CreateClient(HttpClientName1).Returns(httpClient1); + _ = _httpClientFactory.CreateClient(HttpClientName1).Returns(httpClient1); const string HttpClientName2 = $"{PluginConfig.HttpClientNamePrefix}TestPlugin2"; - _httpClientFactory.CreateClient(HttpClientName2).Returns(httpClient2); + _ = _httpClientFactory.CreateClient(HttpClientName2).Returns(httpClient2); _ = _mockTemplateProvider.GetShellDescriptorsTemplateAsync(Arg.Any()).Returns(template); @@ -213,7 +218,7 @@ public async Task GetShellDescriptorByIdAsync_WhenIdentifierIsInValid_Returns400 [Theory] [InlineData("not-valid-base64!!!")] [InlineData("invalid!!base64")] - public async Task GetShellDescriptorById_InvalidBase64_Returns400BadRequest(string invalidBase64) + public async Task GetShellDescriptorById_InvalidBase64_Returns400BadRequestAsync(string invalidBase64) { var response = await _client.GetAsync($"/shell-descriptors/{invalidBase64}"); @@ -227,7 +232,7 @@ public async Task GetShellDescriptorById_InvalidBase64_Returns400BadRequest(stri [InlineData("")] [InlineData("")] [InlineData("")] - public async Task GetShellDescriptorById_XssInDecodedId_Returns400BadRequest(string maliciousContent) + public async Task GetShellDescriptorById_XssInDecodedId_Returns400BadRequestAsync(string maliciousContent) { var encoded = EncodeBase64Url(maliciousContent); @@ -243,7 +248,7 @@ public async Task GetShellDescriptorById_XssInDecodedId_Returns400BadRequest(str [InlineData("'; DROP TABLE shells--")] [InlineData("1 UNION SELECT * FROM descriptors")] [InlineData("admin'; DELETE FROM shells--")] - public async Task GetShellDescriptorById_SqlInjectionInDecodedId_Returns400BadRequest(string maliciousContent) + public async Task GetShellDescriptorById_SqlInjectionInDecodedId_Returns400BadRequestAsync(string maliciousContent) { var encoded = EncodeBase64Url(maliciousContent); @@ -259,7 +264,7 @@ public async Task GetShellDescriptorById_SqlInjectionInDecodedId_Returns400BadRe [InlineData("..\\..\\..\\windows\\system32")] [InlineData("%2e%2e/config")] [InlineData("..%2fconfig")] - public async Task GetShellDescriptorById_PathTraversalInDecodedId_Returns400BadRequest(string maliciousContent) + public async Task GetShellDescriptorById_PathTraversalInDecodedId_Returns400BadRequestAsync(string maliciousContent) { var encoded = EncodeBase64Url(maliciousContent); @@ -275,7 +280,7 @@ public async Task GetShellDescriptorById_PathTraversalInDecodedId_Returns400BadR [InlineData("data:text/html,")] [InlineData("'; DROP TABLE shells--")] - public async Task GetShellById_MaliciousPattern_Returns400BadRequest(string maliciousContent) + public async Task GetShellById_MaliciousPattern_Returns400BadRequestAsync(string maliciousContent) { var encoded = EncodeBase64Url(maliciousContent); @@ -289,7 +294,7 @@ public async Task GetShellById_MaliciousPattern_Returns400BadRequest(string mali [Theory] [InlineData("vbscript:msgbox('xss')")] [InlineData("file:///etc/passwd")] - public async Task GetAssetInformation_MaliciousPattern_Returns400BadRequest(string maliciousContent) + public async Task GetAssetInformation_MaliciousPattern_Returns400BadRequesAsync(string maliciousContent) { var encoded = EncodeBase64Url(maliciousContent); @@ -300,7 +305,7 @@ public async Task GetAssetInformation_MaliciousPattern_Returns400BadRequest(stri [Theory] [InlineData("invalid!!")] - public async Task GetSubmodelRefs_InvalidBase64_Returns400BadRequest(string invalidBase64) + public async Task GetSubmodelRefs_InvalidBase64_Returns400BadRequestAsync(string invalidBase64) { var response = await _client.GetAsync($"/shells/{invalidBase64}/submodel-refs"); @@ -310,7 +315,7 @@ public async Task GetSubmodelRefs_InvalidBase64_Returns400BadRequest(string inva [Theory] [InlineData("https://example.com/shells/shell123")] [InlineData("urn:uuid:test-123")] - public async Task GetShellById_ValidIdentifier_DoesNotReturn400(string validId) + public async Task GetShellById_ValidIdentifier_DoesNotReturn400Async(string validId) { var encoded = EncodeBase64Url(validId); _ = _mockTemplateProvider.GetShellTemplateAsync(Arg.Any(), Arg.Any()) @@ -335,6 +340,16 @@ private static string EncodeBase64Url(string plainText) } } +public class AasRepositoryControllerTests_V1Config : AasRepositoryControllerTestsBase +{ + public AasRepositoryControllerTests_V1Config() : base("v1-config") { } +} + +public class AasRepositoryControllerTests_V2Config : AasRepositoryControllerTestsBase +{ + public AasRepositoryControllerTests_V2Config() : base("v2-config") { } +} + public class FakeHttpMessageHandler(Func> send) : HttpMessageHandler { protected override Task SendAsync(HttpRequestMessage request, CancellationToken cancellationToken) diff --git a/source/AAS.TwinEngine.DataEngine.ModuleTests/Api/Services/ConfigurationMigration/ConfigurationBackwardCompatibilityModuleTests.cs b/source/AAS.TwinEngine.DataEngine.ModuleTests/Api/Services/ConfigurationMigration/ConfigurationBackwardCompatibilityModuleTests.cs new file mode 100644 index 00000000..ee057186 --- /dev/null +++ b/source/AAS.TwinEngine.DataEngine.ModuleTests/Api/Services/ConfigurationMigration/ConfigurationBackwardCompatibilityModuleTests.cs @@ -0,0 +1,397 @@ +using AAS.TwinEngine.DataEngine.ApplicationLogic.Services.Plugin; +using AAS.TwinEngine.DataEngine.ApplicationLogic.Services.Plugin.Providers; +using AAS.TwinEngine.DataEngine.Infrastructure.Http.Clients; +using AAS.TwinEngine.DataEngine.ServiceConfiguration.Config; + +using Microsoft.AspNetCore.Hosting; +using Microsoft.AspNetCore.Mvc.Testing; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; +using Microsoft.Extensions.Options; + +using NSubstitute; + +namespace AAS.TwinEngine.DataEngine.ModuleTests.Api.Services.ConfigurationMigration; + +/// +/// Module tests that boot the full application with the actual V1 (old) and V2 (new) +/// JSON configuration files and verify that the V2 POCO classes are populated correctly +/// in both cases. This validates the complete backward compatibility pipeline end-to-end +/// through the real DI container and configuration system. +/// +public class ConfigurationBackwardCompatibilityModuleTests +{ + /// + /// Custom factory that boots the app using a specific config directory. + /// Each directory contains an appsettings.json in either V1 or V2 format. + /// The content root is set to this directory so WebApplicationBuilder + /// automatically loads the correct appsettings.json. + /// + /// + /// Subdirectory name under TestData (e.g. "v1-config" or "v2-config") + /// that contains the appsettings.json file. + /// + private sealed class ConfigTestFactory(string configDirName) : WebApplicationFactory + { + private readonly string _configDir = Path.Combine(AppContext.BaseDirectory, "TestData", configDirName); + + protected override IHost CreateHost(IHostBuilder builder) + { + // Point content root to the config directory so the default builder + // loads its appsettings.json (V1 or V2 format) automatically. + _ = builder.UseContentRoot(_configDir); + _ = builder.UseEnvironment("ConfigTest"); + + _ = builder.ConfigureServices(services => + { + _ = services.AddSingleton(Substitute.For()); + _ = services.AddSingleton(Substitute.For()); + _ = services.AddSingleton(Substitute.For()); + }); + + return base.CreateHost(builder); + } + } + + // ────────────────────── V1 (Old Config) Tests ────────────────────── + + [Fact] + public void V1Config_ResolvesGeneralConfig_WithCorrectValues() + { + using var appFactory = new ConfigTestFactory("v1-config"); + using var scope = appFactory.Services.CreateScope(); + + var general = scope.ServiceProvider.GetRequiredService>().Value; + + Assert.Equal(new Uri("https://mm-software.com"), general.CustomerDomainUrl); + Assert.Equal(new Uri("http://localhost"), general.DataEngineRepositoryBaseUrl); + Assert.Equal("*", general.AllowedHosts); + Assert.Equal(8192, general.HeaderSanitization.MaxHeaderSize); + Assert.Equal(256, general.HeaderSanitization.MaxHeaderNameSize); + Assert.Equal("http://localhost:4317", general.OpenTelemetry.OtlpEndpoint); + Assert.Equal("TwinEngine", general.OpenTelemetry.ServiceName); + } + + [Fact] + public void V1Config_ResolvesPluginsConfig_WithCorrectValues() + { + using var appFactory = new ConfigTestFactory("v1-config"); + using var scope = appFactory.Services.CreateScope(); + + var plugins = scope.ServiceProvider.GetRequiredService>().Value; + + Assert.Equal("_aastwinengineindex_", plugins.SubmodelElementIndexContextPrefix); + Assert.Equal("_", plugins.MultiLanguageProperty.SemanticPostfixSeparator); + Assert.NotNull(plugins.MultiLanguageProperty.DefaultLanguages); + Assert.Contains("de", plugins.MultiLanguageProperty.DefaultLanguages); + Assert.Contains("en", plugins.MultiLanguageProperty.DefaultLanguages); + Assert.Equal(3, plugins.ResiliencePolicies.Retry.MaxRetryAttempts); + Assert.Equal(10, plugins.ResiliencePolicies.Retry.DelayInSeconds); + _ = Assert.Single(plugins.Instances); + Assert.Equal("Plugin1", plugins.Instances[0].Name); + Assert.Equal(new Uri("http://localhost:8086"), plugins.Instances[0].BaseUrl); + } + + [Fact] + public void V1Config_ResolvesPluginHeaderMappings_FromHeaderForwardingSection() + { + using var appFactory = new ConfigTestFactory("v1-config"); + using var scope = appFactory.Services.CreateScope(); + + var plugins = scope.ServiceProvider.GetRequiredService>().Value; + + _ = Assert.Single(plugins.Instances); + var headers = plugins.Instances[0].HeaderMappings; + Assert.Equal(2, headers.Count); + Assert.Equal("Authorization", headers[0].Source); + Assert.Equal("X-Auth-Token", headers[0].Target); + Assert.Equal("X-Organization-Id", headers[1].Source); + Assert.Equal("X-Tenant-Context", headers[1].Target); + } + + [Fact] + public void V1Config_ResolvesTemplateManagementConfig_WithCorrectValues() + { + using var appFactory = new ConfigTestFactory("v1-config"); + using var scope = appFactory.Services.CreateScope(); + + var tmConfig = scope.ServiceProvider.GetRequiredService>().Value; + + Assert.Equal("InternalSemanticId", tmConfig.Semantics.InternalSemanticId); + Assert.Equal(3, tmConfig.ResiliencePolicies.Retry.MaxRetryAttempts); + Assert.Equal(10, tmConfig.ResiliencePolicies.Retry.DelayInSeconds); + Assert.Equal(new Uri("http://localhost:8081"), tmConfig.AasTemplateRepository.BaseUrl); + Assert.Equal(new Uri("http://localhost:8082"), tmConfig.AasTemplateRegistry.BaseUrl); + Assert.Equal(new Uri("http://localhost:8083"), tmConfig.SubmodelTemplateRegistry.BaseUrl); + Assert.Equal(3, tmConfig.TemplateMappingRules.SubmodelTemplateMappings.Count); + _ = Assert.Single(tmConfig.TemplateMappingRules.ShellTemplateMappings); + _ = Assert.Single(tmConfig.TemplateMappingRules.AasIdExtractionRules); + } + + [Fact] + public void V1Config_ResolvesTemplateRepositoryHeaders_FromHeaderForwardingSection() + { + using var appFactory = new ConfigTestFactory("v1-config"); + using var scope = appFactory.Services.CreateScope(); + + var tmConfig = scope.ServiceProvider.GetRequiredService>().Value; + + Assert.Equal(2, tmConfig.AasTemplateRepository.HeaderMappings.Count); + Assert.Equal("Authorization", tmConfig.AasTemplateRepository.HeaderMappings[0].Source); + Assert.Equal("Authorization", tmConfig.AasTemplateRepository.HeaderMappings[0].Target); + Assert.Equal("X-User-Roles", tmConfig.AasTemplateRepository.HeaderMappings[1].Source); + Assert.Equal("X-Template-Access-Roles", tmConfig.AasTemplateRepository.HeaderMappings[1].Target); + + _ = Assert.Single(tmConfig.AasTemplateRegistry.HeaderMappings); + Assert.Equal("Authorization", tmConfig.AasTemplateRegistry.HeaderMappings[0].Source); + } + + [Fact] + public void V1Config_ResolvesRegistrySettings_WithPropertyRenames() + { + using var appFactory = new ConfigTestFactory("v1-config"); + using var scope = appFactory.Services.CreateScope(); + + var registry = scope.ServiceProvider.GetRequiredService>().Value; + + // V1: IsPreComputed → V2: Enabled + Assert.True(registry.PreComputed.Enabled); + // V1: ShellDescriptorCron → V2: Schedule + Assert.Equal("0 */3 * * * *", registry.PreComputed.Schedule); + } + + // ────────────────────── V2 (New Config) Tests ────────────────────── + + [Fact] + public void V2Config_ResolvesGeneralConfig_WithCorrectValues() + { + using var appFactory = new ConfigTestFactory("v2-config"); + using var scope = appFactory.Services.CreateScope(); + + var general = scope.ServiceProvider.GetRequiredService>().Value; + + Assert.Equal(new Uri("https://mm-software.com"), general.CustomerDomainUrl); + Assert.Equal("*", general.AllowedHosts); + Assert.Equal(8192, general.HeaderSanitization.MaxHeaderSize); + Assert.Equal("http://localhost:4317", general.OpenTelemetry.OtlpEndpoint); + Assert.Equal("TwinEngine", general.OpenTelemetry.ServiceName); + } + + [Fact] + public void V2Config_ResolvesPluginsConfig_WithCorrectValues() + { + using var appFactory = new ConfigTestFactory("v2-config"); + using var scope = appFactory.Services.CreateScope(); + + var plugins = scope.ServiceProvider.GetRequiredService>().Value; + + Assert.Equal("_aastwinengineindex_", plugins.SubmodelElementIndexContextPrefix); + Assert.Equal("_", plugins.MultiLanguageProperty.SemanticPostfixSeparator); + Assert.NotNull(plugins.MultiLanguageProperty.DefaultLanguages); + Assert.Contains("de", plugins.MultiLanguageProperty.DefaultLanguages); + Assert.Contains("en", plugins.MultiLanguageProperty.DefaultLanguages); + Assert.Equal(3, plugins.ResiliencePolicies.Retry.MaxRetryAttempts); + Assert.Equal(10, plugins.ResiliencePolicies.Retry.DelayInSeconds); + Assert.Equal(2.0, plugins.ResiliencePolicies.Retry.BackoffMultiplier); + _ = Assert.Single(plugins.Instances); + Assert.Equal("Plugin1", plugins.Instances[0].Name); + Assert.Equal(new Uri("http://localhost:8086"), plugins.Instances[0].BaseUrl); + } + + [Fact] + public void V2Config_ResolvesPluginHeaderMappings_InlinePerPlugin() + { + using var appFactory = new ConfigTestFactory("v2-config"); + using var scope = appFactory.Services.CreateScope(); + + var plugins = scope.ServiceProvider.GetRequiredService>().Value; + + var headers = plugins.Instances[0].HeaderMappings; + Assert.Equal(2, headers.Count); + Assert.Equal("Authorization", headers[0].Source); + Assert.Equal("X-Auth-Token", headers[0].Target); + Assert.Equal("X-Organization-Id", headers[1].Source); + Assert.Equal("X-Tenant-Context", headers[1].Target); + } + + [Fact] + public void V2Config_ResolvesTemplateManagementConfig_WithCorrectValues() + { + using var appFactory = new ConfigTestFactory("v2-config"); + using var scope = appFactory.Services.CreateScope(); + + var tmConfig = scope.ServiceProvider.GetRequiredService>().Value; + + Assert.Equal("InternalSemanticId", tmConfig.Semantics.InternalSemanticId); + Assert.Equal(3, tmConfig.ResiliencePolicies.Retry.MaxRetryAttempts); + Assert.Equal(10, tmConfig.ResiliencePolicies.Retry.DelayInSeconds); + Assert.Equal(new Uri("http://localhost:8081"), tmConfig.AasTemplateRepository.BaseUrl); + Assert.Equal(new Uri("http://localhost:8082"), tmConfig.AasTemplateRegistry.BaseUrl); + Assert.Equal(new Uri("http://localhost:8083"), tmConfig.SubmodelTemplateRegistry.BaseUrl); + Assert.Equal(3, tmConfig.TemplateMappingRules.SubmodelTemplateMappings.Count); + _ = Assert.Single(tmConfig.TemplateMappingRules.ShellTemplateMappings); + _ = Assert.Single(tmConfig.TemplateMappingRules.AasIdExtractionRules); + } + + [Fact] + public void V2Config_ResolvesEndpointHeaders_InlinePerEndpoint() + { + using var appFactory = new ConfigTestFactory("v2-config"); + using var scope = appFactory.Services.CreateScope(); + + var tmConfig = scope.ServiceProvider.GetRequiredService>().Value; + + Assert.Equal(2, tmConfig.AasTemplateRepository.HeaderMappings.Count); + Assert.Equal("Authorization", tmConfig.AasTemplateRepository.HeaderMappings[0].Source); + _ = Assert.Single(tmConfig.AasTemplateRegistry.HeaderMappings); + } + + [Fact] + public void V2Config_ResolvesRegistrySettings_Directly() + { + using var appFactory = new ConfigTestFactory("v2-config"); + using var scope = appFactory.Services.CreateScope(); + + var registry = scope.ServiceProvider.GetRequiredService>().Value; + + Assert.True(registry.PreComputed.Enabled); + Assert.Equal("0 */3 * * * *", registry.PreComputed.Schedule); + } + + // ────────────────────── Cross-Format Equivalence Tests ────────────────────── + + [Theory] + [InlineData("v1-config")] + [InlineData("v2-config")] + public void BothConfigs_ResolvePluginBaseUrl_ToSameValue(string configFile) + { + using var appFactory = new ConfigTestFactory(configFile); + using var scope = appFactory.Services.CreateScope(); + + var plugins = scope.ServiceProvider.GetRequiredService>().Value; + + _ = Assert.Single(plugins.Instances); + Assert.Equal(new Uri("http://localhost:8086"), plugins.Instances[0].BaseUrl); + } + + [Theory] + [InlineData("v1-config")] + [InlineData("v2-config")] + public void BothConfigs_ResolveTemplateRepositoryBaseUrl_ToSameValue(string configFile) + { + using var appFactory = new ConfigTestFactory(configFile); + using var scope = appFactory.Services.CreateScope(); + + var tmConfig = scope.ServiceProvider.GetRequiredService>().Value; + + Assert.Equal(new Uri("http://localhost:8081"), tmConfig.AasTemplateRepository.BaseUrl); + Assert.Equal(new Uri("http://localhost:8082"), tmConfig.AasTemplateRegistry.BaseUrl); + Assert.Equal(new Uri("http://localhost:8083"), tmConfig.SubmodelTemplateRegistry.BaseUrl); + } + + [Theory] + [InlineData("v1-config")] + [InlineData("v2-config")] + public void BothConfigs_ResolveSemanticsProperties_ToSameValues(string configFile) + { + using var appFactory = new ConfigTestFactory(configFile); + using var scope = appFactory.Services.CreateScope(); + + var plugins = scope.ServiceProvider.GetRequiredService>().Value; + var tmConfig = scope.ServiceProvider.GetRequiredService>().Value; + + Assert.Equal("_aastwinengineindex_", plugins.SubmodelElementIndexContextPrefix); + Assert.Equal("_", plugins.MultiLanguageProperty.SemanticPostfixSeparator); + Assert.Equal("InternalSemanticId", tmConfig.Semantics.InternalSemanticId); + } + + [Theory] + [InlineData("v1-config")] + [InlineData("v2-config")] + public void BothConfigs_ResolveMultiLanguagePropertyDefaults_ToSameValues(string configFile) + { + using var appFactory = new ConfigTestFactory(configFile); + using var scope = appFactory.Services.CreateScope(); + + var plugins = scope.ServiceProvider.GetRequiredService>().Value; + + Assert.NotNull(plugins.MultiLanguageProperty.DefaultLanguages); + Assert.Equal(2, plugins.MultiLanguageProperty.DefaultLanguages.Count); + Assert.Contains("de", plugins.MultiLanguageProperty.DefaultLanguages); + Assert.Contains("en", plugins.MultiLanguageProperty.DefaultLanguages); + } + + [Theory] + [InlineData("v1-config")] + [InlineData("v2-config")] + public void BothConfigs_ResolveRegistryPreComputed_ToSameValues(string configFile) + { + using var appFactory = new ConfigTestFactory(configFile); + using var scope = appFactory.Services.CreateScope(); + + var registry = scope.ServiceProvider.GetRequiredService>().Value; + + Assert.True(registry.PreComputed.Enabled); + Assert.Equal("0 */3 * * * *", registry.PreComputed.Schedule); + } + + [Theory] + [InlineData("v1-config")] + [InlineData("v2-config")] + public void BothConfigs_ResolveCustomerDomainUrl_ToSameValue(string configFile) + { + using var appFactory = new ConfigTestFactory(configFile); + using var scope = appFactory.Services.CreateScope(); + + var general = scope.ServiceProvider.GetRequiredService>().Value; + + Assert.Equal(new Uri("https://mm-software.com"), general.CustomerDomainUrl); + } + + [Theory] + [InlineData("v1-config")] + [InlineData("v2-config")] + public void BothConfigs_ResolveHeaderSanitization_ToSameValues(string configFile) + { + using var appFactory = new ConfigTestFactory(configFile); + using var scope = appFactory.Services.CreateScope(); + + var general = scope.ServiceProvider.GetRequiredService>().Value; + + Assert.Equal(8192, general.HeaderSanitization.MaxHeaderSize); + Assert.Equal(256, general.HeaderSanitization.MaxHeaderNameSize); + } + + [Theory] + [InlineData("v1-config")] + [InlineData("v2-config")] + public void BothConfigs_ResolveRetryPolicies_ToSameValues(string configFile) + { + using var appFactory = new ConfigTestFactory(configFile); + using var scope = appFactory.Services.CreateScope(); + + var plugins = scope.ServiceProvider.GetRequiredService>().Value; + var tmConfig = scope.ServiceProvider.GetRequiredService>().Value; + + Assert.Equal(3, plugins.ResiliencePolicies.Retry.MaxRetryAttempts); + Assert.Equal(10, plugins.ResiliencePolicies.Retry.DelayInSeconds); + Assert.Equal(3, tmConfig.ResiliencePolicies.Retry.MaxRetryAttempts); + Assert.Equal(10, tmConfig.ResiliencePolicies.Retry.DelayInSeconds); + } + + [Theory] + [InlineData("v1-config")] + [InlineData("v2-config")] + public void BothConfigs_ResolveTemplateMappingRules_WithSameCount(string configFile) + { + using var appFactory = new ConfigTestFactory(configFile); + using var scope = appFactory.Services.CreateScope(); + + var tmConfig = scope.ServiceProvider.GetRequiredService>().Value; + + Assert.Equal(3, tmConfig.TemplateMappingRules.SubmodelTemplateMappings.Count); + _ = Assert.Single(tmConfig.TemplateMappingRules.ShellTemplateMappings); + _ = Assert.Single(tmConfig.TemplateMappingRules.AasIdExtractionRules); + } +} + diff --git a/source/AAS.TwinEngine.DataEngine.ModuleTests/Api/Services/SubmodelRegistry/SubmodelDescriptorControllerTests.cs b/source/AAS.TwinEngine.DataEngine.ModuleTests/Api/Services/SubmodelRegistry/SubmodelDescriptorControllerTests.cs index 0c51fb46..2ec00a1a 100644 --- a/source/AAS.TwinEngine.DataEngine.ModuleTests/Api/Services/SubmodelRegistry/SubmodelDescriptorControllerTests.cs +++ b/source/AAS.TwinEngine.DataEngine.ModuleTests/Api/Services/SubmodelRegistry/SubmodelDescriptorControllerTests.cs @@ -6,8 +6,8 @@ using AAS.TwinEngine.DataEngine.ApplicationLogic.Exceptions.Infrastructure; using AAS.TwinEngine.DataEngine.ApplicationLogic.Services.Plugin.Providers; using AAS.TwinEngine.DataEngine.ApplicationLogic.Services.SubmodelRegistry.Providers; +using AAS.TwinEngine.DataEngine.ModuleTests.Common; -using Microsoft.AspNetCore.Mvc.Testing; using Microsoft.AspNetCore.WebUtilities; using Microsoft.Extensions.DependencyInjection; @@ -16,26 +16,31 @@ namespace AAS.TwinEngine.DataEngine.ModuleTests.Api.Services.SubmodelRegistry; -public class SubmodelDescriptorControllerTests : IClassFixture> +public abstract class SubmodelDescriptorControllerTestsBase : IDisposable { + private readonly ConfigTestFactory _factory; private readonly ISubmodelDescriptorProvider _mockSubmodelDescriptorProvider; private readonly HttpClient _client; - public SubmodelDescriptorControllerTests(WebApplicationFactory factory) + protected SubmodelDescriptorControllerTestsBase(string configDir) { _mockSubmodelDescriptorProvider = Substitute.For(); var mockPluginManifestProvider = Substitute.For(); - var factory1 = factory.WithWebHostBuilder(builder => + _factory = new ConfigTestFactory(configDir, services => { - _ = builder.ConfigureServices(services => - { - _ = services.AddSingleton(mockPluginManifestProvider); - _ = services.AddSingleton(_mockSubmodelDescriptorProvider); - }); + _ = services.AddSingleton(mockPluginManifestProvider); + _ = services.AddSingleton(_mockSubmodelDescriptorProvider); }); - _client = factory1.CreateClient(); + _client = _factory.CreateClient(); + } + + public void Dispose() + { + _client.Dispose(); + _factory.Dispose(); + GC.SuppressFinalize(this); } [Fact] @@ -93,7 +98,7 @@ public async Task GetSubmodelDescriptorByIdAsync_WhenIdentifierIsInValid_Returns [Theory] [InlineData("invalid!!base64")] [InlineData("not-valid-base64!!!")] - public async Task GetSubmodelDescriptorById_InvalidBase64_Returns400BadRequest(string invalidBase64) + public async Task GetSubmodelDescriptorById_InvalidBase64_Returns400BadRequestAsync(string invalidBase64) { var response = await _client.GetAsync($"/submodel-descriptors/{invalidBase64}"); @@ -105,7 +110,7 @@ public async Task GetSubmodelDescriptorById_InvalidBase64_Returns400BadRequest(s [InlineData("")] [InlineData("eval(alert('xss'))")] [InlineData("")] - public async Task GetSubmodelDescriptorById_XssInDecodedId_Returns400BadRequest(string maliciousContent) + public async Task GetSubmodelDescriptorById_XssInDecodedId_Returns400BadRequestAsync(string maliciousContent) { var encoded = EncodeBase64Url(maliciousContent); @@ -118,7 +123,7 @@ public async Task GetSubmodelDescriptorById_XssInDecodedId_Returns400BadRequest( [InlineData("' OR '1'='1")] [InlineData("'; DROP TABLE descriptors--")] [InlineData("1 UNION SELECT * FROM submodels")] - public async Task GetSubmodelDescriptorById_SqlInjectionInDecodedId_Returns400BadRequest(string maliciousContent) + public async Task GetSubmodelDescriptorById_SqlInjectionInDecodedId_Returns400BadRequestAsync(string maliciousContent) { var encoded = EncodeBase64Url(maliciousContent); @@ -131,7 +136,7 @@ public async Task GetSubmodelDescriptorById_SqlInjectionInDecodedId_Returns400Ba [InlineData("../../../etc/passwd")] [InlineData("..\\..\\windows\\system32")] [InlineData("%2e%2e/config")] - public async Task GetSubmodelDescriptorById_PathTraversalInDecodedId_Returns400BadRequest(string maliciousContent) + public async Task GetSubmodelDescriptorById_PathTraversalInDecodedId_Returns400BadRequestAsync(string maliciousContent) { var encoded = EncodeBase64Url(maliciousContent); @@ -145,7 +150,7 @@ public async Task GetSubmodelDescriptorById_PathTraversalInDecodedId_Returns400B [InlineData("data:text/html,")] [InlineData("element")] [InlineData("")] - public async Task GetSubmodelElement_XssInIdShortPath_Returns400BadRequest(string maliciousIdShortPath) + public async Task GetSubmodelElement_XssInIdShortPath_Returns400BadRequestAsync(string maliciousIdShortPath) { var validSubmodelId = EncodeBase64Url("https://example.com/submodels/test"); @@ -228,7 +233,7 @@ public async Task GetSubmodelElement_XssInIdShortPath_Returns400BadRequest(strin [InlineData("element'; DROP TABLE--")] [InlineData("1 UNION SELECT *")] [InlineData("admin'--")] - public async Task GetSubmodelElement_SqlInjectionInIdShortPath_Returns400BadRequest(string maliciousIdShortPath) + public async Task GetSubmodelElement_SqlInjectionInIdShortPath_Returns400BadRequestAsync(string maliciousIdShortPath) { var validSubmodelId = EncodeBase64Url("https://example.com/submodels/test"); @@ -242,7 +247,7 @@ public async Task GetSubmodelElement_SqlInjectionInIdShortPath_Returns400BadRequ [InlineData("data:text/html,V1 (old config): If was populated +/// by the legacy adapter, that value is used. +/// V2 (new config): The property is null, so the URL is derived from the incoming +/// HTTP request (Scheme://Host). +/// +/// +public class HttpRequestBaseUrlProvider( + IHttpContextAccessor httpContextAccessor, + IOptions generalConfig) : IBaseUrlProvider +{ + private readonly IHttpContextAccessor _httpContextAccessor = httpContextAccessor; + private readonly Uri? _configuredBaseUrl = generalConfig.Value.DataEngineRepositoryBaseUrl; + + public Uri GetBaseUrl() + { + if (_configuredBaseUrl != null) + { + return _configuredBaseUrl; + } + + var request = _httpContextAccessor.HttpContext?.Request + ?? throw new InvalidOperationException("No HTTP request context available — cannot derive base URL."); + + var baseUrl = $"{request.Scheme}://{request.Host}"; + return new Uri(baseUrl.TrimEnd('/') + "/"); + } +} diff --git a/source/AAS.TwinEngine.DataEngine/ServiceConfiguration/ApplicationDependencyInjectionExtensions.cs b/source/AAS.TwinEngine.DataEngine/ServiceConfiguration/ApplicationDependencyInjectionExtensions.cs index 1c8e58b4..af3c4d41 100644 --- a/source/AAS.TwinEngine.DataEngine/ServiceConfiguration/ApplicationDependencyInjectionExtensions.cs +++ b/source/AAS.TwinEngine.DataEngine/ServiceConfiguration/ApplicationDependencyInjectionExtensions.cs @@ -1,7 +1,7 @@ using AAS.TwinEngine.DataEngine.Api.AasRegistry.Handler; using AAS.TwinEngine.DataEngine.Api.AasRepository.Handler; -using AAS.TwinEngine.DataEngine.Api.Configuration; using AAS.TwinEngine.DataEngine.Api.SubmodelRegistry.Handler; +using AAS.TwinEngine.DataEngine.ServiceConfiguration.Config; using AAS.TwinEngine.DataEngine.Api.SubmodelRepository.Handler; using AAS.TwinEngine.DataEngine.ApplicationLogic.Exceptions; using AAS.TwinEngine.DataEngine.ApplicationLogic.Services.AasRegistry; @@ -27,8 +27,7 @@ public static void ConfigureApplication(this IServiceCollection services, IConfi { _ = services.AddExceptionHandler(); _ = services.AddProblemDetails(); - _ = services.Configure(configuration.GetSection("ApiConfiguration")); - _ = services.AddSingleton(sp => sp.GetRequiredService>().Value); + _ = services.AddSingleton(sp => sp.GetRequiredService>().Value.ApiConfiguration); _ = services.AddScoped(); _ = services.AddScoped(); _ = services.AddScoped(); diff --git a/source/AAS.TwinEngine.DataEngine/ServiceConfiguration/Config/GeneralConfig.cs b/source/AAS.TwinEngine.DataEngine/ServiceConfiguration/Config/GeneralConfig.cs new file mode 100644 index 00000000..44c21384 --- /dev/null +++ b/source/AAS.TwinEngine.DataEngine/ServiceConfiguration/Config/GeneralConfig.cs @@ -0,0 +1,32 @@ +using AAS.TwinEngine.DataEngine.Api.Configuration; +using AAS.TwinEngine.DataEngine.Infrastructure.Http.Authorization.Config; + +namespace AAS.TwinEngine.DataEngine.ServiceConfiguration.Config; + +/// +/// V2 config — binds to the "General" section. +/// Contains cross-cutting / infrastructure concerns. +/// +public class GeneralConfig +{ + public const string Section = "General"; + + public ApiConfiguration ApiConfiguration { get; set; } = new(); + public HeaderSanitizationOptions HeaderSanitization { get; set; } = new(); + public string AllowedHosts { get; set; } = "*"; + public OpenTelemetrySettings OpenTelemetry { get; set; } = new(); + + /// + /// Domain URL of the customer environment (V2: direct property; V1: was AasEnvironment:CustomerDomainUrl). + /// + public Uri CustomerDomainUrl { get; set; } = null!; + + /// + /// Base URL of the DataEngine's own repository. + /// V1: populated by legacy adapter from AasEnvironment:DataEngineRepositoryBaseUrl. + /// V2: null — the URL is derived from the incoming HTTP request at runtime. + /// + public Uri? DataEngineRepositoryBaseUrl { get; set; } + + // Note: Serilog reads directly from IConfiguration, not via POCO +} diff --git a/source/AAS.TwinEngine.DataEngine/ServiceConfiguration/Config/PluginsConfig.cs b/source/AAS.TwinEngine.DataEngine/ServiceConfiguration/Config/PluginsConfig.cs new file mode 100644 index 00000000..a279e1aa --- /dev/null +++ b/source/AAS.TwinEngine.DataEngine/ServiceConfiguration/Config/PluginsConfig.cs @@ -0,0 +1,54 @@ +using AAS.TwinEngine.DataEngine.Infrastructure.Http.Authorization.Config; + +namespace AAS.TwinEngine.DataEngine.ServiceConfiguration.Config; + +/// +/// V2 config — binds to the "Plugins" section. +/// Contains all plugin-related configuration. +/// +public class PluginsConfig +{ + public const string Section = "Plugins"; + + public string SubmodelElementIndexContextPrefix { get; set; } = "_aastwinengineindex_"; + public PluginMultiLanguagePropertyConfig MultiLanguageProperty { get; set; } = new(); + public ResiliencePoliciesConfig ResiliencePolicies { get; set; } = new(); + public IList Instances { get; set; } = []; +} + +/// +/// Multi-language property settings co-located with plugins. +/// Combines the old MultiLanguagePropertySettings.DefaultLanguages with +/// the old Semantics.MultiLanguageSemanticPostfixSeparator (renamed). +/// +public class PluginMultiLanguagePropertyConfig +{ + public IList? DefaultLanguages { get; init; } + public string SemanticPostfixSeparator { get; set; } = "_"; +} + +/// +/// A single plugin instance. Replaces old Plugin class with renamed properties +/// and co-located header mappings. +/// +public class PluginInstance +{ + public required string Name { get; set; } + public required Uri BaseUrl { get; set; } + public IList HeaderMappings { get; init; } = []; +} + +/// +/// Resilience policies shared within a domain (Plugins or TemplateManagement). +/// +public class ResiliencePoliciesConfig +{ + public RetryConfig Retry { get; set; } = new(); +} + +public class RetryConfig +{ + public int MaxRetryAttempts { get; set; } + public int DelayInSeconds { get; set; } + public double BackoffMultiplier { get; set; } = 2.0; +} diff --git a/source/AAS.TwinEngine.DataEngine/ServiceConfiguration/Config/RegistrySettingsConfig.cs b/source/AAS.TwinEngine.DataEngine/ServiceConfiguration/Config/RegistrySettingsConfig.cs new file mode 100644 index 00000000..a367b19f --- /dev/null +++ b/source/AAS.TwinEngine.DataEngine/ServiceConfiguration/Config/RegistrySettingsConfig.cs @@ -0,0 +1,23 @@ +namespace AAS.TwinEngine.DataEngine.ServiceConfiguration.Config; + +/// +/// V2 config — binds to the "RegistrySettings" section. +/// Note: The V2 JSON has a typo "ResgistrySettings" — both spellings are handled by the normalizer. +/// +public class RegistrySettingsConfig +{ + public const string Section = "RegistrySettings"; + + /// + /// Also check for typo variant in new JSON: "ResgistrySettings". + /// + public const string SectionTypoVariant = "ResgistrySettings"; + + public PreComputedConfig PreComputed { get; set; } = new(); +} + +public class PreComputedConfig +{ + public bool Enabled { get; set; } = false; + public string Schedule { get; set; } = "0 */3 * * * *"; +} diff --git a/source/AAS.TwinEngine.DataEngine/ServiceConfiguration/Config/TemplateManagementConfig.cs b/source/AAS.TwinEngine.DataEngine/ServiceConfiguration/Config/TemplateManagementConfig.cs new file mode 100644 index 00000000..96af19c1 --- /dev/null +++ b/source/AAS.TwinEngine.DataEngine/ServiceConfiguration/Config/TemplateManagementConfig.cs @@ -0,0 +1,40 @@ +using AAS.TwinEngine.DataEngine.Infrastructure.Http.Authorization.Config; +using AAS.TwinEngine.DataEngine.Infrastructure.Providers.TemplateProvider.Config; + +namespace AAS.TwinEngine.DataEngine.ServiceConfiguration.Config; + +/// +/// V2 config — binds to the "TemplateManagement" section. +/// Contains template repositories, registries, mapping rules, and semantics. +/// +public class TemplateManagementConfig +{ + public const string Section = "TemplateManagement"; + + public ResiliencePoliciesConfig ResiliencePolicies { get; set; } = new(); + public TemplateMappingRules TemplateMappingRules { get; set; } = new(); + public TemplateSemanticsConfig Semantics { get; set; } = new(); + public ServiceEndpoint AasTemplateRepository { get; set; } = new(); + public ServiceEndpoint SubmodelTemplateRepository { get; set; } = new(); + public ServiceEndpoint AasTemplateRegistry { get; set; } = new(); + public ServiceEndpoint SubmodelTemplateRegistry { get; set; } = new(); +} + +/// +/// Semantics configuration for template management (InternalSemanticId only). +/// +public class TemplateSemanticsConfig +{ + public string InternalSemanticId { get; set; } = "InternalSemanticId"; +} + +/// +/// A named service endpoint with a base URL and co-located header mappings. +/// Used for template repositories and registries in the V2 schema. +/// +public class ServiceEndpoint +{ + public string Name { get; set; } = string.Empty; + public Uri? BaseUrl { get; set; } + public IList HeaderMappings { get; init; } = []; +} diff --git a/source/AAS.TwinEngine.DataEngine/ServiceConfiguration/InfrastructureDependencyInjectionExtensions.cs b/source/AAS.TwinEngine.DataEngine/ServiceConfiguration/InfrastructureDependencyInjectionExtensions.cs index a4a92298..c07bed3b 100644 --- a/source/AAS.TwinEngine.DataEngine/ServiceConfiguration/InfrastructureDependencyInjectionExtensions.cs +++ b/source/AAS.TwinEngine.DataEngine/ServiceConfiguration/InfrastructureDependencyInjectionExtensions.cs @@ -4,16 +4,14 @@ using AAS.TwinEngine.DataEngine.ApplicationLogic.Services.Plugin.Config; using AAS.TwinEngine.DataEngine.ApplicationLogic.Services.Plugin.Helper; using AAS.TwinEngine.DataEngine.ApplicationLogic.Services.Plugin.Providers; +using AAS.TwinEngine.DataEngine.ApplicationLogic.Services.Shared; using AAS.TwinEngine.DataEngine.ApplicationLogic.Services.SubmodelRegistry.Providers; -using AAS.TwinEngine.DataEngine.ApplicationLogic.Services.SubmodelRepository.Config; using AAS.TwinEngine.DataEngine.ApplicationLogic.Services.SubmodelRepository.Config.Helper; -using AAS.TwinEngine.DataEngine.Infrastructure.Http.Authorization.Config; +using AAS.TwinEngine.DataEngine.Infrastructure.Configuration.LegacyV1; using AAS.TwinEngine.DataEngine.Infrastructure.Http.Authorization.Headers; using AAS.TwinEngine.DataEngine.Infrastructure.Http.Clients; -using AAS.TwinEngine.DataEngine.Infrastructure.Http.Config; using AAS.TwinEngine.DataEngine.Infrastructure.Http.Extensions; using AAS.TwinEngine.DataEngine.Infrastructure.Monitoring; -using AAS.TwinEngine.DataEngine.Infrastructure.Providers.AasRegistryProvider.Config; using AAS.TwinEngine.DataEngine.Infrastructure.Providers.AasRegistryProvider.Services; using AAS.TwinEngine.DataEngine.Infrastructure.Providers.PluginDataProvider.Config; using AAS.TwinEngine.DataEngine.Infrastructure.Providers.PluginDataProvider.Helper; @@ -21,6 +19,8 @@ using AAS.TwinEngine.DataEngine.Infrastructure.Providers.SubmodelRegistryProvider.Services; using AAS.TwinEngine.DataEngine.Infrastructure.Providers.TemplateProvider.Config; using AAS.TwinEngine.DataEngine.Infrastructure.Providers.TemplateProvider.Services; +using AAS.TwinEngine.DataEngine.Infrastructure.Shared; +using AAS.TwinEngine.DataEngine.ServiceConfiguration.Config; using Microsoft.Extensions.Options; @@ -34,36 +34,58 @@ public static void ConfigureInfrastructure(this IServiceCollection services, ICo _ = services.AddScoped(); + _ = services.AddScoped(); + _ = services.AddScoped(); _ = services.AddScoped(); _ = services.AddScoped(); _ = services.AddScoped(); - _ = services.Configure(configuration.GetSection(TemplateMappingRules.Section)); - _ = services.Configure(configuration.GetSection(AasEnvironmentConfig.Section)); - _ = services.Configure(configuration.GetSection(AasxExportOptions.Section)); - _ = services.Configure(configuration.GetSection(PluginConfig.Section)); - _ = services.Configure(configuration.GetSection(Semantics.Section)); - var aasEnvironment = configuration.GetSection(AasEnvironmentConfig.Section).Get(); - var plugins = configuration.GetSection(PluginConfig.Section).Get(); - _ = services.AddOptions() - .Bind(configuration.GetSection(MultiLanguagePropertySettings.Section)) + // ── V1 → V2 legacy adapters (IConfigureOptions), no-op when V2 config is present ── +#pragma warning disable CS0618 // Obsolete — intentional V1 backward-compat registration + _ = services.AddLegacyV1ConfigurationAdapters(); +#pragma warning restore CS0618 + + // ── V2 POCO registrations (section-bind overwrites adapter defaults when V2 JSON exists) ── + _ = services.Configure(configuration.GetSection(GeneralConfig.Section)); + + // MultiPluginConflictOptions: V1 config binds the old section value; V2 has no section → default ThrowError + _ = services.Configure(configuration.GetSection(MultiPluginConflictOptions.Section)); + _ = services.Configure(configuration.GetSection(TemplateManagementConfig.Section)); + _ = services.Configure(configuration.GetSection(RegistrySettingsConfig.Section)); + + // PluginsConfig: single registration via AddOptions to avoid double-binding of list properties + _ = services.AddOptions() + .Bind(configuration.GetSection(PluginsConfig.Section)) .ValidateOnStart(); - _ = services.AddSingleton, MultiLanguagePropertySettingsValidator>(); - _ = services.Configure(configuration.GetSection(HeaderForwardingOptions.Section)); + _ = services.AddSingleton, PluginsConfigValidator>(); + + // ── Resolve fully-populated config for HttpClient registration ── + // We need TemplateManagementConfig and PluginsConfig to register HttpClients at startup. + // IOptions is populated by V1 legacy adapters (IConfigureOptions) + V2 section-bind. + // Since we are still inside DI registration (container not built yet), we build a + // temporary provider to resolve the options so both V1 and V2 paths are applied. + using var tempProvider = services.BuildServiceProvider(); + var templateManagement = tempProvider.GetRequiredService>().Value; + var pluginsConfig = tempProvider.GetRequiredService>().Value; - _ = services.AddHttpClientWithResilience(configuration, AasEnvironmentConfig.AasEnvironmentRepoHttpClientName, HttpRetryPolicyOptions.TemplateProvider, aasEnvironment?.AasEnvironmentRepositoryBaseUrl!); - _ = services.AddHttpClientWithResilience(configuration, AasEnvironmentConfig.AasRegistryHttpClientName, HttpRetryPolicyOptions.TemplateProvider, aasEnvironment?.AasRegistryBaseUrl!); - _ = services.AddHttpClientWithResilience(configuration, AasEnvironmentConfig.SubmodelRegistryHttpClientName, HttpRetryPolicyOptions.SubmodelDescriptorProvider, aasEnvironment?.SubModelRegistryBaseUrl!); + // Template repository/registry HttpClients (base URLs from TemplateManagement) + _ = services.AddHttpClientWithResilience(AasEnvironmentConfig.AasEnvironmentRepoHttpClientName, templateManagement.ResiliencePolicies.Retry, templateManagement.AasTemplateRepository.BaseUrl!); + _ = services.AddHttpClientWithResilience(AasEnvironmentConfig.AasRegistryHttpClientName, templateManagement.ResiliencePolicies.Retry, templateManagement.AasTemplateRegistry.BaseUrl!); + _ = services.AddHttpClientWithResilience(AasEnvironmentConfig.SubmodelRegistryHttpClientName, templateManagement.ResiliencePolicies.Retry, templateManagement.SubmodelTemplateRegistry.BaseUrl!); - _ = services.AddHttpClientWithoutResilience(AasEnvironmentConfig.AasEnvironmentRepoHealthCheckHttpClientName, aasEnvironment?.AasEnvironmentRepositoryBaseUrl!); - _ = services.AddHttpClientWithoutResilience(AasEnvironmentConfig.AasRegistryHealthCheckHttpClientName, aasEnvironment?.AasRegistryBaseUrl!); - _ = services.AddHttpClientWithoutResilience(AasEnvironmentConfig.SubmodelRegistryHealthCheckHttpClientName, aasEnvironment?.SubModelRegistryBaseUrl!); + _ = services.AddHttpClientWithoutResilience(AasEnvironmentConfig.AasEnvironmentRepoHealthCheckHttpClientName, templateManagement.AasTemplateRepository.BaseUrl!); + _ = services.AddHttpClientWithoutResilience(AasEnvironmentConfig.AasRegistryHealthCheckHttpClientName, templateManagement.AasTemplateRegistry.BaseUrl!); + _ = services.AddHttpClientWithoutResilience(AasEnvironmentConfig.SubmodelRegistryHealthCheckHttpClientName, templateManagement.SubmodelTemplateRegistry.BaseUrl!); - foreach (var plugin in plugins.Plugins) + // Plugin HttpClients (from PluginsConfig.Instances) + if (pluginsConfig.Instances.Count > 0) { - _ = services.AddHttpClientWithResilience(configuration, PluginConfig.HttpClientNamePrefix + plugin.PluginName, HttpRetryPolicyOptions.PluginDataProvider, plugin.PluginUrl); - _ = services.AddHttpClientWithoutResilience(PluginConfig.HealthCheckHttpClientNamePrefix + plugin.PluginName, plugin.PluginUrl!); + foreach (var plugin in pluginsConfig.Instances) + { + _ = services.AddHttpClientWithResilience(PluginConfig.HttpClientNamePrefix + plugin.Name, pluginsConfig.ResiliencePolicies.Retry, plugin.BaseUrl); + _ = services.AddHttpClientWithoutResilience(PluginConfig.HealthCheckHttpClientNamePrefix + plugin.Name, plugin.BaseUrl!); + } } _ = services.AddScoped(); @@ -74,12 +96,6 @@ public static void ConfigureInfrastructure(this IServiceCollection services, ICo _ = services.AddScoped(); _ = services.AddScoped(); _ = services.AddScoped(); - _ = services.Configure(HttpRetryPolicyOptions.PluginDataProvider, configuration.GetSection($"{HttpRetryPolicyOptions.Section}:{HttpRetryPolicyOptions.PluginDataProvider}")); - _ = services.Configure(HttpRetryPolicyOptions.TemplateProvider, configuration.GetSection($"{HttpRetryPolicyOptions.Section}:{HttpRetryPolicyOptions.TemplateProvider}")); - _ = services.Configure(HttpRetryPolicyOptions.SubmodelDescriptorProvider, configuration.GetSection($"{HttpRetryPolicyOptions.Section}:{HttpRetryPolicyOptions.SubmodelDescriptorProvider}")); - _ = services.Configure(configuration.GetSection(HttpRetryPolicyOptions.Section)); - _ = services.Configure(configuration.GetSection(AasRegistryPreComputed.Section)); - _ = services.Configure(configuration.GetSection(MultiPluginConflictOptions.Section)); _ = services.AddSingleton(); _ = services.AddHostedService(); } diff --git a/source/AAS.TwinEngine.DataEngine/ServiceConfiguration/LoggingConfigurationExtension.cs b/source/AAS.TwinEngine.DataEngine/ServiceConfiguration/LoggingConfigurationExtension.cs index c6f671c7..a6fa496d 100644 --- a/source/AAS.TwinEngine.DataEngine/ServiceConfiguration/LoggingConfigurationExtension.cs +++ b/source/AAS.TwinEngine.DataEngine/ServiceConfiguration/LoggingConfigurationExtension.cs @@ -1,7 +1,6 @@ using System.Diagnostics.CodeAnalysis; using AAS.TwinEngine.DataEngine.Infrastructure.Logging; -using AAS.TwinEngine.DataEngine.ServiceConfiguration.Config; using OpenTelemetry.Logs; using OpenTelemetry.Metrics; @@ -19,7 +18,7 @@ internal static class LoggingConfigurationExtension { public static void ConfigureLogging(this WebApplicationBuilder builder, IConfiguration configuration) { - var otelSettings = configuration.GetSection(OpenTelemetrySettings.Section).Get() ?? new OpenTelemetrySettings(); + var otelSettings = configuration.GetSection($"{Config.GeneralConfig.Section}:{Config.OpenTelemetrySettings.Section}").Get() ?? new Config.OpenTelemetrySettings(); var logLevelSwitch = new LoggingLevelSwitch(LogEventLevel.Information); diff --git a/source/AAS.TwinEngine.DataEngine/appsettings.development.json b/source/AAS.TwinEngine.DataEngine/appsettings.development.json index edd43f73..be109f0f 100644 --- a/source/AAS.TwinEngine.DataEngine/appsettings.development.json +++ b/source/AAS.TwinEngine.DataEngine/appsettings.development.json @@ -1,113 +1,12 @@ { - "ApiConfiguration": { - "BasePath": "" - }, - "Logging": { - "LogLevel": { - "Default": "Information", - "Microsoft.AspNetCore": "Warning" - } - }, - "PluginConfig": { - "Plugins": [ - { - "PluginName": "Plugin1", - "PluginUrl": "http://localhost:8086" - } - ] - }, - "AasEnvironment": { - "DataEngineRepositoryBaseUrl": "https://localhost:5059", - "AasEnvironmentRepositoryBaseUrl": "http://localhost:8081", - "SubModelRepositoryPath": "submodels", - "AasRegistryBaseUrl": "http://localhost:8082", - "AasRegistryPath": "shell-descriptors", - "SubModelRegistryBaseUrl": "http://localhost:8083", - "SubModelRegistryPath": "submodel-descriptors", - "AasRepositoryPath": "shells", - "ConceptDescriptionPath": "concept-descriptions", - "SubmodelRefPath": "submodel-refs", - "CustomerDomainUrl": "https://mm-software.com" - }, - "MultiPluginConflictOption": { - "HandlingMode": "TakeFirst" // "TakeFirst" | "SkipConflictingIds" | "ThrowError" - }, - "AasxExportOptions": { - "RootFolder": "aasx" - }, - "AasRegistryPreComputed": { - "ShellDescriptorCron": "0 */3 * * * *", - "IsPreComputed": true - }, - "TemplateMappingRules": { - "SubmodelTemplateMappings": [ - { - "templateId": "https://admin-shell.io/idta/SubmodelTemplate/Reliability/1/0", - "pattern": [ "Reliability" ] - }, - { - "templateId": "https://admin-shell.io/idta/SubmodelTemplate/DigitalNameplate/3/0", - "pattern": [ "Nameplate" ] - }, - { - "templateId": "https://admin-shell.io/idta/SubmodelTemplate/ContactInformation/1/0", - "pattern": [ "ContactInformation" ] - }, - { - "templateId": "https://admin-shell.io/ZVEI/TechnicalData/Submodel/1/2", - "pattern": [ "TechnicalData" ] - }, - { - "templateId": "https://admin-shell.io/idta/SubmodelTemplate/CarbonFootprint/1/0", - "pattern": [ "CarbonFootprint" ] - }, - { - "templateId": "https://admin-shell.io/idta/SubmodelTemplate/HandoverDocumentation/2/0", - "pattern": [ "HandoverDocumentation" ] - } - ], - "ShellTemplateMappings": [ - { - "templateId": "https://mm-software.com/aas/aasTemplate", - "pattern": [ "" ] + "General": { + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" } - ], - "AasIdExtractionRules": [ - { - "Pattern": "Regex", - "Index": 3, - "Separator": ":" - }, - { - "Pattern": "Regex", - "Index": 6, - "Separator": "/" - } - ] - }, - "Semantics": { - "MultiLanguageSemanticPostfixSeparator": "_", - "SubmodelElementIndexContextPrefix": "_aastwinengineindex_", - "InternalSemanticId": "InternalSemanticId" - }, - "MultiLanguageProperty": { - "DefaultLanguages": [ "de", "en" ] - }, - "HttpRetryPolicyOptions": { - "TemplateProvider": { - "MaxRetryAttempts": 3, - "DelayInSeconds": 10 }, - "PluginDataProvider": { - "MaxRetryAttempts": 3, - "DelayInSeconds": 10 - }, - "SubmodelDescriptorProvider": { - "MaxRetryAttempts": 4, - "DelayInSeconds": 7 - } - }, - "HeaderForwarding": { + "CustomerDomainUrl": "https://mm-software.com", "HeaderSanitization": { "MaxHeaderSize": 8192, "MaxHeaderNameSize": 256, @@ -121,81 +20,202 @@ " Date: Wed, 8 Apr 2026 02:29:09 +0530 Subject: [PATCH 18/71] remove unused usings --- .../SemanticId/Helpers/SubmodelElementHelperTests.cs | 2 -- .../Services/SubmodelRepository/SemanticIdHandlerTests.cs | 5 ++--- .../Http/Authorization/Headers/RequestHeaderMapperTests.cs | 1 - .../AasRegistryProvider/AasRegistryProviderTests.cs | 1 - .../Services/ShellTemplateMappingProviderTests.cs | 6 +++--- .../Services/AasRepository/AasRepositoryTemplateService.cs | 3 +-- .../Infrastructure/Shared/HttpRequestBaseUrlProvider.cs | 1 - .../ApplicationDependencyInjectionExtensions.cs | 2 +- .../InfrastructureDependencyInjectionExtensions.cs | 1 - .../ApiTestBase.cs | 6 +++--- .../Api/Manifest/ManifestController.cs | 2 +- 11 files changed, 11 insertions(+), 19 deletions(-) diff --git a/source/AAS.TwinEngine.DataEngine.UnitTests/ApplicationLogic/Services/SubmodelRepository/SemanticId/Helpers/SubmodelElementHelperTests.cs b/source/AAS.TwinEngine.DataEngine.UnitTests/ApplicationLogic/Services/SubmodelRepository/SemanticId/Helpers/SubmodelElementHelperTests.cs index 67468dcf..78681767 100644 --- a/source/AAS.TwinEngine.DataEngine.UnitTests/ApplicationLogic/Services/SubmodelRepository/SemanticId/Helpers/SubmodelElementHelperTests.cs +++ b/source/AAS.TwinEngine.DataEngine.UnitTests/ApplicationLogic/Services/SubmodelRepository/SemanticId/Helpers/SubmodelElementHelperTests.cs @@ -11,8 +11,6 @@ using static Xunit.Assert; -using File = AasCore.Aas3_0.File; - namespace AAS.TwinEngine.DataEngine.UnitTests.ApplicationLogic.Services.SubmodelRepository.SemanticId.Helpers; public class SubmodelElementHelperTests diff --git a/source/AAS.TwinEngine.DataEngine.UnitTests/ApplicationLogic/Services/SubmodelRepository/SemanticIdHandlerTests.cs b/source/AAS.TwinEngine.DataEngine.UnitTests/ApplicationLogic/Services/SubmodelRepository/SemanticIdHandlerTests.cs index 50e6c2f8..9c97e337 100644 --- a/source/AAS.TwinEngine.DataEngine.UnitTests/ApplicationLogic/Services/SubmodelRepository/SemanticIdHandlerTests.cs +++ b/source/AAS.TwinEngine.DataEngine.UnitTests/ApplicationLogic/Services/SubmodelRepository/SemanticIdHandlerTests.cs @@ -1,6 +1,5 @@ using AAS.TwinEngine.DataEngine.ApplicationLogic.Exceptions.Application; using AAS.TwinEngine.DataEngine.ApplicationLogic.Services.SubmodelRepository; -using AAS.TwinEngine.DataEngine.ApplicationLogic.Services.SubmodelRepository.Config; using AAS.TwinEngine.DataEngine.ApplicationLogic.Services.SubmodelRepository.SemanticId.ElementHandlers; using AAS.TwinEngine.DataEngine.ApplicationLogic.Services.SubmodelRepository.SemanticId.Extraction; using AAS.TwinEngine.DataEngine.ApplicationLogic.Services.SubmodelRepository.SemanticId.FillOut; @@ -8,13 +7,13 @@ using AAS.TwinEngine.DataEngine.DomainModel.SubmodelRepository; using AAS.TwinEngine.DataEngine.ServiceConfiguration.Config; -using MongoDB.Bson; - using AasCore.Aas3_0; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; +using MongoDB.Bson; + using NSubstitute; using static Xunit.Assert; diff --git a/source/AAS.TwinEngine.DataEngine.UnitTests/Infrastructure/Http/Authorization/Headers/RequestHeaderMapperTests.cs b/source/AAS.TwinEngine.DataEngine.UnitTests/Infrastructure/Http/Authorization/Headers/RequestHeaderMapperTests.cs index 49169ce1..75e4efed 100644 --- a/source/AAS.TwinEngine.DataEngine.UnitTests/Infrastructure/Http/Authorization/Headers/RequestHeaderMapperTests.cs +++ b/source/AAS.TwinEngine.DataEngine.UnitTests/Infrastructure/Http/Authorization/Headers/RequestHeaderMapperTests.cs @@ -1,5 +1,4 @@ using AAS.TwinEngine.DataEngine.ApplicationLogic.Exceptions.Application; -using AAS.TwinEngine.DataEngine.ApplicationLogic.Exceptions.Base; using AAS.TwinEngine.DataEngine.ApplicationLogic.Services.Plugin.Config; using AAS.TwinEngine.DataEngine.Infrastructure.Http.Authorization.Config; using AAS.TwinEngine.DataEngine.Infrastructure.Http.Authorization.Headers; diff --git a/source/AAS.TwinEngine.DataEngine.UnitTests/Infrastructure/Providers/AasRegistryProvider/AasRegistryProviderTests.cs b/source/AAS.TwinEngine.DataEngine.UnitTests/Infrastructure/Providers/AasRegistryProvider/AasRegistryProviderTests.cs index ffcecec9..00f6112a 100644 --- a/source/AAS.TwinEngine.DataEngine.UnitTests/Infrastructure/Providers/AasRegistryProvider/AasRegistryProviderTests.cs +++ b/source/AAS.TwinEngine.DataEngine.UnitTests/Infrastructure/Providers/AasRegistryProvider/AasRegistryProviderTests.cs @@ -4,7 +4,6 @@ using AAS.TwinEngine.DataEngine.ApplicationLogic.Exceptions.Infrastructure; using AAS.TwinEngine.DataEngine.ApplicationLogic.Extensions; -using AAS.TwinEngine.DataEngine.ApplicationLogic.Services.Plugin.Config; using AAS.TwinEngine.DataEngine.DomainModel.AasRegistry; using AAS.TwinEngine.DataEngine.Infrastructure.Http.Clients; diff --git a/source/AAS.TwinEngine.DataEngine.UnitTests/Infrastructure/Providers/TemplateProvider/Services/ShellTemplateMappingProviderTests.cs b/source/AAS.TwinEngine.DataEngine.UnitTests/Infrastructure/Providers/TemplateProvider/Services/ShellTemplateMappingProviderTests.cs index 80d2f511..e986c541 100644 --- a/source/AAS.TwinEngine.DataEngine.UnitTests/Infrastructure/Providers/TemplateProvider/Services/ShellTemplateMappingProviderTests.cs +++ b/source/AAS.TwinEngine.DataEngine.UnitTests/Infrastructure/Providers/TemplateProvider/Services/ShellTemplateMappingProviderTests.cs @@ -21,8 +21,8 @@ public ShellTemplateMappingProviderTests() var settings = new TemplateManagementConfig { TemplateMappingRules = new TemplateMappingRules - { - ShellTemplateMappings = + { + ShellTemplateMappings = [ new ShellTemplateMappings { @@ -35,7 +35,7 @@ public ShellTemplateMappingProviderTests() TemplateId = "template2" } ], - AasIdExtractionRules = + AasIdExtractionRules = [ new AasIdExtractionRules { diff --git a/source/AAS.TwinEngine.DataEngine/ApplicationLogic/Services/AasRepository/AasRepositoryTemplateService.cs b/source/AAS.TwinEngine.DataEngine/ApplicationLogic/Services/AasRepository/AasRepositoryTemplateService.cs index 7bb315f7..9f98054d 100644 --- a/source/AAS.TwinEngine.DataEngine/ApplicationLogic/Services/AasRepository/AasRepositoryTemplateService.cs +++ b/source/AAS.TwinEngine.DataEngine/ApplicationLogic/Services/AasRepository/AasRepositoryTemplateService.cs @@ -1,13 +1,12 @@ using AAS.TwinEngine.DataEngine.ApplicationLogic.Exceptions.Application; using AAS.TwinEngine.DataEngine.ApplicationLogic.Exceptions.Infrastructure; using AAS.TwinEngine.DataEngine.ApplicationLogic.Services.AasEnvironment.Providers; +using AAS.TwinEngine.DataEngine.ServiceConfiguration.Config; using AasCore.Aas3_0; using Microsoft.Extensions.Options; -using AAS.TwinEngine.DataEngine.ServiceConfiguration.Config; - namespace AAS.TwinEngine.DataEngine.ApplicationLogic.Services.AasRepository; public class AasRepositoryTemplateService( diff --git a/source/AAS.TwinEngine.DataEngine/Infrastructure/Shared/HttpRequestBaseUrlProvider.cs b/source/AAS.TwinEngine.DataEngine/Infrastructure/Shared/HttpRequestBaseUrlProvider.cs index 9295d6c1..d098c26d 100644 --- a/source/AAS.TwinEngine.DataEngine/Infrastructure/Shared/HttpRequestBaseUrlProvider.cs +++ b/source/AAS.TwinEngine.DataEngine/Infrastructure/Shared/HttpRequestBaseUrlProvider.cs @@ -1,7 +1,6 @@ using AAS.TwinEngine.DataEngine.ApplicationLogic.Services.Shared; using AAS.TwinEngine.DataEngine.ServiceConfiguration.Config; -using Microsoft.AspNetCore.Http; using Microsoft.Extensions.Options; namespace AAS.TwinEngine.DataEngine.Infrastructure.Shared; diff --git a/source/AAS.TwinEngine.DataEngine/ServiceConfiguration/ApplicationDependencyInjectionExtensions.cs b/source/AAS.TwinEngine.DataEngine/ServiceConfiguration/ApplicationDependencyInjectionExtensions.cs index af3c4d41..f1321a12 100644 --- a/source/AAS.TwinEngine.DataEngine/ServiceConfiguration/ApplicationDependencyInjectionExtensions.cs +++ b/source/AAS.TwinEngine.DataEngine/ServiceConfiguration/ApplicationDependencyInjectionExtensions.cs @@ -1,7 +1,6 @@ using AAS.TwinEngine.DataEngine.Api.AasRegistry.Handler; using AAS.TwinEngine.DataEngine.Api.AasRepository.Handler; using AAS.TwinEngine.DataEngine.Api.SubmodelRegistry.Handler; -using AAS.TwinEngine.DataEngine.ServiceConfiguration.Config; using AAS.TwinEngine.DataEngine.Api.SubmodelRepository.Handler; using AAS.TwinEngine.DataEngine.ApplicationLogic.Exceptions; using AAS.TwinEngine.DataEngine.ApplicationLogic.Services.AasRegistry; @@ -16,6 +15,7 @@ using AAS.TwinEngine.DataEngine.ApplicationLogic.Services.SubmodelRepository.SemanticId.Helpers.Interfaces; using AAS.TwinEngine.DataEngine.Infrastructure.Providers.AasRegistryProvider.Services; using AAS.TwinEngine.DataEngine.Infrastructure.Providers.PluginDataProvider.Services; +using AAS.TwinEngine.DataEngine.ServiceConfiguration.Config; using Microsoft.Extensions.Options; diff --git a/source/AAS.TwinEngine.DataEngine/ServiceConfiguration/InfrastructureDependencyInjectionExtensions.cs b/source/AAS.TwinEngine.DataEngine/ServiceConfiguration/InfrastructureDependencyInjectionExtensions.cs index c07bed3b..028ba112 100644 --- a/source/AAS.TwinEngine.DataEngine/ServiceConfiguration/InfrastructureDependencyInjectionExtensions.cs +++ b/source/AAS.TwinEngine.DataEngine/ServiceConfiguration/InfrastructureDependencyInjectionExtensions.cs @@ -17,7 +17,6 @@ using AAS.TwinEngine.DataEngine.Infrastructure.Providers.PluginDataProvider.Helper; using AAS.TwinEngine.DataEngine.Infrastructure.Providers.PluginDataProvider.Services; using AAS.TwinEngine.DataEngine.Infrastructure.Providers.SubmodelRegistryProvider.Services; -using AAS.TwinEngine.DataEngine.Infrastructure.Providers.TemplateProvider.Config; using AAS.TwinEngine.DataEngine.Infrastructure.Providers.TemplateProvider.Services; using AAS.TwinEngine.DataEngine.Infrastructure.Shared; using AAS.TwinEngine.DataEngine.ServiceConfiguration.Config; diff --git a/source/AAS.TwinEngine.Plugin.TestPlugin.PlaywrightTests/ApiTestBase.cs b/source/AAS.TwinEngine.Plugin.TestPlugin.PlaywrightTests/ApiTestBase.cs index e0cf9a49..b8a7e8a3 100644 --- a/source/AAS.TwinEngine.Plugin.TestPlugin.PlaywrightTests/ApiTestBase.cs +++ b/source/AAS.TwinEngine.Plugin.TestPlugin.PlaywrightTests/ApiTestBase.cs @@ -1,8 +1,8 @@ -using Microsoft.Playwright; - -using System.Text; +using System.Text; using System.Text.Json; +using Microsoft.Playwright; + namespace AAS.TwinEngine.Plugin.TestPlugin.PlaywrightTests; /// diff --git a/source/AAS.TwinEngine.Plugin.TestPlugin/Api/Manifest/ManifestController.cs b/source/AAS.TwinEngine.Plugin.TestPlugin/Api/Manifest/ManifestController.cs index 30ea6eab..9d441710 100644 --- a/source/AAS.TwinEngine.Plugin.TestPlugin/Api/Manifest/ManifestController.cs +++ b/source/AAS.TwinEngine.Plugin.TestPlugin/Api/Manifest/ManifestController.cs @@ -12,7 +12,7 @@ namespace AAS.TwinEngine.Plugin.TestPlugin.Api.Manifest; [ApiController] [Route("")] [ApiVersion(1)] -public class ManifestController(IManifestHandler manifestHandler) :ControllerBase +public class ManifestController(IManifestHandler manifestHandler) : ControllerBase { [HttpGet("manifest")] [ProducesResponseType(typeof(JsonObject), StatusCodes.Status200OK)] From 174a7b6c21448aaad6775150188477c4e3c4923c Mon Sep 17 00:00:00 2001 From: Kevalkumar Date: Wed, 8 Apr 2026 02:45:17 +0530 Subject: [PATCH 19/71] git merge parent branch --- .../Helpers/SemanticTreeNavigatorTests.cs | 54 +------------------ 1 file changed, 1 insertion(+), 53 deletions(-) diff --git a/source/AAS.TwinEngine.DataEngine.UnitTests/ApplicationLogic/Services/SubmodelRepository/SemanticId/Helpers/SemanticTreeNavigatorTests.cs b/source/AAS.TwinEngine.DataEngine.UnitTests/ApplicationLogic/Services/SubmodelRepository/SemanticId/Helpers/SemanticTreeNavigatorTests.cs index c7c8784d..05783ef5 100644 --- a/source/AAS.TwinEngine.DataEngine.UnitTests/ApplicationLogic/Services/SubmodelRepository/SemanticId/Helpers/SemanticTreeNavigatorTests.cs +++ b/source/AAS.TwinEngine.DataEngine.UnitTests/ApplicationLogic/Services/SubmodelRepository/SemanticId/Helpers/SemanticTreeNavigatorTests.cs @@ -1,4 +1,4 @@ -using AAS.TwinEngine.DataEngine.ApplicationLogic.Services.SubmodelRepository.SemanticId.Helpers; +using AAS.TwinEngine.DataEngine.ApplicationLogic.Services.SubmodelRepository.SemanticId.Helpers; using AAS.TwinEngine.DataEngine.DomainModel.SubmodelRepository; using static Xunit.Assert; @@ -100,58 +100,6 @@ public void FindNodeBySemanticId_NoMatch_ReturnsEmpty() } [Fact] - public void AreAllNodesOfSameType_EmptyList_ReturnsTrueWithNullType() - { - var result = SemanticTreeNavigator.AreAllNodesOfSameType([], out var nodeType); - - True(result); - Null(nodeType); - } - - [Fact] - public void AreAllNodesOfSameType_AllBranchNodes_ReturnsTrue() - { - var nodes = new List - { - new SemanticBranchNode("a", Cardinality.One), - new SemanticBranchNode("b", Cardinality.ZeroToOne), - }; - - var result = SemanticTreeNavigator.AreAllNodesOfSameType(nodes, out var nodeType); - - True(result); - Equal(typeof(SemanticBranchNode), nodeType); - } - - [Fact] - public void AreAllNodesOfSameType_AllLeafNodes_ReturnsTrue() - { - var nodes = new List - { - new SemanticLeafNode("a", "v1", DataType.String, Cardinality.One), - new SemanticLeafNode("b", "v2", DataType.Integer, Cardinality.ZeroToOne), - }; - - var result = SemanticTreeNavigator.AreAllNodesOfSameType(nodes, out var nodeType); - - True(result); - Equal(typeof(SemanticLeafNode), nodeType); - } - - [Fact] - public void AreAllNodesOfSameType_MixedNodes_ReturnsFalse() - { - var nodes = new List - { - new SemanticBranchNode("a", Cardinality.One), - new SemanticLeafNode("b", "v2", DataType.String, Cardinality.One), - }; - - var result = SemanticTreeNavigator.AreAllNodesOfSameType(nodes, out _); - - False(result); - } - public void FindNodeBySemanticId_Generic_WhenSameSemanticIdOnBothBranchAndLeaf_ReturnsOnlyRequestedType() { var root = new SemanticBranchNode("root", Cardinality.Unknown); From f9f830a3c72154676a360726a8db6e2f5a64a09e Mon Sep 17 00:00:00 2001 From: Kevalkumar Date: Wed, 8 Apr 2026 03:12:50 +0530 Subject: [PATCH 20/71] Added sonarqube suggestions --- example/docker-compose.yml | 6 +- ...figurationBackwardCompatibilityE2ETests.cs | 19 -- .../LegacyConfigurationDetectorTests.cs | 55 +---- ...egacyRegistrySettingsConfigAdapterTests.cs | 43 +--- .../V2DirectBindingTests.cs | 17 -- .../LegacyV1/LegacyConfigurationDetector.cs | 10 - .../LegacyRegistrySettingsConfigAdapter.cs | 10 - .../Config/HeaderForwardingOptions.cs | 2 +- .../Services/SubmodelDescriptorProvider.cs | 4 +- .../Shared/HttpRequestBaseUrlProvider.cs | 2 +- .../Config/RegistrySettingsConfig.cs | 6 - .../appsettings.development-new.json | 221 ++++++++++++++++++ .../appsettings.development.json | 2 +- .../appsettings.json | 2 +- .../Example/docker-compose.yml | 6 +- 15 files changed, 237 insertions(+), 168 deletions(-) create mode 100644 source/AAS.TwinEngine.DataEngine/appsettings.development-new.json diff --git a/example/docker-compose.yml b/example/docker-compose.yml index c6643fc1..01ed785f 100644 --- a/example/docker-compose.yml +++ b/example/docker-compose.yml @@ -52,9 +52,9 @@ services: - TemplateManagement__TemplateMappingRules__SubmodelTemplateMappings__4__pattern__0=HandoverDocumentation - TemplateManagement__TemplateMappingRules__ShellTemplateMappings__0__templateId=https://mm-software.com/aas/aasTemplate - TemplateManagement__TemplateMappingRules__ShellTemplateMappings__0__pattern__0= - - TemplateManagement__TemplateMappingRules__AasIdExtractionRules__1__Pattern=Regex - - TemplateManagement__TemplateMappingRules__AasIdExtractionRules__1__Index=6 - - TemplateManagement__TemplateMappingRules__AasIdExtractionRules__1__Separator=/ + - TemplateManagement__TemplateMappingRules__AasIdExtractionRules__0__Pattern=Regex + - TemplateManagement__TemplateMappingRules__AasIdExtractionRules__0__Index=6 + - TemplateManagement__TemplateMappingRules__AasIdExtractionRules__0__Separator=/ - TemplateManagement__AasTemplateRepository__Name=AasTemplateRepository - TemplateManagement__AasTemplateRepository__baseUrl=http://template-repository:8081 - TemplateManagement__AasTemplateRepository__headerMappings__0__source=Authorization diff --git a/source/AAS.TwinEngine.DataEngine.UnitTests/ServiceConfiguration/ConfigurationMigration/ConfigurationBackwardCompatibilityE2ETests.cs b/source/AAS.TwinEngine.DataEngine.UnitTests/ServiceConfiguration/ConfigurationMigration/ConfigurationBackwardCompatibilityE2ETests.cs index a968b34a..992c0b69 100644 --- a/source/AAS.TwinEngine.DataEngine.UnitTests/ServiceConfiguration/ConfigurationMigration/ConfigurationBackwardCompatibilityE2ETests.cs +++ b/source/AAS.TwinEngine.DataEngine.UnitTests/ServiceConfiguration/ConfigurationMigration/ConfigurationBackwardCompatibilityE2ETests.cs @@ -131,25 +131,6 @@ public void V2Config_ThroughDIPipeline_ProducesCorrectTemplateManagementConfig() Assert.NotEmpty(tmConfig.TemplateMappingRules.SubmodelTemplateMappings); } - [Fact] - public void V2Config_ThroughDIPipeline_RegistrySettingsTypoVariant_Works() - { - // Simulates the actual V2 JSON which has the "ResgistrySettings" typo - var config = BuildConfig(MergeConfigs( - GetV2CoreConfig(), - new Dictionary - { - ["ResgistrySettings:PreComputed:Enabled"] = "true", - ["ResgistrySettings:PreComputed:Schedule"] = "0 */3 * * * *" - })); - - var provider = BuildServiceProvider(config); - var registry = provider.GetRequiredService>().Value; - - Assert.True(registry.PreComputed.Enabled); - Assert.Equal("0 */3 * * * *", registry.PreComputed.Schedule); - } - [Fact] public void V2Config_ThroughDIPipeline_RegistrySettingsCorrectSpelling_Works() { diff --git a/source/AAS.TwinEngine.DataEngine.UnitTests/ServiceConfiguration/ConfigurationMigration/LegacyConfigurationDetectorTests.cs b/source/AAS.TwinEngine.DataEngine.UnitTests/ServiceConfiguration/ConfigurationMigration/LegacyConfigurationDetectorTests.cs index 8dece47a..6519848f 100644 --- a/source/AAS.TwinEngine.DataEngine.UnitTests/ServiceConfiguration/ConfigurationMigration/LegacyConfigurationDetectorTests.cs +++ b/source/AAS.TwinEngine.DataEngine.UnitTests/ServiceConfiguration/ConfigurationMigration/LegacyConfigurationDetectorTests.cs @@ -1,4 +1,4 @@ -using AAS.TwinEngine.DataEngine.Infrastructure.Configuration.LegacyV1; +using AAS.TwinEngine.DataEngine.Infrastructure.Configuration.LegacyV1; using Microsoft.Extensions.Configuration; @@ -57,62 +57,13 @@ public void IsV1Configuration_WithOldFlatConfig_ReturnsTrue() [Fact] public void IsV1Configuration_EmptyConfig_ReturnsTrue() { - var config = BuildConfig(new Dictionary()); + var config = BuildConfig([]); Assert.True(LegacyConfigurationDetector.IsV1Configuration(config)); } [Fact] - public void IsV1Configuration_NullConfig_ThrowsArgumentNullException() - { - Assert.Throws(() => LegacyConfigurationDetector.IsV1Configuration(null!)); - } - - [Fact] - public void HasRegistrySettingsTypo_WithCorrectSpelling_ReturnsFalse() - { - var config = BuildConfig(new Dictionary - { - ["RegistrySettings:PreComputed:Enabled"] = "true" - }); - - Assert.False(LegacyConfigurationDetector.HasRegistrySettingsTypo(config)); - } - - [Fact] - public void HasRegistrySettingsTypo_WithTypoSpelling_ReturnsTrue() - { - var config = BuildConfig(new Dictionary - { - ["ResgistrySettings:PreComputed:Enabled"] = "true" - }); - - Assert.True(LegacyConfigurationDetector.HasRegistrySettingsTypo(config)); - } - - [Fact] - public void HasRegistrySettingsTypo_WithBothSpellings_ReturnsFalse() - { - // When correct spelling exists, typo should not be flagged - var config = BuildConfig(new Dictionary - { - ["RegistrySettings:PreComputed:Enabled"] = "true", - ["ResgistrySettings:PreComputed:Enabled"] = "false" - }); - - Assert.False(LegacyConfigurationDetector.HasRegistrySettingsTypo(config)); - } - - [Fact] - public void HasRegistrySettingsTypo_WithNeitherSpelling_ReturnsFalse() - { - var config = BuildConfig(new Dictionary - { - ["SomeOtherSection:Key"] = "value" - }); - - Assert.False(LegacyConfigurationDetector.HasRegistrySettingsTypo(config)); - } + public void IsV1Configuration_NullConfig_ThrowsArgumentNullException() => Assert.Throws(() => LegacyConfigurationDetector.IsV1Configuration(null!)); private static IConfiguration BuildConfig(Dictionary values) { diff --git a/source/AAS.TwinEngine.DataEngine.UnitTests/ServiceConfiguration/ConfigurationMigration/LegacyRegistrySettingsConfigAdapterTests.cs b/source/AAS.TwinEngine.DataEngine.UnitTests/ServiceConfiguration/ConfigurationMigration/LegacyRegistrySettingsConfigAdapterTests.cs index 447568b3..5bfb267a 100644 --- a/source/AAS.TwinEngine.DataEngine.UnitTests/ServiceConfiguration/ConfigurationMigration/LegacyRegistrySettingsConfigAdapterTests.cs +++ b/source/AAS.TwinEngine.DataEngine.UnitTests/ServiceConfiguration/ConfigurationMigration/LegacyRegistrySettingsConfigAdapterTests.cs @@ -1,4 +1,4 @@ -using AAS.TwinEngine.DataEngine.Infrastructure.Configuration.LegacyV1; +using AAS.TwinEngine.DataEngine.Infrastructure.Configuration.LegacyV1; using AAS.TwinEngine.DataEngine.ServiceConfiguration.Config; using Microsoft.Extensions.Configuration; @@ -82,47 +82,6 @@ public void Configure_WithV2Config_DoesNotModifyOptions() Assert.False(options.PreComputed.Enabled); } - [Fact] - public void Configure_WithV2Config_TypoVariant_MapsCorrectly() - { - // V2 JSON has typo "ResgistrySettings" instead of "RegistrySettings" - var config = BuildConfig(new Dictionary - { - ["General:AllowedHosts"] = "*", - ["ResgistrySettings:PreComputed:Enabled"] = "true", - ["ResgistrySettings:PreComputed:Schedule"] = "0 */7 * * * *" - }); - - var adapter = new LegacyRegistrySettingsConfigAdapter(config); - var options = new RegistrySettingsConfig(); - - adapter.Configure(options); - - Assert.True(options.PreComputed.Enabled); - Assert.Equal("0 */7 * * * *", options.PreComputed.Schedule); - } - - [Fact] - public void Configure_WithV2Config_CorrectSpelling_DoesNotUseTypoVariant() - { - var config = BuildConfig(new Dictionary - { - ["General:AllowedHosts"] = "*", - ["RegistrySettings:PreComputed:Enabled"] = "false", - ["RegistrySettings:PreComputed:Schedule"] = "correct-schedule", - ["ResgistrySettings:PreComputed:Enabled"] = "true", - ["ResgistrySettings:PreComputed:Schedule"] = "typo-schedule" - }); - - var adapter = new LegacyRegistrySettingsConfigAdapter(config); - var options = new RegistrySettingsConfig(); - - adapter.Configure(options); - - // Correct spelling exists → typo handling should not trigger - Assert.False(options.PreComputed.Enabled); - } - private static IConfiguration BuildV1Config(bool isPreComputed = true) { return BuildConfig(new Dictionary diff --git a/source/AAS.TwinEngine.DataEngine.UnitTests/ServiceConfiguration/ConfigurationMigration/V2DirectBindingTests.cs b/source/AAS.TwinEngine.DataEngine.UnitTests/ServiceConfiguration/ConfigurationMigration/V2DirectBindingTests.cs index 738366ed..c2c5c284 100644 --- a/source/AAS.TwinEngine.DataEngine.UnitTests/ServiceConfiguration/ConfigurationMigration/V2DirectBindingTests.cs +++ b/source/AAS.TwinEngine.DataEngine.UnitTests/ServiceConfiguration/ConfigurationMigration/V2DirectBindingTests.cs @@ -171,23 +171,6 @@ public void RegistrySettingsConfig_BindsFromV2Json() Assert.Equal("0 */5 * * * *", registrySettings.PreComputed.Schedule); } - [Fact] - public void RegistrySettingsConfig_TypoVariant_DoesNotBindToCorrectSection() - { - // The typo variant "ResgistrySettings" should NOT bind to the "RegistrySettings" section - var config = BuildConfig(new Dictionary - { - ["ResgistrySettings:PreComputed:Enabled"] = "true", - ["ResgistrySettings:PreComputed:Schedule"] = "0 */5 * * * *" - }); - - var registrySettings = new RegistrySettingsConfig(); - config.GetSection(RegistrySettingsConfig.Section).Bind(registrySettings); - - // Default values since "RegistrySettings" section doesn't exist - Assert.False(registrySettings.PreComputed.Enabled); - } - private static IConfiguration BuildV2Config() { return BuildConfig(new Dictionary diff --git a/source/AAS.TwinEngine.DataEngine/Infrastructure/Configuration/LegacyV1/LegacyConfigurationDetector.cs b/source/AAS.TwinEngine.DataEngine/Infrastructure/Configuration/LegacyV1/LegacyConfigurationDetector.cs index f108a44a..2ee4e3e6 100644 --- a/source/AAS.TwinEngine.DataEngine/Infrastructure/Configuration/LegacyV1/LegacyConfigurationDetector.cs +++ b/source/AAS.TwinEngine.DataEngine/Infrastructure/Configuration/LegacyV1/LegacyConfigurationDetector.cs @@ -20,14 +20,4 @@ public static bool IsV1Configuration(IConfiguration configuration) return !isV2; } - - /// - /// The V2 JSON may contain a typo: "ResgistrySettings" instead of "RegistrySettings". - /// If only the typo variant exists, we still treat it as V2 but need to handle the rename. - /// - public static bool HasRegistrySettingsTypo(IConfiguration configuration) - { - return !configuration.GetSection(RegistrySettingsConfig.Section).Exists() - && configuration.GetSection(RegistrySettingsConfig.SectionTypoVariant).Exists(); - } } diff --git a/source/AAS.TwinEngine.DataEngine/Infrastructure/Configuration/LegacyV1/LegacyRegistrySettingsConfigAdapter.cs b/source/AAS.TwinEngine.DataEngine/Infrastructure/Configuration/LegacyV1/LegacyRegistrySettingsConfigAdapter.cs index 8a80af0e..4e7ca762 100644 --- a/source/AAS.TwinEngine.DataEngine/Infrastructure/Configuration/LegacyV1/LegacyRegistrySettingsConfigAdapter.cs +++ b/source/AAS.TwinEngine.DataEngine/Infrastructure/Configuration/LegacyV1/LegacyRegistrySettingsConfigAdapter.cs @@ -16,16 +16,6 @@ public void Configure(RegistrySettingsConfig options) { if (!LegacyConfigurationDetector.IsV1Configuration(_configuration)) { - // Even for V2, handle the typo variant "ResgistrySettings" - if (LegacyConfigurationDetector.HasRegistrySettingsTypo(_configuration)) - { - var typoSection = _configuration.GetSection(RegistrySettingsConfig.SectionTypoVariant).Get(); - if (typoSection != null) - { - options.PreComputed = typoSection.PreComputed; - } - } - return; } diff --git a/source/AAS.TwinEngine.DataEngine/Infrastructure/Http/Authorization/Config/HeaderForwardingOptions.cs b/source/AAS.TwinEngine.DataEngine/Infrastructure/Http/Authorization/Config/HeaderForwardingOptions.cs index a5b8f70b..fcf7733c 100644 --- a/source/AAS.TwinEngine.DataEngine/Infrastructure/Http/Authorization/Config/HeaderForwardingOptions.cs +++ b/source/AAS.TwinEngine.DataEngine/Infrastructure/Http/Authorization/Config/HeaderForwardingOptions.cs @@ -24,7 +24,7 @@ public class HeaderSanitizationOptions [Required] public AllowedCharactersOptions AllowedCharacters { get; set; } = new(); - public IList BlockedPatterns { get; } = ["\\r|\\n", "\\x00", " BlockedPatterns { get; set; } = ["\\r|\\n", "\\x00", " GetDataForSubmodelDescriptorByIdAsync(stri logger.LogError("Failed to deserialize the submodel descriptor. Submodel ID: {SubmodelId}", id); throw new ResponseParsingException(); } - catch (JsonException) + catch (JsonException ex) { - logger.LogError("Failed to deserialize SubmodelDescriptor from response. Submodel ID: {SubmodelId}, Response: {ResponseContent}", id, responseContent); + logger.LogError(ex, "Failed to deserialize SubmodelDescriptor from response. Submodel ID: {SubmodelId}, Response: {ResponseContent}", id, responseContent); throw new ResponseParsingException(); } } diff --git a/source/AAS.TwinEngine.DataEngine/Infrastructure/Shared/HttpRequestBaseUrlProvider.cs b/source/AAS.TwinEngine.DataEngine/Infrastructure/Shared/HttpRequestBaseUrlProvider.cs index d098c26d..48a3ba08 100644 --- a/source/AAS.TwinEngine.DataEngine/Infrastructure/Shared/HttpRequestBaseUrlProvider.cs +++ b/source/AAS.TwinEngine.DataEngine/Infrastructure/Shared/HttpRequestBaseUrlProvider.cs @@ -32,6 +32,6 @@ public Uri GetBaseUrl() ?? throw new InvalidOperationException("No HTTP request context available — cannot derive base URL."); var baseUrl = $"{request.Scheme}://{request.Host}"; - return new Uri(baseUrl.TrimEnd('/') + "/"); + return new Uri(baseUrl, UriKind.Absolute); } } diff --git a/source/AAS.TwinEngine.DataEngine/ServiceConfiguration/Config/RegistrySettingsConfig.cs b/source/AAS.TwinEngine.DataEngine/ServiceConfiguration/Config/RegistrySettingsConfig.cs index a367b19f..4aafe9ff 100644 --- a/source/AAS.TwinEngine.DataEngine/ServiceConfiguration/Config/RegistrySettingsConfig.cs +++ b/source/AAS.TwinEngine.DataEngine/ServiceConfiguration/Config/RegistrySettingsConfig.cs @@ -2,17 +2,11 @@ /// /// V2 config — binds to the "RegistrySettings" section. -/// Note: The V2 JSON has a typo "ResgistrySettings" — both spellings are handled by the normalizer. /// public class RegistrySettingsConfig { public const string Section = "RegistrySettings"; - /// - /// Also check for typo variant in new JSON: "ResgistrySettings". - /// - public const string SectionTypoVariant = "ResgistrySettings"; - public PreComputedConfig PreComputed { get; set; } = new(); } diff --git a/source/AAS.TwinEngine.DataEngine/appsettings.development-new.json b/source/AAS.TwinEngine.DataEngine/appsettings.development-new.json new file mode 100644 index 00000000..d12f05d1 --- /dev/null +++ b/source/AAS.TwinEngine.DataEngine/appsettings.development-new.json @@ -0,0 +1,221 @@ +{ + "General": { + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + } + }, + "CustomerDomainUrl": "https://mm-software.com", + "HeaderSanitization": { + "MaxHeaderSize": 8192, + "MaxHeaderNameSize": 256, + "AllowedCharacters": { + "HeaderNames": "^[a-zA-Z0-9\\-_]+$", + "HeaderValues": "^[\\x20-\\x7E]+$" + }, + "BlockedPatterns": [ + "\\r|\\n", + "\\x00", + " Date: Wed, 8 Apr 2026 03:14:04 +0530 Subject: [PATCH 21/71] remove extra new config file added for test --- .../appsettings.development-new.json | 221 ------------------ 1 file changed, 221 deletions(-) delete mode 100644 source/AAS.TwinEngine.DataEngine/appsettings.development-new.json diff --git a/source/AAS.TwinEngine.DataEngine/appsettings.development-new.json b/source/AAS.TwinEngine.DataEngine/appsettings.development-new.json deleted file mode 100644 index d12f05d1..00000000 --- a/source/AAS.TwinEngine.DataEngine/appsettings.development-new.json +++ /dev/null @@ -1,221 +0,0 @@ -{ - "General": { - "Logging": { - "LogLevel": { - "Default": "Information", - "Microsoft.AspNetCore": "Warning" - } - }, - "CustomerDomainUrl": "https://mm-software.com", - "HeaderSanitization": { - "MaxHeaderSize": 8192, - "MaxHeaderNameSize": 256, - "AllowedCharacters": { - "HeaderNames": "^[a-zA-Z0-9\\-_]+$", - "HeaderValues": "^[\\x20-\\x7E]+$" - }, - "BlockedPatterns": [ - "\\r|\\n", - "\\x00", - " Date: Wed, 8 Apr 2026 03:42:01 +0530 Subject: [PATCH 22/71] refactor docker compose and logging extenstion to add root level --- .../Http/Authorization/Headers/RequestHeaderMapper.cs | 2 ++ .../LoggingConfigurationExtension.cs | 11 ++++++++++- .../SubmodelRepository/SerializationTests.cs | 6 +++--- .../Example/docker-compose.yml | 2 +- 4 files changed, 16 insertions(+), 5 deletions(-) diff --git a/source/AAS.TwinEngine.DataEngine/Infrastructure/Http/Authorization/Headers/RequestHeaderMapper.cs b/source/AAS.TwinEngine.DataEngine/Infrastructure/Http/Authorization/Headers/RequestHeaderMapper.cs index 8a0a953b..2fde8889 100644 --- a/source/AAS.TwinEngine.DataEngine/Infrastructure/Http/Authorization/Headers/RequestHeaderMapper.cs +++ b/source/AAS.TwinEngine.DataEngine/Infrastructure/Http/Authorization/Headers/RequestHeaderMapper.cs @@ -124,6 +124,8 @@ private List GetAllMappingRules() // Template repository/registry header mappings allRules.AddRange(_templateManagementConfig.AasTemplateRepository.HeaderMappings); allRules.AddRange(_templateManagementConfig.AasTemplateRegistry.HeaderMappings); + allRules.AddRange(_templateManagementConfig.SubmodelTemplateRegistry.HeaderMappings); + allRules.AddRange(_templateManagementConfig.SubmodelTemplateRepository.HeaderMappings); // Plugin header mappings foreach (var plugin in _pluginsConfig.Instances) diff --git a/source/AAS.TwinEngine.DataEngine/ServiceConfiguration/LoggingConfigurationExtension.cs b/source/AAS.TwinEngine.DataEngine/ServiceConfiguration/LoggingConfigurationExtension.cs index a6fa496d..2098b27c 100644 --- a/source/AAS.TwinEngine.DataEngine/ServiceConfiguration/LoggingConfigurationExtension.cs +++ b/source/AAS.TwinEngine.DataEngine/ServiceConfiguration/LoggingConfigurationExtension.cs @@ -26,8 +26,17 @@ public static void ConfigureLogging(this WebApplicationBuilder builder, IConfigu _ = builder.Host.UseSerilog((context, loggerConfig) => { + // V2 config nests Serilog under "General:Serilog"; V1 keeps it at root "Serilog". + // ReadFrom.Configuration looks for a child key named "Serilog" inside the section + // you pass, so we pass the parent section ("General" or root) — not the Serilog + // section itself. + var generalSerilogSection = context.Configuration.GetSection("General:Serilog"); + var serilogParent = generalSerilogSection.Exists() + ? context.Configuration.GetSection("General") + : context.Configuration; + _ = loggerConfig - .ReadFrom.Configuration(context.Configuration) + .ReadFrom.Configuration(serilogParent) .Enrich.FromLogContext() .Enrich.With() .MinimumLevel.ControlledBy(logLevelSwitch); diff --git a/source/AAS.TwinEngine.Plugin.TestPlugin.PlaywrightTests/SubmodelRepository/SerializationTests.cs b/source/AAS.TwinEngine.Plugin.TestPlugin.PlaywrightTests/SubmodelRepository/SerializationTests.cs index 4af1ad41..a66db60f 100644 --- a/source/AAS.TwinEngine.Plugin.TestPlugin.PlaywrightTests/SubmodelRepository/SerializationTests.cs +++ b/source/AAS.TwinEngine.Plugin.TestPlugin.PlaywrightTests/SubmodelRepository/SerializationTests.cs @@ -22,8 +22,8 @@ public async Task GetAppropriateSerialization_WithMultipleSubmodels_ShouldReturn Assert.False(string.IsNullOrEmpty(content)); - Assert.Contains("https://mm-software.com/submodel/000-003/HandoverDocumentation", content, System.StringComparison.Ordinal); - Assert.Contains("https://admin-shell.io/zvei/nameplate/1/0/ContactInformations/ContactInformation", content, System.StringComparison.Ordinal); - Assert.Contains("http://schemas.openxmlformats.org/package/2006/relationships", content, System.StringComparison.Ordinal); + Assert.Contains("https://mm-software.com/submodel/000-003/HandoverDocumentation", content, StringComparison.Ordinal); + Assert.Contains("https://admin-shell.io/zvei/nameplate/1/0/ContactInformations/ContactInformation", content, StringComparison.Ordinal); + Assert.Contains("http://schemas.openxmlformats.org/package/2006/relationships", content, StringComparison.Ordinal); } } diff --git a/source/AAS.TwinEngine.Plugin.TestPlugin/Example/docker-compose.yml b/source/AAS.TwinEngine.Plugin.TestPlugin/Example/docker-compose.yml index 074b5ae9..bf48bc53 100644 --- a/source/AAS.TwinEngine.Plugin.TestPlugin/Example/docker-compose.yml +++ b/source/AAS.TwinEngine.Plugin.TestPlugin/Example/docker-compose.yml @@ -46,7 +46,7 @@ services: - Plugins__Instances__0__headerMappings__1__target=X-Tenant-Context - Plugins__Instances__0__headerMappings__1__required=false - TemplateManagement__TemplateMappingRules__SubmodelTemplateMappings__0__templateId=https://admin-shell.io/idta/SubmodelTemplate/HandoverDocumentation/2/0 - - TemplateManagement__TemplateMappingRules__SubmodelTemplateMappings__0__pattern__0=Reliability + - TemplateManagement__TemplateMappingRules__SubmodelTemplateMappings__0__pattern__0=HandoverDocumentation - TemplateManagement__TemplateMappingRules__SubmodelTemplateMappings__1__templateId=https://admin-shell.io/idta/SubmodelTemplate/ContactInformation/1/0 - TemplateManagement__TemplateMappingRules__SubmodelTemplateMappings__1__pattern__0=ContactInformation - TemplateManagement__TemplateMappingRules__SubmodelTemplateMappings__2__templateId=https://admin-shell.io/idta/CustomSubmodel/Template/0/1 From 3b4703d712fa2535e431ba4b614a6e63f8f3b8ec Mon Sep 17 00:00:00 2001 From: Kevalkumar Date: Wed, 8 Apr 2026 10:44:47 +0530 Subject: [PATCH 23/71] refactor Obsolete tags --- .../Configuration/LegacyV1/LegacyConfigurationDetector.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/source/AAS.TwinEngine.DataEngine/Infrastructure/Configuration/LegacyV1/LegacyConfigurationDetector.cs b/source/AAS.TwinEngine.DataEngine/Infrastructure/Configuration/LegacyV1/LegacyConfigurationDetector.cs index 2ee4e3e6..d8b5ca30 100644 --- a/source/AAS.TwinEngine.DataEngine/Infrastructure/Configuration/LegacyV1/LegacyConfigurationDetector.cs +++ b/source/AAS.TwinEngine.DataEngine/Infrastructure/Configuration/LegacyV1/LegacyConfigurationDetector.cs @@ -1,4 +1,4 @@ -using AAS.TwinEngine.DataEngine.ServiceConfiguration.Config; +using AAS.TwinEngine.DataEngine.ServiceConfiguration.Config; namespace AAS.TwinEngine.DataEngine.Infrastructure.Configuration.LegacyV1; @@ -6,7 +6,7 @@ namespace AAS.TwinEngine.DataEngine.Infrastructure.Configuration.LegacyV1; /// Detects whether the running configuration uses the V1 (flat sections) or V2 (grouped) schema. /// V2 is identified by the existence of "General", "Plugins:Instances", or "TemplateManagement" top-level sections. /// -[Obsolete("Remove in v2.0.0 — V1 configuration support will be dropped.")] +[Obsolete("V1 configuration is deprecated and will be removed in v2.0.0 version.")] public static class LegacyConfigurationDetector { public static bool IsV1Configuration(IConfiguration configuration) From b0f0043994f0a6c3bc13416c20fb9514b66c648e Mon Sep 17 00:00:00 2001 From: Kevalkumar Date: Thu, 9 Apr 2026 16:30:36 +0530 Subject: [PATCH 24/71] Added suppourt for multiple repositories wit backward compebility --- .../TestData/v2-config/appsettings.json | 29 +++++--------- .../Services/TemplateProviderTests.cs | 18 ++++----- ...figurationBackwardCompatibilityE2ETests.cs | 3 ++ ...acyTemplateManagementConfigAdapterTests.cs | 20 ++++++---- .../Plugin/Config/AasEnvironmentConfig.cs | 29 ++++++++++---- .../LegacyTemplateManagementConfigAdapter.cs | 18 ++++----- .../Headers/RequestHeaderMapper.cs | 23 +++++++++-- .../TemplateRepositoryHealthCheck.cs | 15 ++++++-- .../Services/TemplateProvider.cs | 12 +++--- .../Config/TemplateManagementConfig.cs | 3 ++ .../TemplateManagementConfigNormalizer.cs | 38 +++++++++++++++++++ ...astructureDependencyInjectionExtensions.cs | 24 ++++++++---- .../appsettings.development.json | 15 +------- .../appsettings.json | 15 +------- .../Example/docker-compose.yml | 15 +++----- 15 files changed, 167 insertions(+), 110 deletions(-) create mode 100644 source/AAS.TwinEngine.DataEngine/ServiceConfiguration/Config/TemplateManagementConfigNormalizer.cs diff --git a/source/AAS.TwinEngine.DataEngine.ModuleTests/TestData/v2-config/appsettings.json b/source/AAS.TwinEngine.DataEngine.ModuleTests/TestData/v2-config/appsettings.json index 232ca389..6d7a5292 100644 --- a/source/AAS.TwinEngine.DataEngine.ModuleTests/TestData/v2-config/appsettings.json +++ b/source/AAS.TwinEngine.DataEngine.ModuleTests/TestData/v2-config/appsettings.json @@ -27,7 +27,9 @@ "ServiceVersion": "1.0.0" }, "Serilog": { - "Using": [ "Serilog.Sinks.Console" ], + "Using": [ + "Serilog.Sinks.Console" + ], "MinimumLevel": { "Default": "Warning", "Override": { @@ -45,7 +47,10 @@ "Plugins": { "SubmodelElementIndexContextPrefix": "_aastwinengineindex_", "MultiLanguageProperty": { - "DefaultLanguages": ["de", "en"], + "DefaultLanguages": [ + "de", + "en" + ], "SemanticPostfixSeparator": "_" }, "ResiliencePolicies": { @@ -100,7 +105,7 @@ "ShellTemplateMappings": [ { "templateId": "https://mm-software.com/aas/aasTemplate", - "pattern": [""] + "pattern": [ "" ] } ], "AasIdExtractionRules": [ @@ -114,8 +119,8 @@ "Semantics": { "InternalSemanticId": "InternalSemanticId" }, - "AasTemplateRepository": { - "Name": "AasTemplateRepository", + "TemplateRepository": { + "Name": "TemplateRepository", "baseUrl": "http://localhost:8081", "headerMappings": [ { @@ -130,17 +135,6 @@ } ] }, - "SubmodelTemplateRepository": { - "Name": "SubmodelTemplateRepository", - "baseUrl": "http://localhost:8081", - "headerMappings": [ - { - "source": "Authorization", - "target": "Authorization", - "required": false - } - ] - }, "AasTemplateRegistry": { "Name": "AasTemplateRegistry", "baseUrl": "http://localhost:8082", @@ -169,8 +163,5 @@ "Enabled": true, "Schedule": "0 */3 * * * *" } - }, - "MultiPluginConflictOption": { - "HandlingMode": "TakeFirst" } } diff --git a/source/AAS.TwinEngine.DataEngine.UnitTests/Infrastructure/Providers/TemplateProvider/Services/TemplateProviderTests.cs b/source/AAS.TwinEngine.DataEngine.UnitTests/Infrastructure/Providers/TemplateProvider/Services/TemplateProviderTests.cs index 1b4431a8..54128469 100644 --- a/source/AAS.TwinEngine.DataEngine.UnitTests/Infrastructure/Providers/TemplateProvider/Services/TemplateProviderTests.cs +++ b/source/AAS.TwinEngine.DataEngine.UnitTests/Infrastructure/Providers/TemplateProvider/Services/TemplateProviderTests.cs @@ -37,7 +37,7 @@ public async Task GetSubmodelTemplateAsync_ReturnsSubmodel_WhenValidResponse() using var mockHttpMessageHandler = new FakeHttpMessageHandler(mockHttpResponse); using var httpClient = new HttpClient(mockHttpMessageHandler); httpClient.BaseAddress = new Uri("https://www.mm-software.com/fakeurl"); - _httpClientFactory.CreateClient(AasEnvironmentConfig.AasEnvironmentRepoHttpClientName).Returns(httpClient); + _httpClientFactory.CreateClient(AasEnvironmentConfig.SubmodelTemplateRepository).Returns(httpClient); var result = await _sut.GetSubmodelTemplateAsync(TemplateId, CancellationToken.None); @@ -54,7 +54,7 @@ public async Task GetSubmodelTemplateAsync_ThrowsResponseParsingException_WhenIn using var mockHttpMessageHandler = new FakeHttpMessageHandler(mockHttpResponse); using var httpClient = new HttpClient(mockHttpMessageHandler); httpClient.BaseAddress = new Uri("https://www.mm-software.com/fakeurl"); - _httpClientFactory.CreateClient(AasEnvironmentConfig.AasEnvironmentRepoHttpClientName).Returns(httpClient); + _httpClientFactory.CreateClient(AasEnvironmentConfig.SubmodelTemplateRepository).Returns(httpClient); await Assert.ThrowsAsync(() => _sut.GetSubmodelTemplateAsync(TemplateId, CancellationToken.None)); } @@ -67,7 +67,7 @@ public async Task GetSubmodelTemplateAsync_ThrowsResourceNotFoundException_WhenN using var mockHttpMessageHandler = new FakeHttpMessageHandler(mockHttpResponse); using var httpClient = new HttpClient(mockHttpMessageHandler); httpClient.BaseAddress = new Uri("https://www.mm-software.com/fakeurl"); - _httpClientFactory.CreateClient(AasEnvironmentConfig.AasEnvironmentRepoHttpClientName).Returns(httpClient); + _httpClientFactory.CreateClient(AasEnvironmentConfig.SubmodelTemplateRepository).Returns(httpClient); var exception = await Assert.ThrowsAsync(() => _sut.GetSubmodelTemplateAsync(TemplateId, CancellationToken.None)); } @@ -78,7 +78,7 @@ public async Task GetSubmodelTemplateAsync_ThrowsException_WhenHttpClientFails() using var mockHttpMessageHandler = new FakeHttpMessageHandler(new HttpRequestException("Network error")); using var httpClient = new HttpClient(mockHttpMessageHandler); httpClient.BaseAddress = new Uri("https://www.mm-software.com/fakeurl"); - _httpClientFactory.CreateClient(AasEnvironmentConfig.AasEnvironmentRepoHttpClientName).Returns(httpClient); + _httpClientFactory.CreateClient(AasEnvironmentConfig.SubmodelTemplateRepository).Returns(httpClient); var exception = await Assert.ThrowsAsync(() => _sut.GetSubmodelTemplateAsync(TemplateId, CancellationToken.None)); Assert.Equal("Network error", exception.Message); @@ -396,7 +396,7 @@ public async Task GetSubmodelTemplateAsync_ThrowsServiceAuthorizationException_W using var mockHttpMessageHandler = new FakeHttpMessageHandler(mockHttpResponse); using var httpClient = new HttpClient(mockHttpMessageHandler); httpClient.BaseAddress = new Uri("https://www.mm-software.com/fakeurl"); - _httpClientFactory.CreateClient(AasEnvironmentConfig.AasEnvironmentRepoHttpClientName).Returns(httpClient); + _httpClientFactory.CreateClient(AasEnvironmentConfig.SubmodelTemplateRepository).Returns(httpClient); await Assert.ThrowsAsync(() => _sut.GetSubmodelTemplateAsync(TemplateId, CancellationToken.None)); @@ -412,7 +412,7 @@ public async Task GetSubmodelTemplateAsync_ThrowsRequestTimeoutException_WhenTim using var mockHttpMessageHandler = new FakeHttpMessageHandler(mockHttpResponse); using var httpClient = new HttpClient(mockHttpMessageHandler); httpClient.BaseAddress = new Uri("https://www.mm-software.com/fakeurl"); - _httpClientFactory.CreateClient(AasEnvironmentConfig.AasEnvironmentRepoHttpClientName).Returns(httpClient); + _httpClientFactory.CreateClient(AasEnvironmentConfig.SubmodelTemplateRepository).Returns(httpClient); await Assert.ThrowsAsync(() => _sut.GetSubmodelTemplateAsync(TemplateId, CancellationToken.None)); @@ -428,7 +428,7 @@ public async Task GetSubmodelTemplateAsync_ThrowsValidationFailedException_WhenU using var mockHttpMessageHandler = new FakeHttpMessageHandler(mockHttpResponse); using var httpClient = new HttpClient(mockHttpMessageHandler); httpClient.BaseAddress = new Uri("https://www.mm-software.com/fakeurl"); - _httpClientFactory.CreateClient(AasEnvironmentConfig.AasEnvironmentRepoHttpClientName).Returns(httpClient); + _httpClientFactory.CreateClient(AasEnvironmentConfig.SubmodelTemplateRepository).Returns(httpClient); await Assert.ThrowsAsync(() => _sut.GetSubmodelTemplateAsync(TemplateId, CancellationToken.None)); @@ -445,7 +445,7 @@ public async Task GetConceptDescriptionByIdAsync_ReturnsConceptDescription_WhenR using var mockHandler = new FakeHttpMessageHandler(mockHttpResponse); using var httpClient = new HttpClient(mockHandler); httpClient.BaseAddress = new Uri("https://www.mm-software.com/fakeurl"); - _httpClientFactory.CreateClient(AasEnvironmentConfig.AasEnvironmentRepoHttpClientName) + _httpClientFactory.CreateClient(AasEnvironmentConfig.ConceptDescriptorTemplateRepository) .Returns(httpClient); var result = await _sut.GetConceptDescriptionByIdAsync(CdIdentifier, CancellationToken.None); @@ -466,7 +466,7 @@ public async Task GetConceptDescriptionByIdAsync_ReturnsNull_OnHandledExceptions using var mockHandler = new FakeHttpMessageHandler(exception); using var httpClient = new HttpClient(mockHandler); httpClient.BaseAddress = new Uri("https://www.mm-software.com/fakeurl"); - _httpClientFactory.CreateClient(AasEnvironmentConfig.AasEnvironmentRepoHttpClientName) + _httpClientFactory.CreateClient(AasEnvironmentConfig.ConceptDescriptorTemplateRepository) .Returns(httpClient); var result = await _sut.GetConceptDescriptionByIdAsync(CdIdentifier, CancellationToken.None); diff --git a/source/AAS.TwinEngine.DataEngine.UnitTests/ServiceConfiguration/ConfigurationMigration/ConfigurationBackwardCompatibilityE2ETests.cs b/source/AAS.TwinEngine.DataEngine.UnitTests/ServiceConfiguration/ConfigurationMigration/ConfigurationBackwardCompatibilityE2ETests.cs index 992c0b69..ba934861 100644 --- a/source/AAS.TwinEngine.DataEngine.UnitTests/ServiceConfiguration/ConfigurationMigration/ConfigurationBackwardCompatibilityE2ETests.cs +++ b/source/AAS.TwinEngine.DataEngine.UnitTests/ServiceConfiguration/ConfigurationMigration/ConfigurationBackwardCompatibilityE2ETests.cs @@ -282,6 +282,9 @@ private static ServiceProvider BuildServiceProvider(IConfiguration configuration services.Configure(configuration.GetSection(TemplateManagementConfig.Section)); services.Configure(configuration.GetSection(RegistrySettingsConfig.Section)); + // Register the normalizer that propagates TemplateRepository shorthand to individual repos + services.AddSingleton, TemplateManagementConfigNormalizer>(); + return services.BuildServiceProvider(); } diff --git a/source/AAS.TwinEngine.DataEngine.UnitTests/ServiceConfiguration/ConfigurationMigration/LegacyTemplateManagementConfigAdapterTests.cs b/source/AAS.TwinEngine.DataEngine.UnitTests/ServiceConfiguration/ConfigurationMigration/LegacyTemplateManagementConfigAdapterTests.cs index 0776a9d3..666a6c01 100644 --- a/source/AAS.TwinEngine.DataEngine.UnitTests/ServiceConfiguration/ConfigurationMigration/LegacyTemplateManagementConfigAdapterTests.cs +++ b/source/AAS.TwinEngine.DataEngine.UnitTests/ServiceConfiguration/ConfigurationMigration/LegacyTemplateManagementConfigAdapterTests.cs @@ -71,8 +71,10 @@ public void Configure_WithV1Config_MapsAasTemplateRepository() adapter.Configure(options); - Assert.Equal(AasEnvironmentConfig.AasEnvironmentRepoHttpClientName, options.AasTemplateRepository.Name); - Assert.Equal(new Uri("http://localhost:8081"), options.AasTemplateRepository.BaseUrl); + // V1 maps AasEnvironmentRepositoryBaseUrl to the TemplateRepository shorthand + Assert.NotNull(options.TemplateRepository); + Assert.Equal(AasEnvironmentConfig.TemplateRepository, options.TemplateRepository!.Name); + Assert.Equal(new Uri("http://localhost:8081"), options.TemplateRepository.BaseUrl); } [Fact] @@ -110,8 +112,10 @@ public void Configure_WithV1Config_MapsSubmodelTemplateRepository() adapter.Configure(options); - // SubmodelTemplateRepository reuses the AasEnvironmentRepositoryBaseUrl - Assert.Equal(new Uri("http://localhost:8081"), options.SubmodelTemplateRepository.BaseUrl); + // SubmodelTemplateRepository is now populated via TemplateRepository shorthand + normalizer; + // the adapter sets the shorthand, not individual repos directly. + Assert.NotNull(options.TemplateRepository); + Assert.Equal(new Uri("http://localhost:8081"), options.TemplateRepository!.BaseUrl); } [Fact] @@ -123,9 +127,11 @@ public void Configure_WithV1Config_MapsTemplateRepositoryHeaders() adapter.Configure(options); - Assert.Equal(2, options.AasTemplateRepository.HeaderMappings.Count); - Assert.Equal("Authorization", options.AasTemplateRepository.HeaderMappings[0].Source); - Assert.Equal("Authorization", options.AasTemplateRepository.HeaderMappings[0].Target); + // Headers now live on the TemplateRepository shorthand + Assert.NotNull(options.TemplateRepository); + Assert.Equal(2, options.TemplateRepository!.HeaderMappings.Count); + Assert.Equal("Authorization", options.TemplateRepository.HeaderMappings[0].Source); + Assert.Equal("Authorization", options.TemplateRepository.HeaderMappings[0].Target); } [Fact] diff --git a/source/AAS.TwinEngine.DataEngine/ApplicationLogic/Services/Plugin/Config/AasEnvironmentConfig.cs b/source/AAS.TwinEngine.DataEngine/ApplicationLogic/Services/Plugin/Config/AasEnvironmentConfig.cs index db26cf9c..77a1af29 100644 --- a/source/AAS.TwinEngine.DataEngine/ApplicationLogic/Services/Plugin/Config/AasEnvironmentConfig.cs +++ b/source/AAS.TwinEngine.DataEngine/ApplicationLogic/Services/Plugin/Config/AasEnvironmentConfig.cs @@ -4,12 +4,27 @@ public class AasEnvironmentConfig { public const string Section = "AasEnvironment"; - public const string AasEnvironmentRepoHttpClientName = "template-repository"; - public const string AasRegistryHttpClientName = "aas-registry"; - public const string SubmodelRegistryHttpClientName = "submodel-registry"; - public const string AasEnvironmentRepoHealthCheckHttpClientName = "template-repository-healthcheck"; - public const string AasRegistryHealthCheckHttpClientName = "aas-registry-healthcheck"; - public const string SubmodelRegistryHealthCheckHttpClientName = "submodel-registry-healthcheck"; + public const string TemplateRepository = "template-repository"; + public const string AasRegistry = "aas-registry"; + public const string SubmodelRegistry = "submodel-registry"; + public const string SubmodelTemplateRepository = "submodel-template-repository"; + public const string AasTemplateRepository = "aas-template-repository"; + public const string ConceptDescriptorTemplateRepository = "concept-descriptor-template-repository"; + + public const string TemplateRepositoryHealthCheck = TemplateRepository + "-healthcheck"; + public const string AasRegistryHealthCheck = AasRegistry + "-healthcheck"; + public const string SubmodelRegistryHealthCheck = SubmodelRegistry + "-healthcheck"; + public const string SubmodelTemplateRepositoryHealthCheck = SubmodelTemplateRepository + "-healthcheck"; + public const string AasTemplateRepositoryHealthCheck = AasTemplateRepository + "-healthcheck"; + public const string ConceptDescriptorTemplateRepositoryHealthCheck = ConceptDescriptorTemplateRepository + "-healthcheck"; + + // Backward-compatible aliases used by existing code (TemplateProvider, health checks, etc.) + public const string AasEnvironmentRepoHttpClientName = AasTemplateRepository; + public const string AasRegistryHttpClientName = AasRegistry; + public const string SubmodelRegistryHttpClientName = SubmodelRegistry; + public const string AasEnvironmentRepoHealthCheckHttpClientName = AasTemplateRepositoryHealthCheck; + public const string AasRegistryHealthCheckHttpClientName = AasRegistryHealthCheck; + public const string SubmodelRegistryHealthCheckHttpClientName = SubmodelRegistryHealthCheck; // Path constants (no longer configurable — fixed API contracts) public const string SubModelRepositoryPath = "submodels"; @@ -17,7 +32,6 @@ public class AasEnvironmentConfig public const string SubModelRegistryPath = "submodel-descriptors"; public const string AasRepositoryPath = "shells"; public const string SubmodelRefPath = "submodel-refs"; - public const string ConceptDescriptionPath = "concept-descriptions"; // V1-bindable URI properties (used only by LegacyV1 adapter) @@ -28,5 +42,6 @@ public class AasEnvironmentConfig public Uri? AasRegistryBaseUrl { get; set; } = null!; public Uri? SubModelRegistryBaseUrl { get; set; } = null!; + public Uri CustomerDomainUrl { get; set; } = null!; } diff --git a/source/AAS.TwinEngine.DataEngine/Infrastructure/Configuration/LegacyV1/LegacyTemplateManagementConfigAdapter.cs b/source/AAS.TwinEngine.DataEngine/Infrastructure/Configuration/LegacyV1/LegacyTemplateManagementConfigAdapter.cs index 854fdabc..d0e32369 100644 --- a/source/AAS.TwinEngine.DataEngine/Infrastructure/Configuration/LegacyV1/LegacyTemplateManagementConfigAdapter.cs +++ b/source/AAS.TwinEngine.DataEngine/Infrastructure/Configuration/LegacyV1/LegacyTemplateManagementConfigAdapter.cs @@ -55,33 +55,29 @@ public void Configure(TemplateManagementConfig options) if (aasEnv != null) { - options.AasTemplateRepository = new ServiceEndpoint + // V1 uses a single AasEnvironmentRepositoryBaseUrl for all template repositories. + // Map it to the TemplateRepository shorthand so the normalizer propagates + // the same URL and headers to Aas/Submodel/ConceptDescription template repositories. + options.TemplateRepository = new ServiceEndpoint { - Name = AasEnvironmentConfig.AasEnvironmentRepoHttpClientName, + Name = AasEnvironmentConfig.TemplateRepository, BaseUrl = aasEnv.AasEnvironmentRepositoryBaseUrl, HeaderMappings = headerForwarding?.HeaderMappings.TemplateRepository ?? [] }; options.AasTemplateRegistry = new ServiceEndpoint { - Name = AasEnvironmentConfig.AasRegistryHttpClientName, + Name = AasEnvironmentConfig.AasRegistry, BaseUrl = aasEnv.AasRegistryBaseUrl, HeaderMappings = headerForwarding?.HeaderMappings.TemplateRegistry ?? [] }; options.SubmodelTemplateRegistry = new ServiceEndpoint { - Name = AasEnvironmentConfig.SubmodelRegistryHttpClientName, + Name = AasEnvironmentConfig.SubmodelRegistry, BaseUrl = aasEnv.SubModelRegistryBaseUrl, HeaderMappings = [] }; - - options.SubmodelTemplateRepository = new ServiceEndpoint - { - Name = "submodel-template-repository", - BaseUrl = aasEnv.AasEnvironmentRepositoryBaseUrl, - HeaderMappings = [] - }; } } } diff --git a/source/AAS.TwinEngine.DataEngine/Infrastructure/Http/Authorization/Headers/RequestHeaderMapper.cs b/source/AAS.TwinEngine.DataEngine/Infrastructure/Http/Authorization/Headers/RequestHeaderMapper.cs index 2fde8889..bc4650ca 100644 --- a/source/AAS.TwinEngine.DataEngine/Infrastructure/Http/Authorization/Headers/RequestHeaderMapper.cs +++ b/source/AAS.TwinEngine.DataEngine/Infrastructure/Http/Authorization/Headers/RequestHeaderMapper.cs @@ -123,9 +123,10 @@ private List GetAllMappingRules() // Template repository/registry header mappings allRules.AddRange(_templateManagementConfig.AasTemplateRepository.HeaderMappings); + allRules.AddRange(_templateManagementConfig.SubmodelTemplateRepository.HeaderMappings); + allRules.AddRange(_templateManagementConfig.ConceptDescriptionTemplateRepository.HeaderMappings); allRules.AddRange(_templateManagementConfig.AasTemplateRegistry.HeaderMappings); allRules.AddRange(_templateManagementConfig.SubmodelTemplateRegistry.HeaderMappings); - allRules.AddRange(_templateManagementConfig.SubmodelTemplateRepository.HeaderMappings); // Plugin header mappings foreach (var plugin in _pluginsConfig.Instances) @@ -188,17 +189,31 @@ public void ApplyMappings(HttpContext? httpContext, HttpRequestMessage outgoingR private IList? ResolveMappingsForClient(string clientName) { - if (string.Equals(clientName, AasEnvironmentConfig.AasEnvironmentRepoHttpClientName, StringComparison.OrdinalIgnoreCase)) + if (string.Equals(clientName, AasEnvironmentConfig.AasTemplateRepository, StringComparison.OrdinalIgnoreCase)) { return _templateManagementConfig.AasTemplateRepository.HeaderMappings; } - if (string.Equals(clientName, AasEnvironmentConfig.AasRegistryHttpClientName, StringComparison.OrdinalIgnoreCase) || - string.Equals(clientName, AasEnvironmentConfig.SubmodelRegistryHttpClientName, StringComparison.OrdinalIgnoreCase)) + if (string.Equals(clientName, AasEnvironmentConfig.SubmodelTemplateRepository, StringComparison.OrdinalIgnoreCase)) + { + return _templateManagementConfig.SubmodelTemplateRepository.HeaderMappings; + } + + if (string.Equals(clientName, AasEnvironmentConfig.ConceptDescriptorTemplateRepository, StringComparison.OrdinalIgnoreCase)) + { + return _templateManagementConfig.ConceptDescriptionTemplateRepository.HeaderMappings; + } + + if (string.Equals(clientName, AasEnvironmentConfig.AasRegistry, StringComparison.OrdinalIgnoreCase)) { return _templateManagementConfig.AasTemplateRegistry.HeaderMappings; } + if (string.Equals(clientName, AasEnvironmentConfig.SubmodelRegistry, StringComparison.OrdinalIgnoreCase)) + { + return _templateManagementConfig.SubmodelTemplateRegistry.HeaderMappings; + } + if (!clientName.StartsWith(PluginConfig.HttpClientNamePrefix, StringComparison.OrdinalIgnoreCase)) { return null; diff --git a/source/AAS.TwinEngine.DataEngine/Infrastructure/Monitoring/TemplateRepositoryHealthCheck.cs b/source/AAS.TwinEngine.DataEngine/Infrastructure/Monitoring/TemplateRepositoryHealthCheck.cs index 244af09f..ccc0be66 100644 --- a/source/AAS.TwinEngine.DataEngine/Infrastructure/Monitoring/TemplateRepositoryHealthCheck.cs +++ b/source/AAS.TwinEngine.DataEngine/Infrastructure/Monitoring/TemplateRepositoryHealthCheck.cs @@ -9,13 +9,15 @@ public sealed class TemplateRepositoryHealthCheck(ICreateClient clientFactory, I { private const string AasRepositoryPath = AasEnvironmentConfig.AasRepositoryPath; private const string SubModelRepositoryPath = AasEnvironmentConfig.SubModelRepositoryPath; + private const string ConceptDescriptionPath = AasEnvironmentConfig.ConceptDescriptionPath; public async Task CheckHealthAsync(HealthCheckContext context, CancellationToken cancellationToken = default) { - var aasTask = CheckHealthEndpointAsync(AasEnvironmentConfig.AasEnvironmentRepoHealthCheckHttpClientName, AasRepositoryPath, "aas-repository", cancellationToken); - var submodelTask = CheckHealthEndpointAsync(AasEnvironmentConfig.AasEnvironmentRepoHealthCheckHttpClientName, SubModelRepositoryPath, "submodel-repository", cancellationToken); + var aasTask = CheckHealthEndpointAsync(AasEnvironmentConfig.SubmodelTemplateRepositoryHealthCheck, AasRepositoryPath, "aas-template-repository", cancellationToken); + var submodelTask = CheckHealthEndpointAsync(AasEnvironmentConfig.AasTemplateRepositoryHealthCheck, SubModelRepositoryPath, "submodel-template-repository", cancellationToken); + var conceptDiscriptorTask = CheckHealthEndpointAsync(AasEnvironmentConfig.ConceptDescriptorTemplateRepositoryHealthCheck, ConceptDescriptionPath, "concept-descriptor-template-repository", cancellationToken); - var results = await Task.WhenAll(aasTask, submodelTask).ConfigureAwait(false); + var results = await Task.WhenAll(aasTask, submodelTask, conceptDiscriptorTask).ConfigureAwait(false); if (!results[0]) { @@ -27,7 +29,12 @@ public async Task CheckHealthAsync(HealthCheckContext context logger.LogWarning("Submodel Repository health status is unhealthy"); } - return results[0] && results[1] + if (!results[2]) + { + logger.LogWarning("Concept Discriptor Repository health status is unhealthy"); + } + + return results[0] && results[1] && results[2] ? HealthCheckResult.Healthy() : HealthCheckResult.Unhealthy(); } diff --git a/source/AAS.TwinEngine.DataEngine/Infrastructure/Providers/TemplateProvider/Services/TemplateProvider.cs b/source/AAS.TwinEngine.DataEngine/Infrastructure/Providers/TemplateProvider/Services/TemplateProvider.cs index edacb9f4..8871214b 100644 --- a/source/AAS.TwinEngine.DataEngine/Infrastructure/Providers/TemplateProvider/Services/TemplateProvider.cs +++ b/source/AAS.TwinEngine.DataEngine/Infrastructure/Providers/TemplateProvider/Services/TemplateProvider.cs @@ -28,7 +28,7 @@ public async Task GetSubmodelTemplateAsync(string templateId, Cancell var url = $"{SubModelRepositoryPath}/{encodedTemplateId}"; - var response = await SendGetRequestAsync(url, AasEnvironmentConfig.AasEnvironmentRepoHttpClientName, cancellationToken).ConfigureAwait(false); + var response = await SendGetRequestAsync(url, AasEnvironmentConfig.SubmodelTemplateRepository, cancellationToken).ConfigureAwait(false); var content = await response.Content.ReadAsStringAsync(cancellationToken).ConfigureAwait(false); try @@ -49,7 +49,7 @@ public async Task GetShellDescriptorsTemplateAsync(Cancellation { var url = $"{AasRegistryPath}"; - var response = await SendGetRequestAsync(url, AasEnvironmentConfig.AasRegistryHttpClientName, cancellationToken).ConfigureAwait(false); + var response = await SendGetRequestAsync(url, AasEnvironmentConfig.AasRegistry, cancellationToken).ConfigureAwait(false); var content = await response.Content.ReadAsStringAsync(cancellationToken).ConfigureAwait(false); try @@ -90,7 +90,7 @@ public async Task GetShellTemplateAsync(string templa var encodedTemplateId = templateId.EncodeBase64Url(logger); var url = $"{AasRepositoryPath}/{encodedTemplateId}"; - var response = await SendGetRequestAsync(url, AasEnvironmentConfig.AasEnvironmentRepoHttpClientName, cancellationToken).ConfigureAwait(false); + var response = await SendGetRequestAsync(url, AasEnvironmentConfig.AasTemplateRepository, cancellationToken).ConfigureAwait(false); var content = await response.Content.ReadAsStringAsync(cancellationToken).ConfigureAwait(false); try @@ -117,7 +117,7 @@ public async Task GetAssetInformationTemplateAsync(string tem var encodedTemplateId = templateId.EncodeBase64Url(logger); var url = $"{AasRepositoryPath}/{encodedTemplateId}/asset-information"; - var response = await SendGetRequestAsync(url, AasEnvironmentConfig.AasEnvironmentRepoHttpClientName, cancellationToken).ConfigureAwait(false); + var response = await SendGetRequestAsync(url, AasEnvironmentConfig.AasTemplateRepository, cancellationToken).ConfigureAwait(false); var content = await response.Content.ReadAsStringAsync(cancellationToken).ConfigureAwait(false); try @@ -144,7 +144,7 @@ public async Task> GetSubmodelRefByIdAsync(string templateId, C var encodedTemplateId = templateId.EncodeBase64Url(logger); var url = $"{AasRepositoryPath}/{encodedTemplateId}/{SubmodelRefPath}"; - var response = await SendGetRequestAsync(url, AasEnvironmentConfig.AasEnvironmentRepoHttpClientName, cancellationToken).ConfigureAwait(false); + var response = await SendGetRequestAsync(url, AasEnvironmentConfig.AasTemplateRepository, cancellationToken).ConfigureAwait(false); var content = await response.Content.ReadAsStringAsync(cancellationToken).ConfigureAwait(false); try @@ -191,7 +191,7 @@ public async Task> GetSubmodelRefByIdAsync(string templateId, C try { - var response = await SendGetRequestAsync(url, AasEnvironmentConfig.AasEnvironmentRepoHttpClientName, cancellationToken).ConfigureAwait(false); + var response = await SendGetRequestAsync(url, AasEnvironmentConfig.ConceptDescriptorTemplateRepository, cancellationToken).ConfigureAwait(false); var content = await response.Content.ReadAsStringAsync(cancellationToken).ConfigureAwait(false); var jsonNode = JsonNode.Parse(content); return Jsonization.Deserialize.ConceptDescriptionFrom(jsonNode!); diff --git a/source/AAS.TwinEngine.DataEngine/ServiceConfiguration/Config/TemplateManagementConfig.cs b/source/AAS.TwinEngine.DataEngine/ServiceConfiguration/Config/TemplateManagementConfig.cs index 96af19c1..f1e1fd91 100644 --- a/source/AAS.TwinEngine.DataEngine/ServiceConfiguration/Config/TemplateManagementConfig.cs +++ b/source/AAS.TwinEngine.DataEngine/ServiceConfiguration/Config/TemplateManagementConfig.cs @@ -14,8 +14,11 @@ public class TemplateManagementConfig public ResiliencePoliciesConfig ResiliencePolicies { get; set; } = new(); public TemplateMappingRules TemplateMappingRules { get; set; } = new(); public TemplateSemanticsConfig Semantics { get; set; } = new(); + + public ServiceEndpoint? TemplateRepository { get; set; } public ServiceEndpoint AasTemplateRepository { get; set; } = new(); public ServiceEndpoint SubmodelTemplateRepository { get; set; } = new(); + public ServiceEndpoint ConceptDescriptionTemplateRepository { get; set; } = new(); public ServiceEndpoint AasTemplateRegistry { get; set; } = new(); public ServiceEndpoint SubmodelTemplateRegistry { get; set; } = new(); } diff --git a/source/AAS.TwinEngine.DataEngine/ServiceConfiguration/Config/TemplateManagementConfigNormalizer.cs b/source/AAS.TwinEngine.DataEngine/ServiceConfiguration/Config/TemplateManagementConfigNormalizer.cs new file mode 100644 index 00000000..a644ab89 --- /dev/null +++ b/source/AAS.TwinEngine.DataEngine/ServiceConfiguration/Config/TemplateManagementConfigNormalizer.cs @@ -0,0 +1,38 @@ +using Microsoft.Extensions.Options; + +namespace AAS.TwinEngine.DataEngine.ServiceConfiguration.Config; + +/// +/// Post-configuration step that applies the +/// shorthand: when TemplateRepository is provided, its BaseUrl and HeaderMappings are used +/// as defaults for AasTemplateRepository, SubmodelTemplateRepository, and +/// ConceptDescriptionTemplateRepository — unless they already have their own BaseUrl. +/// +public sealed class TemplateManagementConfigNormalizer : IPostConfigureOptions +{ + public void PostConfigure(string? name, TemplateManagementConfig options) + { + var fallback = options.TemplateRepository; + if (fallback?.BaseUrl is null) + { + return; + } + + ApplyFallback(options.AasTemplateRepository, fallback); + ApplyFallback(options.SubmodelTemplateRepository, fallback); + ApplyFallback(options.ConceptDescriptionTemplateRepository, fallback); + } + + private static void ApplyFallback(ServiceEndpoint target, ServiceEndpoint fallback) + { + target.BaseUrl ??= fallback.BaseUrl; + + if (target.HeaderMappings.Count == 0 && fallback.HeaderMappings.Count > 0) + { + foreach (var mapping in fallback.HeaderMappings) + { + target.HeaderMappings.Add(mapping); + } + } + } +} diff --git a/source/AAS.TwinEngine.DataEngine/ServiceConfiguration/InfrastructureDependencyInjectionExtensions.cs b/source/AAS.TwinEngine.DataEngine/ServiceConfiguration/InfrastructureDependencyInjectionExtensions.cs index 028ba112..913616f4 100644 --- a/source/AAS.TwinEngine.DataEngine/ServiceConfiguration/InfrastructureDependencyInjectionExtensions.cs +++ b/source/AAS.TwinEngine.DataEngine/ServiceConfiguration/InfrastructureDependencyInjectionExtensions.cs @@ -53,6 +53,9 @@ public static void ConfigureInfrastructure(this IServiceCollection services, ICo _ = services.Configure(configuration.GetSection(TemplateManagementConfig.Section)); _ = services.Configure(configuration.GetSection(RegistrySettingsConfig.Section)); + // Normalizer: applies TemplateRepository shorthand to individual repository endpoints + _ = services.AddSingleton, TemplateManagementConfigNormalizer>(); + // PluginsConfig: single registration via AddOptions to avoid double-binding of list properties _ = services.AddOptions() .Bind(configuration.GetSection(PluginsConfig.Section)) @@ -68,14 +71,21 @@ public static void ConfigureInfrastructure(this IServiceCollection services, ICo var templateManagement = tempProvider.GetRequiredService>().Value; var pluginsConfig = tempProvider.GetRequiredService>().Value; - // Template repository/registry HttpClients (base URLs from TemplateManagement) - _ = services.AddHttpClientWithResilience(AasEnvironmentConfig.AasEnvironmentRepoHttpClientName, templateManagement.ResiliencePolicies.Retry, templateManagement.AasTemplateRepository.BaseUrl!); - _ = services.AddHttpClientWithResilience(AasEnvironmentConfig.AasRegistryHttpClientName, templateManagement.ResiliencePolicies.Retry, templateManagement.AasTemplateRegistry.BaseUrl!); - _ = services.AddHttpClientWithResilience(AasEnvironmentConfig.SubmodelRegistryHttpClientName, templateManagement.ResiliencePolicies.Retry, templateManagement.SubmodelTemplateRegistry.BaseUrl!); + // Template repository HttpClients (AAS, Submodel, ConceptDescription — separate clients) + _ = services.AddHttpClientWithResilience(AasEnvironmentConfig.AasTemplateRepository, templateManagement.ResiliencePolicies.Retry, templateManagement.AasTemplateRepository.BaseUrl!); + _ = services.AddHttpClientWithResilience(AasEnvironmentConfig.SubmodelTemplateRepository, templateManagement.ResiliencePolicies.Retry, templateManagement.SubmodelTemplateRepository.BaseUrl!); + _ = services.AddHttpClientWithResilience(AasEnvironmentConfig.ConceptDescriptorTemplateRepository, templateManagement.ResiliencePolicies.Retry, templateManagement.ConceptDescriptionTemplateRepository.BaseUrl!); + + // Template registry HttpClients (AAS, Submodel) + _ = services.AddHttpClientWithResilience(AasEnvironmentConfig.AasRegistry, templateManagement.ResiliencePolicies.Retry, templateManagement.AasTemplateRegistry.BaseUrl!); + _ = services.AddHttpClientWithResilience(AasEnvironmentConfig.SubmodelRegistry, templateManagement.ResiliencePolicies.Retry, templateManagement.SubmodelTemplateRegistry.BaseUrl!); - _ = services.AddHttpClientWithoutResilience(AasEnvironmentConfig.AasEnvironmentRepoHealthCheckHttpClientName, templateManagement.AasTemplateRepository.BaseUrl!); - _ = services.AddHttpClientWithoutResilience(AasEnvironmentConfig.AasRegistryHealthCheckHttpClientName, templateManagement.AasTemplateRegistry.BaseUrl!); - _ = services.AddHttpClientWithoutResilience(AasEnvironmentConfig.SubmodelRegistryHealthCheckHttpClientName, templateManagement.SubmodelTemplateRegistry.BaseUrl!); + // Health check clients (without resilience) + _ = services.AddHttpClientWithoutResilience(AasEnvironmentConfig.AasTemplateRepositoryHealthCheck, templateManagement.AasTemplateRepository.BaseUrl!); + _ = services.AddHttpClientWithoutResilience(AasEnvironmentConfig.SubmodelTemplateRepositoryHealthCheck, templateManagement.SubmodelTemplateRepository.BaseUrl!); + _ = services.AddHttpClientWithoutResilience(AasEnvironmentConfig.ConceptDescriptorTemplateRepositoryHealthCheck, templateManagement.ConceptDescriptionTemplateRepository.BaseUrl!); + _ = services.AddHttpClientWithoutResilience(AasEnvironmentConfig.AasRegistryHealthCheck, templateManagement.AasTemplateRegistry.BaseUrl!); + _ = services.AddHttpClientWithoutResilience(AasEnvironmentConfig.SubmodelRegistryHealthCheck, templateManagement.SubmodelTemplateRegistry.BaseUrl!); // Plugin HttpClients (from PluginsConfig.Instances) if (pluginsConfig.Instances.Count > 0) diff --git a/source/AAS.TwinEngine.DataEngine/appsettings.development.json b/source/AAS.TwinEngine.DataEngine/appsettings.development.json index 0f411051..d11834c2 100644 --- a/source/AAS.TwinEngine.DataEngine/appsettings.development.json +++ b/source/AAS.TwinEngine.DataEngine/appsettings.development.json @@ -166,8 +166,8 @@ "Semantics": { "InternalSemanticId": "InternalSemanticId" }, - "AasTemplateRepository": { - "Name": "AasTemplateRepository", + "TemplateRepository": { + "Name": "TemplateRepository", "baseUrl": "http://localhost:8081", "headerMappings": [ { @@ -182,17 +182,6 @@ } ] }, - "SubmodelTemplateRepository": { - "Name": "SubmodelTemplateRepository", - "baseUrl": "http://localhost:8081", - "headerMappings": [ - { - "source": "Authorization", - "target": "Authorization", - "required": false - } - ] - }, "AasTemplateRegistry": { "Name": "AasTemplateRegistry", "baseUrl": "http://localhost:8082", diff --git a/source/AAS.TwinEngine.DataEngine/appsettings.json b/source/AAS.TwinEngine.DataEngine/appsettings.json index e8f701a0..9d0b93d5 100644 --- a/source/AAS.TwinEngine.DataEngine/appsettings.json +++ b/source/AAS.TwinEngine.DataEngine/appsettings.json @@ -139,8 +139,8 @@ "Semantics": { "InternalSemanticId": "InternalSemanticId" }, - "AasTemplateRepository": { - "Name": "AasTemplateRepository", + "TemplateRepository": { + "Name": "TemplateRepository", "baseUrl": "http://localhost:8081", "headerMappings": [ { @@ -155,17 +155,6 @@ } ] }, - "SubmodelTemplateRepository": { - "Name": "SubmodelTemplateRepository", - "baseUrl": "http://localhost:8081", - "headerMappings": [ - { - "source": "Authorization", - "target": "Authorization", - "required": false - } - ] - }, "AasTemplateRegistry": { "Name": "AasTemplateRegistry", "baseUrl": "http://localhost:8082", diff --git a/source/AAS.TwinEngine.Plugin.TestPlugin/Example/docker-compose.yml b/source/AAS.TwinEngine.Plugin.TestPlugin/Example/docker-compose.yml index bf48bc53..7dc61eb6 100644 --- a/source/AAS.TwinEngine.Plugin.TestPlugin/Example/docker-compose.yml +++ b/source/AAS.TwinEngine.Plugin.TestPlugin/Example/docker-compose.yml @@ -56,16 +56,11 @@ services: - TemplateManagement__TemplateMappingRules__AasIdExtractionRules__0__Pattern=Regex - TemplateManagement__TemplateMappingRules__AasIdExtractionRules__0__Index=6 - TemplateManagement__TemplateMappingRules__AasIdExtractionRules__0__Separator=/ - - TemplateManagement__AasTemplateRepository__Name=AasTemplateRepository - - TemplateManagement__AasTemplateRepository__baseUrl=http://template-repository:8081 - - TemplateManagement__AasTemplateRepository__headerMappings__0__source=Authorization - - TemplateManagement__AasTemplateRepository__headerMappings__0__target=Authorization - - TemplateManagement__AasTemplateRepository__headerMappings__0__required=false - - TemplateManagement__SubmodelTemplateRepository__Name=SubmodelTemplateRepository - - TemplateManagement__SubmodelTemplateRepository__baseUrl=http://template-repository:8081 - - TemplateManagement__SubmodelTemplateRepository__headerMappings__0__source=Authorization - - TemplateManagement__SubmodelTemplateRepository__headerMappings__0__target=Authorization - - TemplateManagement__SubmodelTemplateRepository__headerMappings__0__required=false + - TemplateManagement__TemplateRepository__Name=AasTemplateRepository + - TemplateManagement__TemplateRepository__baseUrl=http://template-repository:8081 + - TemplateManagement__TemplateRepository__headerMappings__0__source=Authorization + - TemplateManagement__TemplateRepository__headerMappings__0__target=Authorization + - TemplateManagement__TemplateRepository__headerMappings__0__required=false - TemplateManagement__AasTemplateRegistry__Name=AasTemplateRegistry - TemplateManagement__AasTemplateRegistry__baseUrl=http://aas-template-registry:8080 - TemplateManagement__AasTemplateRegistry__headerMappings__0__source=Authorization From fad381d58614d1fc8e9a4136d88576d2c0e4d683 Mon Sep 17 00:00:00 2001 From: Kevalkumar Date: Thu, 9 Apr 2026 16:47:39 +0530 Subject: [PATCH 25/71] Refactor compose to add template repo --- example/docker-compose.yml | 15 +++++---------- 1 file changed, 5 insertions(+), 10 deletions(-) diff --git a/example/docker-compose.yml b/example/docker-compose.yml index 01ed785f..2308f251 100644 --- a/example/docker-compose.yml +++ b/example/docker-compose.yml @@ -55,16 +55,11 @@ services: - TemplateManagement__TemplateMappingRules__AasIdExtractionRules__0__Pattern=Regex - TemplateManagement__TemplateMappingRules__AasIdExtractionRules__0__Index=6 - TemplateManagement__TemplateMappingRules__AasIdExtractionRules__0__Separator=/ - - TemplateManagement__AasTemplateRepository__Name=AasTemplateRepository - - TemplateManagement__AasTemplateRepository__baseUrl=http://template-repository:8081 - - TemplateManagement__AasTemplateRepository__headerMappings__0__source=Authorization - - TemplateManagement__AasTemplateRepository__headerMappings__0__target=Authorization - - TemplateManagement__AasTemplateRepository__headerMappings__0__required=false - - TemplateManagement__SubmodelTemplateRepository__Name=SubmodelTemplateRepository - - TemplateManagement__SubmodelTemplateRepository__baseUrl=http://template-repository:8081 - - TemplateManagement__SubmodelTemplateRepository__headerMappings__0__source=Authorization - - TemplateManagement__SubmodelTemplateRepository__headerMappings__0__target=Authorization - - TemplateManagement__SubmodelTemplateRepository__headerMappings__0__required=false + - TemplateManagement__TemplateRepository__Name=AasTemplateRepository + - TemplateManagement__TemplateRepository__baseUrl=http://template-repository:8081 + - TemplateManagement__TemplateRepository__headerMappings__0__source=Authorization + - TemplateManagement__TemplateRepository__headerMappings__0__target=Authorization + - TemplateManagement__TemplateRepository__headerMappings__0__required=false - TemplateManagement__AasTemplateRegistry__Name=AasTemplateRegistry - TemplateManagement__AasTemplateRegistry__baseUrl=http://aas-template-registry:8080 - TemplateManagement__AasTemplateRegistry__headerMappings__0__source=Authorization From f7c272a94bf957ab8a92d075d5d4f9ac7f8820b8 Mon Sep 17 00:00:00 2001 From: Kevalkumar Date: Fri, 10 Apr 2026 01:20:29 +0530 Subject: [PATCH 26/71] Added change for http clients to use saperate clients for repositories --- .../Headers/RequestHeaderMapperTests.cs | 267 +++++++++++------- .../Http/Clients/HttpClientFactoryTests.cs | 2 +- .../HttpClientRegistrationExtensionsTests.cs | 22 +- .../TemplateRegistryHealthCheckTests.cs | 44 +-- .../TemplateRepositoryHealthCheckTests.cs | 228 ++++++++------- .../SubmodelDescriptorProviderTests.cs | 16 +- .../Services/TemplateProviderTests.cs | 40 +-- ...acyTemplateManagementConfigAdapterTests.cs | 6 +- .../Plugin/Config/AasEnvironmentConfig.cs | 24 +- .../Monitoring/TemplateRegistryHealthCheck.cs | 4 +- .../Services/AasRegistryProvider.cs | 2 +- .../Services/SubmodelDescriptorProvider.cs | 2 +- 12 files changed, 366 insertions(+), 291 deletions(-) diff --git a/source/AAS.TwinEngine.DataEngine.UnitTests/Infrastructure/Http/Authorization/Headers/RequestHeaderMapperTests.cs b/source/AAS.TwinEngine.DataEngine.UnitTests/Infrastructure/Http/Authorization/Headers/RequestHeaderMapperTests.cs index 75e4efed..88098396 100644 --- a/source/AAS.TwinEngine.DataEngine.UnitTests/Infrastructure/Http/Authorization/Headers/RequestHeaderMapperTests.cs +++ b/source/AAS.TwinEngine.DataEngine.UnitTests/Infrastructure/Http/Authorization/Headers/RequestHeaderMapperTests.cs @@ -18,19 +18,76 @@ private static RequestHeaderMapper CreateService( PluginsConfig? pluginsConfig = null, TemplateManagementConfig? templateManagementConfig = null) { - var logger = new NullLogger(); return new RequestHeaderMapper( - logger, + new NullLogger(), Options.Create(generalConfig), Options.Create(pluginsConfig ?? new PluginsConfig()), Options.Create(templateManagementConfig ?? new TemplateManagementConfig())); } + private static GeneralConfig DefaultConfig() => new() + { + HeaderSanitization = new HeaderSanitizationOptions() + }; + + [Fact] + public void ValidateIncomingHeaders_NullContext_DoesNothing() + { + var service = CreateService(DefaultConfig()); + service.ValidateIncomingHeaders(null); + } + + [Fact] + public void ValidateIncomingHeaders_ValidHeaders_DoesNotThrow() + { + var service = CreateService(DefaultConfig()); + var context = new DefaultHttpContext(); + context.Request.Headers["X-Test"] = "value"; + + service.ValidateIncomingHeaders(context); + } + + [Fact] + public void ValidateIncomingHeaders_InvalidHeaderValue_Throws() + { + var config = DefaultConfig(); + config.HeaderSanitization.BlockedPatterns = ["(() => service.ValidateIncomingHeaders(context)); + } + + [Fact] + public void ValidateIncomingHeaders_EmptyHeaderValue_Throws() + { + var service = CreateService(DefaultConfig()); + var context = new DefaultHttpContext(); + context.Request.Headers["X-Test"] = ""; + + Assert.Throws(() => service.ValidateIncomingHeaders(context)); + } + [Fact] - public void ValidateIncomingHeaders_RequiredHeaderMissing_ThrowsInvalidRequestHeaderException() + public void ValidateIncomingHeaders_HeaderTooLarge_Throws() { - var generalConfig = new GeneralConfig { HeaderSanitization = new HeaderSanitizationOptions() }; - var templateManagementConfig = new TemplateManagementConfig + var config = DefaultConfig(); + config.HeaderSanitization.MaxHeaderSize = 5; + + var service = CreateService(config); + var context = new DefaultHttpContext(); + context.Request.Headers["X-Test"] = "123456"; + + Assert.Throws(() => service.ValidateIncomingHeaders(context)); + } + + [Fact] + public void ValidateIncomingHeaders_RequiredHeaderMissing_Throws() + { + var config = DefaultConfig(); + var templateConfig = new TemplateManagementConfig { AasTemplateRepository = new ServiceEndpoint { @@ -41,186 +98,198 @@ public void ValidateIncomingHeaders_RequiredHeaderMissing_ThrowsInvalidRequestHe } }; - var service = CreateService(generalConfig, templateManagementConfig: templateManagementConfig); + var service = CreateService(config, templateManagementConfig: templateConfig); var context = new DefaultHttpContext(); Assert.Throws(() => service.ValidateIncomingHeaders(context)); } [Fact] - public void ApplyMappings_OptionalHeaderMissing_DoesNotThrow() + public void ApplyMappings_NullHttpContext_DoesNothing() { - var generalConfig = new GeneralConfig { HeaderSanitization = new HeaderSanitizationOptions() }; - var templateManagementConfig = new TemplateManagementConfig - { - AasTemplateRepository = new ServiceEndpoint - { - HeaderMappings = - [ - new HeaderMappingRule { Source = "X-Optional", Target = "X-Optional", Required = false } - ] - } - }; + var service = CreateService(DefaultConfig()); + using var request = new HttpRequestMessage(); + + service.ApplyMappings(null, request, "client"); + } + + [Fact] + public void ApplyMappings_NullRequest_Throws() + { + var service = CreateService(DefaultConfig()); + + Assert.Throws(() => + service.ApplyMappings(new DefaultHttpContext(), null!, "client")); + } + + [Fact] + public void ApplyMappings_NullClientName_Throws() + { + var service = CreateService(DefaultConfig()); + using var request = new HttpRequestMessage(); + + Assert.Throws(() => + service.ApplyMappings(new DefaultHttpContext(), request, null!)); + } - var service = CreateService(generalConfig, templateManagementConfig: templateManagementConfig); + [Fact] + public void ApplyMappings_NoMappings_DoesNothing() + { + var service = CreateService(DefaultConfig()); var context = new DefaultHttpContext(); - using var requestMessage = new HttpRequestMessage(HttpMethod.Get, "http://example.com"); - service.ApplyMappings(context, requestMessage, AasEnvironmentConfig.AasEnvironmentRepoHttpClientName); + using var request = new HttpRequestMessage(); + + service.ApplyMappings(context, request, "unknown-client"); - Assert.False(requestMessage.Headers.Contains("X-Optional")); + Assert.Empty(request.Headers); } [Fact] public void ApplyMappings_MapsAuthorizationHeader() { - var generalConfig = new GeneralConfig { HeaderSanitization = new HeaderSanitizationOptions() }; - var templateManagementConfig = new TemplateManagementConfig + var config = DefaultConfig(); + var templateConfig = new TemplateManagementConfig { AasTemplateRepository = new ServiceEndpoint { HeaderMappings = [ - new HeaderMappingRule { Source = "Authorization", Target = "Authorization", Required = true } + new HeaderMappingRule { Source = "Authorization", Target = "Authorization" } ] } }; - var service = CreateService(generalConfig, templateManagementConfig: templateManagementConfig); + var service = CreateService(config, templateManagementConfig: templateConfig); + var context = new DefaultHttpContext(); context.Request.Headers.Authorization = "Bearer token"; - using var requestMessage = new HttpRequestMessage(HttpMethod.Get, "http://example.com"); + using var request = new HttpRequestMessage(); - service.ApplyMappings(context, requestMessage, AasEnvironmentConfig.AasEnvironmentRepoHttpClientName); + service.ApplyMappings(context, request, AasEnvironmentConfig.AasTemplateRepository); - Assert.Equal("Bearer", requestMessage.Headers.Authorization?.Scheme); - Assert.Equal("token", requestMessage.Headers.Authorization?.Parameter); + Assert.Equal("Bearer", request.Headers.Authorization?.Scheme); + Assert.Equal("token", request.Headers.Authorization?.Parameter); } [Fact] - public void ApplyMappings_PluginSpecificMapping_RenamesHeader() + public void ApplyMappings_RenamesHeader() { - const string PluginName = "MyPlugin"; - var clientName = PluginConfig.HttpClientNamePrefix + PluginName; - - var generalConfig = new GeneralConfig { HeaderSanitization = new HeaderSanitizationOptions() }; - var pluginsConfig = new PluginsConfig + var config = DefaultConfig(); + var templateConfig = new TemplateManagementConfig { - Instances = - [ - new PluginInstance - { - Name = PluginName, - BaseUrl = new Uri("http://example.com"), - HeaderMappings = - [ - new HeaderMappingRule { Source = "X-Source", Target = "X-Target", Required = true } - ] - } - ] + AasTemplateRepository = new ServiceEndpoint + { + HeaderMappings = + [ + new HeaderMappingRule { Source = "X-Source", Target = "X-Target" } + ] + } }; - var service = CreateService(generalConfig, pluginsConfig); + var service = CreateService(config, templateManagementConfig: templateConfig); + var context = new DefaultHttpContext(); context.Request.Headers["X-Source"] = "value"; - using var requestMessage = new HttpRequestMessage(HttpMethod.Get, "http://example.com"); + using var request = new HttpRequestMessage(); - service.ApplyMappings(context, requestMessage, clientName); + service.ApplyMappings(context, request, AasEnvironmentConfig.AasTemplateRepository); - Assert.True(requestMessage.Headers.TryGetValues("X-Target", out var values)); - Assert.Contains("value", values); + Assert.True(request.Headers.Contains("X-Target")); } [Fact] - public void ApplyMappings_MissingOptionalHeader_SkipsHeader() + public void ApplyMappings_OverwriteExistingHeader() { - var generalConfig = new GeneralConfig { HeaderSanitization = new HeaderSanitizationOptions() }; - var templateManagementConfig = new TemplateManagementConfig + var config = DefaultConfig(); + var templateConfig = new TemplateManagementConfig { AasTemplateRepository = new ServiceEndpoint { HeaderMappings = [ - new HeaderMappingRule { Source = "X-Test", Target = "X-Test", Required = false } + new HeaderMappingRule { Source = "X-Source", Target = "X-Test" } ] } }; - var service = CreateService(generalConfig, templateManagementConfig: templateManagementConfig); + var service = CreateService(config, templateManagementConfig: templateConfig); + var context = new DefaultHttpContext(); + context.Request.Headers["X-Source"] = "new"; - using var requestMessage = new HttpRequestMessage(HttpMethod.Get, "http://example.com"); + using var request = new HttpRequestMessage(); + request.Headers.TryAddWithoutValidation("X-Test", "old"); - service.ApplyMappings(context, requestMessage, AasEnvironmentConfig.AasEnvironmentRepoHttpClientName); + service.ApplyMappings(context, request, AasEnvironmentConfig.AasTemplateRepository); - Assert.False(requestMessage.Headers.Contains("X-Test")); + var value = request.Headers.GetValues("X-Test").First(); + Assert.Equal("new", value); } [Fact] - public void ValidateIncomingHeaders_InvalidMappedHeader_ThrowsBadRequest() + public void ApplyMappings_MultipleValues_CombinedCorrectly() { - var generalConfig = new GeneralConfig - { - // Default BlockedPatterns already includes "(() => service.ValidateIncomingHeaders(context)); - } + var context = new DefaultHttpContext(); + context.Request.Headers["X-Source"] = new[] { "a", "b" }; - [Fact] - public void ValidateIncomingHeaders_AllHeadersValid_DoesNotThrow() - { - var generalConfig = new GeneralConfig { HeaderSanitization = new HeaderSanitizationOptions() }; + using var request = new HttpRequestMessage(); - var service = CreateService(generalConfig); - var context = new DefaultHttpContext(); - context.Request.Headers["X-Valid"] = "simple-value"; - context.Request.Headers.Authorization = "Bearer token"; + service.ApplyMappings(context, request, AasEnvironmentConfig.AasTemplateRepository); - service.ValidateIncomingHeaders(context); + var value = request.Headers.GetValues("X-Target").First(); + Assert.Equal("a,b", value); } [Fact] - public void ApplyMappings_TemplateRegistryClient_UsesTemplateRegistryMappings() + public void ApplyMappings_PluginMapping_Works() { - var generalConfig = new GeneralConfig { HeaderSanitization = new HeaderSanitizationOptions() }; - var templateManagementConfig = new TemplateManagementConfig + var config = DefaultConfig(); + var pluginName = "TestPlugin"; + + var pluginsConfig = new PluginsConfig { - AasTemplateRegistry = new ServiceEndpoint - { - HeaderMappings = - [ - new HeaderMappingRule { Source = "X-Source", Target = "X-Registry", Required = true } - ] - } + Instances = + [ + new PluginInstance + { + Name = pluginName, + BaseUrl = new Uri("http://test"), + HeaderMappings = + [ + new HeaderMappingRule { Source = "X-A", Target = "X-B" } + ] + } + ] }; - var service = CreateService(generalConfig, templateManagementConfig: templateManagementConfig); + var service = CreateService(config, pluginsConfig); + var context = new DefaultHttpContext(); - context.Request.Headers["X-Source"] = "value"; + context.Request.Headers["X-A"] = "value"; + + using var request = new HttpRequestMessage(); - using var requestMessage = new HttpRequestMessage(HttpMethod.Get, "http://example.com"); + var clientName = PluginConfig.HttpClientNamePrefix + pluginName; - service.ApplyMappings(context, requestMessage, AasEnvironmentConfig.AasRegistryHttpClientName); + service.ApplyMappings(context, request, clientName); - Assert.True(requestMessage.Headers.TryGetValues("X-Registry", out var values)); - Assert.Contains("value", values); + Assert.True(request.Headers.Contains("X-B")); } } diff --git a/source/AAS.TwinEngine.DataEngine.UnitTests/Infrastructure/Http/Clients/HttpClientFactoryTests.cs b/source/AAS.TwinEngine.DataEngine.UnitTests/Infrastructure/Http/Clients/HttpClientFactoryTests.cs index 92b2b625..4350acf2 100644 --- a/source/AAS.TwinEngine.DataEngine.UnitTests/Infrastructure/Http/Clients/HttpClientFactoryTests.cs +++ b/source/AAS.TwinEngine.DataEngine.UnitTests/Infrastructure/Http/Clients/HttpClientFactoryTests.cs @@ -9,7 +9,7 @@ namespace AAS.TwinEngine.DataEngine.UnitTests.Infrastructure.Http.Clients; public class HttpClientFactoryTests { [Theory] - [InlineData(AasEnvironmentConfig.AasEnvironmentRepoHttpClientName)] + [InlineData(AasEnvironmentConfig.SubmodelTemplateRepository)] [InlineData(PluginConfig.HttpClientNamePrefix + "PluginName")] public void CreateClient_Returns_HttpClient(string clientName) { diff --git a/source/AAS.TwinEngine.DataEngine.UnitTests/Infrastructure/Http/Extensions/HttpClientRegistrationExtensionsTests.cs b/source/AAS.TwinEngine.DataEngine.UnitTests/Infrastructure/Http/Extensions/HttpClientRegistrationExtensionsTests.cs index f5c7f873..f70c1092 100644 --- a/source/AAS.TwinEngine.DataEngine.UnitTests/Infrastructure/Http/Extensions/HttpClientRegistrationExtensionsTests.cs +++ b/source/AAS.TwinEngine.DataEngine.UnitTests/Infrastructure/Http/Extensions/HttpClientRegistrationExtensionsTests.cs @@ -26,12 +26,12 @@ public void HttpClientRegistrationExtensions_RegistersTemplateProviderClientWith var headerMapper = Substitute.For(); _ = services.AddScoped(_ => headerMapper); - services.AddHttpClientWithResilience(AasEnvironmentConfig.AasEnvironmentRepoHttpClientName, DefaultRetryConfig, new Uri("https://example.com")); + services.AddHttpClientWithResilience(AasEnvironmentConfig.SubmodelTemplateRepository, DefaultRetryConfig, new Uri("https://example.com")); var serviceProvider = services.BuildServiceProvider(); var httpClientFactory = serviceProvider.GetRequiredService(); - var httpClient = httpClientFactory.CreateClient(AasEnvironmentConfig.AasEnvironmentRepoHttpClientName); + var httpClient = httpClientFactory.CreateClient(AasEnvironmentConfig.SubmodelTemplateRepository); Assert.Contains(httpClient.DefaultRequestHeaders.Accept, h => h.MediaType == "application/json"); @@ -47,12 +47,12 @@ public void HttpClientRegistrationExtensions_RegistersPluginDataProviderClientWi var headerMapper = Substitute.For(); _ = services.AddScoped(_ => headerMapper); - services.AddHttpClientWithResilience(AasEnvironmentConfig.AasEnvironmentRepoHttpClientName, DefaultRetryConfig, new Uri("https://example.com")); + services.AddHttpClientWithResilience(AasEnvironmentConfig.SubmodelTemplateRepository, DefaultRetryConfig, new Uri("https://example.com")); var serviceProvider = services.BuildServiceProvider(); var httpClientFactory = serviceProvider.GetRequiredService(); - var httpClient = httpClientFactory.CreateClient(AasEnvironmentConfig.AasEnvironmentRepoHttpClientName); + var httpClient = httpClientFactory.CreateClient(AasEnvironmentConfig.SubmodelTemplateRepository); Assert.Contains(httpClient.DefaultRequestHeaders.Accept, h => h.MediaType == "application/json"); @@ -69,12 +69,12 @@ public async Task SendRequest_AfterFourAttempts_ThrowsExceptionAndLogsThreeRetri var headerMapper = Substitute.For(); _ = services.AddScoped(_ => headerMapper); - services.AddHttpClientWithResilience(AasEnvironmentConfig.AasEnvironmentRepoHttpClientName, DefaultRetryConfig, new Uri("https://example.com")); + services.AddHttpClientWithResilience(AasEnvironmentConfig.SubmodelTemplateRepository, DefaultRetryConfig, new Uri("https://example.com")); using var handler = new FaultyHttpMessageHandler(); - services.Configure(AasEnvironmentConfig.AasEnvironmentRepoHttpClientName, options => options.HttpMessageHandlerBuilderActions.Add(builder => builder.PrimaryHandler = handler)); + services.Configure(AasEnvironmentConfig.SubmodelTemplateRepository, options => options.HttpMessageHandlerBuilderActions.Add(builder => builder.PrimaryHandler = handler)); var serviceProvider = services.BuildServiceProvider(); var factory = serviceProvider.GetRequiredService(); - var client = factory.CreateClient(AasEnvironmentConfig.AasEnvironmentRepoHttpClientName); + var client = factory.CreateClient(AasEnvironmentConfig.SubmodelTemplateRepository); await Assert.ThrowsAsync(() => client.GetAsync(new Uri("http://test.com"))); @@ -92,12 +92,12 @@ public async Task AddHttpClientWithResilience_WithForwarding_AddsHeaderForwardin _ = services.AddScoped(_ => mappingService); services.AddHttpClientWithResilience( - AasEnvironmentConfig.AasEnvironmentRepoHttpClientName, + AasEnvironmentConfig.SubmodelTemplateRepository, DefaultRetryConfig, new Uri("https://example.com")); using var handler = new FaultyHttpMessageHandler(); - services.Configure(AasEnvironmentConfig.AasEnvironmentRepoHttpClientName, + services.Configure(AasEnvironmentConfig.SubmodelTemplateRepository, options => options.HttpMessageHandlerBuilderActions.Add(builder => builder.PrimaryHandler = handler)); var serviceProvider = services.BuildServiceProvider(); @@ -109,13 +109,13 @@ public async Task AddHttpClientWithResilience_WithForwarding_AddsHeaderForwardin }; var factory = serviceProvider.GetRequiredService(); - var client = factory.CreateClient(AasEnvironmentConfig.AasEnvironmentRepoHttpClientName); + var client = factory.CreateClient(AasEnvironmentConfig.SubmodelTemplateRepository); _ = await Assert.ThrowsAsync(() => client.GetAsync("/test")).ConfigureAwait(false); mappingService .Received() - .ApplyMappings(httpContextAccessor.HttpContext, Arg.Any(), AasEnvironmentConfig.AasEnvironmentRepoHttpClientName); + .ApplyMappings(httpContextAccessor.HttpContext, Arg.Any(), AasEnvironmentConfig.SubmodelTemplateRepository); } private sealed class FaultyHttpMessageHandler : HttpMessageHandler diff --git a/source/AAS.TwinEngine.DataEngine.UnitTests/Infrastructure/Monitoring/TemplateRegistryHealthCheckTests.cs b/source/AAS.TwinEngine.DataEngine.UnitTests/Infrastructure/Monitoring/TemplateRegistryHealthCheckTests.cs index 97e5dad9..190e11fd 100644 --- a/source/AAS.TwinEngine.DataEngine.UnitTests/Infrastructure/Monitoring/TemplateRegistryHealthCheckTests.cs +++ b/source/AAS.TwinEngine.DataEngine.UnitTests/Infrastructure/Monitoring/TemplateRegistryHealthCheckTests.cs @@ -20,8 +20,8 @@ public async Task CheckHealthAsync_Returns_Healthy_When_Registry_And_Submodel_Ar { var clientFactory = Substitute.For(); - clientFactory.CreateClient(AasEnvironmentConfig.AasRegistryHealthCheckHttpClientName).Returns(CreateHttpClient(HttpStatusCode.OK)); - clientFactory.CreateClient(AasEnvironmentConfig.SubmodelRegistryHealthCheckHttpClientName).Returns(CreateHttpClient(HttpStatusCode.OK)); + clientFactory.CreateClient(AasEnvironmentConfig.AasRegistryHealthCheck).Returns(CreateHttpClient(HttpStatusCode.OK)); + clientFactory.CreateClient(AasEnvironmentConfig.SubmodelRegistryHealthCheck).Returns(CreateHttpClient(HttpStatusCode.OK)); var logger = Substitute.For>(); @@ -37,8 +37,8 @@ public async Task CheckHealthAsync_Returns_Unhealthy_When_AasRegistry_Is_Unhealt { var clientFactory = Substitute.For(); - clientFactory.CreateClient(AasEnvironmentConfig.AasRegistryHealthCheckHttpClientName).Returns(CreateHttpClient(HttpStatusCode.InternalServerError)); - clientFactory.CreateClient(AasEnvironmentConfig.SubmodelRegistryHealthCheckHttpClientName).Returns(CreateHttpClient(HttpStatusCode.OK)); + clientFactory.CreateClient(AasEnvironmentConfig.AasRegistryHealthCheck).Returns(CreateHttpClient(HttpStatusCode.InternalServerError)); + clientFactory.CreateClient(AasEnvironmentConfig.SubmodelRegistryHealthCheck).Returns(CreateHttpClient(HttpStatusCode.OK)); var logger = Substitute.For>(); @@ -54,8 +54,8 @@ public async Task CheckHealthAsync_Returns_Unhealthy_When_SubmodelRegistry_Is_Un { var clientFactory = Substitute.For(); - clientFactory.CreateClient(AasEnvironmentConfig.AasRegistryHealthCheckHttpClientName).Returns(CreateHttpClient(HttpStatusCode.OK)); - clientFactory.CreateClient(AasEnvironmentConfig.SubmodelRegistryHealthCheckHttpClientName).Returns(CreateHttpClient(HttpStatusCode.InternalServerError)); + clientFactory.CreateClient(AasEnvironmentConfig.AasRegistryHealthCheck).Returns(CreateHttpClient(HttpStatusCode.OK)); + clientFactory.CreateClient(AasEnvironmentConfig.SubmodelRegistryHealthCheck).Returns(CreateHttpClient(HttpStatusCode.InternalServerError)); var logger = Substitute.For>(); @@ -70,8 +70,8 @@ public async Task CheckHealthAsync_Returns_Unhealthy_When_SubmodelRegistry_Is_Un public async Task CheckHealthAsync_Returns_Unhealthy_When_AasRegistry_Request_Throws_HttpRequestException() { var clientFactory = Substitute.For(); - clientFactory.CreateClient(AasEnvironmentConfig.AasRegistryHealthCheckHttpClientName).Returns(CreateThrowingHttpClient(new HttpRequestException("network"))); - clientFactory.CreateClient(AasEnvironmentConfig.SubmodelRegistryHealthCheckHttpClientName).Returns(CreateHttpClient(HttpStatusCode.OK)); + clientFactory.CreateClient(AasEnvironmentConfig.AasRegistryHealthCheck).Returns(CreateThrowingHttpClient(new HttpRequestException("network"))); + clientFactory.CreateClient(AasEnvironmentConfig.SubmodelRegistryHealthCheck).Returns(CreateHttpClient(HttpStatusCode.OK)); var logger = Substitute.For>(); @@ -87,8 +87,8 @@ public async Task CheckHealthAsync_Returns_Unhealthy_When_SubmodelRegistry_Reque { var clientFactory = Substitute.For(); - clientFactory.CreateClient(AasEnvironmentConfig.AasRegistryHealthCheckHttpClientName).Returns(CreateHttpClient(HttpStatusCode.OK)); - clientFactory.CreateClient(AasEnvironmentConfig.SubmodelRegistryHealthCheckHttpClientName).Returns(CreateThrowingHttpClient(new TaskCanceledException("timeout"))); + clientFactory.CreateClient(AasEnvironmentConfig.AasRegistryHealthCheck).Returns(CreateHttpClient(HttpStatusCode.OK)); + clientFactory.CreateClient(AasEnvironmentConfig.SubmodelRegistryHealthCheck).Returns(CreateThrowingHttpClient(new TaskCanceledException("timeout"))); var logger = Substitute.For>(); @@ -103,9 +103,9 @@ public async Task CheckHealthAsync_Returns_Unhealthy_When_SubmodelRegistry_Reque public async Task CheckHealthAsync_Returns_Unhealthy_When_AasRegistry_Request_Throws_Exception() { var clientFactory = Substitute.For(); - clientFactory.CreateClient(AasEnvironmentConfig.AasRegistryHealthCheckHttpClientName) + clientFactory.CreateClient(AasEnvironmentConfig.AasRegistryHealthCheck) .Returns(CreateThrowingHttpClient(new Exception("unexpected"))); - clientFactory.CreateClient(AasEnvironmentConfig.SubmodelRegistryHealthCheckHttpClientName).Returns(CreateHttpClient(HttpStatusCode.OK)); + clientFactory.CreateClient(AasEnvironmentConfig.SubmodelRegistryHealthCheck).Returns(CreateHttpClient(HttpStatusCode.OK)); var logger = Substitute.For>(); @@ -120,8 +120,8 @@ public async Task CheckHealthAsync_Returns_Unhealthy_When_AasRegistry_Request_Th public async Task CheckHealthAsync_Checks_Both_Endpoints_In_Parallel_Even_When_AasRegistry_Is_Unhealthy() { var clientFactory = Substitute.For(); - clientFactory.CreateClient(AasEnvironmentConfig.AasRegistryHealthCheckHttpClientName).Returns(CreateHttpClient(HttpStatusCode.InternalServerError)); - clientFactory.CreateClient(AasEnvironmentConfig.SubmodelRegistryHealthCheckHttpClientName).Returns(CreateHttpClient(HttpStatusCode.OK)); + clientFactory.CreateClient(AasEnvironmentConfig.AasRegistryHealthCheck).Returns(CreateHttpClient(HttpStatusCode.InternalServerError)); + clientFactory.CreateClient(AasEnvironmentConfig.SubmodelRegistryHealthCheck).Returns(CreateHttpClient(HttpStatusCode.OK)); var logger = Substitute.For>(); @@ -129,16 +129,16 @@ public async Task CheckHealthAsync_Checks_Both_Endpoints_In_Parallel_Even_When_A _ = await sut.CheckHealthAsync(new HealthCheckContext(), CancellationToken.None); - clientFactory.Received(1).CreateClient(AasEnvironmentConfig.AasRegistryHealthCheckHttpClientName); - clientFactory.Received(1).CreateClient(AasEnvironmentConfig.SubmodelRegistryHealthCheckHttpClientName); + clientFactory.Received(1).CreateClient(AasEnvironmentConfig.AasRegistryHealthCheck); + clientFactory.Received(1).CreateClient(AasEnvironmentConfig.SubmodelRegistryHealthCheck); } [Fact] public async Task CheckHealthAsync_Uses_HealthCheck_Client_Names_Without_Retry_Policy() { var clientFactory = Substitute.For(); - clientFactory.CreateClient(AasEnvironmentConfig.AasRegistryHealthCheckHttpClientName).Returns(CreateHttpClient(HttpStatusCode.OK)); - clientFactory.CreateClient(AasEnvironmentConfig.SubmodelRegistryHealthCheckHttpClientName).Returns(CreateHttpClient(HttpStatusCode.OK)); + clientFactory.CreateClient(AasEnvironmentConfig.AasRegistryHealthCheck).Returns(CreateHttpClient(HttpStatusCode.OK)); + clientFactory.CreateClient(AasEnvironmentConfig.SubmodelRegistryHealthCheck).Returns(CreateHttpClient(HttpStatusCode.OK)); var logger = Substitute.For>(); @@ -146,10 +146,10 @@ public async Task CheckHealthAsync_Uses_HealthCheck_Client_Names_Without_Retry_P _ = await sut.CheckHealthAsync(new HealthCheckContext(), CancellationToken.None); - clientFactory.Received().CreateClient(AasEnvironmentConfig.AasRegistryHealthCheckHttpClientName); - clientFactory.Received().CreateClient(AasEnvironmentConfig.SubmodelRegistryHealthCheckHttpClientName); - clientFactory.DidNotReceive().CreateClient(AasEnvironmentConfig.AasRegistryHttpClientName); - clientFactory.DidNotReceive().CreateClient(AasEnvironmentConfig.SubmodelRegistryHttpClientName); + clientFactory.Received().CreateClient(AasEnvironmentConfig.AasRegistryHealthCheck); + clientFactory.Received().CreateClient(AasEnvironmentConfig.SubmodelRegistryHealthCheck); + clientFactory.DidNotReceive().CreateClient(AasEnvironmentConfig.AasRegistry); + clientFactory.DidNotReceive().CreateClient(AasEnvironmentConfig.SubmodelRegistry); } private static HttpClient CreateThrowingHttpClient(Exception exception) diff --git a/source/AAS.TwinEngine.DataEngine.UnitTests/Infrastructure/Monitoring/TemplateRepositoryHealthCheckTests.cs b/source/AAS.TwinEngine.DataEngine.UnitTests/Infrastructure/Monitoring/TemplateRepositoryHealthCheckTests.cs index 2b17a926..82aca052 100644 --- a/source/AAS.TwinEngine.DataEngine.UnitTests/Infrastructure/Monitoring/TemplateRepositoryHealthCheckTests.cs +++ b/source/AAS.TwinEngine.DataEngine.UnitTests/Infrastructure/Monitoring/TemplateRepositoryHealthCheckTests.cs @@ -9,186 +9,196 @@ using NSubstitute; -using HealthStatus = Microsoft.Extensions.Diagnostics.HealthChecks.HealthStatus; - namespace AAS.TwinEngine.DataEngine.UnitTests.Infrastructure.Monitoring; public class TemplateRepositoryHealthCheckTests { - [Fact] - public async Task CheckHealthAsync_Returns_Healthy_When_Repository_And_Submodel_Are_Healthy() + private readonly ICreateClient _clientFactory; + private readonly ILogger _logger; + + public TemplateRepositoryHealthCheckTests() { - var clientFactory = Substitute.For(); + _clientFactory = Substitute.For(); + _logger = Substitute.For>(); + } - clientFactory.CreateClient(AasEnvironmentConfig.AasEnvironmentRepoHealthCheckHttpClientName) - .Returns( - _ => CreateHttpClient(HttpStatusCode.OK), - _ => CreateHttpClient(HttpStatusCode.OK)); + private TemplateRepositoryHealthCheck CreateSut() => new(_clientFactory, _logger); - var logger = Substitute.For>(); + private static HttpClient CreateHttpClient(HttpStatusCode statusCode) + { + var handler = new FakeHttpMessageHandler(_ => new HttpResponseMessage(statusCode)); - var sut = new TemplateRepositoryHealthCheck(clientFactory, logger); + return new HttpClient(handler) + { + BaseAddress = new Uri("http://localhost") + }; + } - var result = await sut.CheckHealthAsync(new HealthCheckContext(), CancellationToken.None); + private static HttpClient CreateHttpClientThatThrows(Exception ex) + { + var handler = new ExceptionHttpMessageHandler(ex); - Assert.Equal(HealthStatus.Healthy, result.Status); + return new HttpClient(handler) + { + BaseAddress = new Uri("http://localhost") + }; } - [Fact] - public async Task CheckHealthAsync_Returns_Unhealthy_When_Repository_Is_Unhealthy() + private sealed class FakeHttpMessageHandler : HttpMessageHandler { - var clientFactory = Substitute.For(); - - clientFactory.CreateClient(AasEnvironmentConfig.AasEnvironmentRepoHealthCheckHttpClientName) - .Returns( - _ => CreateHttpClient(HttpStatusCode.InternalServerError), - _ => CreateHttpClient(HttpStatusCode.OK)); + private readonly Func _handler; - var logger = Substitute.For>(); + public FakeHttpMessageHandler(Func handler) => _handler = handler; - var sut = new TemplateRepositoryHealthCheck(clientFactory, logger); + protected override Task SendAsync(HttpRequestMessage request, CancellationToken cancellationToken) => Task.FromResult(_handler(request)); + } - var result = await sut.CheckHealthAsync(new HealthCheckContext(), CancellationToken.None); + private sealed class ExceptionHttpMessageHandler(Exception exception) : HttpMessageHandler + { + private readonly Exception _exception = exception; - Assert.Equal(HealthStatus.Unhealthy, result.Status); + protected override Task SendAsync(HttpRequestMessage request, CancellationToken cancellationToken) => throw _exception; } [Fact] - public async Task CheckHealthAsync_Returns_Unhealthy_When_SubmodelRepository_Is_Unhealthy() + public async Task CheckHealthAsync_AllRepositoriesHealthy_ReturnsHealthy() { - var clientFactory = Substitute.For(); + // Arrange + var client = CreateHttpClient(HttpStatusCode.OK); + _clientFactory.CreateClient(Arg.Any()).Returns(client); - clientFactory.CreateClient(AasEnvironmentConfig.AasEnvironmentRepoHealthCheckHttpClientName) - .Returns( - _ => CreateHttpClient(HttpStatusCode.OK), - _ => CreateHttpClient(HttpStatusCode.InternalServerError)); + var sut = CreateSut(); - var logger = Substitute.For>(); + // Act + var result = await sut.CheckHealthAsync(new HealthCheckContext()); - var sut = new TemplateRepositoryHealthCheck(clientFactory, logger); - - var result = await sut.CheckHealthAsync(new HealthCheckContext(), CancellationToken.None); - - Assert.Equal(HealthStatus.Unhealthy, result.Status); + // Assert + Assert.Equal(HealthStatus.Healthy, result.Status); } [Fact] - public async Task CheckHealthAsync_Returns_Unhealthy_When_Repository_Request_Throws_HttpRequestException() + public async Task CheckHealthAsync_OneRepositoryFails_ReturnsUnhealthy() { - var clientFactory = Substitute.For(); - clientFactory.CreateClient(AasEnvironmentConfig.AasEnvironmentRepoHealthCheckHttpClientName) - .Returns( - _ => CreateThrowingHttpClient(new HttpRequestException("network")), - _ => CreateHttpClient(HttpStatusCode.OK)); + // Arrange + var successClient = CreateHttpClient(HttpStatusCode.OK); + var failClient = CreateHttpClient(HttpStatusCode.InternalServerError); + + _clientFactory.CreateClient(AasEnvironmentConfig.SubmodelTemplateRepositoryHealthCheck) + .Returns(successClient); + + _clientFactory.CreateClient(AasEnvironmentConfig.AasTemplateRepositoryHealthCheck) + .Returns(failClient); - var logger = Substitute.For>(); + _clientFactory.CreateClient(AasEnvironmentConfig.ConceptDescriptorTemplateRepositoryHealthCheck) + .Returns(successClient); - var sut = new TemplateRepositoryHealthCheck(clientFactory, logger); + var sut = CreateSut(); - var result = await sut.CheckHealthAsync(new HealthCheckContext(), CancellationToken.None); + // Act + var result = await sut.CheckHealthAsync(new HealthCheckContext()); + // Assert Assert.Equal(HealthStatus.Unhealthy, result.Status); } [Fact] - public async Task CheckHealthAsync_Returns_Unhealthy_When_Repository_Request_Throws_Unexpected_Exception() + public async Task CheckHealthAsync_AllRepositoriesFail_ReturnsUnhealthy() { - var clientFactory = Substitute.For(); - clientFactory.CreateClient(AasEnvironmentConfig.AasEnvironmentRepoHealthCheckHttpClientName) - .Returns( - _ => CreateThrowingHttpClient(new InvalidOperationException("unexpected")), - _ => CreateHttpClient(HttpStatusCode.OK)); + // Arrange + var failClient = CreateHttpClient(HttpStatusCode.InternalServerError); + _clientFactory.CreateClient(Arg.Any()).Returns(failClient); - var logger = Substitute.For>(); + var sut = CreateSut(); - var sut = new TemplateRepositoryHealthCheck(clientFactory, logger); - - var result = await sut.CheckHealthAsync(new HealthCheckContext(), CancellationToken.None); + // Act + var result = await sut.CheckHealthAsync(new HealthCheckContext()); + // Assert Assert.Equal(HealthStatus.Unhealthy, result.Status); } [Fact] - public async Task CheckHealthAsync_Returns_Unhealthy_When_SubmodelRepository_Request_Throws_TaskCanceledException() + public async Task CheckHealthAsync_WhenHttpRequestException_ReturnsUnhealthy() { - var clientFactory = Substitute.For(); - clientFactory.CreateClient(AasEnvironmentConfig.AasEnvironmentRepoHealthCheckHttpClientName) - .Returns( - _ => CreateHttpClient(HttpStatusCode.OK), - _ => CreateThrowingHttpClient(new TaskCanceledException("timeout"))); - - var logger = Substitute.For>(); + // Arrange + var client = CreateHttpClientThatThrows(new HttpRequestException()); + _clientFactory.CreateClient(Arg.Any()).Returns(client); - var sut = new TemplateRepositoryHealthCheck(clientFactory, logger); + var sut = CreateSut(); - var result = await sut.CheckHealthAsync(new HealthCheckContext(), CancellationToken.None); + // Act + var result = await sut.CheckHealthAsync(new HealthCheckContext()); + // Assert Assert.Equal(HealthStatus.Unhealthy, result.Status); } [Fact] - public async Task CheckHealthAsync_Checks_Both_Endpoints_In_Parallel_Even_When_First_Is_Unhealthy() + public async Task CheckHealthAsync_WhenTimeout_ReturnsUnhealthy() { - var clientFactory = Substitute.For(); - - clientFactory.CreateClient(AasEnvironmentConfig.AasEnvironmentRepoHealthCheckHttpClientName) - .Returns( - _ => CreateHttpClient(HttpStatusCode.InternalServerError), - _ => CreateHttpClient(HttpStatusCode.OK)); + // Arrange + var client = CreateHttpClientThatThrows(new TaskCanceledException()); + _clientFactory.CreateClient(Arg.Any()).Returns(client); - var logger = Substitute.For>(); + var sut = CreateSut(); - var sut = new TemplateRepositoryHealthCheck(clientFactory, logger); + // Act + var result = await sut.CheckHealthAsync(new HealthCheckContext()); - _ = await sut.CheckHealthAsync(new HealthCheckContext(), CancellationToken.None); - - clientFactory.Received(2).CreateClient(AasEnvironmentConfig.AasEnvironmentRepoHealthCheckHttpClientName); + // Assert + Assert.Equal(HealthStatus.Unhealthy, result.Status); } [Fact] - public async Task CheckHealthAsync_Uses_HealthCheck_Client_Name_Without_Retry_Policy() + public async Task CheckHealthAsync_WhenGenericException_ReturnsUnhealthy() { - var clientFactory = Substitute.For(); - clientFactory.CreateClient(AasEnvironmentConfig.AasEnvironmentRepoHealthCheckHttpClientName) - .Returns( - _ => CreateHttpClient(HttpStatusCode.OK), - _ => CreateHttpClient(HttpStatusCode.OK)); - - var logger = Substitute.For>(); + // Arrange + var client = CreateHttpClientThatThrows(new Exception("boom")); + _clientFactory.CreateClient(Arg.Any()).Returns(client); - var sut = new TemplateRepositoryHealthCheck(clientFactory, logger); + var sut = CreateSut(); - _ = await sut.CheckHealthAsync(new HealthCheckContext(), CancellationToken.None); + // Act + var result = await sut.CheckHealthAsync(new HealthCheckContext()); - clientFactory.Received(2).CreateClient(AasEnvironmentConfig.AasEnvironmentRepoHealthCheckHttpClientName); - clientFactory.DidNotReceive().CreateClient(AasEnvironmentConfig.AasEnvironmentRepoHttpClientName); + // Assert + Assert.Equal(HealthStatus.Unhealthy, result.Status); } - private static HttpClient CreateHttpClient(HttpStatusCode statusCode) + [Fact] + public async Task CheckHealthAsync_NonSuccessStatusCodes_ReturnUnhealthy() { - var handler = new StubHttpMessageHandler((_, _) => - Task.FromResult(new HttpResponseMessage(statusCode))); + // Arrange + var client = CreateHttpClient(HttpStatusCode.NotFound); + _clientFactory.CreateClient(Arg.Any()).Returns(client); - return new HttpClient(handler) - { - BaseAddress = new Uri("http://localhost") - }; - } + var sut = CreateSut(); - private static HttpClient CreateThrowingHttpClient(Exception exception) - { - var handler = new StubHttpMessageHandler((_, _) => throw exception); + // Act + var result = await sut.CheckHealthAsync(new HealthCheckContext()); - return new HttpClient(handler) - { - BaseAddress = new Uri("http://localhost") - }; + // Assert + Assert.Equal(HealthStatus.Unhealthy, result.Status); } - private sealed class StubHttpMessageHandler(Func> handler) - : HttpMessageHandler + [Fact] + public async Task CheckHealthAsync_LogsWarning_WhenRepositoryFails() { - protected override Task SendAsync(HttpRequestMessage request, CancellationToken cancellationToken) - => handler(request, cancellationToken); + // Arrange + var failClient = CreateHttpClient(HttpStatusCode.InternalServerError); + _clientFactory.CreateClient(Arg.Any()).Returns(failClient); + + var sut = CreateSut(); + + // Act + await sut.CheckHealthAsync(new HealthCheckContext()); + + // Assert + _logger.Received().Log( + LogLevel.Warning, + Arg.Any(), + Arg.Any(), + Arg.Any(), + Arg.Any>()); } } diff --git a/source/AAS.TwinEngine.DataEngine.UnitTests/Infrastructure/Providers/SubmodelRegistryProvider/Services/SubmodelDescriptorProviderTests.cs b/source/AAS.TwinEngine.DataEngine.UnitTests/Infrastructure/Providers/SubmodelRegistryProvider/Services/SubmodelDescriptorProviderTests.cs index ac99cc68..b763e11a 100644 --- a/source/AAS.TwinEngine.DataEngine.UnitTests/Infrastructure/Providers/SubmodelRegistryProvider/Services/SubmodelDescriptorProviderTests.cs +++ b/source/AAS.TwinEngine.DataEngine.UnitTests/Infrastructure/Providers/SubmodelRegistryProvider/Services/SubmodelDescriptorProviderTests.cs @@ -36,7 +36,7 @@ public async Task GetDataForSubmodelDescriptorByIdAsync_ReturnsSubmodelDesciptor })); using var httpClient = new HttpClient(messageHandler); httpClient.BaseAddress = new Uri("https://mm-software/fakeUrl"); - _clientFactory.CreateClient(AasEnvironmentConfig.SubmodelRegistryHttpClientName) + _clientFactory.CreateClient(AasEnvironmentConfig.SubmodelRegistry) .Returns(httpClient); var result = await _sut.GetDataForSubmodelDescriptorByIdAsync(Id, CancellationToken.None); @@ -56,7 +56,7 @@ public async Task GetDataForSubmodelDescriptorByIdAsync_ThrowsResponseParsingExc })); using var httpClient = new HttpClient(messageHandler); httpClient.BaseAddress = new Uri("https://mm-software/fakeUrl"); - _clientFactory.CreateClient(AasEnvironmentConfig.SubmodelRegistryHttpClientName) + _clientFactory.CreateClient(AasEnvironmentConfig.SubmodelRegistry) .Returns(httpClient); await Assert.ThrowsAsync(() => _sut.GetDataForSubmodelDescriptorByIdAsync(Id, CancellationToken.None)); @@ -74,7 +74,7 @@ public async Task GetDataForSubmodelDescriptorByIdAsync_ThrowsResponseParsingExc })); using var httpClient = new HttpClient(messageHandler); httpClient.BaseAddress = new Uri("https://mm-software/fakeUrl"); - _clientFactory.CreateClient(AasEnvironmentConfig.SubmodelRegistryHttpClientName) + _clientFactory.CreateClient(AasEnvironmentConfig.SubmodelRegistry) .Returns(httpClient); await Assert.ThrowsAsync(() => _sut.GetDataForSubmodelDescriptorByIdAsync(Id, CancellationToken.None)); @@ -92,7 +92,7 @@ public async Task GetDataForSubmodelDescriptorByIdAsync_ThrowsResourceNotFoundEx })); using var httpClient = new HttpClient(messageHandler); httpClient.BaseAddress = new Uri("https://mm-software/fakeUrl"); - _clientFactory.CreateClient(AasEnvironmentConfig.SubmodelRegistryHttpClientName) + _clientFactory.CreateClient(AasEnvironmentConfig.SubmodelRegistry) .Returns(httpClient); await Assert.ThrowsAsync(() => @@ -111,7 +111,7 @@ public async Task GetDataForSubmodelDescriptorByIdAsync_ThrowsServiceAuthorizati })); using var httpClient = new HttpClient(messageHandler); httpClient.BaseAddress = new Uri("https://mm-software/fakeUrl"); - _clientFactory.CreateClient(AasEnvironmentConfig.SubmodelRegistryHttpClientName) + _clientFactory.CreateClient(AasEnvironmentConfig.SubmodelRegistry) .Returns(httpClient); await Assert.ThrowsAsync(() => @@ -130,7 +130,7 @@ public async Task GetDataForSubmodelDescriptorByIdAsync_ThrowsServiceAuthorizati })); using var httpClient = new HttpClient(messageHandler); httpClient.BaseAddress = new Uri("https://mm-software/fakeUrl"); - _clientFactory.CreateClient(AasEnvironmentConfig.SubmodelRegistryHttpClientName) + _clientFactory.CreateClient(AasEnvironmentConfig.SubmodelRegistry) .Returns(httpClient); await Assert.ThrowsAsync(() => @@ -149,7 +149,7 @@ public async Task GetDataForSubmodelDescriptorByIdAsync_ThrowsRequestTimeoutExce })); using var httpClient = new HttpClient(messageHandler); httpClient.BaseAddress = new Uri("https://mm-software/fakeUrl"); - _clientFactory.CreateClient(AasEnvironmentConfig.SubmodelRegistryHttpClientName) + _clientFactory.CreateClient(AasEnvironmentConfig.SubmodelRegistry) .Returns(httpClient); await Assert.ThrowsAsync(() => @@ -168,7 +168,7 @@ public async Task GetDataForSubmodelDescriptorByIdAsync_ThrowsValidationFailedEx })); using var httpClient = new HttpClient(messageHandler); httpClient.BaseAddress = new Uri("https://mm-software/fakeUrl"); - _clientFactory.CreateClient(AasEnvironmentConfig.SubmodelRegistryHttpClientName) + _clientFactory.CreateClient(AasEnvironmentConfig.SubmodelRegistry) .Returns(httpClient); await Assert.ThrowsAsync(() => diff --git a/source/AAS.TwinEngine.DataEngine.UnitTests/Infrastructure/Providers/TemplateProvider/Services/TemplateProviderTests.cs b/source/AAS.TwinEngine.DataEngine.UnitTests/Infrastructure/Providers/TemplateProvider/Services/TemplateProviderTests.cs index 54128469..9a8a77c1 100644 --- a/source/AAS.TwinEngine.DataEngine.UnitTests/Infrastructure/Providers/TemplateProvider/Services/TemplateProviderTests.cs +++ b/source/AAS.TwinEngine.DataEngine.UnitTests/Infrastructure/Providers/TemplateProvider/Services/TemplateProviderTests.cs @@ -92,7 +92,7 @@ public async Task GetShellDescriptorsTemplateAsync_ReturnsShellDescriptor_WhenVa using var mockHttpMessageHandler = new FakeHttpMessageHandler(mockHttpResponse); using var httpClient = new HttpClient(mockHttpMessageHandler); httpClient.BaseAddress = new Uri("https://www.mm-software.com/fakeurl"); - _httpClientFactory.CreateClient(AasEnvironmentConfig.AasRegistryHttpClientName).Returns(httpClient); + _httpClientFactory.CreateClient(AasEnvironmentConfig.AasRegistry).Returns(httpClient); var result = await _sut.GetShellDescriptorsTemplateAsync(CancellationToken.None); @@ -110,7 +110,7 @@ public async Task GetShellDescriptorsTemplateAsync_ThrowsResponseParsingExceptio using var mockHttpMessageHandler = new FakeHttpMessageHandler(mockHttpResponse); using var httpClient = new HttpClient(mockHttpMessageHandler); httpClient.BaseAddress = new Uri("https://www.mm-software.com/fakeurl"); - _httpClientFactory.CreateClient(AasEnvironmentConfig.AasRegistryHttpClientName).Returns(httpClient); + _httpClientFactory.CreateClient(AasEnvironmentConfig.AasRegistry).Returns(httpClient); await Assert.ThrowsAsync(() => _sut.GetShellDescriptorsTemplateAsync(CancellationToken.None)); } @@ -125,7 +125,7 @@ public async Task GetShellDescriptorsTemplateAsync_ThrowsResourceNotFoundExcepti using var mockHttpMessageHandler = new FakeHttpMessageHandler(mockHttpResponse); using var httpClient = new HttpClient(mockHttpMessageHandler); httpClient.BaseAddress = new Uri("https://www.mm-software.com/fakeurl"); - _httpClientFactory.CreateClient(AasEnvironmentConfig.AasRegistryHttpClientName).Returns(httpClient); + _httpClientFactory.CreateClient(AasEnvironmentConfig.AasRegistry).Returns(httpClient); await Assert.ThrowsAsync(() => _sut.GetShellDescriptorsTemplateAsync(CancellationToken.None)); @@ -141,7 +141,7 @@ public async Task GetShellDescriptorsTemplateAsync_ReturnsDefaultShellDescriptor using var mockHttpMessageHandler = new FakeHttpMessageHandler(mockHttpResponse); using var httpClient = new HttpClient(mockHttpMessageHandler); httpClient.BaseAddress = new Uri("https://www.mm-software.com/fakeurl"); - _httpClientFactory.CreateClient(AasEnvironmentConfig.AasRegistryHttpClientName).Returns(httpClient); + _httpClientFactory.CreateClient(AasEnvironmentConfig.AasRegistry).Returns(httpClient); var result = await _sut.GetShellDescriptorsTemplateAsync(CancellationToken.None); @@ -160,7 +160,7 @@ public async Task GetShellDescriptorsTemplateAsync_ThrowsResponseParsingExceptio using var mockHttpMessageHandler = new FakeHttpMessageHandler(mockHttpResponse); using var httpClient = new HttpClient(mockHttpMessageHandler); httpClient.BaseAddress = new Uri("https://www.mm-software.com/fakeurl"); - _httpClientFactory.CreateClient(AasEnvironmentConfig.AasRegistryHttpClientName).Returns(httpClient); + _httpClientFactory.CreateClient(AasEnvironmentConfig.AasRegistry).Returns(httpClient); await Assert.ThrowsAsync(() => _sut.GetShellDescriptorsTemplateAsync(CancellationToken.None)); } @@ -173,7 +173,7 @@ public async Task GetShellDescriptorsTemplateAsync_ThrowsResourceNotFoundExcepti using var mockHttpMessageHandler = new FakeHttpMessageHandler(mockHttpResponse); using var httpClient = new HttpClient(mockHttpMessageHandler); httpClient.BaseAddress = new Uri("https://www.mm-software.com/fakeurl"); - _httpClientFactory.CreateClient(AasEnvironmentConfig.AasRegistryHttpClientName).Returns(httpClient); + _httpClientFactory.CreateClient(AasEnvironmentConfig.AasRegistry).Returns(httpClient); await Assert.ThrowsAsync(() => _sut.GetShellDescriptorsTemplateAsync(CancellationToken.None)); } @@ -184,7 +184,7 @@ public async Task GetShellDescriptorsTemplateAsync_ThrowsException_WhenHttpClien using var mockHttpMessageHandler = new FakeHttpMessageHandler(new HttpRequestException("Network error")); using var httpClient = new HttpClient(mockHttpMessageHandler); httpClient.BaseAddress = new Uri("https://www.mm-software.com/fakeurl"); - _httpClientFactory.CreateClient(AasEnvironmentConfig.AasRegistryHttpClientName).Returns(httpClient); + _httpClientFactory.CreateClient(AasEnvironmentConfig.AasRegistry).Returns(httpClient); var exception = await Assert.ThrowsAsync(() => _sut.GetShellDescriptorsTemplateAsync(CancellationToken.None)); Assert.Equal("Network error", exception.Message); @@ -197,7 +197,7 @@ public async Task GetShellTemplateAsync_ReturnsShell_WhenValidResponse() using var mockHttpMessageHandler = new FakeHttpMessageHandler(mockHttpResponse); using var httpClient = new HttpClient(mockHttpMessageHandler); httpClient.BaseAddress = new Uri("https://www.mm-software.com/fakeurl"); - _httpClientFactory.CreateClient(AasEnvironmentConfig.AasEnvironmentRepoHttpClientName).Returns(httpClient); + _httpClientFactory.CreateClient(AasEnvironmentConfig.AasTemplateRepository).Returns(httpClient); var result = await _sut.GetShellTemplateAsync(TemplateId, CancellationToken.None); @@ -216,7 +216,7 @@ public async Task GetShellTemplateAsync_ThrowsResponseParsingException_WhenInval using var mockHttpMessageHandler = new FakeHttpMessageHandler(mockHttpResponse); using var httpClient = new HttpClient(mockHttpMessageHandler); httpClient.BaseAddress = new Uri("https://www.mm-software.com/fakeurl"); - _httpClientFactory.CreateClient(AasEnvironmentConfig.AasEnvironmentRepoHttpClientName).Returns(httpClient); + _httpClientFactory.CreateClient(AasEnvironmentConfig.AasTemplateRepository).Returns(httpClient); await Assert.ThrowsAsync(() => _sut.GetShellTemplateAsync(TemplateId, CancellationToken.None)); } @@ -231,7 +231,7 @@ public async Task GetShellTemplateAsync_ThrowsResourceNotFoundException_WhenNotF using var mockHttpMessageHandler = new FakeHttpMessageHandler(mockHttpResponse); using var httpClient = new HttpClient(mockHttpMessageHandler); httpClient.BaseAddress = new Uri("https://www.mm-software.com/fakeurl"); - _httpClientFactory.CreateClient(AasEnvironmentConfig.AasEnvironmentRepoHttpClientName).Returns(httpClient); + _httpClientFactory.CreateClient(AasEnvironmentConfig.AasTemplateRepository).Returns(httpClient); await Assert.ThrowsAsync(() => _sut.GetShellTemplateAsync(TemplateId, CancellationToken.None)); @@ -243,7 +243,7 @@ public async Task GetShellTemplateAsync_ThrowsException_WhenHttpClientFails() using var mockHttpMessageHandler = new FakeHttpMessageHandler(new HttpRequestException("Network error")); using var httpClient = new HttpClient(mockHttpMessageHandler); httpClient.BaseAddress = new Uri("https://www.mm-software.com/fakeurl"); - _httpClientFactory.CreateClient(AasEnvironmentConfig.AasEnvironmentRepoHttpClientName).Returns(httpClient); + _httpClientFactory.CreateClient(AasEnvironmentConfig.AasTemplateRepository).Returns(httpClient); var exception = await Assert.ThrowsAsync(() => _sut.GetShellTemplateAsync(TemplateId, CancellationToken.None)); @@ -257,7 +257,7 @@ public async Task GetAssetInformationTemplateAsync_ReturnsAssetInformation_WhenV using var mockHttpMessageHandler = new FakeHttpMessageHandler(mockHttpResponse); using var httpClient = new HttpClient(mockHttpMessageHandler); httpClient.BaseAddress = new Uri("https://www.mm-software.com/fakeurl"); - _httpClientFactory.CreateClient(AasEnvironmentConfig.AasEnvironmentRepoHttpClientName).Returns(httpClient); + _httpClientFactory.CreateClient(AasEnvironmentConfig.AasTemplateRepository).Returns(httpClient); var result = await _sut.GetAssetInformationTemplateAsync(TemplateId, CancellationToken.None); @@ -275,7 +275,7 @@ public async Task GetAssetInformationTemplateAsync_ThrowsResponseParsingExceptio using var mockHttpMessageHandler = new FakeHttpMessageHandler(mockHttpResponse); using var httpClient = new HttpClient(mockHttpMessageHandler); httpClient.BaseAddress = new Uri("https://www.mm-software.com/fakeurl"); - _httpClientFactory.CreateClient(AasEnvironmentConfig.AasEnvironmentRepoHttpClientName).Returns(httpClient); + _httpClientFactory.CreateClient(AasEnvironmentConfig.AasTemplateRepository).Returns(httpClient); await Assert.ThrowsAsync(() => _sut.GetAssetInformationTemplateAsync(TemplateId, CancellationToken.None)); } @@ -290,7 +290,7 @@ public async Task GetAssetInformationTemplateAsync_ThrowsResourceNotFoundExcepti using var mockHttpMessageHandler = new FakeHttpMessageHandler(mockHttpResponse); using var httpClient = new HttpClient(mockHttpMessageHandler); httpClient.BaseAddress = new Uri("https://www.mm-software.com/fakeurl"); - _httpClientFactory.CreateClient(AasEnvironmentConfig.AasEnvironmentRepoHttpClientName).Returns(httpClient); + _httpClientFactory.CreateClient(AasEnvironmentConfig.AasTemplateRepository).Returns(httpClient); await Assert.ThrowsAsync(() => _sut.GetAssetInformationTemplateAsync(TemplateId, CancellationToken.None)); @@ -302,7 +302,7 @@ public async Task GetAssetInformationTemplateAsync_ThrowsException_WhenHttpClien using var mockHttpMessageHandler = new FakeHttpMessageHandler(new HttpRequestException("Network error")); using var httpClient = new HttpClient(mockHttpMessageHandler); httpClient.BaseAddress = new Uri("https://www.mm-software.com/fakeurl"); - _httpClientFactory.CreateClient(AasEnvironmentConfig.AasEnvironmentRepoHttpClientName).Returns(httpClient); + _httpClientFactory.CreateClient(AasEnvironmentConfig.AasTemplateRepository).Returns(httpClient); var exception = await Assert.ThrowsAsync(() => _sut.GetAssetInformationTemplateAsync(TemplateId, CancellationToken.None)); @@ -319,7 +319,7 @@ public async Task GetSubmodelRefByIdAsync_ReturnsSubmodelRefs_WhenValidResponse( using var mockHttpMessageHandler = new FakeHttpMessageHandler(mockHttpResponse); using var httpClient = new HttpClient(mockHttpMessageHandler); httpClient.BaseAddress = new Uri("https://www.mm-software.com/fakeurl"); - _httpClientFactory.CreateClient(AasEnvironmentConfig.AasEnvironmentRepoHttpClientName).Returns(httpClient); + _httpClientFactory.CreateClient(AasEnvironmentConfig.AasTemplateRepository).Returns(httpClient); var result = await _sut.GetSubmodelRefByIdAsync(TemplateId, CancellationToken.None); @@ -338,7 +338,7 @@ public async Task GetSubmodelRefByIdAsync_ThrowsResourceNotFoundException_WhenRe using var mockHttpMessageHandler = new FakeHttpMessageHandler(mockHttpResponse); using var httpClient = new HttpClient(mockHttpMessageHandler); httpClient.BaseAddress = new Uri("https://www.mm-software.com/fakeurl"); - _httpClientFactory.CreateClient(AasEnvironmentConfig.AasEnvironmentRepoHttpClientName).Returns(httpClient); + _httpClientFactory.CreateClient(AasEnvironmentConfig.AasTemplateRepository).Returns(httpClient); await Assert.ThrowsAsync(() => _sut.GetSubmodelRefByIdAsync(TemplateId, CancellationToken.None)); } @@ -353,7 +353,7 @@ public async Task GetSubmodelRefByIdAsync_ThrowsResourceNotFoundException_WhenRe using var mockHttpMessageHandler = new FakeHttpMessageHandler(mockHttpResponse); using var httpClient = new HttpClient(mockHttpMessageHandler); httpClient.BaseAddress = new Uri("https://www.mm-software.com/fakeurl"); - _httpClientFactory.CreateClient(AasEnvironmentConfig.AasEnvironmentRepoHttpClientName).Returns(httpClient); + _httpClientFactory.CreateClient(AasEnvironmentConfig.AasTemplateRepository).Returns(httpClient); await Assert.ThrowsAsync(() => _sut.GetSubmodelRefByIdAsync(TemplateId, CancellationToken.None)); } @@ -368,7 +368,7 @@ public async Task GetSubmodelRefByIdAsync_ThrowsResponseParsingException_WhenInv using var mockHttpMessageHandler = new FakeHttpMessageHandler(mockHttpResponse); using var httpClient = new HttpClient(mockHttpMessageHandler); httpClient.BaseAddress = new Uri("https://www.mm-software.com/fakeurl"); - _httpClientFactory.CreateClient(AasEnvironmentConfig.AasEnvironmentRepoHttpClientName).Returns(httpClient); + _httpClientFactory.CreateClient(AasEnvironmentConfig.AasTemplateRepository).Returns(httpClient); await Assert.ThrowsAsync(() => _sut.GetSubmodelRefByIdAsync(TemplateId, CancellationToken.None)); } @@ -379,7 +379,7 @@ public async Task GetSubmodelRefByIdAsync_ThrowsHttpRequestException_WhenHttpFai using var mockHttpMessageHandler = new FakeHttpMessageHandler(new HttpRequestException("Network error")); using var httpClient = new HttpClient(mockHttpMessageHandler); httpClient.BaseAddress = new Uri("https://www.mm-software.com/fakeurl"); - _httpClientFactory.CreateClient(AasEnvironmentConfig.AasEnvironmentRepoHttpClientName).Returns(httpClient); + _httpClientFactory.CreateClient(AasEnvironmentConfig.AasTemplateRepository).Returns(httpClient); var exception = await Assert.ThrowsAsync(() => _sut.GetSubmodelRefByIdAsync(TemplateId, CancellationToken.None)); diff --git a/source/AAS.TwinEngine.DataEngine.UnitTests/ServiceConfiguration/ConfigurationMigration/LegacyTemplateManagementConfigAdapterTests.cs b/source/AAS.TwinEngine.DataEngine.UnitTests/ServiceConfiguration/ConfigurationMigration/LegacyTemplateManagementConfigAdapterTests.cs index 666a6c01..83dbf9bc 100644 --- a/source/AAS.TwinEngine.DataEngine.UnitTests/ServiceConfiguration/ConfigurationMigration/LegacyTemplateManagementConfigAdapterTests.cs +++ b/source/AAS.TwinEngine.DataEngine.UnitTests/ServiceConfiguration/ConfigurationMigration/LegacyTemplateManagementConfigAdapterTests.cs @@ -1,4 +1,4 @@ -using AAS.TwinEngine.DataEngine.ApplicationLogic.Services.Plugin.Config; +using AAS.TwinEngine.DataEngine.ApplicationLogic.Services.Plugin.Config; using AAS.TwinEngine.DataEngine.Infrastructure.Configuration.LegacyV1; using AAS.TwinEngine.DataEngine.ServiceConfiguration.Config; @@ -86,7 +86,7 @@ public void Configure_WithV1Config_MapsAasTemplateRegistry() adapter.Configure(options); - Assert.Equal(AasEnvironmentConfig.AasRegistryHttpClientName, options.AasTemplateRegistry.Name); + Assert.Equal(AasEnvironmentConfig.AasRegistry, options.AasTemplateRegistry.Name); Assert.Equal(new Uri("http://localhost:8082"), options.AasTemplateRegistry.BaseUrl); } @@ -99,7 +99,7 @@ public void Configure_WithV1Config_MapsSubmodelTemplateRegistry() adapter.Configure(options); - Assert.Equal(AasEnvironmentConfig.SubmodelRegistryHttpClientName, options.SubmodelTemplateRegistry.Name); + Assert.Equal(AasEnvironmentConfig.SubmodelRegistry, options.SubmodelTemplateRegistry.Name); Assert.Equal(new Uri("http://localhost:8083"), options.SubmodelTemplateRegistry.BaseUrl); } diff --git a/source/AAS.TwinEngine.DataEngine/ApplicationLogic/Services/Plugin/Config/AasEnvironmentConfig.cs b/source/AAS.TwinEngine.DataEngine/ApplicationLogic/Services/Plugin/Config/AasEnvironmentConfig.cs index 77a1af29..a5d5abeb 100644 --- a/source/AAS.TwinEngine.DataEngine/ApplicationLogic/Services/Plugin/Config/AasEnvironmentConfig.cs +++ b/source/AAS.TwinEngine.DataEngine/ApplicationLogic/Services/Plugin/Config/AasEnvironmentConfig.cs @@ -11,20 +11,16 @@ public class AasEnvironmentConfig public const string AasTemplateRepository = "aas-template-repository"; public const string ConceptDescriptorTemplateRepository = "concept-descriptor-template-repository"; - public const string TemplateRepositoryHealthCheck = TemplateRepository + "-healthcheck"; - public const string AasRegistryHealthCheck = AasRegistry + "-healthcheck"; - public const string SubmodelRegistryHealthCheck = SubmodelRegistry + "-healthcheck"; - public const string SubmodelTemplateRepositoryHealthCheck = SubmodelTemplateRepository + "-healthcheck"; - public const string AasTemplateRepositoryHealthCheck = AasTemplateRepository + "-healthcheck"; - public const string ConceptDescriptorTemplateRepositoryHealthCheck = ConceptDescriptorTemplateRepository + "-healthcheck"; - - // Backward-compatible aliases used by existing code (TemplateProvider, health checks, etc.) - public const string AasEnvironmentRepoHttpClientName = AasTemplateRepository; - public const string AasRegistryHttpClientName = AasRegistry; - public const string SubmodelRegistryHttpClientName = SubmodelRegistry; - public const string AasEnvironmentRepoHealthCheckHttpClientName = AasTemplateRepositoryHealthCheck; - public const string AasRegistryHealthCheckHttpClientName = AasRegistryHealthCheck; - public const string SubmodelRegistryHealthCheckHttpClientName = SubmodelRegistryHealthCheck; + private const string HealthCheckSuffix = "-healthcheck"; + + public static string GetHealthCheckName(string clientName) => $"{clientName}{HealthCheckSuffix}"; + + public static string TemplateRepositoryHealthCheck => GetHealthCheckName(TemplateRepository); + public static string AasRegistryHealthCheck => GetHealthCheckName(AasRegistry); + public static string SubmodelRegistryHealthCheck => GetHealthCheckName(SubmodelRegistry); + public static string SubmodelTemplateRepositoryHealthCheck => GetHealthCheckName(SubmodelTemplateRepository); + public static string AasTemplateRepositoryHealthCheck => GetHealthCheckName(AasTemplateRepository); + public static string ConceptDescriptorTemplateRepositoryHealthCheck => GetHealthCheckName(ConceptDescriptorTemplateRepository); // Path constants (no longer configurable — fixed API contracts) public const string SubModelRepositoryPath = "submodels"; diff --git a/source/AAS.TwinEngine.DataEngine/Infrastructure/Monitoring/TemplateRegistryHealthCheck.cs b/source/AAS.TwinEngine.DataEngine/Infrastructure/Monitoring/TemplateRegistryHealthCheck.cs index 81cdb5e7..470cfe30 100644 --- a/source/AAS.TwinEngine.DataEngine/Infrastructure/Monitoring/TemplateRegistryHealthCheck.cs +++ b/source/AAS.TwinEngine.DataEngine/Infrastructure/Monitoring/TemplateRegistryHealthCheck.cs @@ -13,8 +13,8 @@ public sealed class TemplateRegistryHealthCheck(ICreateClient clientFactory, public async Task CheckHealthAsync(HealthCheckContext context, CancellationToken cancellationToken = default) { - var aasTask = CheckEndpointAsync(AasEnvironmentConfig.AasRegistryHealthCheckHttpClientName, AasRegistryPath, "aas-registry", cancellationToken); - var submodelTask = CheckEndpointAsync(AasEnvironmentConfig.SubmodelRegistryHealthCheckHttpClientName, SubModelRegistryPath, "submodel-registry", cancellationToken); + var aasTask = CheckEndpointAsync(AasEnvironmentConfig.AasRegistryHealthCheck, AasRegistryPath, "aas-registry", cancellationToken); + var submodelTask = CheckEndpointAsync(AasEnvironmentConfig.SubmodelRegistryHealthCheck, SubModelRegistryPath, "submodel-registry", cancellationToken); var results = await Task.WhenAll(aasTask, submodelTask).ConfigureAwait(false); diff --git a/source/AAS.TwinEngine.DataEngine/Infrastructure/Providers/AasRegistryProvider/Services/AasRegistryProvider.cs b/source/AAS.TwinEngine.DataEngine/Infrastructure/Providers/AasRegistryProvider/Services/AasRegistryProvider.cs index e3b02cc3..dadeaa4a 100644 --- a/source/AAS.TwinEngine.DataEngine/Infrastructure/Providers/AasRegistryProvider/Services/AasRegistryProvider.cs +++ b/source/AAS.TwinEngine.DataEngine/Infrastructure/Providers/AasRegistryProvider/Services/AasRegistryProvider.cs @@ -16,7 +16,7 @@ namespace AAS.TwinEngine.DataEngine.Infrastructure.Providers.AasRegistryProvider public class AasRegistryProvider(ILogger logger, ICreateClient clientFactory) : IAasRegistryProvider { private const string AasRegistryPath = AasEnvironmentConfig.AasRegistryPath; - private const string HttpClientName = AasEnvironmentConfig.AasRegistryHttpClientName; + private const string HttpClientName = AasEnvironmentConfig.AasRegistry; public async Task> GetAllAsync(CancellationToken cancellationToken) { diff --git a/source/AAS.TwinEngine.DataEngine/Infrastructure/Providers/SubmodelRegistryProvider/Services/SubmodelDescriptorProvider.cs b/source/AAS.TwinEngine.DataEngine/Infrastructure/Providers/SubmodelRegistryProvider/Services/SubmodelDescriptorProvider.cs index dd75b292..01f7227b 100644 --- a/source/AAS.TwinEngine.DataEngine/Infrastructure/Providers/SubmodelRegistryProvider/Services/SubmodelDescriptorProvider.cs +++ b/source/AAS.TwinEngine.DataEngine/Infrastructure/Providers/SubmodelRegistryProvider/Services/SubmodelDescriptorProvider.cs @@ -24,7 +24,7 @@ public async Task GetDataForSubmodelDescriptorByIdAsync(stri var relativeUri = new Uri(url, UriKind.Relative); - var httpClient = clientFactory.CreateClient(AasEnvironmentConfig.SubmodelRegistryHttpClientName); + var httpClient = clientFactory.CreateClient(AasEnvironmentConfig.SubmodelRegistry); var response = await httpClient.GetAsync(relativeUri, cancellationToken).ConfigureAwait(false); From f820b659066fb38f4d2e2157655d8b8f57549b80 Mon Sep 17 00:00:00 2001 From: Kevalkumar Date: Fri, 10 Apr 2026 01:22:33 +0530 Subject: [PATCH 27/71] remove extra usings --- .../MappingProfiles/ShellDescriptorMapperProfileTests.cs | 4 +--- .../SemanticId/Helpers/ReferenceHelperTests.cs | 1 - .../SemanticId/Helpers/SemanticIdResolverTests.cs | 1 - .../SemanticId/Helpers/SubmodelElementHelperTests.cs | 3 --- .../Services/SubmodelRepository/SemanticIdHandlerTests.cs | 4 ++-- .../SemanticId/Helpers/SemanticIdResolver.cs | 1 - .../SemanticId/Helpers/SubmodelElementHelper.cs | 1 - 7 files changed, 3 insertions(+), 12 deletions(-) diff --git a/source/AAS.TwinEngine.DataEngine.UnitTests/Api/AasRegistry/MappingProfiles/ShellDescriptorMapperProfileTests.cs b/source/AAS.TwinEngine.DataEngine.UnitTests/Api/AasRegistry/MappingProfiles/ShellDescriptorMapperProfileTests.cs index edce82f0..c70811d9 100644 --- a/source/AAS.TwinEngine.DataEngine.UnitTests/Api/AasRegistry/MappingProfiles/ShellDescriptorMapperProfileTests.cs +++ b/source/AAS.TwinEngine.DataEngine.UnitTests/Api/AasRegistry/MappingProfiles/ShellDescriptorMapperProfileTests.cs @@ -1,6 +1,4 @@ -using System.Text.Json; - -using AAS.TwinEngine.DataEngine.Api.AasRegistry.MappingProfiles; +using AAS.TwinEngine.DataEngine.Api.AasRegistry.MappingProfiles; using AAS.TwinEngine.DataEngine.DomainModel.AasRegistry; using AAS.TwinEngine.DataEngine.UnitTests.Api.Shared.MappingProfiles; diff --git a/source/AAS.TwinEngine.DataEngine.UnitTests/ApplicationLogic/Services/SubmodelRepository/SemanticId/Helpers/ReferenceHelperTests.cs b/source/AAS.TwinEngine.DataEngine.UnitTests/ApplicationLogic/Services/SubmodelRepository/SemanticId/Helpers/ReferenceHelperTests.cs index 8976a0fb..3ceb3906 100644 --- a/source/AAS.TwinEngine.DataEngine.UnitTests/ApplicationLogic/Services/SubmodelRepository/SemanticId/Helpers/ReferenceHelperTests.cs +++ b/source/AAS.TwinEngine.DataEngine.UnitTests/ApplicationLogic/Services/SubmodelRepository/SemanticId/Helpers/ReferenceHelperTests.cs @@ -1,4 +1,3 @@ -using AAS.TwinEngine.DataEngine.ApplicationLogic.Services.SubmodelRepository.Config; using AAS.TwinEngine.DataEngine.ApplicationLogic.Services.SubmodelRepository.SemanticId.Helpers; using AAS.TwinEngine.DataEngine.ApplicationLogic.Services.SubmodelRepository.SemanticId.Helpers.Interfaces; using AAS.TwinEngine.DataEngine.DomainModel.SubmodelRepository; diff --git a/source/AAS.TwinEngine.DataEngine.UnitTests/ApplicationLogic/Services/SubmodelRepository/SemanticId/Helpers/SemanticIdResolverTests.cs b/source/AAS.TwinEngine.DataEngine.UnitTests/ApplicationLogic/Services/SubmodelRepository/SemanticId/Helpers/SemanticIdResolverTests.cs index 1bdc9d8d..bf381f0f 100644 --- a/source/AAS.TwinEngine.DataEngine.UnitTests/ApplicationLogic/Services/SubmodelRepository/SemanticId/Helpers/SemanticIdResolverTests.cs +++ b/source/AAS.TwinEngine.DataEngine.UnitTests/ApplicationLogic/Services/SubmodelRepository/SemanticId/Helpers/SemanticIdResolverTests.cs @@ -1,4 +1,3 @@ -using AAS.TwinEngine.DataEngine.ApplicationLogic.Services.SubmodelRepository.Config; using AAS.TwinEngine.DataEngine.ApplicationLogic.Services.SubmodelRepository.SemanticId.Helpers; using AAS.TwinEngine.DataEngine.DomainModel.SubmodelRepository; using AAS.TwinEngine.DataEngine.ServiceConfiguration.Config; diff --git a/source/AAS.TwinEngine.DataEngine.UnitTests/ApplicationLogic/Services/SubmodelRepository/SemanticId/Helpers/SubmodelElementHelperTests.cs b/source/AAS.TwinEngine.DataEngine.UnitTests/ApplicationLogic/Services/SubmodelRepository/SemanticId/Helpers/SubmodelElementHelperTests.cs index 602ee015..78681767 100644 --- a/source/AAS.TwinEngine.DataEngine.UnitTests/ApplicationLogic/Services/SubmodelRepository/SemanticId/Helpers/SubmodelElementHelperTests.cs +++ b/source/AAS.TwinEngine.DataEngine.UnitTests/ApplicationLogic/Services/SubmodelRepository/SemanticId/Helpers/SubmodelElementHelperTests.cs @@ -1,5 +1,4 @@ using AAS.TwinEngine.DataEngine.ApplicationLogic.Exceptions.Application; -using AAS.TwinEngine.DataEngine.ApplicationLogic.Services.SubmodelRepository.Config; using AAS.TwinEngine.DataEngine.ApplicationLogic.Services.SubmodelRepository.SemanticId.Helpers; using AAS.TwinEngine.DataEngine.ServiceConfiguration.Config; @@ -12,8 +11,6 @@ using static Xunit.Assert; -using File = AasCore.Aas3_0.File; - namespace AAS.TwinEngine.DataEngine.UnitTests.ApplicationLogic.Services.SubmodelRepository.SemanticId.Helpers; public class SubmodelElementHelperTests diff --git a/source/AAS.TwinEngine.DataEngine.UnitTests/ApplicationLogic/Services/SubmodelRepository/SemanticIdHandlerTests.cs b/source/AAS.TwinEngine.DataEngine.UnitTests/ApplicationLogic/Services/SubmodelRepository/SemanticIdHandlerTests.cs index 6ca66298..561f3cc5 100644 --- a/source/AAS.TwinEngine.DataEngine.UnitTests/ApplicationLogic/Services/SubmodelRepository/SemanticIdHandlerTests.cs +++ b/source/AAS.TwinEngine.DataEngine.UnitTests/ApplicationLogic/Services/SubmodelRepository/SemanticIdHandlerTests.cs @@ -7,13 +7,13 @@ using AAS.TwinEngine.DataEngine.DomainModel.SubmodelRepository; using AAS.TwinEngine.DataEngine.ServiceConfiguration.Config; -using MongoDB.Bson; - using AasCore.Aas3_0; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; +using MongoDB.Bson; + using NSubstitute; using static Xunit.Assert; diff --git a/source/AAS.TwinEngine.DataEngine/ApplicationLogic/Services/SubmodelRepository/SemanticId/Helpers/SemanticIdResolver.cs b/source/AAS.TwinEngine.DataEngine/ApplicationLogic/Services/SubmodelRepository/SemanticId/Helpers/SemanticIdResolver.cs index 6c7ec302..3b911bea 100644 --- a/source/AAS.TwinEngine.DataEngine/ApplicationLogic/Services/SubmodelRepository/SemanticId/Helpers/SemanticIdResolver.cs +++ b/source/AAS.TwinEngine.DataEngine/ApplicationLogic/Services/SubmodelRepository/SemanticId/Helpers/SemanticIdResolver.cs @@ -1,6 +1,5 @@ using System.Text.RegularExpressions; -using AAS.TwinEngine.DataEngine.ApplicationLogic.Services.SubmodelRepository.Config; using AAS.TwinEngine.DataEngine.ApplicationLogic.Services.SubmodelRepository.SemanticId.Helpers.Interfaces; using AAS.TwinEngine.DataEngine.DomainModel.SubmodelRepository; using AAS.TwinEngine.DataEngine.ServiceConfiguration.Config; diff --git a/source/AAS.TwinEngine.DataEngine/ApplicationLogic/Services/SubmodelRepository/SemanticId/Helpers/SubmodelElementHelper.cs b/source/AAS.TwinEngine.DataEngine/ApplicationLogic/Services/SubmodelRepository/SemanticId/Helpers/SubmodelElementHelper.cs index 57329060..892a275b 100644 --- a/source/AAS.TwinEngine.DataEngine/ApplicationLogic/Services/SubmodelRepository/SemanticId/Helpers/SubmodelElementHelper.cs +++ b/source/AAS.TwinEngine.DataEngine/ApplicationLogic/Services/SubmodelRepository/SemanticId/Helpers/SubmodelElementHelper.cs @@ -2,7 +2,6 @@ using System.Text.RegularExpressions; using AAS.TwinEngine.DataEngine.ApplicationLogic.Exceptions.Application; -using AAS.TwinEngine.DataEngine.ApplicationLogic.Services.SubmodelRepository.Config; using AAS.TwinEngine.DataEngine.ApplicationLogic.Services.SubmodelRepository.SemanticId.Helpers.Interfaces; using AAS.TwinEngine.DataEngine.ServiceConfiguration.Config; From 711441e10a45746735a42bb2d64a08eb3d0d5246 Mon Sep 17 00:00:00 2001 From: Kevalkumar Date: Fri, 10 Apr 2026 01:26:24 +0530 Subject: [PATCH 28/71] remove sonarqube warning --- .../Authorization/Headers/RequestHeaderMapperTests.cs | 9 --------- .../LegacyV1/LegacyConfigurationDetector.cs | 1 + 2 files changed, 1 insertion(+), 9 deletions(-) diff --git a/source/AAS.TwinEngine.DataEngine.UnitTests/Infrastructure/Http/Authorization/Headers/RequestHeaderMapperTests.cs b/source/AAS.TwinEngine.DataEngine.UnitTests/Infrastructure/Http/Authorization/Headers/RequestHeaderMapperTests.cs index 88098396..cc5c3c7a 100644 --- a/source/AAS.TwinEngine.DataEngine.UnitTests/Infrastructure/Http/Authorization/Headers/RequestHeaderMapperTests.cs +++ b/source/AAS.TwinEngine.DataEngine.UnitTests/Infrastructure/Http/Authorization/Headers/RequestHeaderMapperTests.cs @@ -104,15 +104,6 @@ public void ValidateIncomingHeaders_RequiredHeaderMissing_Throws() Assert.Throws(() => service.ValidateIncomingHeaders(context)); } - [Fact] - public void ApplyMappings_NullHttpContext_DoesNothing() - { - var service = CreateService(DefaultConfig()); - using var request = new HttpRequestMessage(); - - service.ApplyMappings(null, request, "client"); - } - [Fact] public void ApplyMappings_NullRequest_Throws() { diff --git a/source/AAS.TwinEngine.DataEngine/Infrastructure/Configuration/LegacyV1/LegacyConfigurationDetector.cs b/source/AAS.TwinEngine.DataEngine/Infrastructure/Configuration/LegacyV1/LegacyConfigurationDetector.cs index d8b5ca30..36b0e204 100644 --- a/source/AAS.TwinEngine.DataEngine/Infrastructure/Configuration/LegacyV1/LegacyConfigurationDetector.cs +++ b/source/AAS.TwinEngine.DataEngine/Infrastructure/Configuration/LegacyV1/LegacyConfigurationDetector.cs @@ -6,6 +6,7 @@ namespace AAS.TwinEngine.DataEngine.Infrastructure.Configuration.LegacyV1; /// Detects whether the running configuration uses the V1 (flat sections) or V2 (grouped) schema. /// V2 is identified by the existence of "General", "Plugins:Instances", or "TemplateManagement" top-level sections. /// +#pragma warning disable S1133 [Obsolete("V1 configuration is deprecated and will be removed in v2.0.0 version.")] public static class LegacyConfigurationDetector { From 2eb08512798b64596103f640bcdfeec0f7984683 Mon Sep 17 00:00:00 2001 From: Kevalkumar Date: Fri, 10 Apr 2026 01:30:08 +0530 Subject: [PATCH 29/71] Remove sonarqube warnings for obsolute tag --- .../LegacyV1/ConfigV1/AasRegistryPreComputed.cs | 2 ++ .../LegacyV1/ConfigV1/HttpRetryPolicyOptions.cs | 2 ++ .../LegacyV1/ConfigV1/MultiLanguagePropertySettings.cs | 2 ++ .../ConfigV1/MultiLanguagePropertySettingsValidator.cs | 2 ++ .../Configuration/LegacyV1/ConfigV1/Semantics.cs | 2 ++ .../Configuration/LegacyV1/LegacyGeneralConfigAdapter.cs | 3 ++- .../Configuration/LegacyV1/LegacyPluginsConfigAdapter.cs | 3 ++- .../LegacyV1/LegacyRegistrySettingsConfigAdapter.cs | 3 ++- .../LegacyV1/LegacyTemplateManagementConfigAdapter.cs | 3 ++- .../LegacyV1/LegacyV1ConfigurationExtensions.cs | 8 +++++--- 10 files changed, 23 insertions(+), 7 deletions(-) diff --git a/source/AAS.TwinEngine.DataEngine/Infrastructure/Configuration/LegacyV1/ConfigV1/AasRegistryPreComputed.cs b/source/AAS.TwinEngine.DataEngine/Infrastructure/Configuration/LegacyV1/ConfigV1/AasRegistryPreComputed.cs index a9e58784..06a0f3d1 100644 --- a/source/AAS.TwinEngine.DataEngine/Infrastructure/Configuration/LegacyV1/ConfigV1/AasRegistryPreComputed.cs +++ b/source/AAS.TwinEngine.DataEngine/Infrastructure/Configuration/LegacyV1/ConfigV1/AasRegistryPreComputed.cs @@ -1,5 +1,7 @@ namespace AAS.TwinEngine.DataEngine.Infrastructure.Configuration.LegacyV1; +#pragma warning disable S1133 +[Obsolete("V1 configuration is deprecated and will be removed in v2.0.0 version.")] public class AasRegistryPreComputed { public const string Section = "AasRegistryPreComputed"; diff --git a/source/AAS.TwinEngine.DataEngine/Infrastructure/Configuration/LegacyV1/ConfigV1/HttpRetryPolicyOptions.cs b/source/AAS.TwinEngine.DataEngine/Infrastructure/Configuration/LegacyV1/ConfigV1/HttpRetryPolicyOptions.cs index c00fcb28..ff08f343 100644 --- a/source/AAS.TwinEngine.DataEngine/Infrastructure/Configuration/LegacyV1/ConfigV1/HttpRetryPolicyOptions.cs +++ b/source/AAS.TwinEngine.DataEngine/Infrastructure/Configuration/LegacyV1/ConfigV1/HttpRetryPolicyOptions.cs @@ -2,6 +2,8 @@ namespace AAS.TwinEngine.DataEngine.Infrastructure.Configuration.LegacyV1; +#pragma warning disable S1133 +[Obsolete("V1 configuration is deprecated and will be removed in v2.0.0 version.")] public class HttpRetryPolicyOptions { public const string Section = "HttpRetryPolicyOptions"; diff --git a/source/AAS.TwinEngine.DataEngine/Infrastructure/Configuration/LegacyV1/ConfigV1/MultiLanguagePropertySettings.cs b/source/AAS.TwinEngine.DataEngine/Infrastructure/Configuration/LegacyV1/ConfigV1/MultiLanguagePropertySettings.cs index e7e7f132..d9cd7a5b 100644 --- a/source/AAS.TwinEngine.DataEngine/Infrastructure/Configuration/LegacyV1/ConfigV1/MultiLanguagePropertySettings.cs +++ b/source/AAS.TwinEngine.DataEngine/Infrastructure/Configuration/LegacyV1/ConfigV1/MultiLanguagePropertySettings.cs @@ -1,5 +1,7 @@ namespace AAS.TwinEngine.DataEngine.Infrastructure.Configuration.LegacyV1; +#pragma warning disable S1133 +[Obsolete("V1 configuration is deprecated and will be removed in v2.0.0 version.")] public class MultiLanguagePropertySettings { public const string Section = "MultiLanguageProperty"; diff --git a/source/AAS.TwinEngine.DataEngine/Infrastructure/Configuration/LegacyV1/ConfigV1/MultiLanguagePropertySettingsValidator.cs b/source/AAS.TwinEngine.DataEngine/Infrastructure/Configuration/LegacyV1/ConfigV1/MultiLanguagePropertySettingsValidator.cs index 4781fe60..e72822eb 100644 --- a/source/AAS.TwinEngine.DataEngine/Infrastructure/Configuration/LegacyV1/ConfigV1/MultiLanguagePropertySettingsValidator.cs +++ b/source/AAS.TwinEngine.DataEngine/Infrastructure/Configuration/LegacyV1/ConfigV1/MultiLanguagePropertySettingsValidator.cs @@ -4,6 +4,8 @@ namespace AAS.TwinEngine.DataEngine.Infrastructure.Configuration.LegacyV1; +#pragma warning disable S1133 +[Obsolete("V1 configuration is deprecated and will be removed in v2.0.0 version.")] public partial class MultiLanguagePropertySettingsValidator : IValidateOptions { public ValidateOptionsResult Validate(string? name, MultiLanguagePropertySettings options) diff --git a/source/AAS.TwinEngine.DataEngine/Infrastructure/Configuration/LegacyV1/ConfigV1/Semantics.cs b/source/AAS.TwinEngine.DataEngine/Infrastructure/Configuration/LegacyV1/ConfigV1/Semantics.cs index 0db017d6..56e4450a 100644 --- a/source/AAS.TwinEngine.DataEngine/Infrastructure/Configuration/LegacyV1/ConfigV1/Semantics.cs +++ b/source/AAS.TwinEngine.DataEngine/Infrastructure/Configuration/LegacyV1/ConfigV1/Semantics.cs @@ -1,5 +1,7 @@ namespace AAS.TwinEngine.DataEngine.Infrastructure.Configuration.LegacyV1; +#pragma warning disable S1133 +[Obsolete("V1 configuration is deprecated and will be removed in v2.0.0 version.")] public class Semantics { public const string Section = "Semantics"; diff --git a/source/AAS.TwinEngine.DataEngine/Infrastructure/Configuration/LegacyV1/LegacyGeneralConfigAdapter.cs b/source/AAS.TwinEngine.DataEngine/Infrastructure/Configuration/LegacyV1/LegacyGeneralConfigAdapter.cs index cc2c53df..f72bf766 100644 --- a/source/AAS.TwinEngine.DataEngine/Infrastructure/Configuration/LegacyV1/LegacyGeneralConfigAdapter.cs +++ b/source/AAS.TwinEngine.DataEngine/Infrastructure/Configuration/LegacyV1/LegacyGeneralConfigAdapter.cs @@ -11,7 +11,8 @@ namespace AAS.TwinEngine.DataEngine.Infrastructure.Configuration.LegacyV1; /// Registered as so the Options system /// merges these values before any consumer resolves IOptions<GeneralConfig>. /// -[Obsolete("Remove in v2.0.0 — V1 configuration support will be dropped.")] +#pragma warning disable S1133 +[Obsolete("V1 configuration is deprecated and will be removed in v2.0.0 version.")] public sealed class LegacyGeneralConfigAdapter(IConfiguration configuration) : IConfigureOptions { private readonly IConfiguration _configuration = configuration; diff --git a/source/AAS.TwinEngine.DataEngine/Infrastructure/Configuration/LegacyV1/LegacyPluginsConfigAdapter.cs b/source/AAS.TwinEngine.DataEngine/Infrastructure/Configuration/LegacyV1/LegacyPluginsConfigAdapter.cs index ed40e419..2c3f6084 100644 --- a/source/AAS.TwinEngine.DataEngine/Infrastructure/Configuration/LegacyV1/LegacyPluginsConfigAdapter.cs +++ b/source/AAS.TwinEngine.DataEngine/Infrastructure/Configuration/LegacyV1/LegacyPluginsConfigAdapter.cs @@ -9,7 +9,8 @@ namespace AAS.TwinEngine.DataEngine.Infrastructure.Configuration.LegacyV1; /// /// Reads V1 flat config sections and maps them into the V2 shape. /// -[Obsolete("Remove in v2.0.0 — V1 configuration support will be dropped.")] +#pragma warning disable S1133 +[Obsolete("V1 configuration is deprecated and will be removed in v2.0.0 version.")] public sealed class LegacyPluginsConfigAdapter(IConfiguration configuration) : IConfigureOptions { private readonly IConfiguration _configuration = configuration; diff --git a/source/AAS.TwinEngine.DataEngine/Infrastructure/Configuration/LegacyV1/LegacyRegistrySettingsConfigAdapter.cs b/source/AAS.TwinEngine.DataEngine/Infrastructure/Configuration/LegacyV1/LegacyRegistrySettingsConfigAdapter.cs index 4e7ca762..cef963cf 100644 --- a/source/AAS.TwinEngine.DataEngine/Infrastructure/Configuration/LegacyV1/LegacyRegistrySettingsConfigAdapter.cs +++ b/source/AAS.TwinEngine.DataEngine/Infrastructure/Configuration/LegacyV1/LegacyRegistrySettingsConfigAdapter.cs @@ -7,7 +7,8 @@ namespace AAS.TwinEngine.DataEngine.Infrastructure.Configuration.LegacyV1; /// /// Reads V1 flat config sections and maps them into the V2 shape. /// -[Obsolete("Remove in v2.0.0 — V1 configuration support will be dropped.")] +#pragma warning disable S1133 +[Obsolete("V1 configuration is deprecated and will be removed in v2.0.0 version.")] public sealed class LegacyRegistrySettingsConfigAdapter(IConfiguration configuration) : IConfigureOptions { private readonly IConfiguration _configuration = configuration; diff --git a/source/AAS.TwinEngine.DataEngine/Infrastructure/Configuration/LegacyV1/LegacyTemplateManagementConfigAdapter.cs b/source/AAS.TwinEngine.DataEngine/Infrastructure/Configuration/LegacyV1/LegacyTemplateManagementConfigAdapter.cs index d0e32369..132a0ce2 100644 --- a/source/AAS.TwinEngine.DataEngine/Infrastructure/Configuration/LegacyV1/LegacyTemplateManagementConfigAdapter.cs +++ b/source/AAS.TwinEngine.DataEngine/Infrastructure/Configuration/LegacyV1/LegacyTemplateManagementConfigAdapter.cs @@ -10,7 +10,8 @@ namespace AAS.TwinEngine.DataEngine.Infrastructure.Configuration.LegacyV1; /// /// Reads V1 flat config sections and maps them into the V2 shape. /// -[Obsolete("Remove in v2.0.0 — V1 configuration support will be dropped.")] +#pragma warning disable S1133 +[Obsolete("V1 configuration is deprecated and will be removed in v2.0.0 version.")] public sealed class LegacyTemplateManagementConfigAdapter(IConfiguration configuration) : IConfigureOptions { private readonly IConfiguration _configuration = configuration; diff --git a/source/AAS.TwinEngine.DataEngine/Infrastructure/Configuration/LegacyV1/LegacyV1ConfigurationExtensions.cs b/source/AAS.TwinEngine.DataEngine/Infrastructure/Configuration/LegacyV1/LegacyV1ConfigurationExtensions.cs index f69d01f0..5ec72070 100644 --- a/source/AAS.TwinEngine.DataEngine/Infrastructure/Configuration/LegacyV1/LegacyV1ConfigurationExtensions.cs +++ b/source/AAS.TwinEngine.DataEngine/Infrastructure/Configuration/LegacyV1/LegacyV1ConfigurationExtensions.cs @@ -1,4 +1,4 @@ -using AAS.TwinEngine.DataEngine.ServiceConfiguration.Config; +using AAS.TwinEngine.DataEngine.ServiceConfiguration.Config; using Microsoft.Extensions.Options; @@ -10,7 +10,8 @@ namespace AAS.TwinEngine.DataEngine.Infrastructure.Configuration.LegacyV1; /// then map them into the new V2 POCO shapes via . /// When V2 config is present, the adapters are registered but short-circuit (no-op). /// -[Obsolete("Remove in v2.0.0 — V1 configuration support will be dropped.")] +#pragma warning disable S1133 +[Obsolete("V1 configuration is deprecated and will be removed in v2.0.0 version.")] public static class LegacyV1ConfigurationExtensions { /// @@ -18,7 +19,8 @@ public static class LegacyV1ConfigurationExtensions /// Must be called BEFORE services.Configure<GeneralConfig>(…) etc. so that the /// V2 section-bind (if present) overwrites the adapter-provided defaults. /// - [Obsolete("Remove in v2.0.0 — V1 configuration support will be dropped.")] + + [Obsolete("V1 configuration is deprecated and will be removed in v2.0.0 version.")] public static IServiceCollection AddLegacyV1ConfigurationAdapters(this IServiceCollection services) { _ = services.AddSingleton, LegacyGeneralConfigAdapter>(); From 2f2fbb750a34e91e3cf5c708a1c9ae4d80090ee1 Mon Sep 17 00:00:00 2001 From: Kevalkumar Date: Sun, 12 Apr 2026 01:30:09 +0530 Subject: [PATCH 30/71] Refactoration of implementation upon code review --- example/docker-compose.yml | 2 +- .../Headers/RequestHeaderMapperTests.cs | 13 -- .../Shared/HttpRequestBaseUrlProviderTests.cs | 151 ++++++++++++++++++ .../Config/HeaderForwardingOptions.cs | 2 +- .../Headers/RequestHeaderMapper.cs | 47 +++--- .../Shared/HttpRequestBaseUrlProvider.cs | 20 +++ source/AAS.TwinEngine.DataEngine/Program.cs | 10 ++ .../appsettings.json | 57 +------ 8 files changed, 210 insertions(+), 92 deletions(-) create mode 100644 source/AAS.TwinEngine.DataEngine.UnitTests/Infrastructure/Shared/HttpRequestBaseUrlProviderTests.cs diff --git a/example/docker-compose.yml b/example/docker-compose.yml index 2308f251..7d24114f 100644 --- a/example/docker-compose.yml +++ b/example/docker-compose.yml @@ -55,7 +55,7 @@ services: - TemplateManagement__TemplateMappingRules__AasIdExtractionRules__0__Pattern=Regex - TemplateManagement__TemplateMappingRules__AasIdExtractionRules__0__Index=6 - TemplateManagement__TemplateMappingRules__AasIdExtractionRules__0__Separator=/ - - TemplateManagement__TemplateRepository__Name=AasTemplateRepository + - TemplateManagement__TemplateRepository__Name=TemplateRepository - TemplateManagement__TemplateRepository__baseUrl=http://template-repository:8081 - TemplateManagement__TemplateRepository__headerMappings__0__source=Authorization - TemplateManagement__TemplateRepository__headerMappings__0__target=Authorization diff --git a/source/AAS.TwinEngine.DataEngine.UnitTests/Infrastructure/Http/Authorization/Headers/RequestHeaderMapperTests.cs b/source/AAS.TwinEngine.DataEngine.UnitTests/Infrastructure/Http/Authorization/Headers/RequestHeaderMapperTests.cs index cc5c3c7a..646eecec 100644 --- a/source/AAS.TwinEngine.DataEngine.UnitTests/Infrastructure/Http/Authorization/Headers/RequestHeaderMapperTests.cs +++ b/source/AAS.TwinEngine.DataEngine.UnitTests/Infrastructure/Http/Authorization/Headers/RequestHeaderMapperTests.cs @@ -47,19 +47,6 @@ public void ValidateIncomingHeaders_ValidHeaders_DoesNotThrow() service.ValidateIncomingHeaders(context); } - [Fact] - public void ValidateIncomingHeaders_InvalidHeaderValue_Throws() - { - var config = DefaultConfig(); - config.HeaderSanitization.BlockedPatterns = ["(() => service.ValidateIncomingHeaders(context)); - } - [Fact] public void ValidateIncomingHeaders_EmptyHeaderValue_Throws() { diff --git a/source/AAS.TwinEngine.DataEngine.UnitTests/Infrastructure/Shared/HttpRequestBaseUrlProviderTests.cs b/source/AAS.TwinEngine.DataEngine.UnitTests/Infrastructure/Shared/HttpRequestBaseUrlProviderTests.cs new file mode 100644 index 00000000..2439177c --- /dev/null +++ b/source/AAS.TwinEngine.DataEngine.UnitTests/Infrastructure/Shared/HttpRequestBaseUrlProviderTests.cs @@ -0,0 +1,151 @@ +using AAS.TwinEngine.DataEngine.Infrastructure.Shared; +using AAS.TwinEngine.DataEngine.ServiceConfiguration.Config; + +using Microsoft.AspNetCore.Http; +using Microsoft.Extensions.Options; + +using NSubstitute; + +namespace AAS.TwinEngine.DataEngine.UnitTests.Infrastructure.Shared; + +public class HttpRequestBaseUrlProviderTests +{ + private readonly IHttpContextAccessor _httpContextAccessor = Substitute.For(); + + private static IOptions CreateOptions(Uri? baseUrl, string allowedHosts = "*") + { + return Options.Create(new GeneralConfig + { + DataEngineRepositoryBaseUrl = baseUrl, + AllowedHosts = allowedHosts + }); + } + + private static DefaultHttpContext CreateHttpContext(string scheme = "https", string host = "example.com") + { + var context = new DefaultHttpContext(); + context.Request.Scheme = scheme; + context.Request.Host = new HostString(host); + return context; + } + + // ✅ 1. Configured URL should take priority + [Fact] + public void GetBaseUrl_WithConfiguredUrl_ReturnsConfiguredValue() + { + // Arrange + var configuredUrl = new Uri("https://configured.com/"); + var options = CreateOptions(configuredUrl); + + var sut = new HttpRequestBaseUrlProvider(_httpContextAccessor, options); + + // Act + var result = sut.GetBaseUrl(); + + // Assert + Assert.Equal(configuredUrl, result); + } + + // ✅ 2. Dynamic URL from HTTP request + [Fact] + public void GetBaseUrl_WithDynamicUrl_ExtractsFromHttpRequest() + { + // Arrange + var context = CreateHttpContext("https", "mydomain.com"); + _httpContextAccessor.HttpContext.Returns(context); + + var options = CreateOptions(null); + var sut = new HttpRequestBaseUrlProvider(_httpContextAccessor, options); + + // Act + var result = sut.GetBaseUrl(); + + // Assert + Assert.Equal(new Uri("https://mydomain.com"), result); + } + + // ✅ 3. Null HttpContext should throw + [Fact] + public void GetBaseUrl_WithNullHttpContext_ThrowsInvalidOperationException() + { + // Arrange + _httpContextAccessor.HttpContext.Returns((HttpContext?)null); + + var options = CreateOptions(null); + var sut = new HttpRequestBaseUrlProvider(_httpContextAccessor, options); + + // Act & Assert + var ex = Assert.Throws(() => sut.GetBaseUrl()); + Assert.Contains("No HTTP request context", ex.Message); + } + + // 🔒 4. Host header injection is now rejected when AllowedHosts is configured + [Fact] + public void GetBaseUrl_WithMaliciousHostHeader_ThrowsWhenAllowedHostsConfigured() + { + // Arrange + var context = CreateHttpContext("https", "evil.com"); + _httpContextAccessor.HttpContext.Returns(context); + + var options = CreateOptions(null, allowedHosts: "example.com;mydomain.com"); + var sut = new HttpRequestBaseUrlProvider(_httpContextAccessor, options); + + // Act & Assert + var ex = Assert.Throws(() => sut.GetBaseUrl()); + Assert.Contains("not in the configured AllowedHosts", ex.Message); + } + + // 🔒 5. Allowed host passes validation + [Fact] + public void GetBaseUrl_WithAllowedHost_ReturnsUrl() + { + // Arrange + var context = CreateHttpContext("https", "mydomain.com"); + _httpContextAccessor.HttpContext.Returns(context); + + var options = CreateOptions(null, allowedHosts: "example.com;mydomain.com"); + var sut = new HttpRequestBaseUrlProvider(_httpContextAccessor, options); + + // Act + var result = sut.GetBaseUrl(); + + // Assert + Assert.Equal(new Uri("https://mydomain.com"), result); + } + + // 🔒 6. Wildcard AllowedHosts permits any host (backward compatible default) + [Fact] + public void GetBaseUrl_WithWildcardAllowedHosts_PermitsAnyHost() + { + // Arrange + var context = CreateHttpContext("https", "anyhost.com"); + _httpContextAccessor.HttpContext.Returns(context); + + var options = CreateOptions(null, allowedHosts: "*"); + var sut = new HttpRequestBaseUrlProvider(_httpContextAccessor, options); + + // Act + var result = sut.GetBaseUrl(); + + // Assert + Assert.Equal(new Uri("https://anyhost.com"), result); + } + + // 🔒 7. Host matching is case-insensitive + [Fact] + public void GetBaseUrl_WithDifferentCaseHost_PermitsHost() + { + // Arrange + var context = CreateHttpContext("https", "MyDomain.COM"); + _httpContextAccessor.HttpContext.Returns(context); + + var options = CreateOptions(null, allowedHosts: "mydomain.com"); + var sut = new HttpRequestBaseUrlProvider(_httpContextAccessor, options); + + // Act + var result = sut.GetBaseUrl(); + + // Assert + Assert.Equal(new Uri("https://MyDomain.COM"), result); + } +} diff --git a/source/AAS.TwinEngine.DataEngine/Infrastructure/Http/Authorization/Config/HeaderForwardingOptions.cs b/source/AAS.TwinEngine.DataEngine/Infrastructure/Http/Authorization/Config/HeaderForwardingOptions.cs index fcf7733c..49437b32 100644 --- a/source/AAS.TwinEngine.DataEngine/Infrastructure/Http/Authorization/Config/HeaderForwardingOptions.cs +++ b/source/AAS.TwinEngine.DataEngine/Infrastructure/Http/Authorization/Config/HeaderForwardingOptions.cs @@ -24,7 +24,7 @@ public class HeaderSanitizationOptions [Required] public AllowedCharactersOptions AllowedCharacters { get; set; } = new(); - public IList BlockedPatterns { get; set; } = ["\\r|\\n", "\\x00", " BlockedPatterns { get; init; } = ["\\r|\\n", "\\x00", " logger, + IOptions generalConfig, + IOptions pluginsConfig, + IOptions templateManagementConfig) : IRequestHeaderMapper { - private readonly ILogger _logger; - private readonly HeaderSanitizationOptions _sanitization; - private readonly PluginsConfig _pluginsConfig; - private readonly TemplateManagementConfig _templateManagementConfig; - private readonly Regex _headerNameRegex; - private readonly Regex _headerValueRegex; - private readonly List _blockedPatterns; - - public RequestHeaderMapper( - ILogger logger, - IOptions generalConfig, - IOptions pluginsConfig, - IOptions templateManagementConfig) + private readonly ILogger _logger = logger; + private readonly HeaderSanitizationOptions _sanitization = generalConfig.Value.HeaderSanitization; + private readonly PluginsConfig _pluginsConfig = pluginsConfig.Value; + private readonly TemplateManagementConfig _templateManagementConfig = templateManagementConfig.Value; + + private readonly Regex _headerNameRegex = + new(generalConfig.Value.HeaderSanitization.AllowedCharacters.HeaderNames, RegexOptions.Compiled, TimeSpan.FromMilliseconds(1000)); + + private readonly Regex _headerValueRegex = + new(generalConfig.Value.HeaderSanitization.AllowedCharacters.HeaderValues, RegexOptions.Compiled, TimeSpan.FromMilliseconds(1000)); + + private readonly List _blockedPatterns = + CreateBlockedPatterns(generalConfig.Value.HeaderSanitization); + + private static List CreateBlockedPatterns(HeaderSanitizationOptions sanitization) { - _logger = logger; - _sanitization = generalConfig.Value.HeaderSanitization; - _pluginsConfig = pluginsConfig.Value; - _templateManagementConfig = templateManagementConfig.Value; - - _headerNameRegex = new Regex(_sanitization.AllowedCharacters.HeaderNames, RegexOptions.Compiled, TimeSpan.FromMilliseconds(1000)); - _headerValueRegex = new Regex(_sanitization.AllowedCharacters.HeaderValues, RegexOptions.Compiled, TimeSpan.FromMilliseconds(1000)); - _blockedPatterns = _sanitization.BlockedPatterns + return sanitization.BlockedPatterns .Where(p => !string.IsNullOrWhiteSpace(p)) .Select(p => new Regex(p, RegexOptions.Compiled, TimeSpan.FromMilliseconds(1000))) .ToList(); @@ -57,7 +56,7 @@ private void ValidateHeaderFormats(HttpContext httpContext) { foreach (var (headerName, values) in httpContext.Request.Headers) { - if (values.Count == 0 || StringValues.IsNullOrEmpty(values)) + if (StringValues.IsNullOrEmpty(values)) { _logger.LogWarning("Incoming header '{HeaderName}' failed sanitization.", headerName); throw new InvalidRequestHeaderException($"Invalid request header: {headerName}"); @@ -163,7 +162,7 @@ public void ApplyMappings(HttpContext? httpContext, HttpRequestMessage outgoingR var sourceName = rule.Source; var targetName = rule.Target; - if (!httpContext.Request.Headers.TryGetValue(sourceName, out var values) || values.Count == 0 || StringValues.IsNullOrEmpty(values)) + if (!httpContext.Request.Headers.TryGetValue(sourceName, out var values) || StringValues.IsNullOrEmpty(values)) { continue; } diff --git a/source/AAS.TwinEngine.DataEngine/Infrastructure/Shared/HttpRequestBaseUrlProvider.cs b/source/AAS.TwinEngine.DataEngine/Infrastructure/Shared/HttpRequestBaseUrlProvider.cs index 48a3ba08..3515195e 100644 --- a/source/AAS.TwinEngine.DataEngine/Infrastructure/Shared/HttpRequestBaseUrlProvider.cs +++ b/source/AAS.TwinEngine.DataEngine/Infrastructure/Shared/HttpRequestBaseUrlProvider.cs @@ -13,6 +13,8 @@ namespace AAS.TwinEngine.DataEngine.Infrastructure.Shared; /// V2 (new config): The property is null, so the URL is derived from the incoming /// HTTP request (Scheme://Host). /// +/// The Host header is validated against to prevent +/// Host Header Injection attacks (OWASP A05:2021). /// public class HttpRequestBaseUrlProvider( IHttpContextAccessor httpContextAccessor, @@ -20,6 +22,7 @@ public class HttpRequestBaseUrlProvider( { private readonly IHttpContextAccessor _httpContextAccessor = httpContextAccessor; private readonly Uri? _configuredBaseUrl = generalConfig.Value.DataEngineRepositoryBaseUrl; + private readonly string _allowedHosts = generalConfig.Value.AllowedHosts; public Uri GetBaseUrl() { @@ -31,7 +34,24 @@ public Uri GetBaseUrl() var request = _httpContextAccessor.HttpContext?.Request ?? throw new InvalidOperationException("No HTTP request context available — cannot derive base URL."); + if (!IsHostAllowed(request.Host.Host)) + { + throw new InvalidOperationException( + $"Host header '{request.Host}' is not in the configured AllowedHosts."); + } + var baseUrl = $"{request.Scheme}://{request.Host}"; return new Uri(baseUrl, UriKind.Absolute); } + + private bool IsHostAllowed(string host) + { + if (string.IsNullOrEmpty(_allowedHosts) || _allowedHosts == "*") + { + return true; + } + + var allowed = _allowedHosts.Split(';', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries); + return allowed.Any(a => string.Equals(a, host, StringComparison.OrdinalIgnoreCase)); + } } diff --git a/source/AAS.TwinEngine.DataEngine/Program.cs b/source/AAS.TwinEngine.DataEngine/Program.cs index 0856e3df..9aed68d8 100644 --- a/source/AAS.TwinEngine.DataEngine/Program.cs +++ b/source/AAS.TwinEngine.DataEngine/Program.cs @@ -30,6 +30,15 @@ public static async Task Main(string[] args) .AddCheck("template_repository"); _ = builder.Services.AddHttpContextAccessor(); + + var allowedHosts = builder.Configuration.GetValue("General:AllowedHosts") ?? "*"; + _ = builder.Services.Configure(options => + { + options.AllowedHosts = allowedHosts + .Split(';', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries) + .ToList(); + }); + builder.Services.ConfigureInfrastructure(builder.Configuration); builder.Services.ConfigureApplication(builder.Configuration); builder.Services.ConfigureResponseCompression(); @@ -78,6 +87,7 @@ public static async Task Main(string[] args) _ = app.UseExceptionHandler(); _ = app.UseResponseCompression(); + _ = app.UseHostFiltering(); _ = app.UseMiddleware(); _ = app.UseHttpsRedirection(); _ = app.UseAuthorization(); diff --git a/source/AAS.TwinEngine.DataEngine/appsettings.json b/source/AAS.TwinEngine.DataEngine/appsettings.json index 9d0b93d5..2913cd1b 100644 --- a/source/AAS.TwinEngine.DataEngine/appsettings.json +++ b/source/AAS.TwinEngine.DataEngine/appsettings.json @@ -83,21 +83,6 @@ "Name": "RelationalDatabasePlugin", "baseUrl": "http://localhost:8086", "headerMappings": [ - { - "source": "Authorization", - "target": "X-Auth-Token", - "required": false - }, - { - "source": "X-Organization-Id", - "target": "X-Tenant-Context", - "required": false - }, - { - "source": "X-Correlation-Id", - "target": "X-Request-Id", - "required": false - } ] } ] @@ -123,18 +108,7 @@ "pattern": [ "" ] } ], - "AasIdExtractionRules": [ - { - "Pattern": "Regex", - "Index": 3, - "Separator": ":" - }, - { - "Pattern": "Regex", - "Index": 6, - "Separator": "/" - } - ] + "AasIdExtractionRules": [] }, "Semantics": { "InternalSemanticId": "InternalSemanticId" @@ -142,40 +116,17 @@ "TemplateRepository": { "Name": "TemplateRepository", "baseUrl": "http://localhost:8081", - "headerMappings": [ - { - "source": "Authorization", - "target": "Authorization", - "required": false - }, - { - "source": "X-User-Roles", - "target": "X-Template-Access-Roles", - "required": false - } - ] + "headerMappings": [] }, "AasTemplateRegistry": { "Name": "AasTemplateRegistry", "baseUrl": "http://localhost:8082", - "headerMappings": [ - { - "source": "Authorization", - "target": "Authorization", - "required": false - } - ] + "headerMappings": [] }, "SubmodelTemplateRegistry": { "Name": "SubmodelTemplateRegistry", "baseUrl": "http://localhost:8083", - "headerMappings": [ - { - "source": "Authorization", - "target": "Authorization", - "required": false - } - ] + "headerMappings": [] } }, "RegistrySettings": { From 521d85d7ab97fba7f638f9900ad96549dc2f0e48 Mon Sep 17 00:00:00 2001 From: Kevalkumar Date: Mon, 13 Apr 2026 10:15:12 +0530 Subject: [PATCH 31/71] Remove redundant comments from HttpRequestBaseUrlProviderTests --- .../Shared/HttpRequestBaseUrlProviderTests.cs | 7 ------- 1 file changed, 7 deletions(-) diff --git a/source/AAS.TwinEngine.DataEngine.UnitTests/Infrastructure/Shared/HttpRequestBaseUrlProviderTests.cs b/source/AAS.TwinEngine.DataEngine.UnitTests/Infrastructure/Shared/HttpRequestBaseUrlProviderTests.cs index 2439177c..46efc122 100644 --- a/source/AAS.TwinEngine.DataEngine.UnitTests/Infrastructure/Shared/HttpRequestBaseUrlProviderTests.cs +++ b/source/AAS.TwinEngine.DataEngine.UnitTests/Infrastructure/Shared/HttpRequestBaseUrlProviderTests.cs @@ -29,7 +29,6 @@ private static DefaultHttpContext CreateHttpContext(string scheme = "https", str return context; } - // ✅ 1. Configured URL should take priority [Fact] public void GetBaseUrl_WithConfiguredUrl_ReturnsConfiguredValue() { @@ -46,7 +45,6 @@ public void GetBaseUrl_WithConfiguredUrl_ReturnsConfiguredValue() Assert.Equal(configuredUrl, result); } - // ✅ 2. Dynamic URL from HTTP request [Fact] public void GetBaseUrl_WithDynamicUrl_ExtractsFromHttpRequest() { @@ -64,7 +62,6 @@ public void GetBaseUrl_WithDynamicUrl_ExtractsFromHttpRequest() Assert.Equal(new Uri("https://mydomain.com"), result); } - // ✅ 3. Null HttpContext should throw [Fact] public void GetBaseUrl_WithNullHttpContext_ThrowsInvalidOperationException() { @@ -79,7 +76,6 @@ public void GetBaseUrl_WithNullHttpContext_ThrowsInvalidOperationException() Assert.Contains("No HTTP request context", ex.Message); } - // 🔒 4. Host header injection is now rejected when AllowedHosts is configured [Fact] public void GetBaseUrl_WithMaliciousHostHeader_ThrowsWhenAllowedHostsConfigured() { @@ -95,7 +91,6 @@ public void GetBaseUrl_WithMaliciousHostHeader_ThrowsWhenAllowedHostsConfigured( Assert.Contains("not in the configured AllowedHosts", ex.Message); } - // 🔒 5. Allowed host passes validation [Fact] public void GetBaseUrl_WithAllowedHost_ReturnsUrl() { @@ -113,7 +108,6 @@ public void GetBaseUrl_WithAllowedHost_ReturnsUrl() Assert.Equal(new Uri("https://mydomain.com"), result); } - // 🔒 6. Wildcard AllowedHosts permits any host (backward compatible default) [Fact] public void GetBaseUrl_WithWildcardAllowedHosts_PermitsAnyHost() { @@ -131,7 +125,6 @@ public void GetBaseUrl_WithWildcardAllowedHosts_PermitsAnyHost() Assert.Equal(new Uri("https://anyhost.com"), result); } - // 🔒 7. Host matching is case-insensitive [Fact] public void GetBaseUrl_WithDifferentCaseHost_PermitsHost() { From 88d11ea1ebbabf800c7e46f30a19a54f03277214 Mon Sep 17 00:00:00 2001 From: Kevalkumar Date: Wed, 15 Apr 2026 11:40:28 +0530 Subject: [PATCH 32/71] Added changes to move oold config to legacy --- .../ShellDescriptorControllerTests.cs | 13 ++-- .../AasRepositoryControllerTests.cs | 11 ++-- .../SubmodelRepositoryControllerTests.cs | 11 ++-- .../HeaderForwardingHandlerTests.cs | 5 +- .../Headers/RequestHeaderMapperTests.cs | 15 +++-- .../Http/Clients/HttpClientFactoryTests.cs | 7 +-- .../HttpClientRegistrationExtensionsTests.cs | 24 +++---- .../PluginAvailabilityHealthCheckTests.cs | 15 +++-- .../TemplateRegistryHealthCheckTests.cs | 48 +++++++------- .../TemplateRepositoryHealthCheckTests.cs | 10 +-- .../Services/PluginDataHandlerTests.cs | 13 ++-- .../Services/PluginDataProviderTests.cs | 49 ++++++++------- .../Services/PluginManifestProviderTests.cs | 5 +- .../Services/PluginRequestBuilderTests.cs | 21 ++++--- .../SubmodelDescriptorProviderTests.cs | 20 +++--- .../Services/TemplateProviderTests.cs | 62 +++++++++---------- ...acyTemplateManagementConfigAdapterTests.cs | 9 ++- .../SubmodelDescriptorService.cs | 4 +- .../LegacyV1/ConfigV1/AasEnvironmentConfig.cs | 22 +++++++ .../LegacyV1/ConfigV1/PluginConfig.cs | 32 ++++++++++ .../LegacyV1/LegacyPluginsConfigAdapter.cs | 20 +++--- .../LegacyTemplateManagementConfigAdapter.cs | 25 +++++--- .../Headers/RequestHeaderMapper.cs | 16 +++-- .../PluginAvailabilityHealthCheck.cs | 3 +- .../Monitoring/TemplateRegistryHealthCheck.cs | 12 ++-- .../TemplateRepositoryHealthCheck.cs | 16 ++--- .../Services/AasRegistryProvider.cs | 6 +- .../PluginDataProvider/Config/PluginConfig.cs | 21 ------- .../Helper/RegisterPluginHttpClients.cs | 3 +- .../Services/PluginDataProvider.cs | 6 +- .../Services/PluginManifestProvider.cs | 3 +- .../Services/PluginRequestBuilder.cs | 6 +- .../Services/SubmodelDescriptorProvider.cs | 6 +- .../Services/TemplateProvider.cs | 24 +++---- .../ServiceConfiguration/Config/ApiPaths.cs | 19 ++++++ .../RegistrySettingsConfigValidator.cs | 37 +++++++++++ .../TemplateManagementConfigNormalizer.cs | 0 .../TemplateManagementConfigValidator.cs | 39 ++++++++++++ .../Config/HttpClientNames.cs} | 37 ++++------- ...astructureDependencyInjectionExtensions.cs | 59 +++++++++++------- 40 files changed, 444 insertions(+), 310 deletions(-) create mode 100644 source/AAS.TwinEngine.DataEngine/Infrastructure/Configuration/LegacyV1/ConfigV1/AasEnvironmentConfig.cs create mode 100644 source/AAS.TwinEngine.DataEngine/Infrastructure/Configuration/LegacyV1/ConfigV1/PluginConfig.cs delete mode 100644 source/AAS.TwinEngine.DataEngine/Infrastructure/Providers/PluginDataProvider/Config/PluginConfig.cs create mode 100644 source/AAS.TwinEngine.DataEngine/ServiceConfiguration/Config/ApiPaths.cs create mode 100644 source/AAS.TwinEngine.DataEngine/ServiceConfiguration/Config/Helpers/RegistrySettingsConfigValidator.cs rename source/AAS.TwinEngine.DataEngine/ServiceConfiguration/Config/{ => Helpers}/TemplateManagementConfigNormalizer.cs (100%) create mode 100644 source/AAS.TwinEngine.DataEngine/ServiceConfiguration/Config/Helpers/TemplateManagementConfigValidator.cs rename source/AAS.TwinEngine.DataEngine/{ApplicationLogic/Services/Plugin/Config/AasEnvironmentConfig.cs => ServiceConfiguration/Config/HttpClientNames.cs} (55%) diff --git a/source/AAS.TwinEngine.DataEngine.ModuleTests/Api/Services/AasRegistry/ShellDescriptorControllerTests.cs b/source/AAS.TwinEngine.DataEngine.ModuleTests/Api/Services/AasRegistry/ShellDescriptorControllerTests.cs index 6e2cb509..da2d0058 100644 --- a/source/AAS.TwinEngine.DataEngine.ModuleTests/Api/Services/AasRegistry/ShellDescriptorControllerTests.cs +++ b/source/AAS.TwinEngine.DataEngine.ModuleTests/Api/Services/AasRegistry/ShellDescriptorControllerTests.cs @@ -1,4 +1,4 @@ -using System.Net; +using System.Net; using System.Net.Http.Json; using System.Text; using System.Text.Json.Nodes; @@ -8,7 +8,6 @@ using AAS.TwinEngine.DataEngine.ApplicationLogic.Services.Plugin; using AAS.TwinEngine.DataEngine.ApplicationLogic.Services.Plugin.Providers; using AAS.TwinEngine.DataEngine.Infrastructure.Http.Clients; -using AAS.TwinEngine.DataEngine.Infrastructure.Providers.PluginDataProvider.Config; using AAS.TwinEngine.DataEngine.ModuleTests.Common; using Microsoft.AspNetCore.WebUtilities; @@ -17,6 +16,8 @@ using NSubstitute; using NSubstitute.ExceptionExtensions; +using AAS.TwinEngine.DataEngine.ServiceConfiguration.Config; + namespace AAS.TwinEngine.DataEngine.ModuleTests.Api.Services.AasRegistry; public abstract class ShellDescriptorControllerTestsBase : IDisposable @@ -75,10 +76,10 @@ public async Task GetAllShellDescriptorsAsync_ReturnsOkAsync() using var httpClientPlugin2 = new HttpClient(messageHandlerPlugin2); httpClientPlugin2.BaseAddress = new Uri("https://testendpoint2.com"); - const string HttpClientNamePlugin1 = $"{PluginConfig.HttpClientNamePrefix}TestPlugin1"; + const string HttpClientNamePlugin1 = $"{HttpClientNames.PluginDataProviderPrefix}TestPlugin1"; _ = _httpClientFactory.CreateClient(HttpClientNamePlugin1).Returns(httpClientPlugin1); - const string HttpClientNamePlugin2 = $"{PluginConfig.HttpClientNamePrefix}TestPlugin2"; + const string HttpClientNamePlugin2 = $"{HttpClientNames.PluginDataProviderPrefix}TestPlugin2"; _ = _httpClientFactory.CreateClient(HttpClientNamePlugin2).Returns(httpClientPlugin2); _ = _mockTemplateProvider.GetShellDescriptorsTemplateAsync(Arg.Any()).Returns(template); @@ -159,10 +160,10 @@ public async Task GetShellDescriptorByIdAsync_ReturnsOkAsync() using var httpClient2 = new HttpClient(messageHandler2); httpClient2.BaseAddress = new Uri("https://testendpoint2.com"); - const string HttpClientName1 = $"{PluginConfig.HttpClientNamePrefix}TestPlugin1"; + const string HttpClientName1 = $"{HttpClientNames.PluginDataProviderPrefix}TestPlugin1"; _ = _httpClientFactory.CreateClient(HttpClientName1).Returns(httpClient1); - const string HttpClientName2 = $"{PluginConfig.HttpClientNamePrefix}TestPlugin2"; + const string HttpClientName2 = $"{HttpClientNames.PluginDataProviderPrefix}TestPlugin2"; _ = _httpClientFactory.CreateClient(HttpClientName2).Returns(httpClient2); _ = _mockTemplateProvider.GetShellDescriptorsTemplateAsync(Arg.Any()).Returns(template); diff --git a/source/AAS.TwinEngine.DataEngine.ModuleTests/Api/Services/AasRepository/AasRepositoryControllerTests.cs b/source/AAS.TwinEngine.DataEngine.ModuleTests/Api/Services/AasRepository/AasRepositoryControllerTests.cs index 7915b37c..4e32094b 100644 --- a/source/AAS.TwinEngine.DataEngine.ModuleTests/Api/Services/AasRepository/AasRepositoryControllerTests.cs +++ b/source/AAS.TwinEngine.DataEngine.ModuleTests/Api/Services/AasRepository/AasRepositoryControllerTests.cs @@ -1,4 +1,4 @@ -using System.Net; +using System.Net; using System.Net.Http.Json; using System.Text; using System.Text.Json.Nodes; @@ -9,7 +9,6 @@ using AAS.TwinEngine.DataEngine.ApplicationLogic.Services.Plugin; using AAS.TwinEngine.DataEngine.ApplicationLogic.Services.Plugin.Providers; using AAS.TwinEngine.DataEngine.Infrastructure.Http.Clients; -using AAS.TwinEngine.DataEngine.Infrastructure.Providers.PluginDataProvider.Config; using AAS.TwinEngine.DataEngine.ModuleTests.Common; using AasCore.Aas3_0; @@ -20,6 +19,8 @@ using NSubstitute; using NSubstitute.ExceptionExtensions; +using AAS.TwinEngine.DataEngine.ServiceConfiguration.Config; + namespace AAS.TwinEngine.DataEngine.ModuleTests.Api.Services.AasRepository; public abstract class AasRepositoryControllerTestsBase : IDisposable @@ -71,7 +72,7 @@ public async Task GetShellByIdAsync_ReturnsOkAsync() using var httpClient = new HttpClient(messageHandler); httpClient.BaseAddress = new Uri("https://testendpoint.com"); - const string HttpClientName = $"{PluginConfig.HttpClientNamePrefix}TestPlugin1"; + const string HttpClientName = $"{HttpClientNames.PluginDataProviderPrefix}TestPlugin1"; _ = _httpClientFactory.CreateClient(HttpClientName).Returns(httpClient); _ = _mockTemplateProvider.GetShellTemplateAsync(Arg.Any(), Arg.Any()).Returns(mockShellTemplate); @@ -112,7 +113,7 @@ public async Task GetShellByIdAsync_ReturnsNotFoundAsync_WhenErrorWhileExtractio using var httpClient = new HttpClient(messageHandler); httpClient.BaseAddress = new Uri("https://testendpoint.com"); - var httpClientName = $"{PluginConfig.HttpClientNamePrefix}TestPlugin1"; + var httpClientName = $"{HttpClientNames.PluginDataProviderPrefix}TestPlugin1"; _ = _httpClientFactory.CreateClient(httpClientName).Returns(httpClient); _ = _mockTemplateProvider.GetShellTemplateAsync(Arg.Any(), Arg.Any()).Returns(mockShellTemplate); @@ -141,7 +142,7 @@ public async Task GetAssetInformationByIdAsync_ReturnsOkAsync() using var httpClient = new HttpClient(messageHandler); httpClient.BaseAddress = new Uri("https://testendpoint.com"); - const string HttpClientName = $"{PluginConfig.HttpClientNamePrefix}TestPlugin1"; + const string HttpClientName = $"{HttpClientNames.PluginDataProviderPrefix}TestPlugin1"; _ = _httpClientFactory.CreateClient(HttpClientName).Returns(httpClient); _ = _mockTemplateProvider.GetAssetInformationTemplateAsync(Arg.Any(), Arg.Any()).Returns(mockAssetInformationTemplate); diff --git a/source/AAS.TwinEngine.DataEngine.ModuleTests/Api/Services/SubmodelRepository/SubmodelRepositoryControllerTests.cs b/source/AAS.TwinEngine.DataEngine.ModuleTests/Api/Services/SubmodelRepository/SubmodelRepositoryControllerTests.cs index 99225615..491954de 100644 --- a/source/AAS.TwinEngine.DataEngine.ModuleTests/Api/Services/SubmodelRepository/SubmodelRepositoryControllerTests.cs +++ b/source/AAS.TwinEngine.DataEngine.ModuleTests/Api/Services/SubmodelRepository/SubmodelRepositoryControllerTests.cs @@ -1,4 +1,4 @@ -using System.Net; +using System.Net; using System.Net.Http.Json; using System.Text; using System.Text.Json.Nodes; @@ -8,7 +8,6 @@ using AAS.TwinEngine.DataEngine.ApplicationLogic.Services.Plugin; using AAS.TwinEngine.DataEngine.ApplicationLogic.Services.Plugin.Providers; using AAS.TwinEngine.DataEngine.Infrastructure.Http.Clients; -using AAS.TwinEngine.DataEngine.Infrastructure.Providers.PluginDataProvider.Config; using AAS.TwinEngine.DataEngine.ModuleTests.Common; using Microsoft.AspNetCore.WebUtilities; @@ -17,6 +16,8 @@ using NSubstitute; using NSubstitute.ExceptionExtensions; +using AAS.TwinEngine.DataEngine.ServiceConfiguration.Config; + namespace AAS.TwinEngine.DataEngine.ModuleTests.Api.Services.SubmodelRepository; public abstract class SubmodelRepositoryControllerTestsBase : IDisposable @@ -74,10 +75,10 @@ public async Task GetSubmodelAsync_WithValidIdentifier_ReturnsOkAsync() using var httpClientPlugin2 = new HttpClient(messageHandlerPlugin2); httpClientPlugin2.BaseAddress = new Uri("https://testendpoint2.com"); - const string HttpClientNamePlugin1 = $"{PluginConfig.HttpClientNamePrefix}TestPlugin1"; + const string HttpClientNamePlugin1 = $"{HttpClientNames.PluginDataProviderPrefix}TestPlugin1"; _ = _httpClientFactory.CreateClient(HttpClientNamePlugin1).Returns(httpClientPlugin1); - const string HttpClientNamePlugin2 = $"{PluginConfig.HttpClientNamePrefix}TestPlugin2"; + const string HttpClientNamePlugin2 = $"{HttpClientNames.PluginDataProviderPrefix}TestPlugin2"; _ = _httpClientFactory.CreateClient(HttpClientNamePlugin2).Returns(httpClientPlugin2); const string SubmodelId = "Q29udGFjdEluZm9ybWF0aW9u"; @@ -149,7 +150,7 @@ public async Task GetSubmodelElementAsync_ReturnsOkAsync() using var httpClient = new HttpClient(messageHandler); httpClient.BaseAddress = new Uri("https://testendpoint.com"); - const string HttpClientName = $"{PluginConfig.HttpClientNamePrefix}TestPlugin1"; + const string HttpClientName = $"{HttpClientNames.PluginDataProviderPrefix}TestPlugin1"; _ = _httpClientFactory.CreateClient(HttpClientName).Returns(httpClient); _ = _mockTemplateProvider.GetSubmodelTemplateAsync(Arg.Any(), Arg.Any()).Returns(mockSubmodel); diff --git a/source/AAS.TwinEngine.DataEngine.UnitTests/Infrastructure/Http/Authorization/HeaderForwardingHandlerTests.cs b/source/AAS.TwinEngine.DataEngine.UnitTests/Infrastructure/Http/Authorization/HeaderForwardingHandlerTests.cs index ab7353fc..e1aa7a89 100644 --- a/source/AAS.TwinEngine.DataEngine.UnitTests/Infrastructure/Http/Authorization/HeaderForwardingHandlerTests.cs +++ b/source/AAS.TwinEngine.DataEngine.UnitTests/Infrastructure/Http/Authorization/HeaderForwardingHandlerTests.cs @@ -1,9 +1,8 @@ -using System.Net; +using System.Net; using AAS.TwinEngine.DataEngine.Infrastructure.Http.Authorization; using AAS.TwinEngine.DataEngine.Infrastructure.Http.Authorization.Config; using AAS.TwinEngine.DataEngine.Infrastructure.Http.Authorization.Headers; -using AAS.TwinEngine.DataEngine.Infrastructure.Providers.PluginDataProvider.Config; using AAS.TwinEngine.DataEngine.ServiceConfiguration.Config; using Microsoft.AspNetCore.Http; @@ -64,7 +63,7 @@ private static HeaderForwardingHandler CreateHandler(HttpContext httpContext, st public async Task HeaderForwardingHandler_ForwardsMappedHeaderToInnerHandler() { const string pluginName = "TestPlugin"; - var clientName = PluginConfig.HttpClientNamePrefix + pluginName; + var clientName = HttpClientNames.PluginDataProviderPrefix + pluginName; var httpContext = new DefaultHttpContext(); httpContext.Request.Headers.Authorization = "Bearer test-token"; diff --git a/source/AAS.TwinEngine.DataEngine.UnitTests/Infrastructure/Http/Authorization/Headers/RequestHeaderMapperTests.cs b/source/AAS.TwinEngine.DataEngine.UnitTests/Infrastructure/Http/Authorization/Headers/RequestHeaderMapperTests.cs index 646eecec..2697b3ca 100644 --- a/source/AAS.TwinEngine.DataEngine.UnitTests/Infrastructure/Http/Authorization/Headers/RequestHeaderMapperTests.cs +++ b/source/AAS.TwinEngine.DataEngine.UnitTests/Infrastructure/Http/Authorization/Headers/RequestHeaderMapperTests.cs @@ -1,8 +1,7 @@ -using AAS.TwinEngine.DataEngine.ApplicationLogic.Exceptions.Application; -using AAS.TwinEngine.DataEngine.ApplicationLogic.Services.Plugin.Config; +using AAS.TwinEngine.DataEngine.ApplicationLogic.Exceptions.Application; +using AAS.TwinEngine.DataEngine.ServiceConfiguration.Config; using AAS.TwinEngine.DataEngine.Infrastructure.Http.Authorization.Config; using AAS.TwinEngine.DataEngine.Infrastructure.Http.Authorization.Headers; -using AAS.TwinEngine.DataEngine.Infrastructure.Providers.PluginDataProvider.Config; using AAS.TwinEngine.DataEngine.ServiceConfiguration.Config; using Microsoft.AspNetCore.Http; @@ -145,7 +144,7 @@ public void ApplyMappings_MapsAuthorizationHeader() using var request = new HttpRequestMessage(); - service.ApplyMappings(context, request, AasEnvironmentConfig.AasTemplateRepository); + service.ApplyMappings(context, request, HttpClientNames.AasTemplateRepository); Assert.Equal("Bearer", request.Headers.Authorization?.Scheme); Assert.Equal("token", request.Headers.Authorization?.Parameter); @@ -173,7 +172,7 @@ public void ApplyMappings_RenamesHeader() using var request = new HttpRequestMessage(); - service.ApplyMappings(context, request, AasEnvironmentConfig.AasTemplateRepository); + service.ApplyMappings(context, request, HttpClientNames.AasTemplateRepository); Assert.True(request.Headers.Contains("X-Target")); } @@ -201,7 +200,7 @@ public void ApplyMappings_OverwriteExistingHeader() using var request = new HttpRequestMessage(); request.Headers.TryAddWithoutValidation("X-Test", "old"); - service.ApplyMappings(context, request, AasEnvironmentConfig.AasTemplateRepository); + service.ApplyMappings(context, request, HttpClientNames.AasTemplateRepository); var value = request.Headers.GetValues("X-Test").First(); Assert.Equal("new", value); @@ -229,7 +228,7 @@ public void ApplyMappings_MultipleValues_CombinedCorrectly() using var request = new HttpRequestMessage(); - service.ApplyMappings(context, request, AasEnvironmentConfig.AasTemplateRepository); + service.ApplyMappings(context, request, HttpClientNames.AasTemplateRepository); var value = request.Headers.GetValues("X-Target").First(); Assert.Equal("a,b", value); @@ -264,7 +263,7 @@ public void ApplyMappings_PluginMapping_Works() using var request = new HttpRequestMessage(); - var clientName = PluginConfig.HttpClientNamePrefix + pluginName; + var clientName = HttpClientNames.PluginDataProviderPrefix + pluginName; service.ApplyMappings(context, request, clientName); diff --git a/source/AAS.TwinEngine.DataEngine.UnitTests/Infrastructure/Http/Clients/HttpClientFactoryTests.cs b/source/AAS.TwinEngine.DataEngine.UnitTests/Infrastructure/Http/Clients/HttpClientFactoryTests.cs index 4350acf2..783769ef 100644 --- a/source/AAS.TwinEngine.DataEngine.UnitTests/Infrastructure/Http/Clients/HttpClientFactoryTests.cs +++ b/source/AAS.TwinEngine.DataEngine.UnitTests/Infrastructure/Http/Clients/HttpClientFactoryTests.cs @@ -1,6 +1,5 @@ -using AAS.TwinEngine.DataEngine.ApplicationLogic.Services.Plugin.Config; +using AAS.TwinEngine.DataEngine.ServiceConfiguration.Config; using AAS.TwinEngine.DataEngine.Infrastructure.Http.Clients; -using AAS.TwinEngine.DataEngine.Infrastructure.Providers.PluginDataProvider.Config; using NSubstitute; @@ -9,8 +8,8 @@ namespace AAS.TwinEngine.DataEngine.UnitTests.Infrastructure.Http.Clients; public class HttpClientFactoryTests { [Theory] - [InlineData(AasEnvironmentConfig.SubmodelTemplateRepository)] - [InlineData(PluginConfig.HttpClientNamePrefix + "PluginName")] + [InlineData(HttpClientNames.SubmodelTemplateRepository)] + [InlineData(HttpClientNames.PluginDataProviderPrefix + "PluginName")] public void CreateClient_Returns_HttpClient(string clientName) { var httpClientFactory = Substitute.For(); diff --git a/source/AAS.TwinEngine.DataEngine.UnitTests/Infrastructure/Http/Extensions/HttpClientRegistrationExtensionsTests.cs b/source/AAS.TwinEngine.DataEngine.UnitTests/Infrastructure/Http/Extensions/HttpClientRegistrationExtensionsTests.cs index f70c1092..b9583eb9 100644 --- a/source/AAS.TwinEngine.DataEngine.UnitTests/Infrastructure/Http/Extensions/HttpClientRegistrationExtensionsTests.cs +++ b/source/AAS.TwinEngine.DataEngine.UnitTests/Infrastructure/Http/Extensions/HttpClientRegistrationExtensionsTests.cs @@ -1,4 +1,4 @@ -using AAS.TwinEngine.DataEngine.ApplicationLogic.Services.Plugin.Config; +using AAS.TwinEngine.DataEngine.ServiceConfiguration.Config; using AAS.TwinEngine.DataEngine.Infrastructure.Http.Authorization.Headers; using AAS.TwinEngine.DataEngine.Infrastructure.Http.Extensions; using AAS.TwinEngine.DataEngine.ServiceConfiguration.Config; @@ -26,12 +26,12 @@ public void HttpClientRegistrationExtensions_RegistersTemplateProviderClientWith var headerMapper = Substitute.For(); _ = services.AddScoped(_ => headerMapper); - services.AddHttpClientWithResilience(AasEnvironmentConfig.SubmodelTemplateRepository, DefaultRetryConfig, new Uri("https://example.com")); + services.AddHttpClientWithResilience(HttpClientNames.SubmodelTemplateRepository, DefaultRetryConfig, new Uri("https://example.com")); var serviceProvider = services.BuildServiceProvider(); var httpClientFactory = serviceProvider.GetRequiredService(); - var httpClient = httpClientFactory.CreateClient(AasEnvironmentConfig.SubmodelTemplateRepository); + var httpClient = httpClientFactory.CreateClient(HttpClientNames.SubmodelTemplateRepository); Assert.Contains(httpClient.DefaultRequestHeaders.Accept, h => h.MediaType == "application/json"); @@ -47,12 +47,12 @@ public void HttpClientRegistrationExtensions_RegistersPluginDataProviderClientWi var headerMapper = Substitute.For(); _ = services.AddScoped(_ => headerMapper); - services.AddHttpClientWithResilience(AasEnvironmentConfig.SubmodelTemplateRepository, DefaultRetryConfig, new Uri("https://example.com")); + services.AddHttpClientWithResilience(HttpClientNames.SubmodelTemplateRepository, DefaultRetryConfig, new Uri("https://example.com")); var serviceProvider = services.BuildServiceProvider(); var httpClientFactory = serviceProvider.GetRequiredService(); - var httpClient = httpClientFactory.CreateClient(AasEnvironmentConfig.SubmodelTemplateRepository); + var httpClient = httpClientFactory.CreateClient(HttpClientNames.SubmodelTemplateRepository); Assert.Contains(httpClient.DefaultRequestHeaders.Accept, h => h.MediaType == "application/json"); @@ -69,12 +69,12 @@ public async Task SendRequest_AfterFourAttempts_ThrowsExceptionAndLogsThreeRetri var headerMapper = Substitute.For(); _ = services.AddScoped(_ => headerMapper); - services.AddHttpClientWithResilience(AasEnvironmentConfig.SubmodelTemplateRepository, DefaultRetryConfig, new Uri("https://example.com")); + services.AddHttpClientWithResilience(HttpClientNames.SubmodelTemplateRepository, DefaultRetryConfig, new Uri("https://example.com")); using var handler = new FaultyHttpMessageHandler(); - services.Configure(AasEnvironmentConfig.SubmodelTemplateRepository, options => options.HttpMessageHandlerBuilderActions.Add(builder => builder.PrimaryHandler = handler)); + services.Configure(HttpClientNames.SubmodelTemplateRepository, options => options.HttpMessageHandlerBuilderActions.Add(builder => builder.PrimaryHandler = handler)); var serviceProvider = services.BuildServiceProvider(); var factory = serviceProvider.GetRequiredService(); - var client = factory.CreateClient(AasEnvironmentConfig.SubmodelTemplateRepository); + var client = factory.CreateClient(HttpClientNames.SubmodelTemplateRepository); await Assert.ThrowsAsync(() => client.GetAsync(new Uri("http://test.com"))); @@ -92,12 +92,12 @@ public async Task AddHttpClientWithResilience_WithForwarding_AddsHeaderForwardin _ = services.AddScoped(_ => mappingService); services.AddHttpClientWithResilience( - AasEnvironmentConfig.SubmodelTemplateRepository, + HttpClientNames.SubmodelTemplateRepository, DefaultRetryConfig, new Uri("https://example.com")); using var handler = new FaultyHttpMessageHandler(); - services.Configure(AasEnvironmentConfig.SubmodelTemplateRepository, + services.Configure(HttpClientNames.SubmodelTemplateRepository, options => options.HttpMessageHandlerBuilderActions.Add(builder => builder.PrimaryHandler = handler)); var serviceProvider = services.BuildServiceProvider(); @@ -109,13 +109,13 @@ public async Task AddHttpClientWithResilience_WithForwarding_AddsHeaderForwardin }; var factory = serviceProvider.GetRequiredService(); - var client = factory.CreateClient(AasEnvironmentConfig.SubmodelTemplateRepository); + var client = factory.CreateClient(HttpClientNames.SubmodelTemplateRepository); _ = await Assert.ThrowsAsync(() => client.GetAsync("/test")).ConfigureAwait(false); mappingService .Received() - .ApplyMappings(httpContextAccessor.HttpContext, Arg.Any(), AasEnvironmentConfig.SubmodelTemplateRepository); + .ApplyMappings(httpContextAccessor.HttpContext, Arg.Any(), HttpClientNames.SubmodelTemplateRepository); } private sealed class FaultyHttpMessageHandler : HttpMessageHandler diff --git a/source/AAS.TwinEngine.DataEngine.UnitTests/Infrastructure/Monitoring/PluginAvailabilityHealthCheckTests.cs b/source/AAS.TwinEngine.DataEngine.UnitTests/Infrastructure/Monitoring/PluginAvailabilityHealthCheckTests.cs index c06b2718..5f0c0657 100644 --- a/source/AAS.TwinEngine.DataEngine.UnitTests/Infrastructure/Monitoring/PluginAvailabilityHealthCheckTests.cs +++ b/source/AAS.TwinEngine.DataEngine.UnitTests/Infrastructure/Monitoring/PluginAvailabilityHealthCheckTests.cs @@ -1,8 +1,7 @@ -using System.Net; +using System.Net; using AAS.TwinEngine.DataEngine.Infrastructure.Http.Clients; using AAS.TwinEngine.DataEngine.Infrastructure.Monitoring; -using AAS.TwinEngine.DataEngine.Infrastructure.Providers.PluginDataProvider.Config; using AAS.TwinEngine.DataEngine.ServiceConfiguration.Config; using Microsoft.Extensions.Diagnostics.HealthChecks; @@ -231,11 +230,11 @@ public async Task CheckHealthAsync_Checks_All_Plugins_In_Parallel_Even_When_One_ var clientFactory = Substitute.For(); clientFactory - .CreateClient($"{PluginConfig.HealthCheckHttpClientNamePrefix}Plugin1") + .CreateClient($"{HttpClientNames.PluginHealthCheckPrefix}Plugin1") .Returns(CreateHttpClient(HttpStatusCode.InternalServerError)); clientFactory - .CreateClient($"{PluginConfig.HealthCheckHttpClientNamePrefix}Plugin2") + .CreateClient($"{HttpClientNames.PluginHealthCheckPrefix}Plugin2") .Returns(CreateHttpClient(HttpStatusCode.OK)); var pluginConfig = Options.Create(new PluginsConfig @@ -253,8 +252,8 @@ public async Task CheckHealthAsync_Checks_All_Plugins_In_Parallel_Even_When_One_ _ = await sut.CheckHealthAsync(new HealthCheckContext(), CancellationToken.None); - clientFactory.Received(1).CreateClient($"{PluginConfig.HealthCheckHttpClientNamePrefix}Plugin1"); - clientFactory.Received(1).CreateClient($"{PluginConfig.HealthCheckHttpClientNamePrefix}Plugin2"); + clientFactory.Received(1).CreateClient($"{HttpClientNames.PluginHealthCheckPrefix}Plugin1"); + clientFactory.Received(1).CreateClient($"{HttpClientNames.PluginHealthCheckPrefix}Plugin2"); } [Fact] @@ -283,8 +282,8 @@ public async Task CheckHealthAsync_Uses_HealthCheck_Client_Names_Without_Retry_P _ = await sut.CheckHealthAsync(new HealthCheckContext(), CancellationToken.None); - clientFactory.Received(1).CreateClient($"{PluginConfig.HealthCheckHttpClientNamePrefix}Plugin1"); - clientFactory.DidNotReceive().CreateClient($"{PluginConfig.HttpClientNamePrefix}Plugin1"); + clientFactory.Received(1).CreateClient($"{HttpClientNames.PluginHealthCheckPrefix}Plugin1"); + clientFactory.DidNotReceive().CreateClient($"{HttpClientNames.PluginDataProviderPrefix}Plugin1"); } private static HttpClient CreateHttpClient(HttpStatusCode statusCode) diff --git a/source/AAS.TwinEngine.DataEngine.UnitTests/Infrastructure/Monitoring/TemplateRegistryHealthCheckTests.cs b/source/AAS.TwinEngine.DataEngine.UnitTests/Infrastructure/Monitoring/TemplateRegistryHealthCheckTests.cs index 190e11fd..edf55e0e 100644 --- a/source/AAS.TwinEngine.DataEngine.UnitTests/Infrastructure/Monitoring/TemplateRegistryHealthCheckTests.cs +++ b/source/AAS.TwinEngine.DataEngine.UnitTests/Infrastructure/Monitoring/TemplateRegistryHealthCheckTests.cs @@ -1,6 +1,6 @@ -using System.Net; +using System.Net; -using AAS.TwinEngine.DataEngine.ApplicationLogic.Services.Plugin.Config; +using AAS.TwinEngine.DataEngine.ServiceConfiguration.Config; using AAS.TwinEngine.DataEngine.Infrastructure.Http.Clients; using AAS.TwinEngine.DataEngine.Infrastructure.Monitoring; @@ -20,8 +20,8 @@ public async Task CheckHealthAsync_Returns_Healthy_When_Registry_And_Submodel_Ar { var clientFactory = Substitute.For(); - clientFactory.CreateClient(AasEnvironmentConfig.AasRegistryHealthCheck).Returns(CreateHttpClient(HttpStatusCode.OK)); - clientFactory.CreateClient(AasEnvironmentConfig.SubmodelRegistryHealthCheck).Returns(CreateHttpClient(HttpStatusCode.OK)); + clientFactory.CreateClient(HttpClientNames.AasRegistryHealthCheck).Returns(CreateHttpClient(HttpStatusCode.OK)); + clientFactory.CreateClient(HttpClientNames.SubmodelRegistryHealthCheck).Returns(CreateHttpClient(HttpStatusCode.OK)); var logger = Substitute.For>(); @@ -37,8 +37,8 @@ public async Task CheckHealthAsync_Returns_Unhealthy_When_AasRegistry_Is_Unhealt { var clientFactory = Substitute.For(); - clientFactory.CreateClient(AasEnvironmentConfig.AasRegistryHealthCheck).Returns(CreateHttpClient(HttpStatusCode.InternalServerError)); - clientFactory.CreateClient(AasEnvironmentConfig.SubmodelRegistryHealthCheck).Returns(CreateHttpClient(HttpStatusCode.OK)); + clientFactory.CreateClient(HttpClientNames.AasRegistryHealthCheck).Returns(CreateHttpClient(HttpStatusCode.InternalServerError)); + clientFactory.CreateClient(HttpClientNames.SubmodelRegistryHealthCheck).Returns(CreateHttpClient(HttpStatusCode.OK)); var logger = Substitute.For>(); @@ -54,8 +54,8 @@ public async Task CheckHealthAsync_Returns_Unhealthy_When_SubmodelRegistry_Is_Un { var clientFactory = Substitute.For(); - clientFactory.CreateClient(AasEnvironmentConfig.AasRegistryHealthCheck).Returns(CreateHttpClient(HttpStatusCode.OK)); - clientFactory.CreateClient(AasEnvironmentConfig.SubmodelRegistryHealthCheck).Returns(CreateHttpClient(HttpStatusCode.InternalServerError)); + clientFactory.CreateClient(HttpClientNames.AasRegistryHealthCheck).Returns(CreateHttpClient(HttpStatusCode.OK)); + clientFactory.CreateClient(HttpClientNames.SubmodelRegistryHealthCheck).Returns(CreateHttpClient(HttpStatusCode.InternalServerError)); var logger = Substitute.For>(); @@ -70,8 +70,8 @@ public async Task CheckHealthAsync_Returns_Unhealthy_When_SubmodelRegistry_Is_Un public async Task CheckHealthAsync_Returns_Unhealthy_When_AasRegistry_Request_Throws_HttpRequestException() { var clientFactory = Substitute.For(); - clientFactory.CreateClient(AasEnvironmentConfig.AasRegistryHealthCheck).Returns(CreateThrowingHttpClient(new HttpRequestException("network"))); - clientFactory.CreateClient(AasEnvironmentConfig.SubmodelRegistryHealthCheck).Returns(CreateHttpClient(HttpStatusCode.OK)); + clientFactory.CreateClient(HttpClientNames.AasRegistryHealthCheck).Returns(CreateThrowingHttpClient(new HttpRequestException("network"))); + clientFactory.CreateClient(HttpClientNames.SubmodelRegistryHealthCheck).Returns(CreateHttpClient(HttpStatusCode.OK)); var logger = Substitute.For>(); @@ -87,8 +87,8 @@ public async Task CheckHealthAsync_Returns_Unhealthy_When_SubmodelRegistry_Reque { var clientFactory = Substitute.For(); - clientFactory.CreateClient(AasEnvironmentConfig.AasRegistryHealthCheck).Returns(CreateHttpClient(HttpStatusCode.OK)); - clientFactory.CreateClient(AasEnvironmentConfig.SubmodelRegistryHealthCheck).Returns(CreateThrowingHttpClient(new TaskCanceledException("timeout"))); + clientFactory.CreateClient(HttpClientNames.AasRegistryHealthCheck).Returns(CreateHttpClient(HttpStatusCode.OK)); + clientFactory.CreateClient(HttpClientNames.SubmodelRegistryHealthCheck).Returns(CreateThrowingHttpClient(new TaskCanceledException("timeout"))); var logger = Substitute.For>(); @@ -103,9 +103,9 @@ public async Task CheckHealthAsync_Returns_Unhealthy_When_SubmodelRegistry_Reque public async Task CheckHealthAsync_Returns_Unhealthy_When_AasRegistry_Request_Throws_Exception() { var clientFactory = Substitute.For(); - clientFactory.CreateClient(AasEnvironmentConfig.AasRegistryHealthCheck) + clientFactory.CreateClient(HttpClientNames.AasRegistryHealthCheck) .Returns(CreateThrowingHttpClient(new Exception("unexpected"))); - clientFactory.CreateClient(AasEnvironmentConfig.SubmodelRegistryHealthCheck).Returns(CreateHttpClient(HttpStatusCode.OK)); + clientFactory.CreateClient(HttpClientNames.SubmodelRegistryHealthCheck).Returns(CreateHttpClient(HttpStatusCode.OK)); var logger = Substitute.For>(); @@ -120,8 +120,8 @@ public async Task CheckHealthAsync_Returns_Unhealthy_When_AasRegistry_Request_Th public async Task CheckHealthAsync_Checks_Both_Endpoints_In_Parallel_Even_When_AasRegistry_Is_Unhealthy() { var clientFactory = Substitute.For(); - clientFactory.CreateClient(AasEnvironmentConfig.AasRegistryHealthCheck).Returns(CreateHttpClient(HttpStatusCode.InternalServerError)); - clientFactory.CreateClient(AasEnvironmentConfig.SubmodelRegistryHealthCheck).Returns(CreateHttpClient(HttpStatusCode.OK)); + clientFactory.CreateClient(HttpClientNames.AasRegistryHealthCheck).Returns(CreateHttpClient(HttpStatusCode.InternalServerError)); + clientFactory.CreateClient(HttpClientNames.SubmodelRegistryHealthCheck).Returns(CreateHttpClient(HttpStatusCode.OK)); var logger = Substitute.For>(); @@ -129,16 +129,16 @@ public async Task CheckHealthAsync_Checks_Both_Endpoints_In_Parallel_Even_When_A _ = await sut.CheckHealthAsync(new HealthCheckContext(), CancellationToken.None); - clientFactory.Received(1).CreateClient(AasEnvironmentConfig.AasRegistryHealthCheck); - clientFactory.Received(1).CreateClient(AasEnvironmentConfig.SubmodelRegistryHealthCheck); + clientFactory.Received(1).CreateClient(HttpClientNames.AasRegistryHealthCheck); + clientFactory.Received(1).CreateClient(HttpClientNames.SubmodelRegistryHealthCheck); } [Fact] public async Task CheckHealthAsync_Uses_HealthCheck_Client_Names_Without_Retry_Policy() { var clientFactory = Substitute.For(); - clientFactory.CreateClient(AasEnvironmentConfig.AasRegistryHealthCheck).Returns(CreateHttpClient(HttpStatusCode.OK)); - clientFactory.CreateClient(AasEnvironmentConfig.SubmodelRegistryHealthCheck).Returns(CreateHttpClient(HttpStatusCode.OK)); + clientFactory.CreateClient(HttpClientNames.AasRegistryHealthCheck).Returns(CreateHttpClient(HttpStatusCode.OK)); + clientFactory.CreateClient(HttpClientNames.SubmodelRegistryHealthCheck).Returns(CreateHttpClient(HttpStatusCode.OK)); var logger = Substitute.For>(); @@ -146,10 +146,10 @@ public async Task CheckHealthAsync_Uses_HealthCheck_Client_Names_Without_Retry_P _ = await sut.CheckHealthAsync(new HealthCheckContext(), CancellationToken.None); - clientFactory.Received().CreateClient(AasEnvironmentConfig.AasRegistryHealthCheck); - clientFactory.Received().CreateClient(AasEnvironmentConfig.SubmodelRegistryHealthCheck); - clientFactory.DidNotReceive().CreateClient(AasEnvironmentConfig.AasRegistry); - clientFactory.DidNotReceive().CreateClient(AasEnvironmentConfig.SubmodelRegistry); + clientFactory.Received().CreateClient(HttpClientNames.AasRegistryHealthCheck); + clientFactory.Received().CreateClient(HttpClientNames.SubmodelRegistryHealthCheck); + clientFactory.DidNotReceive().CreateClient(HttpClientNames.AasRegistry); + clientFactory.DidNotReceive().CreateClient(HttpClientNames.SubmodelRegistry); } private static HttpClient CreateThrowingHttpClient(Exception exception) diff --git a/source/AAS.TwinEngine.DataEngine.UnitTests/Infrastructure/Monitoring/TemplateRepositoryHealthCheckTests.cs b/source/AAS.TwinEngine.DataEngine.UnitTests/Infrastructure/Monitoring/TemplateRepositoryHealthCheckTests.cs index 82aca052..30f58f61 100644 --- a/source/AAS.TwinEngine.DataEngine.UnitTests/Infrastructure/Monitoring/TemplateRepositoryHealthCheckTests.cs +++ b/source/AAS.TwinEngine.DataEngine.UnitTests/Infrastructure/Monitoring/TemplateRepositoryHealthCheckTests.cs @@ -1,6 +1,6 @@ -using System.Net; +using System.Net; -using AAS.TwinEngine.DataEngine.ApplicationLogic.Services.Plugin.Config; +using AAS.TwinEngine.DataEngine.ServiceConfiguration.Config; using AAS.TwinEngine.DataEngine.Infrastructure.Http.Clients; using AAS.TwinEngine.DataEngine.Infrastructure.Monitoring; @@ -83,13 +83,13 @@ public async Task CheckHealthAsync_OneRepositoryFails_ReturnsUnhealthy() var successClient = CreateHttpClient(HttpStatusCode.OK); var failClient = CreateHttpClient(HttpStatusCode.InternalServerError); - _clientFactory.CreateClient(AasEnvironmentConfig.SubmodelTemplateRepositoryHealthCheck) + _clientFactory.CreateClient(HttpClientNames.SubmodelTemplateRepositoryHealthCheck) .Returns(successClient); - _clientFactory.CreateClient(AasEnvironmentConfig.AasTemplateRepositoryHealthCheck) + _clientFactory.CreateClient(HttpClientNames.AasTemplateRepositoryHealthCheck) .Returns(failClient); - _clientFactory.CreateClient(AasEnvironmentConfig.ConceptDescriptorTemplateRepositoryHealthCheck) + _clientFactory.CreateClient(HttpClientNames.ConceptDescriptorTemplateRepositoryHealthCheck) .Returns(successClient); var sut = CreateSut(); diff --git a/source/AAS.TwinEngine.DataEngine.UnitTests/Infrastructure/Providers/PluginDataProvider/Services/PluginDataHandlerTests.cs b/source/AAS.TwinEngine.DataEngine.UnitTests/Infrastructure/Providers/PluginDataProvider/Services/PluginDataHandlerTests.cs index 10d178de..46a8d696 100644 --- a/source/AAS.TwinEngine.DataEngine.UnitTests/Infrastructure/Providers/PluginDataProvider/Services/PluginDataHandlerTests.cs +++ b/source/AAS.TwinEngine.DataEngine.UnitTests/Infrastructure/Providers/PluginDataProvider/Services/PluginDataHandlerTests.cs @@ -1,4 +1,4 @@ -using System.Net; +using System.Net; using System.Net.Http.Json; using System.Text; using System.Text.Json; @@ -14,7 +14,6 @@ using AAS.TwinEngine.DataEngine.DomainModel.Plugin; using AAS.TwinEngine.DataEngine.DomainModel.Shared; using AAS.TwinEngine.DataEngine.DomainModel.SubmodelRepository; -using AAS.TwinEngine.DataEngine.Infrastructure.Providers.PluginDataProvider.Config; using AAS.TwinEngine.DataEngine.Infrastructure.Providers.PluginDataProvider.Services; using Json.Schema; @@ -23,6 +22,8 @@ using NSubstitute; +using AAS.TwinEngine.DataEngine.ServiceConfiguration.Config; + namespace AAS.TwinEngine.DataEngine.UnitTests.Infrastructure.Providers.PluginDataProvider.Services; public class PluginDataHandlerTests @@ -86,7 +87,7 @@ public async Task TryGetValuesAsync_WithValidManifestAndResponse_ReturnsMergedSe var requestList = new List { - new($"{PluginConfig.HttpClientNamePrefix}TestPlugin", jsonContent) + new($"{HttpClientNames.PluginDataProviderPrefix}TestPlugin", jsonContent) }; var manifests = new List @@ -174,7 +175,7 @@ public async Task GetDataForAllShellDescriptorsAsync_ReturnsListWithHrefSet() .Returns(new List { "PluginA" }); _pluginRequestBuilder.Build(Arg.Any>()) - .Returns(new List { new($"{PluginConfig.HttpClientNamePrefix}PluginA", "") }); + .Returns(new List { new($"{HttpClientNames.PluginDataProviderPrefix}PluginA", "") }); _pluginDataProvider .GetDataForAllShellDescriptorsAsync(null, null, Arg.Any>(), Arg.Any()) @@ -242,7 +243,7 @@ public async Task GetDataForShellDescriptorAsync_ReturnsSingleWithHrefSet() .Returns(new List { "PluginA" }); _pluginRequestBuilder.Build(Arg.Any>(), Arg.Any()) - .Returns(new List { new($"{PluginConfig.HttpClientNamePrefix}PluginA", "") }); + .Returns(new List { new($"{HttpClientNames.PluginDataProviderPrefix}PluginA", "") }); _pluginDataProvider .GetDataForShellDescriptorByIdAsync(Arg.Any>(), Arg.Any()) @@ -307,7 +308,7 @@ public async Task GetDataForAssetInformationByIdAsync_ReturnsAssetInformation() .Returns(new List { "PluginA" }); _pluginRequestBuilder.Build(Arg.Any>(), Arg.Any()) - .Returns(new List { new($"{PluginConfig.HttpClientNamePrefix}PluginA", "") }); + .Returns(new List { new($"{HttpClientNames.PluginDataProviderPrefix}PluginA", "") }); _pluginDataProvider .GetDataForAssetInformationByIdAsync(Arg.Any>(), Arg.Any()) diff --git a/source/AAS.TwinEngine.DataEngine.UnitTests/Infrastructure/Providers/PluginDataProvider/Services/PluginDataProviderTests.cs b/source/AAS.TwinEngine.DataEngine.UnitTests/Infrastructure/Providers/PluginDataProvider/Services/PluginDataProviderTests.cs index 6f694496..788302ea 100644 --- a/source/AAS.TwinEngine.DataEngine.UnitTests/Infrastructure/Providers/PluginDataProvider/Services/PluginDataProviderTests.cs +++ b/source/AAS.TwinEngine.DataEngine.UnitTests/Infrastructure/Providers/PluginDataProvider/Services/PluginDataProviderTests.cs @@ -1,4 +1,4 @@ -using System.Net; +using System.Net; using System.Net.Http.Json; using System.Text; using System.Text.Json; @@ -8,7 +8,6 @@ using AAS.TwinEngine.DataEngine.DomainModel.AasRegistry; using AAS.TwinEngine.DataEngine.DomainModel.Plugin; using AAS.TwinEngine.DataEngine.Infrastructure.Http.Clients; -using AAS.TwinEngine.DataEngine.Infrastructure.Providers.PluginDataProvider.Config; using AasCore.Aas3_0; @@ -19,6 +18,8 @@ using PluginDataProviderRepo = AAS.TwinEngine.DataEngine.Infrastructure.Providers.PluginDataProvider.Services; using UnauthorizedAccessException = AAS.TwinEngine.DataEngine.ApplicationLogic.Exceptions.Infrastructure.UnauthorizedAccessException; +using AAS.TwinEngine.DataEngine.ServiceConfiguration.Config; + namespace AAS.TwinEngine.DataEngine.UnitTests.Infrastructure.Providers.PluginDataProvider.Services; public class PluginDataProviderTests @@ -63,7 +64,7 @@ public async Task GetDataForSemanticIdsAsync_ReturnsValuesAsync(string endpoint) using var httpClient = new HttpClient(messageHandler); httpClient.BaseAddress = new Uri(endpoint); - var httpClientName = $"{PluginConfig.HttpClientNamePrefix}PluginName"; + var httpClientName = $"{HttpClientNames.PluginDataProviderPrefix}PluginName"; _httpClientFactory.CreateClient(httpClientName).Returns(httpClient); using var simpleRequestSchema = ConvertToJsonContent(SimpleRequestSchema); @@ -86,7 +87,7 @@ public async Task GetDataForSemanticIdsAsync_ReturnsValuesAsync(string endpoint) public async Task GetDataForSemanticIdsAsync_404Response_ThrowsResourceNotFoundExceptionAsync() { using var simpleRequestSchema = ConvertToJsonContent(SimpleRequestSchema); - var pluginRequest = new PluginRequestSubmodel($"{PluginConfig.HttpClientNamePrefix}PluginName", simpleRequestSchema); + var pluginRequest = new PluginRequestSubmodel($"{HttpClientNames.PluginDataProviderPrefix}PluginName", simpleRequestSchema); var pluginRequests = new List { pluginRequest }; using var messageHandler = new FakeHttpMessageHandler((_, _) => @@ -121,11 +122,11 @@ public async Task GetDataForAllShellDescriptorsAsync_ShouldReturnRawContent() using var httpClient = new HttpClient(messageHandler); httpClient.BaseAddress = new Uri("https://example.com"); - _httpClientFactory.CreateClient(PluginConfig.MetaData).Returns(httpClient); + _httpClientFactory.CreateClient(ApiPaths.PluginMetadata).Returns(httpClient); var metadata = new List { - new(PluginConfig.MetaData, "") + new(ApiPaths.PluginMetadata, "") }; var result = await _sut.GetDataForAllShellDescriptorsAsync(null, null, metadata, CancellationToken.None); @@ -149,12 +150,12 @@ public async Task GetDataForAllShellDescriptorsAsync_ShouldThrowNotFound_WhenAll return Task.FromResult(new HttpResponseMessage(callCount % 2 == 0 ? HttpStatusCode.NotFound : HttpStatusCode.NotFound)); }); - _httpClientFactory.CreateClient(PluginConfig.MetaData).Returns(_ => new HttpClient(messageHandler) { BaseAddress = new Uri("https://example.com") }); + _httpClientFactory.CreateClient(ApiPaths.PluginMetadata).Returns(_ => new HttpClient(messageHandler) { BaseAddress = new Uri("https://example.com") }); var metadata = new List { - new(PluginConfig.MetaData, "plugin1"), - new(PluginConfig.MetaData, "plugin2"), + new(ApiPaths.PluginMetadata, "plugin1"), + new(ApiPaths.PluginMetadata, "plugin2"), }; await Assert.ThrowsAsync(() => _sut.GetDataForAllShellDescriptorsAsync(null, null, metadata, CancellationToken.None)); @@ -167,11 +168,11 @@ public async Task GetDataForAllShellDescriptorsAsync_ShouldThrowUnauthorized_Whe using var httpClient = new HttpClient(messageHandler); httpClient.BaseAddress = new Uri("https://example.com"); - _httpClientFactory.CreateClient(PluginConfig.MetaData).Returns(httpClient); + _httpClientFactory.CreateClient(ApiPaths.PluginMetadata).Returns(httpClient); var metadata = new List { - new(PluginConfig.MetaData, "") + new(ApiPaths.PluginMetadata, "") }; await Assert.ThrowsAsync(() => _sut.GetDataForAllShellDescriptorsAsync(null, null, metadata, CancellationToken.None)); @@ -187,12 +188,12 @@ public async Task GetDataForAllShellDescriptorsAsync_ShouldThrowResponseParsingE return Task.FromResult(new HttpResponseMessage(callCount % 2 == 0 ? HttpStatusCode.Conflict : HttpStatusCode.Conflict)); }); - _httpClientFactory.CreateClient(PluginConfig.MetaData).Returns(_ => new HttpClient(messageHandler) { BaseAddress = new Uri("https://example.com") }); + _httpClientFactory.CreateClient(ApiPaths.PluginMetadata).Returns(_ => new HttpClient(messageHandler) { BaseAddress = new Uri("https://example.com") }); var metadata = new List { - new(PluginConfig.MetaData, "plugin1"), - new(PluginConfig.MetaData, "plugin2"), + new(ApiPaths.PluginMetadata, "plugin1"), + new(ApiPaths.PluginMetadata, "plugin2"), }; await Assert.ThrowsAsync(() => _sut.GetDataForAllShellDescriptorsAsync(null, null, metadata, CancellationToken.None)); @@ -208,12 +209,12 @@ public async Task GetDataForAllShellDescriptorsAsync_ShouldThrowBadRequest_WhenR return Task.FromResult(new HttpResponseMessage(callCount % 2 == 0 ? HttpStatusCode.Unauthorized : HttpStatusCode.NotFound)); }); - _httpClientFactory.CreateClient(PluginConfig.MetaData).Returns(_ => new HttpClient(messageHandler) { BaseAddress = new Uri("https://example.com") }); + _httpClientFactory.CreateClient(ApiPaths.PluginMetadata).Returns(_ => new HttpClient(messageHandler) { BaseAddress = new Uri("https://example.com") }); var metadata = new List { - new(PluginConfig.MetaData, "plugin1"), - new(PluginConfig.MetaData, "plugin2"), + new(ApiPaths.PluginMetadata, "plugin1"), + new(ApiPaths.PluginMetadata, "plugin2"), }; await Assert.ThrowsAsync(() => _sut.GetDataForAllShellDescriptorsAsync(null, null, metadata, CancellationToken.None)); @@ -235,14 +236,14 @@ public async Task GetDataForShellDescriptorByIdAsync_ShouldReturnRawContent() }); using var httpClient = new HttpClient(messageHandler) { BaseAddress = new Uri("https://testendpoint.com") }; - _httpClientFactory.CreateClient(PluginConfig.MetaData).Returns(httpClient); + _httpClientFactory.CreateClient(ApiPaths.PluginMetadata).Returns(httpClient); var testId = GetTestShellDescriptorDataList().First().Id!; var expectedEncoded = testId.EncodeBase64Url(); var metadata = new List { - new(PluginConfig.MetaData, testId) + new(ApiPaths.PluginMetadata, testId) }; var result = await _sut.GetDataForShellDescriptorByIdAsync(metadata, CancellationToken.None); @@ -275,14 +276,14 @@ public async Task GetDataForAssetInformationByIdAsync_ShouldReturnRawContent() }); using var httpClient = new HttpClient(messageHandler) { BaseAddress = new Uri("https://testendpoint.com") }; - _httpClientFactory.CreateClient(PluginConfig.MetaData).Returns(httpClient); + _httpClientFactory.CreateClient(ApiPaths.PluginMetadata).Returns(httpClient); var testId = CreateAssetInformation().GlobalAssetId!; var expectedEncoded = testId.EncodeBase64Url(); var metadata = new List { - new(PluginConfig.MetaData, testId) + new(ApiPaths.PluginMetadata, testId) }; var result = await _sut.GetDataForAssetInformationByIdAsync(metadata, CancellationToken.None); @@ -309,7 +310,7 @@ await Assert.ThrowsAsync(() => public async Task GetDataForSemanticIdsAsync_WhenTaskCanceled_ThrowsRequestTimeoutException() { using var simpleRequestSchema = ConvertToJsonContent(SimpleRequestSchema); - var pluginRequest = new PluginRequestSubmodel($"{PluginConfig.HttpClientNamePrefix}PluginName", simpleRequestSchema); + var pluginRequest = new PluginRequestSubmodel($"{HttpClientNames.PluginDataProviderPrefix}PluginName", simpleRequestSchema); var pluginRequests = new List { pluginRequest }; using var messageHandler = new FakeHttpMessageHandler((_, _) => throw new TaskCanceledException("timeout")); @@ -326,7 +327,7 @@ await Assert.ThrowsAsync(() => public async Task GetDataForSemanticIdsAsync_WhenUnauthorizedOrForbidden_ThrowsServiceAuthorizationException(HttpStatusCode statusCode) { using var simpleRequestSchema = ConvertToJsonContent(SimpleRequestSchema); - var pluginRequest = new PluginRequestSubmodel($"{PluginConfig.HttpClientNamePrefix}PluginName", simpleRequestSchema); + var pluginRequest = new PluginRequestSubmodel($"{HttpClientNames.PluginDataProviderPrefix}PluginName", simpleRequestSchema); var pluginRequests = new List { pluginRequest }; using var messageHandler = new FakeHttpMessageHandler((_, _) => @@ -347,7 +348,7 @@ await Assert.ThrowsAsync(() => public async Task GetDataForSemanticIdsAsync_WhenUnexpectedStatusCode_ThrowsResponseParsingException() { using var simpleRequestSchema = ConvertToJsonContent(SimpleRequestSchema); - var pluginRequest = new PluginRequestSubmodel($"{PluginConfig.HttpClientNamePrefix}PluginName", simpleRequestSchema); + var pluginRequest = new PluginRequestSubmodel($"{HttpClientNames.PluginDataProviderPrefix}PluginName", simpleRequestSchema); var pluginRequests = new List { pluginRequest }; using var messageHandler = new FakeHttpMessageHandler((_, _) => diff --git a/source/AAS.TwinEngine.DataEngine.UnitTests/Infrastructure/Providers/PluginDataProvider/Services/PluginManifestProviderTests.cs b/source/AAS.TwinEngine.DataEngine.UnitTests/Infrastructure/Providers/PluginDataProvider/Services/PluginManifestProviderTests.cs index abe6bf07..393ed92a 100644 --- a/source/AAS.TwinEngine.DataEngine.UnitTests/Infrastructure/Providers/PluginDataProvider/Services/PluginManifestProviderTests.cs +++ b/source/AAS.TwinEngine.DataEngine.UnitTests/Infrastructure/Providers/PluginDataProvider/Services/PluginManifestProviderTests.cs @@ -1,8 +1,7 @@ -using System.Net; +using System.Net; using AAS.TwinEngine.DataEngine.ApplicationLogic.Exceptions.Infrastructure; using AAS.TwinEngine.DataEngine.Infrastructure.Http.Clients; -using AAS.TwinEngine.DataEngine.Infrastructure.Providers.PluginDataProvider.Config; using AAS.TwinEngine.DataEngine.Infrastructure.Providers.PluginDataProvider.Services; using AAS.TwinEngine.DataEngine.ServiceConfiguration.Config; @@ -32,7 +31,7 @@ public class PluginManifestProviderTests private PluginManifestProvider CreateSut(HttpClient httpClient) { - _clientFactory.CreateClient($"{PluginConfig.HttpClientNamePrefix}TestPlugin").Returns(httpClient); + _clientFactory.CreateClient($"{HttpClientNames.PluginDataProviderPrefix}TestPlugin").Returns(httpClient); return new PluginManifestProvider(_logger, _options, _clientFactory); } diff --git a/source/AAS.TwinEngine.DataEngine.UnitTests/Infrastructure/Providers/PluginDataProvider/Services/PluginRequestBuilderTests.cs b/source/AAS.TwinEngine.DataEngine.UnitTests/Infrastructure/Providers/PluginDataProvider/Services/PluginRequestBuilderTests.cs index 69d73d10..a6281592 100644 --- a/source/AAS.TwinEngine.DataEngine.UnitTests/Infrastructure/Providers/PluginDataProvider/Services/PluginRequestBuilderTests.cs +++ b/source/AAS.TwinEngine.DataEngine.UnitTests/Infrastructure/Providers/PluginDataProvider/Services/PluginRequestBuilderTests.cs @@ -1,14 +1,15 @@ -using System.Net.Http.Json; +using System.Net.Http.Json; using AAS.TwinEngine.DataEngine.ApplicationLogic.Exceptions.Infrastructure; using AAS.TwinEngine.DataEngine.Infrastructure.Monitoring; -using AAS.TwinEngine.DataEngine.Infrastructure.Providers.PluginDataProvider.Config; using AAS.TwinEngine.DataEngine.Infrastructure.Providers.PluginDataProvider.Services; using Json.Schema; using NSubstitute; +using AAS.TwinEngine.DataEngine.ServiceConfiguration.Config; + namespace AAS.TwinEngine.DataEngine.UnitTests.Infrastructure.Providers.PluginDataProvider.Services; public class PluginRequestBuilderTests @@ -49,8 +50,8 @@ public void Build_WhenManifestIsHealthy_ShouldCreatePluginRequestSubmodels() var result = _sut.Build(jsonSchemas).ToList(); Assert.Equal(2, result.Count); - Assert.Contains(result, r => r.HttpClientName == $"{PluginConfig.HttpClientNamePrefix}schema1"); - Assert.Contains(result, r => r.HttpClientName == $"{PluginConfig.HttpClientNamePrefix}schema2"); + Assert.Contains(result, r => r.HttpClientName == $"{HttpClientNames.PluginDataProviderPrefix}schema1"); + Assert.Contains(result, r => r.HttpClientName == $"{HttpClientNames.PluginDataProviderPrefix}schema2"); } [Fact] @@ -80,8 +81,8 @@ public void Build_ShouldCreatePluginRequestSubmodels_ForEachSchema() var result = _sut.Build(jsonSchemas).ToList(); Assert.Equal(2, result.Count); - Assert.Contains(result, r => r.HttpClientName == $"{PluginConfig.HttpClientNamePrefix}schema1"); - Assert.Contains(result, r => r.HttpClientName == $"{PluginConfig.HttpClientNamePrefix}schema2"); + Assert.Contains(result, r => r.HttpClientName == $"{HttpClientNames.PluginDataProviderPrefix}schema1"); + Assert.Contains(result, r => r.HttpClientName == $"{HttpClientNames.PluginDataProviderPrefix}schema2"); } [Fact] @@ -116,12 +117,12 @@ public void Build_WhenPluginsProvided_ReturnsCorrectMetaData() Assert.Collection(result, item => { - Assert.Equal($"{PluginConfig.HttpClientNamePrefix}PluginA", item.HttpClientName); + Assert.Equal($"{HttpClientNames.PluginDataProviderPrefix}PluginA", item.HttpClientName); Assert.Equal(string.Empty, item.AasIdentifier); }, item => { - Assert.Equal($"{PluginConfig.HttpClientNamePrefix}PluginB", item.HttpClientName); + Assert.Equal($"{HttpClientNames.PluginDataProviderPrefix}PluginB", item.HttpClientName); Assert.Equal(string.Empty, item.AasIdentifier); }); } @@ -136,7 +137,7 @@ public void Build_WhenAasIdentifierProvided_AssignsToAllResults() var result = _sut.Build(plugins, AasIdentifier); var item = Assert.Single(result); - Assert.Equal($"{PluginConfig.HttpClientNamePrefix}PluginA", item.HttpClientName); + Assert.Equal($"{HttpClientNames.PluginDataProviderPrefix}PluginA", item.HttpClientName); Assert.Equal("aas-123", item.AasIdentifier); } @@ -149,7 +150,7 @@ public void Build_WhenAasIdentifierIsNull_AssignsEmptyString() var result = _sut.Build(plugins, null); var item = Assert.Single(result); - Assert.Equal($"{PluginConfig.HttpClientNamePrefix}PluginA", item.HttpClientName); + Assert.Equal($"{HttpClientNames.PluginDataProviderPrefix}PluginA", item.HttpClientName); Assert.Equal(string.Empty, item.AasIdentifier); } diff --git a/source/AAS.TwinEngine.DataEngine.UnitTests/Infrastructure/Providers/SubmodelRegistryProvider/Services/SubmodelDescriptorProviderTests.cs b/source/AAS.TwinEngine.DataEngine.UnitTests/Infrastructure/Providers/SubmodelRegistryProvider/Services/SubmodelDescriptorProviderTests.cs index b763e11a..dcbea15c 100644 --- a/source/AAS.TwinEngine.DataEngine.UnitTests/Infrastructure/Providers/SubmodelRegistryProvider/Services/SubmodelDescriptorProviderTests.cs +++ b/source/AAS.TwinEngine.DataEngine.UnitTests/Infrastructure/Providers/SubmodelRegistryProvider/Services/SubmodelDescriptorProviderTests.cs @@ -1,7 +1,7 @@ -using System.Net; +using System.Net; using AAS.TwinEngine.DataEngine.ApplicationLogic.Exceptions.Infrastructure; -using AAS.TwinEngine.DataEngine.ApplicationLogic.Services.Plugin.Config; +using AAS.TwinEngine.DataEngine.ServiceConfiguration.Config; using AAS.TwinEngine.DataEngine.DomainModel.SubmodelRegistry; using AAS.TwinEngine.DataEngine.Infrastructure.Http.Clients; using AAS.TwinEngine.DataEngine.Infrastructure.Providers.SubmodelRegistryProvider.Services; @@ -36,7 +36,7 @@ public async Task GetDataForSubmodelDescriptorByIdAsync_ReturnsSubmodelDesciptor })); using var httpClient = new HttpClient(messageHandler); httpClient.BaseAddress = new Uri("https://mm-software/fakeUrl"); - _clientFactory.CreateClient(AasEnvironmentConfig.SubmodelRegistry) + _clientFactory.CreateClient(HttpClientNames.SubmodelRegistry) .Returns(httpClient); var result = await _sut.GetDataForSubmodelDescriptorByIdAsync(Id, CancellationToken.None); @@ -56,7 +56,7 @@ public async Task GetDataForSubmodelDescriptorByIdAsync_ThrowsResponseParsingExc })); using var httpClient = new HttpClient(messageHandler); httpClient.BaseAddress = new Uri("https://mm-software/fakeUrl"); - _clientFactory.CreateClient(AasEnvironmentConfig.SubmodelRegistry) + _clientFactory.CreateClient(HttpClientNames.SubmodelRegistry) .Returns(httpClient); await Assert.ThrowsAsync(() => _sut.GetDataForSubmodelDescriptorByIdAsync(Id, CancellationToken.None)); @@ -74,7 +74,7 @@ public async Task GetDataForSubmodelDescriptorByIdAsync_ThrowsResponseParsingExc })); using var httpClient = new HttpClient(messageHandler); httpClient.BaseAddress = new Uri("https://mm-software/fakeUrl"); - _clientFactory.CreateClient(AasEnvironmentConfig.SubmodelRegistry) + _clientFactory.CreateClient(HttpClientNames.SubmodelRegistry) .Returns(httpClient); await Assert.ThrowsAsync(() => _sut.GetDataForSubmodelDescriptorByIdAsync(Id, CancellationToken.None)); @@ -92,7 +92,7 @@ public async Task GetDataForSubmodelDescriptorByIdAsync_ThrowsResourceNotFoundEx })); using var httpClient = new HttpClient(messageHandler); httpClient.BaseAddress = new Uri("https://mm-software/fakeUrl"); - _clientFactory.CreateClient(AasEnvironmentConfig.SubmodelRegistry) + _clientFactory.CreateClient(HttpClientNames.SubmodelRegistry) .Returns(httpClient); await Assert.ThrowsAsync(() => @@ -111,7 +111,7 @@ public async Task GetDataForSubmodelDescriptorByIdAsync_ThrowsServiceAuthorizati })); using var httpClient = new HttpClient(messageHandler); httpClient.BaseAddress = new Uri("https://mm-software/fakeUrl"); - _clientFactory.CreateClient(AasEnvironmentConfig.SubmodelRegistry) + _clientFactory.CreateClient(HttpClientNames.SubmodelRegistry) .Returns(httpClient); await Assert.ThrowsAsync(() => @@ -130,7 +130,7 @@ public async Task GetDataForSubmodelDescriptorByIdAsync_ThrowsServiceAuthorizati })); using var httpClient = new HttpClient(messageHandler); httpClient.BaseAddress = new Uri("https://mm-software/fakeUrl"); - _clientFactory.CreateClient(AasEnvironmentConfig.SubmodelRegistry) + _clientFactory.CreateClient(HttpClientNames.SubmodelRegistry) .Returns(httpClient); await Assert.ThrowsAsync(() => @@ -149,7 +149,7 @@ public async Task GetDataForSubmodelDescriptorByIdAsync_ThrowsRequestTimeoutExce })); using var httpClient = new HttpClient(messageHandler); httpClient.BaseAddress = new Uri("https://mm-software/fakeUrl"); - _clientFactory.CreateClient(AasEnvironmentConfig.SubmodelRegistry) + _clientFactory.CreateClient(HttpClientNames.SubmodelRegistry) .Returns(httpClient); await Assert.ThrowsAsync(() => @@ -168,7 +168,7 @@ public async Task GetDataForSubmodelDescriptorByIdAsync_ThrowsValidationFailedEx })); using var httpClient = new HttpClient(messageHandler); httpClient.BaseAddress = new Uri("https://mm-software/fakeUrl"); - _clientFactory.CreateClient(AasEnvironmentConfig.SubmodelRegistry) + _clientFactory.CreateClient(HttpClientNames.SubmodelRegistry) .Returns(httpClient); await Assert.ThrowsAsync(() => diff --git a/source/AAS.TwinEngine.DataEngine.UnitTests/Infrastructure/Providers/TemplateProvider/Services/TemplateProviderTests.cs b/source/AAS.TwinEngine.DataEngine.UnitTests/Infrastructure/Providers/TemplateProvider/Services/TemplateProviderTests.cs index 9a8a77c1..d17d9d7b 100644 --- a/source/AAS.TwinEngine.DataEngine.UnitTests/Infrastructure/Providers/TemplateProvider/Services/TemplateProviderTests.cs +++ b/source/AAS.TwinEngine.DataEngine.UnitTests/Infrastructure/Providers/TemplateProvider/Services/TemplateProviderTests.cs @@ -1,8 +1,8 @@ -using System.Net; +using System.Net; using System.Text.Json; using AAS.TwinEngine.DataEngine.ApplicationLogic.Exceptions.Infrastructure; -using AAS.TwinEngine.DataEngine.ApplicationLogic.Services.Plugin.Config; +using AAS.TwinEngine.DataEngine.ServiceConfiguration.Config; using AAS.TwinEngine.DataEngine.DomainModel.AasRegistry; using AAS.TwinEngine.DataEngine.Infrastructure.Http.Clients; @@ -37,7 +37,7 @@ public async Task GetSubmodelTemplateAsync_ReturnsSubmodel_WhenValidResponse() using var mockHttpMessageHandler = new FakeHttpMessageHandler(mockHttpResponse); using var httpClient = new HttpClient(mockHttpMessageHandler); httpClient.BaseAddress = new Uri("https://www.mm-software.com/fakeurl"); - _httpClientFactory.CreateClient(AasEnvironmentConfig.SubmodelTemplateRepository).Returns(httpClient); + _httpClientFactory.CreateClient(HttpClientNames.SubmodelTemplateRepository).Returns(httpClient); var result = await _sut.GetSubmodelTemplateAsync(TemplateId, CancellationToken.None); @@ -54,7 +54,7 @@ public async Task GetSubmodelTemplateAsync_ThrowsResponseParsingException_WhenIn using var mockHttpMessageHandler = new FakeHttpMessageHandler(mockHttpResponse); using var httpClient = new HttpClient(mockHttpMessageHandler); httpClient.BaseAddress = new Uri("https://www.mm-software.com/fakeurl"); - _httpClientFactory.CreateClient(AasEnvironmentConfig.SubmodelTemplateRepository).Returns(httpClient); + _httpClientFactory.CreateClient(HttpClientNames.SubmodelTemplateRepository).Returns(httpClient); await Assert.ThrowsAsync(() => _sut.GetSubmodelTemplateAsync(TemplateId, CancellationToken.None)); } @@ -67,7 +67,7 @@ public async Task GetSubmodelTemplateAsync_ThrowsResourceNotFoundException_WhenN using var mockHttpMessageHandler = new FakeHttpMessageHandler(mockHttpResponse); using var httpClient = new HttpClient(mockHttpMessageHandler); httpClient.BaseAddress = new Uri("https://www.mm-software.com/fakeurl"); - _httpClientFactory.CreateClient(AasEnvironmentConfig.SubmodelTemplateRepository).Returns(httpClient); + _httpClientFactory.CreateClient(HttpClientNames.SubmodelTemplateRepository).Returns(httpClient); var exception = await Assert.ThrowsAsync(() => _sut.GetSubmodelTemplateAsync(TemplateId, CancellationToken.None)); } @@ -78,7 +78,7 @@ public async Task GetSubmodelTemplateAsync_ThrowsException_WhenHttpClientFails() using var mockHttpMessageHandler = new FakeHttpMessageHandler(new HttpRequestException("Network error")); using var httpClient = new HttpClient(mockHttpMessageHandler); httpClient.BaseAddress = new Uri("https://www.mm-software.com/fakeurl"); - _httpClientFactory.CreateClient(AasEnvironmentConfig.SubmodelTemplateRepository).Returns(httpClient); + _httpClientFactory.CreateClient(HttpClientNames.SubmodelTemplateRepository).Returns(httpClient); var exception = await Assert.ThrowsAsync(() => _sut.GetSubmodelTemplateAsync(TemplateId, CancellationToken.None)); Assert.Equal("Network error", exception.Message); @@ -92,7 +92,7 @@ public async Task GetShellDescriptorsTemplateAsync_ReturnsShellDescriptor_WhenVa using var mockHttpMessageHandler = new FakeHttpMessageHandler(mockHttpResponse); using var httpClient = new HttpClient(mockHttpMessageHandler); httpClient.BaseAddress = new Uri("https://www.mm-software.com/fakeurl"); - _httpClientFactory.CreateClient(AasEnvironmentConfig.AasRegistry).Returns(httpClient); + _httpClientFactory.CreateClient(HttpClientNames.AasRegistry).Returns(httpClient); var result = await _sut.GetShellDescriptorsTemplateAsync(CancellationToken.None); @@ -110,7 +110,7 @@ public async Task GetShellDescriptorsTemplateAsync_ThrowsResponseParsingExceptio using var mockHttpMessageHandler = new FakeHttpMessageHandler(mockHttpResponse); using var httpClient = new HttpClient(mockHttpMessageHandler); httpClient.BaseAddress = new Uri("https://www.mm-software.com/fakeurl"); - _httpClientFactory.CreateClient(AasEnvironmentConfig.AasRegistry).Returns(httpClient); + _httpClientFactory.CreateClient(HttpClientNames.AasRegistry).Returns(httpClient); await Assert.ThrowsAsync(() => _sut.GetShellDescriptorsTemplateAsync(CancellationToken.None)); } @@ -125,7 +125,7 @@ public async Task GetShellDescriptorsTemplateAsync_ThrowsResourceNotFoundExcepti using var mockHttpMessageHandler = new FakeHttpMessageHandler(mockHttpResponse); using var httpClient = new HttpClient(mockHttpMessageHandler); httpClient.BaseAddress = new Uri("https://www.mm-software.com/fakeurl"); - _httpClientFactory.CreateClient(AasEnvironmentConfig.AasRegistry).Returns(httpClient); + _httpClientFactory.CreateClient(HttpClientNames.AasRegistry).Returns(httpClient); await Assert.ThrowsAsync(() => _sut.GetShellDescriptorsTemplateAsync(CancellationToken.None)); @@ -141,7 +141,7 @@ public async Task GetShellDescriptorsTemplateAsync_ReturnsDefaultShellDescriptor using var mockHttpMessageHandler = new FakeHttpMessageHandler(mockHttpResponse); using var httpClient = new HttpClient(mockHttpMessageHandler); httpClient.BaseAddress = new Uri("https://www.mm-software.com/fakeurl"); - _httpClientFactory.CreateClient(AasEnvironmentConfig.AasRegistry).Returns(httpClient); + _httpClientFactory.CreateClient(HttpClientNames.AasRegistry).Returns(httpClient); var result = await _sut.GetShellDescriptorsTemplateAsync(CancellationToken.None); @@ -160,7 +160,7 @@ public async Task GetShellDescriptorsTemplateAsync_ThrowsResponseParsingExceptio using var mockHttpMessageHandler = new FakeHttpMessageHandler(mockHttpResponse); using var httpClient = new HttpClient(mockHttpMessageHandler); httpClient.BaseAddress = new Uri("https://www.mm-software.com/fakeurl"); - _httpClientFactory.CreateClient(AasEnvironmentConfig.AasRegistry).Returns(httpClient); + _httpClientFactory.CreateClient(HttpClientNames.AasRegistry).Returns(httpClient); await Assert.ThrowsAsync(() => _sut.GetShellDescriptorsTemplateAsync(CancellationToken.None)); } @@ -173,7 +173,7 @@ public async Task GetShellDescriptorsTemplateAsync_ThrowsResourceNotFoundExcepti using var mockHttpMessageHandler = new FakeHttpMessageHandler(mockHttpResponse); using var httpClient = new HttpClient(mockHttpMessageHandler); httpClient.BaseAddress = new Uri("https://www.mm-software.com/fakeurl"); - _httpClientFactory.CreateClient(AasEnvironmentConfig.AasRegistry).Returns(httpClient); + _httpClientFactory.CreateClient(HttpClientNames.AasRegistry).Returns(httpClient); await Assert.ThrowsAsync(() => _sut.GetShellDescriptorsTemplateAsync(CancellationToken.None)); } @@ -184,7 +184,7 @@ public async Task GetShellDescriptorsTemplateAsync_ThrowsException_WhenHttpClien using var mockHttpMessageHandler = new FakeHttpMessageHandler(new HttpRequestException("Network error")); using var httpClient = new HttpClient(mockHttpMessageHandler); httpClient.BaseAddress = new Uri("https://www.mm-software.com/fakeurl"); - _httpClientFactory.CreateClient(AasEnvironmentConfig.AasRegistry).Returns(httpClient); + _httpClientFactory.CreateClient(HttpClientNames.AasRegistry).Returns(httpClient); var exception = await Assert.ThrowsAsync(() => _sut.GetShellDescriptorsTemplateAsync(CancellationToken.None)); Assert.Equal("Network error", exception.Message); @@ -197,7 +197,7 @@ public async Task GetShellTemplateAsync_ReturnsShell_WhenValidResponse() using var mockHttpMessageHandler = new FakeHttpMessageHandler(mockHttpResponse); using var httpClient = new HttpClient(mockHttpMessageHandler); httpClient.BaseAddress = new Uri("https://www.mm-software.com/fakeurl"); - _httpClientFactory.CreateClient(AasEnvironmentConfig.AasTemplateRepository).Returns(httpClient); + _httpClientFactory.CreateClient(HttpClientNames.AasTemplateRepository).Returns(httpClient); var result = await _sut.GetShellTemplateAsync(TemplateId, CancellationToken.None); @@ -216,7 +216,7 @@ public async Task GetShellTemplateAsync_ThrowsResponseParsingException_WhenInval using var mockHttpMessageHandler = new FakeHttpMessageHandler(mockHttpResponse); using var httpClient = new HttpClient(mockHttpMessageHandler); httpClient.BaseAddress = new Uri("https://www.mm-software.com/fakeurl"); - _httpClientFactory.CreateClient(AasEnvironmentConfig.AasTemplateRepository).Returns(httpClient); + _httpClientFactory.CreateClient(HttpClientNames.AasTemplateRepository).Returns(httpClient); await Assert.ThrowsAsync(() => _sut.GetShellTemplateAsync(TemplateId, CancellationToken.None)); } @@ -231,7 +231,7 @@ public async Task GetShellTemplateAsync_ThrowsResourceNotFoundException_WhenNotF using var mockHttpMessageHandler = new FakeHttpMessageHandler(mockHttpResponse); using var httpClient = new HttpClient(mockHttpMessageHandler); httpClient.BaseAddress = new Uri("https://www.mm-software.com/fakeurl"); - _httpClientFactory.CreateClient(AasEnvironmentConfig.AasTemplateRepository).Returns(httpClient); + _httpClientFactory.CreateClient(HttpClientNames.AasTemplateRepository).Returns(httpClient); await Assert.ThrowsAsync(() => _sut.GetShellTemplateAsync(TemplateId, CancellationToken.None)); @@ -243,7 +243,7 @@ public async Task GetShellTemplateAsync_ThrowsException_WhenHttpClientFails() using var mockHttpMessageHandler = new FakeHttpMessageHandler(new HttpRequestException("Network error")); using var httpClient = new HttpClient(mockHttpMessageHandler); httpClient.BaseAddress = new Uri("https://www.mm-software.com/fakeurl"); - _httpClientFactory.CreateClient(AasEnvironmentConfig.AasTemplateRepository).Returns(httpClient); + _httpClientFactory.CreateClient(HttpClientNames.AasTemplateRepository).Returns(httpClient); var exception = await Assert.ThrowsAsync(() => _sut.GetShellTemplateAsync(TemplateId, CancellationToken.None)); @@ -257,7 +257,7 @@ public async Task GetAssetInformationTemplateAsync_ReturnsAssetInformation_WhenV using var mockHttpMessageHandler = new FakeHttpMessageHandler(mockHttpResponse); using var httpClient = new HttpClient(mockHttpMessageHandler); httpClient.BaseAddress = new Uri("https://www.mm-software.com/fakeurl"); - _httpClientFactory.CreateClient(AasEnvironmentConfig.AasTemplateRepository).Returns(httpClient); + _httpClientFactory.CreateClient(HttpClientNames.AasTemplateRepository).Returns(httpClient); var result = await _sut.GetAssetInformationTemplateAsync(TemplateId, CancellationToken.None); @@ -275,7 +275,7 @@ public async Task GetAssetInformationTemplateAsync_ThrowsResponseParsingExceptio using var mockHttpMessageHandler = new FakeHttpMessageHandler(mockHttpResponse); using var httpClient = new HttpClient(mockHttpMessageHandler); httpClient.BaseAddress = new Uri("https://www.mm-software.com/fakeurl"); - _httpClientFactory.CreateClient(AasEnvironmentConfig.AasTemplateRepository).Returns(httpClient); + _httpClientFactory.CreateClient(HttpClientNames.AasTemplateRepository).Returns(httpClient); await Assert.ThrowsAsync(() => _sut.GetAssetInformationTemplateAsync(TemplateId, CancellationToken.None)); } @@ -290,7 +290,7 @@ public async Task GetAssetInformationTemplateAsync_ThrowsResourceNotFoundExcepti using var mockHttpMessageHandler = new FakeHttpMessageHandler(mockHttpResponse); using var httpClient = new HttpClient(mockHttpMessageHandler); httpClient.BaseAddress = new Uri("https://www.mm-software.com/fakeurl"); - _httpClientFactory.CreateClient(AasEnvironmentConfig.AasTemplateRepository).Returns(httpClient); + _httpClientFactory.CreateClient(HttpClientNames.AasTemplateRepository).Returns(httpClient); await Assert.ThrowsAsync(() => _sut.GetAssetInformationTemplateAsync(TemplateId, CancellationToken.None)); @@ -302,7 +302,7 @@ public async Task GetAssetInformationTemplateAsync_ThrowsException_WhenHttpClien using var mockHttpMessageHandler = new FakeHttpMessageHandler(new HttpRequestException("Network error")); using var httpClient = new HttpClient(mockHttpMessageHandler); httpClient.BaseAddress = new Uri("https://www.mm-software.com/fakeurl"); - _httpClientFactory.CreateClient(AasEnvironmentConfig.AasTemplateRepository).Returns(httpClient); + _httpClientFactory.CreateClient(HttpClientNames.AasTemplateRepository).Returns(httpClient); var exception = await Assert.ThrowsAsync(() => _sut.GetAssetInformationTemplateAsync(TemplateId, CancellationToken.None)); @@ -319,7 +319,7 @@ public async Task GetSubmodelRefByIdAsync_ReturnsSubmodelRefs_WhenValidResponse( using var mockHttpMessageHandler = new FakeHttpMessageHandler(mockHttpResponse); using var httpClient = new HttpClient(mockHttpMessageHandler); httpClient.BaseAddress = new Uri("https://www.mm-software.com/fakeurl"); - _httpClientFactory.CreateClient(AasEnvironmentConfig.AasTemplateRepository).Returns(httpClient); + _httpClientFactory.CreateClient(HttpClientNames.AasTemplateRepository).Returns(httpClient); var result = await _sut.GetSubmodelRefByIdAsync(TemplateId, CancellationToken.None); @@ -338,7 +338,7 @@ public async Task GetSubmodelRefByIdAsync_ThrowsResourceNotFoundException_WhenRe using var mockHttpMessageHandler = new FakeHttpMessageHandler(mockHttpResponse); using var httpClient = new HttpClient(mockHttpMessageHandler); httpClient.BaseAddress = new Uri("https://www.mm-software.com/fakeurl"); - _httpClientFactory.CreateClient(AasEnvironmentConfig.AasTemplateRepository).Returns(httpClient); + _httpClientFactory.CreateClient(HttpClientNames.AasTemplateRepository).Returns(httpClient); await Assert.ThrowsAsync(() => _sut.GetSubmodelRefByIdAsync(TemplateId, CancellationToken.None)); } @@ -353,7 +353,7 @@ public async Task GetSubmodelRefByIdAsync_ThrowsResourceNotFoundException_WhenRe using var mockHttpMessageHandler = new FakeHttpMessageHandler(mockHttpResponse); using var httpClient = new HttpClient(mockHttpMessageHandler); httpClient.BaseAddress = new Uri("https://www.mm-software.com/fakeurl"); - _httpClientFactory.CreateClient(AasEnvironmentConfig.AasTemplateRepository).Returns(httpClient); + _httpClientFactory.CreateClient(HttpClientNames.AasTemplateRepository).Returns(httpClient); await Assert.ThrowsAsync(() => _sut.GetSubmodelRefByIdAsync(TemplateId, CancellationToken.None)); } @@ -368,7 +368,7 @@ public async Task GetSubmodelRefByIdAsync_ThrowsResponseParsingException_WhenInv using var mockHttpMessageHandler = new FakeHttpMessageHandler(mockHttpResponse); using var httpClient = new HttpClient(mockHttpMessageHandler); httpClient.BaseAddress = new Uri("https://www.mm-software.com/fakeurl"); - _httpClientFactory.CreateClient(AasEnvironmentConfig.AasTemplateRepository).Returns(httpClient); + _httpClientFactory.CreateClient(HttpClientNames.AasTemplateRepository).Returns(httpClient); await Assert.ThrowsAsync(() => _sut.GetSubmodelRefByIdAsync(TemplateId, CancellationToken.None)); } @@ -379,7 +379,7 @@ public async Task GetSubmodelRefByIdAsync_ThrowsHttpRequestException_WhenHttpFai using var mockHttpMessageHandler = new FakeHttpMessageHandler(new HttpRequestException("Network error")); using var httpClient = new HttpClient(mockHttpMessageHandler); httpClient.BaseAddress = new Uri("https://www.mm-software.com/fakeurl"); - _httpClientFactory.CreateClient(AasEnvironmentConfig.AasTemplateRepository).Returns(httpClient); + _httpClientFactory.CreateClient(HttpClientNames.AasTemplateRepository).Returns(httpClient); var exception = await Assert.ThrowsAsync(() => _sut.GetSubmodelRefByIdAsync(TemplateId, CancellationToken.None)); @@ -396,7 +396,7 @@ public async Task GetSubmodelTemplateAsync_ThrowsServiceAuthorizationException_W using var mockHttpMessageHandler = new FakeHttpMessageHandler(mockHttpResponse); using var httpClient = new HttpClient(mockHttpMessageHandler); httpClient.BaseAddress = new Uri("https://www.mm-software.com/fakeurl"); - _httpClientFactory.CreateClient(AasEnvironmentConfig.SubmodelTemplateRepository).Returns(httpClient); + _httpClientFactory.CreateClient(HttpClientNames.SubmodelTemplateRepository).Returns(httpClient); await Assert.ThrowsAsync(() => _sut.GetSubmodelTemplateAsync(TemplateId, CancellationToken.None)); @@ -412,7 +412,7 @@ public async Task GetSubmodelTemplateAsync_ThrowsRequestTimeoutException_WhenTim using var mockHttpMessageHandler = new FakeHttpMessageHandler(mockHttpResponse); using var httpClient = new HttpClient(mockHttpMessageHandler); httpClient.BaseAddress = new Uri("https://www.mm-software.com/fakeurl"); - _httpClientFactory.CreateClient(AasEnvironmentConfig.SubmodelTemplateRepository).Returns(httpClient); + _httpClientFactory.CreateClient(HttpClientNames.SubmodelTemplateRepository).Returns(httpClient); await Assert.ThrowsAsync(() => _sut.GetSubmodelTemplateAsync(TemplateId, CancellationToken.None)); @@ -428,7 +428,7 @@ public async Task GetSubmodelTemplateAsync_ThrowsValidationFailedException_WhenU using var mockHttpMessageHandler = new FakeHttpMessageHandler(mockHttpResponse); using var httpClient = new HttpClient(mockHttpMessageHandler); httpClient.BaseAddress = new Uri("https://www.mm-software.com/fakeurl"); - _httpClientFactory.CreateClient(AasEnvironmentConfig.SubmodelTemplateRepository).Returns(httpClient); + _httpClientFactory.CreateClient(HttpClientNames.SubmodelTemplateRepository).Returns(httpClient); await Assert.ThrowsAsync(() => _sut.GetSubmodelTemplateAsync(TemplateId, CancellationToken.None)); @@ -445,7 +445,7 @@ public async Task GetConceptDescriptionByIdAsync_ReturnsConceptDescription_WhenR using var mockHandler = new FakeHttpMessageHandler(mockHttpResponse); using var httpClient = new HttpClient(mockHandler); httpClient.BaseAddress = new Uri("https://www.mm-software.com/fakeurl"); - _httpClientFactory.CreateClient(AasEnvironmentConfig.ConceptDescriptorTemplateRepository) + _httpClientFactory.CreateClient(HttpClientNames.ConceptDescriptorTemplateRepository) .Returns(httpClient); var result = await _sut.GetConceptDescriptionByIdAsync(CdIdentifier, CancellationToken.None); @@ -466,7 +466,7 @@ public async Task GetConceptDescriptionByIdAsync_ReturnsNull_OnHandledExceptions using var mockHandler = new FakeHttpMessageHandler(exception); using var httpClient = new HttpClient(mockHandler); httpClient.BaseAddress = new Uri("https://www.mm-software.com/fakeurl"); - _httpClientFactory.CreateClient(AasEnvironmentConfig.ConceptDescriptorTemplateRepository) + _httpClientFactory.CreateClient(HttpClientNames.ConceptDescriptorTemplateRepository) .Returns(httpClient); var result = await _sut.GetConceptDescriptionByIdAsync(CdIdentifier, CancellationToken.None); diff --git a/source/AAS.TwinEngine.DataEngine.UnitTests/ServiceConfiguration/ConfigurationMigration/LegacyTemplateManagementConfigAdapterTests.cs b/source/AAS.TwinEngine.DataEngine.UnitTests/ServiceConfiguration/ConfigurationMigration/LegacyTemplateManagementConfigAdapterTests.cs index 83dbf9bc..bf464290 100644 --- a/source/AAS.TwinEngine.DataEngine.UnitTests/ServiceConfiguration/ConfigurationMigration/LegacyTemplateManagementConfigAdapterTests.cs +++ b/source/AAS.TwinEngine.DataEngine.UnitTests/ServiceConfiguration/ConfigurationMigration/LegacyTemplateManagementConfigAdapterTests.cs @@ -1,5 +1,4 @@ -using AAS.TwinEngine.DataEngine.ApplicationLogic.Services.Plugin.Config; -using AAS.TwinEngine.DataEngine.Infrastructure.Configuration.LegacyV1; +using AAS.TwinEngine.DataEngine.Infrastructure.Configuration.LegacyV1; using AAS.TwinEngine.DataEngine.ServiceConfiguration.Config; using Microsoft.Extensions.Configuration; @@ -73,7 +72,7 @@ public void Configure_WithV1Config_MapsAasTemplateRepository() // V1 maps AasEnvironmentRepositoryBaseUrl to the TemplateRepository shorthand Assert.NotNull(options.TemplateRepository); - Assert.Equal(AasEnvironmentConfig.TemplateRepository, options.TemplateRepository!.Name); + Assert.Equal(HttpClientNames.TemplateRepository, options.TemplateRepository!.Name); Assert.Equal(new Uri("http://localhost:8081"), options.TemplateRepository.BaseUrl); } @@ -86,7 +85,7 @@ public void Configure_WithV1Config_MapsAasTemplateRegistry() adapter.Configure(options); - Assert.Equal(AasEnvironmentConfig.AasRegistry, options.AasTemplateRegistry.Name); + Assert.Equal(HttpClientNames.AasRegistry, options.AasTemplateRegistry.Name); Assert.Equal(new Uri("http://localhost:8082"), options.AasTemplateRegistry.BaseUrl); } @@ -99,7 +98,7 @@ public void Configure_WithV1Config_MapsSubmodelTemplateRegistry() adapter.Configure(options); - Assert.Equal(AasEnvironmentConfig.SubmodelRegistry, options.SubmodelTemplateRegistry.Name); + Assert.Equal(HttpClientNames.SubmodelRegistry, options.SubmodelTemplateRegistry.Name); Assert.Equal(new Uri("http://localhost:8083"), options.SubmodelTemplateRegistry.BaseUrl); } diff --git a/source/AAS.TwinEngine.DataEngine/ApplicationLogic/Services/SubmodelRegistry/SubmodelDescriptorService.cs b/source/AAS.TwinEngine.DataEngine/ApplicationLogic/Services/SubmodelRegistry/SubmodelDescriptorService.cs index 8a38243e..d23ad27f 100644 --- a/source/AAS.TwinEngine.DataEngine/ApplicationLogic/Services/SubmodelRegistry/SubmodelDescriptorService.cs +++ b/source/AAS.TwinEngine.DataEngine/ApplicationLogic/Services/SubmodelRegistry/SubmodelDescriptorService.cs @@ -2,11 +2,11 @@ using AAS.TwinEngine.DataEngine.ApplicationLogic.Exceptions.Infrastructure; using AAS.TwinEngine.DataEngine.ApplicationLogic.Extensions; using AAS.TwinEngine.DataEngine.ApplicationLogic.Services.AasEnvironment.Providers; -using AAS.TwinEngine.DataEngine.ApplicationLogic.Services.Plugin.Config; using AAS.TwinEngine.DataEngine.ApplicationLogic.Services.Shared; using AAS.TwinEngine.DataEngine.ApplicationLogic.Services.SubmodelRegistry.Providers; using AAS.TwinEngine.DataEngine.DomainModel.Shared; using AAS.TwinEngine.DataEngine.DomainModel.SubmodelRegistry; +using AAS.TwinEngine.DataEngine.ServiceConfiguration.Config; using UnauthorizedAccessException = AAS.TwinEngine.DataEngine.ApplicationLogic.Exceptions.Infrastructure.UnauthorizedAccessException; @@ -85,5 +85,5 @@ private static void SetHref(EndpointData endpoint, string href) endpoint.ProtocolInformation.Href = href; } - private string GenerateHref(string encodedId) => $"{baseUrlProvider.GetBaseUrl()}{AasEnvironmentConfig.SubModelRepositoryPath}/{encodedId}"; + private string GenerateHref(string encodedId) => $"{baseUrlProvider.GetBaseUrl()}{ApiPaths.Submodels}/{encodedId}"; } diff --git a/source/AAS.TwinEngine.DataEngine/Infrastructure/Configuration/LegacyV1/ConfigV1/AasEnvironmentConfig.cs b/source/AAS.TwinEngine.DataEngine/Infrastructure/Configuration/LegacyV1/ConfigV1/AasEnvironmentConfig.cs new file mode 100644 index 00000000..830903b8 --- /dev/null +++ b/source/AAS.TwinEngine.DataEngine/Infrastructure/Configuration/LegacyV1/ConfigV1/AasEnvironmentConfig.cs @@ -0,0 +1,22 @@ +namespace AAS.TwinEngine.DataEngine.Infrastructure.Configuration.LegacyV1; + +/// +/// V1 configuration POCO for the "AasEnvironment" section. +/// The constants have been moved to and . +/// Only the URI properties and Section remain for V1 legacy adapter deserialization. +/// +[Obsolete("V1 configuration is deprecated and will be removed in v2.0.0 version. Use HttpClientNames and ApiPaths instead.")] +public class AasEnvironmentConfig +{ + public const string Section = "AasEnvironment"; + + public Uri DataEngineRepositoryBaseUrl { get; set; } = null!; + + public Uri? AasEnvironmentRepositoryBaseUrl { get; set; } = null!; + + public Uri? AasRegistryBaseUrl { get; set; } = null!; + + public Uri? SubModelRegistryBaseUrl { get; set; } = null!; + + public Uri CustomerDomainUrl { get; set; } = null!; +} diff --git a/source/AAS.TwinEngine.DataEngine/Infrastructure/Configuration/LegacyV1/ConfigV1/PluginConfig.cs b/source/AAS.TwinEngine.DataEngine/Infrastructure/Configuration/LegacyV1/ConfigV1/PluginConfig.cs new file mode 100644 index 00000000..da8844a5 --- /dev/null +++ b/source/AAS.TwinEngine.DataEngine/Infrastructure/Configuration/LegacyV1/ConfigV1/PluginConfig.cs @@ -0,0 +1,32 @@ +using AAS.TwinEngine.DataEngine.ServiceConfiguration.Config; + +namespace AAS.TwinEngine.DataEngine.Infrastructure.Configuration.LegacyV1; + +/// +/// V1 configuration POCO for the "PluginConfig" section. +/// The constants have been moved to and . +/// Only the Section, Plugins list, and Plugin child class remain for V1 legacy adapter deserialization. +/// +[Obsolete("V1 configuration is deprecated and will be removed in v2.0.0 version. Use HttpClientNames and ApiPaths instead.")] +public class PluginConfig +{ + public const string Section = "PluginConfig"; + + // ── Forwarded constants (kept temporarily so legacy adapters compile) ── + + public const string HttpClientNamePrefix = HttpClientNames.PluginDataProviderPrefix; + + public const string HealthCheckHttpClientNamePrefix = HttpClientNames.PluginHealthCheckPrefix; + + public const string MetaData = ApiPaths.PluginMetadata; + + public required List Plugins { get; set; } +} + +[Obsolete("V1 configuration is deprecated and will be removed in v2.0.0 version. Use PluginInstance instead.")] +public class Plugin +{ + public required string PluginName { get; set; } + + public required Uri PluginUrl { get; set; } +} diff --git a/source/AAS.TwinEngine.DataEngine/Infrastructure/Configuration/LegacyV1/LegacyPluginsConfigAdapter.cs b/source/AAS.TwinEngine.DataEngine/Infrastructure/Configuration/LegacyV1/LegacyPluginsConfigAdapter.cs index 2c3f6084..1e01ed39 100644 --- a/source/AAS.TwinEngine.DataEngine/Infrastructure/Configuration/LegacyV1/LegacyPluginsConfigAdapter.cs +++ b/source/AAS.TwinEngine.DataEngine/Infrastructure/Configuration/LegacyV1/LegacyPluginsConfigAdapter.cs @@ -1,5 +1,4 @@ using AAS.TwinEngine.DataEngine.Infrastructure.Http.Authorization.Config; -using AAS.TwinEngine.DataEngine.Infrastructure.Providers.PluginDataProvider.Config; using AAS.TwinEngine.DataEngine.ServiceConfiguration.Config; using Microsoft.Extensions.Options; @@ -15,15 +14,20 @@ public sealed class LegacyPluginsConfigAdapter(IConfiguration configuration) : I { private readonly IConfiguration _configuration = configuration; - public void Configure(PluginsConfig options) + public void Configure(PluginsConfig options) => MapToConfig(_configuration, options); + + /// + /// Static entry point used during DI registration to apply V1 mapping without BuildServiceProvider(). + /// + public static void MapToConfig(IConfiguration configuration, PluginsConfig options) { - if (!LegacyConfigurationDetector.IsV1Configuration(_configuration)) + if (!LegacyConfigurationDetector.IsV1Configuration(configuration)) { return; } // Semantics (V1: "Semantics") → split into Plugins + TemplateManagement - var semantics = _configuration.GetSection(Semantics.Section).Get(); + var semantics = configuration.GetSection(Semantics.Section).Get(); if (semantics != null) { options.SubmodelElementIndexContextPrefix = semantics.SubmodelElementIndexContextPrefix; @@ -31,7 +35,7 @@ public void Configure(PluginsConfig options) } // MultiLanguageProperty (V1: "MultiLanguageProperty") - var mlpSettings = _configuration.GetSection(MultiLanguagePropertySettings.Section).Get(); + var mlpSettings = configuration.GetSection(MultiLanguagePropertySettings.Section).Get(); if (mlpSettings?.DefaultLanguages != null) { options.MultiLanguageProperty = new PluginMultiLanguagePropertyConfig @@ -42,7 +46,7 @@ public void Configure(PluginsConfig options) } // Resilience → Retry (V1: "HttpRetryPolicyOptions:PluginDataProvider") - var retryPolicy = _configuration.GetSection($"{HttpRetryPolicyOptions.Section}:{HttpRetryPolicyOptions.PluginDataProvider}").Get(); + var retryPolicy = configuration.GetSection($"{HttpRetryPolicyOptions.Section}:{HttpRetryPolicyOptions.PluginDataProvider}").Get(); if (retryPolicy != null) { options.ResiliencePolicies.Retry.MaxRetryAttempts = retryPolicy.MaxRetryAttempts; @@ -50,8 +54,8 @@ public void Configure(PluginsConfig options) } // Plugin instances (V1: "PluginConfig:Plugins") → Plugins:Instances with property renames - var pluginConfig = _configuration.GetSection(PluginConfig.Section).Get(); - var headerForwarding = _configuration.GetSection(HeaderForwardingOptions.Section).Get(); + var pluginConfig = configuration.GetSection(PluginConfig.Section).Get(); + var headerForwarding = configuration.GetSection(HeaderForwardingOptions.Section).Get(); if (pluginConfig?.Plugins != null) { diff --git a/source/AAS.TwinEngine.DataEngine/Infrastructure/Configuration/LegacyV1/LegacyTemplateManagementConfigAdapter.cs b/source/AAS.TwinEngine.DataEngine/Infrastructure/Configuration/LegacyV1/LegacyTemplateManagementConfigAdapter.cs index 132a0ce2..4932eaaf 100644 --- a/source/AAS.TwinEngine.DataEngine/Infrastructure/Configuration/LegacyV1/LegacyTemplateManagementConfigAdapter.cs +++ b/source/AAS.TwinEngine.DataEngine/Infrastructure/Configuration/LegacyV1/LegacyTemplateManagementConfigAdapter.cs @@ -16,15 +16,20 @@ public sealed class LegacyTemplateManagementConfigAdapter(IConfiguration configu { private readonly IConfiguration _configuration = configuration; - public void Configure(TemplateManagementConfig options) + public void Configure(TemplateManagementConfig options) => MapToConfig(_configuration, options); + + /// + /// Static entry point used during DI registration to apply V1 mapping without BuildServiceProvider(). + /// + public static void MapToConfig(IConfiguration configuration, TemplateManagementConfig options) { - if (!LegacyConfigurationDetector.IsV1Configuration(_configuration)) + if (!LegacyConfigurationDetector.IsV1Configuration(configuration)) { return; } // Semantics:InternalSemanticId → TemplateManagement:Semantics:InternalSemanticId - var semantics = _configuration.GetSection(Semantics.Section).Get(); + var semantics = configuration.GetSection(Semantics.Section).Get(); if (semantics != null) { options.Semantics = new TemplateSemanticsConfig @@ -34,14 +39,14 @@ public void Configure(TemplateManagementConfig options) } // TemplateMappingRules (V1: top-level "TemplateMappingRules") - var mappingRules = _configuration.GetSection(TemplateMappingRules.Section).Get(); + var mappingRules = configuration.GetSection(TemplateMappingRules.Section).Get(); if (mappingRules != null) { options.TemplateMappingRules = mappingRules; } // Resilience → Retry (V1: "HttpRetryPolicyOptions:TemplateProvider") - var retryPolicy = _configuration.GetSection($"{HttpRetryPolicyOptions.Section}:{HttpRetryPolicyOptions.TemplateProvider}").Get(); + var retryPolicy = configuration.GetSection($"{HttpRetryPolicyOptions.Section}:{HttpRetryPolicyOptions.TemplateProvider}").Get(); if (retryPolicy != null) { options.ResiliencePolicies.Retry.MaxRetryAttempts = retryPolicy.MaxRetryAttempts; @@ -49,10 +54,10 @@ public void Configure(TemplateManagementConfig options) } // AasEnvironment base URLs → service endpoints - var aasEnv = _configuration.GetSection(AasEnvironmentConfig.Section).Get(); + var aasEnv = configuration.GetSection(AasEnvironmentConfig.Section).Get(); // Header mappings from HeaderForwarding - var headerForwarding = _configuration.GetSection(HeaderForwardingOptions.Section).Get(); + var headerForwarding = configuration.GetSection(HeaderForwardingOptions.Section).Get(); if (aasEnv != null) { @@ -61,21 +66,21 @@ public void Configure(TemplateManagementConfig options) // the same URL and headers to Aas/Submodel/ConceptDescription template repositories. options.TemplateRepository = new ServiceEndpoint { - Name = AasEnvironmentConfig.TemplateRepository, + Name = HttpClientNames.TemplateRepository, BaseUrl = aasEnv.AasEnvironmentRepositoryBaseUrl, HeaderMappings = headerForwarding?.HeaderMappings.TemplateRepository ?? [] }; options.AasTemplateRegistry = new ServiceEndpoint { - Name = AasEnvironmentConfig.AasRegistry, + Name = HttpClientNames.AasRegistry, BaseUrl = aasEnv.AasRegistryBaseUrl, HeaderMappings = headerForwarding?.HeaderMappings.TemplateRegistry ?? [] }; options.SubmodelTemplateRegistry = new ServiceEndpoint { - Name = AasEnvironmentConfig.SubmodelRegistry, + Name = HttpClientNames.SubmodelRegistry, BaseUrl = aasEnv.SubModelRegistryBaseUrl, HeaderMappings = [] }; diff --git a/source/AAS.TwinEngine.DataEngine/Infrastructure/Http/Authorization/Headers/RequestHeaderMapper.cs b/source/AAS.TwinEngine.DataEngine/Infrastructure/Http/Authorization/Headers/RequestHeaderMapper.cs index 6483506c..5f7e9b73 100644 --- a/source/AAS.TwinEngine.DataEngine/Infrastructure/Http/Authorization/Headers/RequestHeaderMapper.cs +++ b/source/AAS.TwinEngine.DataEngine/Infrastructure/Http/Authorization/Headers/RequestHeaderMapper.cs @@ -2,9 +2,7 @@ using System.Text.RegularExpressions; using AAS.TwinEngine.DataEngine.ApplicationLogic.Exceptions.Application; -using AAS.TwinEngine.DataEngine.ApplicationLogic.Services.Plugin.Config; using AAS.TwinEngine.DataEngine.Infrastructure.Http.Authorization.Config; -using AAS.TwinEngine.DataEngine.Infrastructure.Providers.PluginDataProvider.Config; using AAS.TwinEngine.DataEngine.ServiceConfiguration.Config; using Microsoft.Extensions.Options; @@ -188,37 +186,37 @@ public void ApplyMappings(HttpContext? httpContext, HttpRequestMessage outgoingR private IList? ResolveMappingsForClient(string clientName) { - if (string.Equals(clientName, AasEnvironmentConfig.AasTemplateRepository, StringComparison.OrdinalIgnoreCase)) + if (string.Equals(clientName, HttpClientNames.AasTemplateRepository, StringComparison.OrdinalIgnoreCase)) { return _templateManagementConfig.AasTemplateRepository.HeaderMappings; } - if (string.Equals(clientName, AasEnvironmentConfig.SubmodelTemplateRepository, StringComparison.OrdinalIgnoreCase)) + if (string.Equals(clientName, HttpClientNames.SubmodelTemplateRepository, StringComparison.OrdinalIgnoreCase)) { return _templateManagementConfig.SubmodelTemplateRepository.HeaderMappings; } - if (string.Equals(clientName, AasEnvironmentConfig.ConceptDescriptorTemplateRepository, StringComparison.OrdinalIgnoreCase)) + if (string.Equals(clientName, HttpClientNames.ConceptDescriptorTemplateRepository, StringComparison.OrdinalIgnoreCase)) { return _templateManagementConfig.ConceptDescriptionTemplateRepository.HeaderMappings; } - if (string.Equals(clientName, AasEnvironmentConfig.AasRegistry, StringComparison.OrdinalIgnoreCase)) + if (string.Equals(clientName, HttpClientNames.AasRegistry, StringComparison.OrdinalIgnoreCase)) { return _templateManagementConfig.AasTemplateRegistry.HeaderMappings; } - if (string.Equals(clientName, AasEnvironmentConfig.SubmodelRegistry, StringComparison.OrdinalIgnoreCase)) + if (string.Equals(clientName, HttpClientNames.SubmodelRegistry, StringComparison.OrdinalIgnoreCase)) { return _templateManagementConfig.SubmodelTemplateRegistry.HeaderMappings; } - if (!clientName.StartsWith(PluginConfig.HttpClientNamePrefix, StringComparison.OrdinalIgnoreCase)) + if (!clientName.StartsWith(HttpClientNames.PluginDataProviderPrefix, StringComparison.OrdinalIgnoreCase)) { return null; } - var pluginName = clientName[PluginConfig.HttpClientNamePrefix.Length..]; + var pluginName = clientName[HttpClientNames.PluginDataProviderPrefix.Length..]; return _pluginsConfig.Instances .FirstOrDefault(p => string.Equals(p.Name, pluginName, StringComparison.OrdinalIgnoreCase)) diff --git a/source/AAS.TwinEngine.DataEngine/Infrastructure/Monitoring/PluginAvailabilityHealthCheck.cs b/source/AAS.TwinEngine.DataEngine/Infrastructure/Monitoring/PluginAvailabilityHealthCheck.cs index d80ea7ae..156a545c 100644 --- a/source/AAS.TwinEngine.DataEngine/Infrastructure/Monitoring/PluginAvailabilityHealthCheck.cs +++ b/source/AAS.TwinEngine.DataEngine/Infrastructure/Monitoring/PluginAvailabilityHealthCheck.cs @@ -1,5 +1,4 @@ using AAS.TwinEngine.DataEngine.Infrastructure.Http.Clients; -using AAS.TwinEngine.DataEngine.Infrastructure.Providers.PluginDataProvider.Config; using AAS.TwinEngine.DataEngine.ServiceConfiguration.Config; using Microsoft.Extensions.Diagnostics.HealthChecks; @@ -39,7 +38,7 @@ private async Task CheckSinglePluginAsync(PluginInstance plugin, Cancellat { try { - var httpClient = clientFactory.CreateClient($"{PluginConfig.HealthCheckHttpClientNamePrefix}{plugin.Name}"); + var httpClient = clientFactory.CreateClient($"{HttpClientNames.PluginHealthCheckPrefix}{plugin.Name}"); using var response = await httpClient .GetAsync(new Uri(HealthEndpoint, UriKind.Relative), cancellationToken) diff --git a/source/AAS.TwinEngine.DataEngine/Infrastructure/Monitoring/TemplateRegistryHealthCheck.cs b/source/AAS.TwinEngine.DataEngine/Infrastructure/Monitoring/TemplateRegistryHealthCheck.cs index 470cfe30..b78819b7 100644 --- a/source/AAS.TwinEngine.DataEngine/Infrastructure/Monitoring/TemplateRegistryHealthCheck.cs +++ b/source/AAS.TwinEngine.DataEngine/Infrastructure/Monitoring/TemplateRegistryHealthCheck.cs @@ -1,5 +1,5 @@ -using AAS.TwinEngine.DataEngine.ApplicationLogic.Services.Plugin.Config; -using AAS.TwinEngine.DataEngine.Infrastructure.Http.Clients; +using AAS.TwinEngine.DataEngine.Infrastructure.Http.Clients; +using AAS.TwinEngine.DataEngine.ServiceConfiguration.Config; using Microsoft.Extensions.Diagnostics.HealthChecks; @@ -8,13 +8,13 @@ namespace AAS.TwinEngine.DataEngine.Infrastructure.Monitoring; public sealed class TemplateRegistryHealthCheck(ICreateClient clientFactory, ILogger logger) : IHealthCheck { - private const string AasRegistryPath = AasEnvironmentConfig.AasRegistryPath; - private const string SubModelRegistryPath = AasEnvironmentConfig.SubModelRegistryPath; + private const string AasRegistryPath = ApiPaths.ShellDescriptors; + private const string SubModelRegistryPath = ApiPaths.SubmodelDescriptors; public async Task CheckHealthAsync(HealthCheckContext context, CancellationToken cancellationToken = default) { - var aasTask = CheckEndpointAsync(AasEnvironmentConfig.AasRegistryHealthCheck, AasRegistryPath, "aas-registry", cancellationToken); - var submodelTask = CheckEndpointAsync(AasEnvironmentConfig.SubmodelRegistryHealthCheck, SubModelRegistryPath, "submodel-registry", cancellationToken); + var aasTask = CheckEndpointAsync(HttpClientNames.AasRegistryHealthCheck, AasRegistryPath, "aas-registry", cancellationToken); + var submodelTask = CheckEndpointAsync(HttpClientNames.SubmodelRegistryHealthCheck, SubModelRegistryPath, "submodel-registry", cancellationToken); var results = await Task.WhenAll(aasTask, submodelTask).ConfigureAwait(false); diff --git a/source/AAS.TwinEngine.DataEngine/Infrastructure/Monitoring/TemplateRepositoryHealthCheck.cs b/source/AAS.TwinEngine.DataEngine/Infrastructure/Monitoring/TemplateRepositoryHealthCheck.cs index ccc0be66..03d85223 100644 --- a/source/AAS.TwinEngine.DataEngine/Infrastructure/Monitoring/TemplateRepositoryHealthCheck.cs +++ b/source/AAS.TwinEngine.DataEngine/Infrastructure/Monitoring/TemplateRepositoryHealthCheck.cs @@ -1,5 +1,5 @@ -using AAS.TwinEngine.DataEngine.ApplicationLogic.Services.Plugin.Config; -using AAS.TwinEngine.DataEngine.Infrastructure.Http.Clients; +using AAS.TwinEngine.DataEngine.Infrastructure.Http.Clients; +using AAS.TwinEngine.DataEngine.ServiceConfiguration.Config; using Microsoft.Extensions.Diagnostics.HealthChecks; @@ -7,15 +7,15 @@ namespace AAS.TwinEngine.DataEngine.Infrastructure.Monitoring; public sealed class TemplateRepositoryHealthCheck(ICreateClient clientFactory, ILogger logger) : IHealthCheck { - private const string AasRepositoryPath = AasEnvironmentConfig.AasRepositoryPath; - private const string SubModelRepositoryPath = AasEnvironmentConfig.SubModelRepositoryPath; - private const string ConceptDescriptionPath = AasEnvironmentConfig.ConceptDescriptionPath; + private const string AasRepositoryPath = ApiPaths.Shells; + private const string SubModelRepositoryPath = ApiPaths.Submodels; + private const string ConceptDescriptionPath = ApiPaths.ConceptDescriptions; public async Task CheckHealthAsync(HealthCheckContext context, CancellationToken cancellationToken = default) { - var aasTask = CheckHealthEndpointAsync(AasEnvironmentConfig.SubmodelTemplateRepositoryHealthCheck, AasRepositoryPath, "aas-template-repository", cancellationToken); - var submodelTask = CheckHealthEndpointAsync(AasEnvironmentConfig.AasTemplateRepositoryHealthCheck, SubModelRepositoryPath, "submodel-template-repository", cancellationToken); - var conceptDiscriptorTask = CheckHealthEndpointAsync(AasEnvironmentConfig.ConceptDescriptorTemplateRepositoryHealthCheck, ConceptDescriptionPath, "concept-descriptor-template-repository", cancellationToken); + var aasTask = CheckHealthEndpointAsync(HttpClientNames.SubmodelTemplateRepositoryHealthCheck, AasRepositoryPath, "aas-template-repository", cancellationToken); + var submodelTask = CheckHealthEndpointAsync(HttpClientNames.AasTemplateRepositoryHealthCheck, SubModelRepositoryPath, "submodel-template-repository", cancellationToken); + var conceptDiscriptorTask = CheckHealthEndpointAsync(HttpClientNames.ConceptDescriptorTemplateRepositoryHealthCheck, ConceptDescriptionPath, "concept-descriptor-template-repository", cancellationToken); var results = await Task.WhenAll(aasTask, submodelTask, conceptDiscriptorTask).ConfigureAwait(false); diff --git a/source/AAS.TwinEngine.DataEngine/Infrastructure/Providers/AasRegistryProvider/Services/AasRegistryProvider.cs b/source/AAS.TwinEngine.DataEngine/Infrastructure/Providers/AasRegistryProvider/Services/AasRegistryProvider.cs index dadeaa4a..b7701993 100644 --- a/source/AAS.TwinEngine.DataEngine/Infrastructure/Providers/AasRegistryProvider/Services/AasRegistryProvider.cs +++ b/source/AAS.TwinEngine.DataEngine/Infrastructure/Providers/AasRegistryProvider/Services/AasRegistryProvider.cs @@ -4,10 +4,10 @@ using AAS.TwinEngine.DataEngine.ApplicationLogic.Exceptions.Infrastructure; using AAS.TwinEngine.DataEngine.ApplicationLogic.Extensions; using AAS.TwinEngine.DataEngine.ApplicationLogic.Services.AasRegistry.Providers; -using AAS.TwinEngine.DataEngine.ApplicationLogic.Services.Plugin.Config; using AAS.TwinEngine.DataEngine.DomainModel.AasRegistry; using AAS.TwinEngine.DataEngine.Infrastructure.Http.Clients; using AAS.TwinEngine.DataEngine.Infrastructure.Shared; +using AAS.TwinEngine.DataEngine.ServiceConfiguration.Config; using UnauthorizedAccessException = AAS.TwinEngine.DataEngine.ApplicationLogic.Exceptions.Infrastructure.UnauthorizedAccessException; @@ -15,8 +15,8 @@ namespace AAS.TwinEngine.DataEngine.Infrastructure.Providers.AasRegistryProvider public class AasRegistryProvider(ILogger logger, ICreateClient clientFactory) : IAasRegistryProvider { - private const string AasRegistryPath = AasEnvironmentConfig.AasRegistryPath; - private const string HttpClientName = AasEnvironmentConfig.AasRegistry; + private const string AasRegistryPath = ApiPaths.ShellDescriptors; + private const string HttpClientName = HttpClientNames.AasRegistry; public async Task> GetAllAsync(CancellationToken cancellationToken) { diff --git a/source/AAS.TwinEngine.DataEngine/Infrastructure/Providers/PluginDataProvider/Config/PluginConfig.cs b/source/AAS.TwinEngine.DataEngine/Infrastructure/Providers/PluginDataProvider/Config/PluginConfig.cs deleted file mode 100644 index 8bd6eb1f..00000000 --- a/source/AAS.TwinEngine.DataEngine/Infrastructure/Providers/PluginDataProvider/Config/PluginConfig.cs +++ /dev/null @@ -1,21 +0,0 @@ -namespace AAS.TwinEngine.DataEngine.Infrastructure.Providers.PluginDataProvider.Config; - -public class PluginConfig -{ - public const string Section = "PluginConfig"; - - public const string HttpClientNamePrefix = "plugin-data-provider"; - - public const string HealthCheckHttpClientNamePrefix = "plugin-healthcheck"; - - public const string MetaData = "metadata"; - - public required List Plugins { get; set; } -} - -public class Plugin -{ - public required string PluginName { get; set; } - - public required Uri PluginUrl { get; set; } -} diff --git a/source/AAS.TwinEngine.DataEngine/Infrastructure/Providers/PluginDataProvider/Helper/RegisterPluginHttpClients.cs b/source/AAS.TwinEngine.DataEngine/Infrastructure/Providers/PluginDataProvider/Helper/RegisterPluginHttpClients.cs index 1bc8cfc7..33897319 100644 --- a/source/AAS.TwinEngine.DataEngine/Infrastructure/Providers/PluginDataProvider/Helper/RegisterPluginHttpClients.cs +++ b/source/AAS.TwinEngine.DataEngine/Infrastructure/Providers/PluginDataProvider/Helper/RegisterPluginHttpClients.cs @@ -1,6 +1,5 @@ using AAS.TwinEngine.DataEngine.DomainModel.Plugin; using AAS.TwinEngine.DataEngine.Infrastructure.Http.Extensions; -using AAS.TwinEngine.DataEngine.Infrastructure.Providers.PluginDataProvider.Config; using AAS.TwinEngine.DataEngine.ServiceConfiguration.Config; namespace AAS.TwinEngine.DataEngine.Infrastructure.Providers.PluginDataProvider.Helper; @@ -15,7 +14,7 @@ public static void RegisterHttpClients( foreach (var manifest in manifests) { _ = services.AddHttpClientWithResilience( - $"{PluginConfig.HttpClientNamePrefix}{manifest.PluginName}", + $"{HttpClientNames.PluginDataProviderPrefix}{manifest.PluginName}", retryConfig, manifest.PluginUrl! ); diff --git a/source/AAS.TwinEngine.DataEngine/Infrastructure/Providers/PluginDataProvider/Services/PluginDataProvider.cs b/source/AAS.TwinEngine.DataEngine/Infrastructure/Providers/PluginDataProvider/Services/PluginDataProvider.cs index 48611950..afe48b15 100644 --- a/source/AAS.TwinEngine.DataEngine/Infrastructure/Providers/PluginDataProvider/Services/PluginDataProvider.cs +++ b/source/AAS.TwinEngine.DataEngine/Infrastructure/Providers/PluginDataProvider/Services/PluginDataProvider.cs @@ -5,7 +5,7 @@ using AAS.TwinEngine.DataEngine.ApplicationLogic.Services.Plugin.Providers; using AAS.TwinEngine.DataEngine.DomainModel.Plugin; using AAS.TwinEngine.DataEngine.Infrastructure.Http.Clients; -using AAS.TwinEngine.DataEngine.Infrastructure.Providers.PluginDataProvider.Config; +using AAS.TwinEngine.DataEngine.ServiceConfiguration.Config; using Microsoft.AspNetCore.WebUtilities; @@ -137,7 +137,7 @@ private async Task> GetAndProcessAsync(IList CountShellDescriptorsAsync(HttpContent responseCo private static string BuildShellsUrl(int? limit, string? cursor) { - const string BaseUrl = $"{PluginConfig.MetaData}/{ShellsEndpoint}"; + const string BaseUrl = $"{ApiPaths.PluginMetadata}/{ShellsEndpoint}"; var queryParams = new Dictionary(); if (limit is > 0) diff --git a/source/AAS.TwinEngine.DataEngine/Infrastructure/Providers/PluginDataProvider/Services/PluginManifestProvider.cs b/source/AAS.TwinEngine.DataEngine/Infrastructure/Providers/PluginDataProvider/Services/PluginManifestProvider.cs index 1034e24b..8baec38e 100644 --- a/source/AAS.TwinEngine.DataEngine/Infrastructure/Providers/PluginDataProvider/Services/PluginManifestProvider.cs +++ b/source/AAS.TwinEngine.DataEngine/Infrastructure/Providers/PluginDataProvider/Services/PluginManifestProvider.cs @@ -4,7 +4,6 @@ using AAS.TwinEngine.DataEngine.ApplicationLogic.Services.Plugin.Providers; using AAS.TwinEngine.DataEngine.DomainModel.Plugin; using AAS.TwinEngine.DataEngine.Infrastructure.Http.Clients; -using AAS.TwinEngine.DataEngine.Infrastructure.Providers.PluginDataProvider.Config; using AAS.TwinEngine.DataEngine.ServiceConfiguration.Config; using Microsoft.Extensions.Options; @@ -27,7 +26,7 @@ public async Task> GetAllPluginManifestsAsync(Cancellation foreach (var plugin in _plugins) { - using var httpClient = CreateClient($"{PluginConfig.HttpClientNamePrefix}{plugin.Name}"); + using var httpClient = CreateClient($"{HttpClientNames.PluginDataProviderPrefix}{plugin.Name}"); try { var response = await httpClient.GetAsync(relativeUri, cancellationToken).ConfigureAwait(false); diff --git a/source/AAS.TwinEngine.DataEngine/Infrastructure/Providers/PluginDataProvider/Services/PluginRequestBuilder.cs b/source/AAS.TwinEngine.DataEngine/Infrastructure/Providers/PluginDataProvider/Services/PluginRequestBuilder.cs index 1cfc4654..9bd318c0 100644 --- a/source/AAS.TwinEngine.DataEngine/Infrastructure/Providers/PluginDataProvider/Services/PluginRequestBuilder.cs +++ b/source/AAS.TwinEngine.DataEngine/Infrastructure/Providers/PluginDataProvider/Services/PluginRequestBuilder.cs @@ -2,8 +2,8 @@ using AAS.TwinEngine.DataEngine.ApplicationLogic.Services.Plugin; using AAS.TwinEngine.DataEngine.DomainModel.Plugin; using AAS.TwinEngine.DataEngine.Infrastructure.Monitoring; -using AAS.TwinEngine.DataEngine.Infrastructure.Providers.PluginDataProvider.Config; using AAS.TwinEngine.DataEngine.Infrastructure.Shared; +using AAS.TwinEngine.DataEngine.ServiceConfiguration.Config; using Json.Schema; @@ -17,7 +17,7 @@ public IList Build(IDictionary jsonSc return jsonSchema .Select(kvp => new PluginRequestSubmodel( - $"{PluginConfig.HttpClientNamePrefix}{kvp.Key}", + $"{HttpClientNames.PluginDataProviderPrefix}{kvp.Key}", CreateHttpContent(kvp.Value))) .ToList(); } @@ -28,7 +28,7 @@ public IList Build(IList plugins, string? aasIden return plugins .Select(plugin => new PluginRequestMetaData( - $"{PluginConfig.HttpClientNamePrefix}{plugin}", + $"{HttpClientNames.PluginDataProviderPrefix}{plugin}", aasIdentifier ?? string.Empty)) .ToList(); } diff --git a/source/AAS.TwinEngine.DataEngine/Infrastructure/Providers/SubmodelRegistryProvider/Services/SubmodelDescriptorProvider.cs b/source/AAS.TwinEngine.DataEngine/Infrastructure/Providers/SubmodelRegistryProvider/Services/SubmodelDescriptorProvider.cs index 01f7227b..f7ed9f18 100644 --- a/source/AAS.TwinEngine.DataEngine/Infrastructure/Providers/SubmodelRegistryProvider/Services/SubmodelDescriptorProvider.cs +++ b/source/AAS.TwinEngine.DataEngine/Infrastructure/Providers/SubmodelRegistryProvider/Services/SubmodelDescriptorProvider.cs @@ -3,10 +3,10 @@ using AAS.TwinEngine.DataEngine.ApplicationLogic.Exceptions.Infrastructure; using AAS.TwinEngine.DataEngine.ApplicationLogic.Extensions; -using AAS.TwinEngine.DataEngine.ApplicationLogic.Services.Plugin.Config; using AAS.TwinEngine.DataEngine.ApplicationLogic.Services.SubmodelRegistry.Providers; using AAS.TwinEngine.DataEngine.DomainModel.SubmodelRegistry; using AAS.TwinEngine.DataEngine.Infrastructure.Http.Clients; +using AAS.TwinEngine.DataEngine.ServiceConfiguration.Config; using UnauthorizedAccessException = AAS.TwinEngine.DataEngine.ApplicationLogic.Exceptions.Infrastructure.UnauthorizedAccessException; @@ -14,7 +14,7 @@ namespace AAS.TwinEngine.DataEngine.Infrastructure.Providers.SubmodelRegistryPro public class SubmodelDescriptorProvider(ILogger logger, ICreateClient clientFactory) : ISubmodelDescriptorProvider { - private const string SubModelRegistryPath = AasEnvironmentConfig.SubModelRegistryPath; + private const string SubModelRegistryPath = ApiPaths.SubmodelDescriptors; public async Task GetDataForSubmodelDescriptorByIdAsync(string id, CancellationToken cancellationToken) { @@ -24,7 +24,7 @@ public async Task GetDataForSubmodelDescriptorByIdAsync(stri var relativeUri = new Uri(url, UriKind.Relative); - var httpClient = clientFactory.CreateClient(AasEnvironmentConfig.SubmodelRegistry); + var httpClient = clientFactory.CreateClient(HttpClientNames.SubmodelRegistry); var response = await httpClient.GetAsync(relativeUri, cancellationToken).ConfigureAwait(false); diff --git a/source/AAS.TwinEngine.DataEngine/Infrastructure/Providers/TemplateProvider/Services/TemplateProvider.cs b/source/AAS.TwinEngine.DataEngine/Infrastructure/Providers/TemplateProvider/Services/TemplateProvider.cs index 8871214b..893e32f6 100644 --- a/source/AAS.TwinEngine.DataEngine/Infrastructure/Providers/TemplateProvider/Services/TemplateProvider.cs +++ b/source/AAS.TwinEngine.DataEngine/Infrastructure/Providers/TemplateProvider/Services/TemplateProvider.cs @@ -4,9 +4,9 @@ using AAS.TwinEngine.DataEngine.ApplicationLogic.Exceptions.Infrastructure; using AAS.TwinEngine.DataEngine.ApplicationLogic.Extensions; using AAS.TwinEngine.DataEngine.ApplicationLogic.Services.AasEnvironment.Providers; -using AAS.TwinEngine.DataEngine.ApplicationLogic.Services.Plugin.Config; using AAS.TwinEngine.DataEngine.DomainModel.AasRegistry; using AAS.TwinEngine.DataEngine.Infrastructure.Http.Clients; +using AAS.TwinEngine.DataEngine.ServiceConfiguration.Config; using AasCore.Aas3_0; @@ -16,11 +16,11 @@ namespace AAS.TwinEngine.DataEngine.Infrastructure.Providers.TemplateProvider.Se public class TemplateProvider(ILogger logger, ICreateClient clientFactory) : ITemplateProvider { - private const string SubModelRepositoryPath = AasEnvironmentConfig.SubModelRepositoryPath; - private const string AasRegistryPath = AasEnvironmentConfig.AasRegistryPath; - private const string AasRepositoryPath = AasEnvironmentConfig.AasRepositoryPath; - private const string SubmodelRefPath = AasEnvironmentConfig.SubmodelRefPath; - private const string ConceptDescriptionPath = AasEnvironmentConfig.ConceptDescriptionPath; + private const string SubModelRepositoryPath = ApiPaths.Submodels; + private const string AasRegistryPath = ApiPaths.ShellDescriptors; + private const string AasRepositoryPath = ApiPaths.Shells; + private const string SubmodelRefPath = ApiPaths.SubmodelRefs; + private const string ConceptDescriptionPath = ApiPaths.ConceptDescriptions; public async Task GetSubmodelTemplateAsync(string templateId, CancellationToken cancellationToken) { @@ -28,7 +28,7 @@ public async Task GetSubmodelTemplateAsync(string templateId, Cancell var url = $"{SubModelRepositoryPath}/{encodedTemplateId}"; - var response = await SendGetRequestAsync(url, AasEnvironmentConfig.SubmodelTemplateRepository, cancellationToken).ConfigureAwait(false); + var response = await SendGetRequestAsync(url, HttpClientNames.SubmodelTemplateRepository, cancellationToken).ConfigureAwait(false); var content = await response.Content.ReadAsStringAsync(cancellationToken).ConfigureAwait(false); try @@ -49,7 +49,7 @@ public async Task GetShellDescriptorsTemplateAsync(Cancellation { var url = $"{AasRegistryPath}"; - var response = await SendGetRequestAsync(url, AasEnvironmentConfig.AasRegistry, cancellationToken).ConfigureAwait(false); + var response = await SendGetRequestAsync(url, HttpClientNames.AasRegistry, cancellationToken).ConfigureAwait(false); var content = await response.Content.ReadAsStringAsync(cancellationToken).ConfigureAwait(false); try @@ -90,7 +90,7 @@ public async Task GetShellTemplateAsync(string templa var encodedTemplateId = templateId.EncodeBase64Url(logger); var url = $"{AasRepositoryPath}/{encodedTemplateId}"; - var response = await SendGetRequestAsync(url, AasEnvironmentConfig.AasTemplateRepository, cancellationToken).ConfigureAwait(false); + var response = await SendGetRequestAsync(url, HttpClientNames.AasTemplateRepository, cancellationToken).ConfigureAwait(false); var content = await response.Content.ReadAsStringAsync(cancellationToken).ConfigureAwait(false); try @@ -117,7 +117,7 @@ public async Task GetAssetInformationTemplateAsync(string tem var encodedTemplateId = templateId.EncodeBase64Url(logger); var url = $"{AasRepositoryPath}/{encodedTemplateId}/asset-information"; - var response = await SendGetRequestAsync(url, AasEnvironmentConfig.AasTemplateRepository, cancellationToken).ConfigureAwait(false); + var response = await SendGetRequestAsync(url, HttpClientNames.AasTemplateRepository, cancellationToken).ConfigureAwait(false); var content = await response.Content.ReadAsStringAsync(cancellationToken).ConfigureAwait(false); try @@ -144,7 +144,7 @@ public async Task> GetSubmodelRefByIdAsync(string templateId, C var encodedTemplateId = templateId.EncodeBase64Url(logger); var url = $"{AasRepositoryPath}/{encodedTemplateId}/{SubmodelRefPath}"; - var response = await SendGetRequestAsync(url, AasEnvironmentConfig.AasTemplateRepository, cancellationToken).ConfigureAwait(false); + var response = await SendGetRequestAsync(url, HttpClientNames.AasTemplateRepository, cancellationToken).ConfigureAwait(false); var content = await response.Content.ReadAsStringAsync(cancellationToken).ConfigureAwait(false); try @@ -191,7 +191,7 @@ public async Task> GetSubmodelRefByIdAsync(string templateId, C try { - var response = await SendGetRequestAsync(url, AasEnvironmentConfig.ConceptDescriptorTemplateRepository, cancellationToken).ConfigureAwait(false); + var response = await SendGetRequestAsync(url, HttpClientNames.ConceptDescriptorTemplateRepository, cancellationToken).ConfigureAwait(false); var content = await response.Content.ReadAsStringAsync(cancellationToken).ConfigureAwait(false); var jsonNode = JsonNode.Parse(content); return Jsonization.Deserialize.ConceptDescriptionFrom(jsonNode!); diff --git a/source/AAS.TwinEngine.DataEngine/ServiceConfiguration/Config/ApiPaths.cs b/source/AAS.TwinEngine.DataEngine/ServiceConfiguration/Config/ApiPaths.cs new file mode 100644 index 00000000..7163227e --- /dev/null +++ b/source/AAS.TwinEngine.DataEngine/ServiceConfiguration/Config/ApiPaths.cs @@ -0,0 +1,19 @@ +namespace AAS.TwinEngine.DataEngine.ServiceConfiguration.Config; + +/// +/// Fixed API path segments used to build request URIs for AAS-specification endpoints. +/// These are protocol constants, not user-configurable values. +/// +public static class ApiPaths +{ + public const string Submodels = "submodels"; + public const string ShellDescriptors = "shell-descriptors"; + public const string SubmodelDescriptors = "submodel-descriptors"; + public const string Shells = "shells"; + public const string SubmodelRefs = "submodel-refs"; + public const string ConceptDescriptions = "concept-descriptions"; + + // ── Plugin API paths ── + + public const string PluginMetadata = "metadata"; +} diff --git a/source/AAS.TwinEngine.DataEngine/ServiceConfiguration/Config/Helpers/RegistrySettingsConfigValidator.cs b/source/AAS.TwinEngine.DataEngine/ServiceConfiguration/Config/Helpers/RegistrySettingsConfigValidator.cs new file mode 100644 index 00000000..c251f4c5 --- /dev/null +++ b/source/AAS.TwinEngine.DataEngine/ServiceConfiguration/Config/Helpers/RegistrySettingsConfigValidator.cs @@ -0,0 +1,37 @@ +using Cronos; + +using Microsoft.Extensions.Options; + +namespace AAS.TwinEngine.DataEngine.ServiceConfiguration.Config; + +public class RegistrySettingsConfigValidator : IValidateOptions +{ + public ValidateOptionsResult Validate(string? name, RegistrySettingsConfig options) + { + ArgumentNullException.ThrowIfNull(options); + + if (!options.PreComputed.Enabled) + { + return ValidateOptionsResult.Success; + } + + var schedule = options.PreComputed.Schedule; + if (string.IsNullOrWhiteSpace(schedule)) + { + return ValidateOptionsResult.Fail( + $"{RegistrySettingsConfig.Section}.PreComputed.Schedule is required when PreComputed.Enabled is true."); + } + + try + { + CronExpression.Parse(schedule, CronFormat.IncludeSeconds); + } + catch (CronFormatException ex) + { + return ValidateOptionsResult.Fail( + $"{RegistrySettingsConfig.Section}.PreComputed.Schedule is not a valid cron expression: '{schedule}'. {ex.Message}"); + } + + return ValidateOptionsResult.Success; + } +} diff --git a/source/AAS.TwinEngine.DataEngine/ServiceConfiguration/Config/TemplateManagementConfigNormalizer.cs b/source/AAS.TwinEngine.DataEngine/ServiceConfiguration/Config/Helpers/TemplateManagementConfigNormalizer.cs similarity index 100% rename from source/AAS.TwinEngine.DataEngine/ServiceConfiguration/Config/TemplateManagementConfigNormalizer.cs rename to source/AAS.TwinEngine.DataEngine/ServiceConfiguration/Config/Helpers/TemplateManagementConfigNormalizer.cs diff --git a/source/AAS.TwinEngine.DataEngine/ServiceConfiguration/Config/Helpers/TemplateManagementConfigValidator.cs b/source/AAS.TwinEngine.DataEngine/ServiceConfiguration/Config/Helpers/TemplateManagementConfigValidator.cs new file mode 100644 index 00000000..722734e6 --- /dev/null +++ b/source/AAS.TwinEngine.DataEngine/ServiceConfiguration/Config/Helpers/TemplateManagementConfigValidator.cs @@ -0,0 +1,39 @@ +using Microsoft.Extensions.Options; + +namespace AAS.TwinEngine.DataEngine.ServiceConfiguration.Config; + +public class TemplateManagementConfigValidator : IValidateOptions +{ + private static readonly (string Name, Func Accessor)[] Endpoints = + [ + ("AasTemplateRepository", c => c.AasTemplateRepository), + ("SubmodelTemplateRepository", c => c.SubmodelTemplateRepository), + ("ConceptDescriptionTemplateRepository", c => c.ConceptDescriptionTemplateRepository), + ("AasTemplateRegistry", c => c.AasTemplateRegistry), + ("SubmodelTemplateRegistry", c => c.SubmodelTemplateRegistry), + ]; + + public ValidateOptionsResult Validate(string? name, TemplateManagementConfig options) + { + ArgumentNullException.ThrowIfNull(options); + + var errors = new List(); + + foreach (var (endpointName, accessor) in Endpoints) + { + var endpoint = accessor(options); + if (endpoint.BaseUrl is null) + { + errors.Add($"{TemplateManagementConfig.Section}.{endpointName}.BaseUrl is required."); + } + else if (!endpoint.BaseUrl.IsAbsoluteUri) + { + errors.Add($"{TemplateManagementConfig.Section}.{endpointName}.BaseUrl must be an absolute URI, got: '{endpoint.BaseUrl}'."); + } + } + + return errors.Count > 0 + ? ValidateOptionsResult.Fail(errors) + : ValidateOptionsResult.Success; + } +} diff --git a/source/AAS.TwinEngine.DataEngine/ApplicationLogic/Services/Plugin/Config/AasEnvironmentConfig.cs b/source/AAS.TwinEngine.DataEngine/ServiceConfiguration/Config/HttpClientNames.cs similarity index 55% rename from source/AAS.TwinEngine.DataEngine/ApplicationLogic/Services/Plugin/Config/AasEnvironmentConfig.cs rename to source/AAS.TwinEngine.DataEngine/ServiceConfiguration/Config/HttpClientNames.cs index a5d5abeb..ff6c9f3f 100644 --- a/source/AAS.TwinEngine.DataEngine/ApplicationLogic/Services/Plugin/Config/AasEnvironmentConfig.cs +++ b/source/AAS.TwinEngine.DataEngine/ServiceConfiguration/Config/HttpClientNames.cs @@ -1,9 +1,12 @@ -namespace AAS.TwinEngine.DataEngine.ApplicationLogic.Services.Plugin.Config; - -public class AasEnvironmentConfig +namespace AAS.TwinEngine.DataEngine.ServiceConfiguration.Config; + +/// +/// Named HttpClient identifiers used for DI registration, HttpClientFactory resolution, +/// and health check client creation. Centralises all client-name constants so they are +/// independent of legacy configuration classes. +/// +public static class HttpClientNames { - public const string Section = "AasEnvironment"; - public const string TemplateRepository = "template-repository"; public const string AasRegistry = "aas-registry"; public const string SubmodelRegistry = "submodel-registry"; @@ -11,6 +14,11 @@ public class AasEnvironmentConfig public const string AasTemplateRepository = "aas-template-repository"; public const string ConceptDescriptorTemplateRepository = "concept-descriptor-template-repository"; + // ── Plugin client name prefixes ── + + public const string PluginDataProviderPrefix = "plugin-data-provider"; + public const string PluginHealthCheckPrefix = "plugin-healthcheck"; + private const string HealthCheckSuffix = "-healthcheck"; public static string GetHealthCheckName(string clientName) => $"{clientName}{HealthCheckSuffix}"; @@ -21,23 +29,4 @@ public class AasEnvironmentConfig public static string SubmodelTemplateRepositoryHealthCheck => GetHealthCheckName(SubmodelTemplateRepository); public static string AasTemplateRepositoryHealthCheck => GetHealthCheckName(AasTemplateRepository); public static string ConceptDescriptorTemplateRepositoryHealthCheck => GetHealthCheckName(ConceptDescriptorTemplateRepository); - - // Path constants (no longer configurable — fixed API contracts) - public const string SubModelRepositoryPath = "submodels"; - public const string AasRegistryPath = "shell-descriptors"; - public const string SubModelRegistryPath = "submodel-descriptors"; - public const string AasRepositoryPath = "shells"; - public const string SubmodelRefPath = "submodel-refs"; - public const string ConceptDescriptionPath = "concept-descriptions"; - - // V1-bindable URI properties (used only by LegacyV1 adapter) - public Uri DataEngineRepositoryBaseUrl { get; set; } = null!; - - public Uri? AasEnvironmentRepositoryBaseUrl { get; set; } = null!; - - public Uri? AasRegistryBaseUrl { get; set; } = null!; - - public Uri? SubModelRegistryBaseUrl { get; set; } = null!; - - public Uri CustomerDomainUrl { get; set; } = null!; } diff --git a/source/AAS.TwinEngine.DataEngine/ServiceConfiguration/InfrastructureDependencyInjectionExtensions.cs b/source/AAS.TwinEngine.DataEngine/ServiceConfiguration/InfrastructureDependencyInjectionExtensions.cs index 913616f4..e61cc67c 100644 --- a/source/AAS.TwinEngine.DataEngine/ServiceConfiguration/InfrastructureDependencyInjectionExtensions.cs +++ b/source/AAS.TwinEngine.DataEngine/ServiceConfiguration/InfrastructureDependencyInjectionExtensions.cs @@ -13,7 +13,6 @@ using AAS.TwinEngine.DataEngine.Infrastructure.Http.Extensions; using AAS.TwinEngine.DataEngine.Infrastructure.Monitoring; using AAS.TwinEngine.DataEngine.Infrastructure.Providers.AasRegistryProvider.Services; -using AAS.TwinEngine.DataEngine.Infrastructure.Providers.PluginDataProvider.Config; using AAS.TwinEngine.DataEngine.Infrastructure.Providers.PluginDataProvider.Helper; using AAS.TwinEngine.DataEngine.Infrastructure.Providers.PluginDataProvider.Services; using AAS.TwinEngine.DataEngine.Infrastructure.Providers.SubmodelRegistryProvider.Services; @@ -50,50 +49,64 @@ public static void ConfigureInfrastructure(this IServiceCollection services, ICo // MultiPluginConflictOptions: V1 config binds the old section value; V2 has no section → default ThrowError _ = services.Configure(configuration.GetSection(MultiPluginConflictOptions.Section)); - _ = services.Configure(configuration.GetSection(TemplateManagementConfig.Section)); - _ = services.Configure(configuration.GetSection(RegistrySettingsConfig.Section)); + _ = services.AddOptions() + .Bind(configuration.GetSection(TemplateManagementConfig.Section)) + .ValidateOnStart(); + _ = services.AddOptions() + .Bind(configuration.GetSection(RegistrySettingsConfig.Section)) + .ValidateOnStart(); // Normalizer: applies TemplateRepository shorthand to individual repository endpoints _ = services.AddSingleton, TemplateManagementConfigNormalizer>(); + // Validators + _ = services.AddSingleton, TemplateManagementConfigValidator>(); + _ = services.AddSingleton, RegistrySettingsConfigValidator>(); + // PluginsConfig: single registration via AddOptions to avoid double-binding of list properties _ = services.AddOptions() .Bind(configuration.GetSection(PluginsConfig.Section)) .ValidateOnStart(); _ = services.AddSingleton, PluginsConfigValidator>(); - // ── Resolve fully-populated config for HttpClient registration ── - // We need TemplateManagementConfig and PluginsConfig to register HttpClients at startup. - // IOptions is populated by V1 legacy adapters (IConfigureOptions) + V2 section-bind. - // Since we are still inside DI registration (container not built yet), we build a - // temporary provider to resolve the options so both V1 and V2 paths are applied. - using var tempProvider = services.BuildServiceProvider(); - var templateManagement = tempProvider.GetRequiredService>().Value; - var pluginsConfig = tempProvider.GetRequiredService>().Value; + // ── Resolve config for HttpClient registration (no BuildServiceProvider) ── + // Bind V2 sections, apply V1 adapter + normalizer manually. + var templateManagement = new TemplateManagementConfig(); + configuration.GetSection(TemplateManagementConfig.Section).Bind(templateManagement); +#pragma warning disable CS0618 // Obsolete — intentional V1 backward-compat mapping + LegacyTemplateManagementConfigAdapter.MapToConfig(configuration, templateManagement); +#pragma warning restore CS0618 + new TemplateManagementConfigNormalizer().PostConfigure(Options.DefaultName, templateManagement); + + var pluginsConfig = new PluginsConfig(); + configuration.GetSection(PluginsConfig.Section).Bind(pluginsConfig); +#pragma warning disable CS0618 // Obsolete — intentional V1 backward-compat mapping + LegacyPluginsConfigAdapter.MapToConfig(configuration, pluginsConfig); +#pragma warning restore CS0618 // Template repository HttpClients (AAS, Submodel, ConceptDescription — separate clients) - _ = services.AddHttpClientWithResilience(AasEnvironmentConfig.AasTemplateRepository, templateManagement.ResiliencePolicies.Retry, templateManagement.AasTemplateRepository.BaseUrl!); - _ = services.AddHttpClientWithResilience(AasEnvironmentConfig.SubmodelTemplateRepository, templateManagement.ResiliencePolicies.Retry, templateManagement.SubmodelTemplateRepository.BaseUrl!); - _ = services.AddHttpClientWithResilience(AasEnvironmentConfig.ConceptDescriptorTemplateRepository, templateManagement.ResiliencePolicies.Retry, templateManagement.ConceptDescriptionTemplateRepository.BaseUrl!); + _ = services.AddHttpClientWithResilience(HttpClientNames.AasTemplateRepository, templateManagement.ResiliencePolicies.Retry, templateManagement.AasTemplateRepository.BaseUrl!); + _ = services.AddHttpClientWithResilience(HttpClientNames.SubmodelTemplateRepository, templateManagement.ResiliencePolicies.Retry, templateManagement.SubmodelTemplateRepository.BaseUrl!); + _ = services.AddHttpClientWithResilience(HttpClientNames.ConceptDescriptorTemplateRepository, templateManagement.ResiliencePolicies.Retry, templateManagement.ConceptDescriptionTemplateRepository.BaseUrl!); // Template registry HttpClients (AAS, Submodel) - _ = services.AddHttpClientWithResilience(AasEnvironmentConfig.AasRegistry, templateManagement.ResiliencePolicies.Retry, templateManagement.AasTemplateRegistry.BaseUrl!); - _ = services.AddHttpClientWithResilience(AasEnvironmentConfig.SubmodelRegistry, templateManagement.ResiliencePolicies.Retry, templateManagement.SubmodelTemplateRegistry.BaseUrl!); + _ = services.AddHttpClientWithResilience(HttpClientNames.AasRegistry, templateManagement.ResiliencePolicies.Retry, templateManagement.AasTemplateRegistry.BaseUrl!); + _ = services.AddHttpClientWithResilience(HttpClientNames.SubmodelRegistry, templateManagement.ResiliencePolicies.Retry, templateManagement.SubmodelTemplateRegistry.BaseUrl!); // Health check clients (without resilience) - _ = services.AddHttpClientWithoutResilience(AasEnvironmentConfig.AasTemplateRepositoryHealthCheck, templateManagement.AasTemplateRepository.BaseUrl!); - _ = services.AddHttpClientWithoutResilience(AasEnvironmentConfig.SubmodelTemplateRepositoryHealthCheck, templateManagement.SubmodelTemplateRepository.BaseUrl!); - _ = services.AddHttpClientWithoutResilience(AasEnvironmentConfig.ConceptDescriptorTemplateRepositoryHealthCheck, templateManagement.ConceptDescriptionTemplateRepository.BaseUrl!); - _ = services.AddHttpClientWithoutResilience(AasEnvironmentConfig.AasRegistryHealthCheck, templateManagement.AasTemplateRegistry.BaseUrl!); - _ = services.AddHttpClientWithoutResilience(AasEnvironmentConfig.SubmodelRegistryHealthCheck, templateManagement.SubmodelTemplateRegistry.BaseUrl!); + _ = services.AddHttpClientWithoutResilience(HttpClientNames.AasTemplateRepositoryHealthCheck, templateManagement.AasTemplateRepository.BaseUrl!); + _ = services.AddHttpClientWithoutResilience(HttpClientNames.SubmodelTemplateRepositoryHealthCheck, templateManagement.SubmodelTemplateRepository.BaseUrl!); + _ = services.AddHttpClientWithoutResilience(HttpClientNames.ConceptDescriptorTemplateRepositoryHealthCheck, templateManagement.ConceptDescriptionTemplateRepository.BaseUrl!); + _ = services.AddHttpClientWithoutResilience(HttpClientNames.AasRegistryHealthCheck, templateManagement.AasTemplateRegistry.BaseUrl!); + _ = services.AddHttpClientWithoutResilience(HttpClientNames.SubmodelRegistryHealthCheck, templateManagement.SubmodelTemplateRegistry.BaseUrl!); // Plugin HttpClients (from PluginsConfig.Instances) if (pluginsConfig.Instances.Count > 0) { foreach (var plugin in pluginsConfig.Instances) { - _ = services.AddHttpClientWithResilience(PluginConfig.HttpClientNamePrefix + plugin.Name, pluginsConfig.ResiliencePolicies.Retry, plugin.BaseUrl); - _ = services.AddHttpClientWithoutResilience(PluginConfig.HealthCheckHttpClientNamePrefix + plugin.Name, plugin.BaseUrl!); + _ = services.AddHttpClientWithResilience(HttpClientNames.PluginDataProviderPrefix + plugin.Name, pluginsConfig.ResiliencePolicies.Retry, plugin.BaseUrl); + _ = services.AddHttpClientWithoutResilience(HttpClientNames.PluginHealthCheckPrefix + plugin.Name, plugin.BaseUrl!); } } From c46f74da1f4ca955ef4e28d3bc79c6c701bc2e2f Mon Sep 17 00:00:00 2001 From: Kevalkumar Date: Wed, 15 Apr 2026 12:02:45 +0530 Subject: [PATCH 33/71] Add PluginsConfigValidator and refactor namespace for config validators --- .../ConfigurationBackwardCompatibilityE2ETests.cs | 3 ++- .../Config/Helpers}/PluginsConfigValidator.cs | 6 ++---- .../Config/Helpers/RegistrySettingsConfigValidator.cs | 4 ++-- .../Config/Helpers/TemplateManagementConfigNormalizer.cs | 4 ++-- .../Config/Helpers/TemplateManagementConfigValidator.cs | 4 ++-- .../InfrastructureDependencyInjectionExtensions.cs | 2 +- 6 files changed, 11 insertions(+), 12 deletions(-) rename source/AAS.TwinEngine.DataEngine/{ApplicationLogic/Services/SubmodelRepository/Config/Helper => ServiceConfiguration/Config/Helpers}/PluginsConfigValidator.cs (89%) diff --git a/source/AAS.TwinEngine.DataEngine.UnitTests/ServiceConfiguration/ConfigurationMigration/ConfigurationBackwardCompatibilityE2ETests.cs b/source/AAS.TwinEngine.DataEngine.UnitTests/ServiceConfiguration/ConfigurationMigration/ConfigurationBackwardCompatibilityE2ETests.cs index ba934861..25c9f074 100644 --- a/source/AAS.TwinEngine.DataEngine.UnitTests/ServiceConfiguration/ConfigurationMigration/ConfigurationBackwardCompatibilityE2ETests.cs +++ b/source/AAS.TwinEngine.DataEngine.UnitTests/ServiceConfiguration/ConfigurationMigration/ConfigurationBackwardCompatibilityE2ETests.cs @@ -1,5 +1,6 @@ -using AAS.TwinEngine.DataEngine.Infrastructure.Configuration.LegacyV1; +using AAS.TwinEngine.DataEngine.Infrastructure.Configuration.LegacyV1; using AAS.TwinEngine.DataEngine.ServiceConfiguration.Config; +using AAS.TwinEngine.DataEngine.ServiceConfiguration.Config.Helpers; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; diff --git a/source/AAS.TwinEngine.DataEngine/ApplicationLogic/Services/SubmodelRepository/Config/Helper/PluginsConfigValidator.cs b/source/AAS.TwinEngine.DataEngine/ServiceConfiguration/Config/Helpers/PluginsConfigValidator.cs similarity index 89% rename from source/AAS.TwinEngine.DataEngine/ApplicationLogic/Services/SubmodelRepository/Config/Helper/PluginsConfigValidator.cs rename to source/AAS.TwinEngine.DataEngine/ServiceConfiguration/Config/Helpers/PluginsConfigValidator.cs index a4f6e690..fb2d2aa4 100644 --- a/source/AAS.TwinEngine.DataEngine/ApplicationLogic/Services/SubmodelRepository/Config/Helper/PluginsConfigValidator.cs +++ b/source/AAS.TwinEngine.DataEngine/ServiceConfiguration/Config/Helpers/PluginsConfigValidator.cs @@ -1,10 +1,8 @@ -using System.Text.RegularExpressions; - -using AAS.TwinEngine.DataEngine.ServiceConfiguration.Config; +using System.Text.RegularExpressions; using Microsoft.Extensions.Options; -namespace AAS.TwinEngine.DataEngine.ApplicationLogic.Services.SubmodelRepository.Config.Helper; +namespace AAS.TwinEngine.DataEngine.ServiceConfiguration.Config.Helpers; /// /// Validates the MultiLanguageProperty settings within PluginsConfig. diff --git a/source/AAS.TwinEngine.DataEngine/ServiceConfiguration/Config/Helpers/RegistrySettingsConfigValidator.cs b/source/AAS.TwinEngine.DataEngine/ServiceConfiguration/Config/Helpers/RegistrySettingsConfigValidator.cs index c251f4c5..ff6c30a5 100644 --- a/source/AAS.TwinEngine.DataEngine/ServiceConfiguration/Config/Helpers/RegistrySettingsConfigValidator.cs +++ b/source/AAS.TwinEngine.DataEngine/ServiceConfiguration/Config/Helpers/RegistrySettingsConfigValidator.cs @@ -1,8 +1,8 @@ -using Cronos; +using Cronos; using Microsoft.Extensions.Options; -namespace AAS.TwinEngine.DataEngine.ServiceConfiguration.Config; +namespace AAS.TwinEngine.DataEngine.ServiceConfiguration.Config.Helpers; public class RegistrySettingsConfigValidator : IValidateOptions { diff --git a/source/AAS.TwinEngine.DataEngine/ServiceConfiguration/Config/Helpers/TemplateManagementConfigNormalizer.cs b/source/AAS.TwinEngine.DataEngine/ServiceConfiguration/Config/Helpers/TemplateManagementConfigNormalizer.cs index a644ab89..430d2734 100644 --- a/source/AAS.TwinEngine.DataEngine/ServiceConfiguration/Config/Helpers/TemplateManagementConfigNormalizer.cs +++ b/source/AAS.TwinEngine.DataEngine/ServiceConfiguration/Config/Helpers/TemplateManagementConfigNormalizer.cs @@ -1,6 +1,6 @@ -using Microsoft.Extensions.Options; +using Microsoft.Extensions.Options; -namespace AAS.TwinEngine.DataEngine.ServiceConfiguration.Config; +namespace AAS.TwinEngine.DataEngine.ServiceConfiguration.Config.Helpers; /// /// Post-configuration step that applies the diff --git a/source/AAS.TwinEngine.DataEngine/ServiceConfiguration/Config/Helpers/TemplateManagementConfigValidator.cs b/source/AAS.TwinEngine.DataEngine/ServiceConfiguration/Config/Helpers/TemplateManagementConfigValidator.cs index 722734e6..cbb4262f 100644 --- a/source/AAS.TwinEngine.DataEngine/ServiceConfiguration/Config/Helpers/TemplateManagementConfigValidator.cs +++ b/source/AAS.TwinEngine.DataEngine/ServiceConfiguration/Config/Helpers/TemplateManagementConfigValidator.cs @@ -1,6 +1,6 @@ -using Microsoft.Extensions.Options; +using Microsoft.Extensions.Options; -namespace AAS.TwinEngine.DataEngine.ServiceConfiguration.Config; +namespace AAS.TwinEngine.DataEngine.ServiceConfiguration.Config.Helpers; public class TemplateManagementConfigValidator : IValidateOptions { diff --git a/source/AAS.TwinEngine.DataEngine/ServiceConfiguration/InfrastructureDependencyInjectionExtensions.cs b/source/AAS.TwinEngine.DataEngine/ServiceConfiguration/InfrastructureDependencyInjectionExtensions.cs index e61cc67c..75b34590 100644 --- a/source/AAS.TwinEngine.DataEngine/ServiceConfiguration/InfrastructureDependencyInjectionExtensions.cs +++ b/source/AAS.TwinEngine.DataEngine/ServiceConfiguration/InfrastructureDependencyInjectionExtensions.cs @@ -6,7 +6,6 @@ using AAS.TwinEngine.DataEngine.ApplicationLogic.Services.Plugin.Providers; using AAS.TwinEngine.DataEngine.ApplicationLogic.Services.Shared; using AAS.TwinEngine.DataEngine.ApplicationLogic.Services.SubmodelRegistry.Providers; -using AAS.TwinEngine.DataEngine.ApplicationLogic.Services.SubmodelRepository.Config.Helper; using AAS.TwinEngine.DataEngine.Infrastructure.Configuration.LegacyV1; using AAS.TwinEngine.DataEngine.Infrastructure.Http.Authorization.Headers; using AAS.TwinEngine.DataEngine.Infrastructure.Http.Clients; @@ -19,6 +18,7 @@ using AAS.TwinEngine.DataEngine.Infrastructure.Providers.TemplateProvider.Services; using AAS.TwinEngine.DataEngine.Infrastructure.Shared; using AAS.TwinEngine.DataEngine.ServiceConfiguration.Config; +using AAS.TwinEngine.DataEngine.ServiceConfiguration.Config.Helpers; using Microsoft.Extensions.Options; From 3432e94782675bb78eae730dfe142326b4fd88a3 Mon Sep 17 00:00:00 2001 From: Kevalkumar Date: Thu, 16 Apr 2026 03:46:20 +0530 Subject: [PATCH 34/71] changes for backward compebility --- .../LegacyV1/LegacyGeneralConfigAdapter.cs | 24 ++++++++ .../LegacyV1/LegacyPluginsConfigAdapter.cs | 14 ++++- .../LegacyRegistrySettingsConfigAdapter.cs | 16 ++++- .../LegacyTemplateManagementConfigAdapter.cs | 60 +++++++++++++++++-- ...astructureDependencyInjectionExtensions.cs | 24 +++++--- 5 files changed, 124 insertions(+), 14 deletions(-) diff --git a/source/AAS.TwinEngine.DataEngine/Infrastructure/Configuration/LegacyV1/LegacyGeneralConfigAdapter.cs b/source/AAS.TwinEngine.DataEngine/Infrastructure/Configuration/LegacyV1/LegacyGeneralConfigAdapter.cs index f72bf766..086a18cb 100644 --- a/source/AAS.TwinEngine.DataEngine/Infrastructure/Configuration/LegacyV1/LegacyGeneralConfigAdapter.cs +++ b/source/AAS.TwinEngine.DataEngine/Infrastructure/Configuration/LegacyV1/LegacyGeneralConfigAdapter.cs @@ -21,6 +21,7 @@ public void Configure(GeneralConfig options) { if (!LegacyConfigurationDetector.IsV1Configuration(_configuration)) { + ApplyV1Overrides(_configuration, options); return; } @@ -57,4 +58,27 @@ public void Configure(GeneralConfig options) options.OpenTelemetry = otel; } } + + /// + /// If V1-specific sections exist (e.g. from V1-style env vars), overrides the corresponding + /// V2 values. Called in both V1 and V2 modes so that legacy env vars work even when + /// appsettings.json already ships V2 sections. + /// + public static void ApplyV1Overrides(IConfiguration configuration, GeneralConfig options) + { + // AasEnvironment (V1-only top-level section) + var aasEnv = configuration.GetSection(AasEnvironmentConfig.Section).Get(); + if (aasEnv != null) + { + if (aasEnv.CustomerDomainUrl != null) + { + options.CustomerDomainUrl = aasEnv.CustomerDomainUrl; + } + + if (aasEnv.DataEngineRepositoryBaseUrl != null) + { + options.DataEngineRepositoryBaseUrl = aasEnv.DataEngineRepositoryBaseUrl; + } + } + } } diff --git a/source/AAS.TwinEngine.DataEngine/Infrastructure/Configuration/LegacyV1/LegacyPluginsConfigAdapter.cs b/source/AAS.TwinEngine.DataEngine/Infrastructure/Configuration/LegacyV1/LegacyPluginsConfigAdapter.cs index 1e01ed39..1d5b1094 100644 --- a/source/AAS.TwinEngine.DataEngine/Infrastructure/Configuration/LegacyV1/LegacyPluginsConfigAdapter.cs +++ b/source/AAS.TwinEngine.DataEngine/Infrastructure/Configuration/LegacyV1/LegacyPluginsConfigAdapter.cs @@ -23,6 +23,7 @@ public static void MapToConfig(IConfiguration configuration, PluginsConfig optio { if (!LegacyConfigurationDetector.IsV1Configuration(configuration)) { + ApplyV1PluginInstanceOverrides(configuration, options); return; } @@ -54,10 +55,21 @@ public static void MapToConfig(IConfiguration configuration, PluginsConfig optio } // Plugin instances (V1: "PluginConfig:Plugins") → Plugins:Instances with property renames + ApplyV1PluginInstanceOverrides(configuration, options); + } + + /// + /// If the V1 PluginConfig:Plugins section contains values (e.g. from V1-style env vars), + /// overrides with the mapped V1 values. + /// Called in both V1 and V2 modes so that legacy env vars work even when + /// appsettings.json already ships V2 sections. + /// + public static void ApplyV1PluginInstanceOverrides(IConfiguration configuration, PluginsConfig options) + { var pluginConfig = configuration.GetSection(PluginConfig.Section).Get(); var headerForwarding = configuration.GetSection(HeaderForwardingOptions.Section).Get(); - if (pluginConfig?.Plugins != null) + if (pluginConfig?.Plugins != null && pluginConfig.Plugins.Count > 0) { options.Instances = pluginConfig.Plugins.Select(plugin => new PluginInstance { diff --git a/source/AAS.TwinEngine.DataEngine/Infrastructure/Configuration/LegacyV1/LegacyRegistrySettingsConfigAdapter.cs b/source/AAS.TwinEngine.DataEngine/Infrastructure/Configuration/LegacyV1/LegacyRegistrySettingsConfigAdapter.cs index cef963cf..4162e442 100644 --- a/source/AAS.TwinEngine.DataEngine/Infrastructure/Configuration/LegacyV1/LegacyRegistrySettingsConfigAdapter.cs +++ b/source/AAS.TwinEngine.DataEngine/Infrastructure/Configuration/LegacyV1/LegacyRegistrySettingsConfigAdapter.cs @@ -17,11 +17,25 @@ public void Configure(RegistrySettingsConfig options) { if (!LegacyConfigurationDetector.IsV1Configuration(_configuration)) { + ApplyV1Overrides(_configuration, options); return; } // V1: "AasRegistryPreComputed" → V2: "RegistrySettings:PreComputed" - var preComputed = _configuration.GetSection(AasRegistryPreComputed.Section).Get(); + ApplyV1Overrides(_configuration, options); + } + + /// + /// If the V1 AasRegistryPreComputed section exists, overrides the corresponding V2 values. + /// + public static void ApplyV1Overrides(IConfiguration configuration, RegistrySettingsConfig options) + { + if (!configuration.GetSection(AasRegistryPreComputed.Section).Exists()) + { + return; + } + + var preComputed = configuration.GetSection(AasRegistryPreComputed.Section).Get(); if (preComputed != null) { options.PreComputed = new PreComputedConfig diff --git a/source/AAS.TwinEngine.DataEngine/Infrastructure/Configuration/LegacyV1/LegacyTemplateManagementConfigAdapter.cs b/source/AAS.TwinEngine.DataEngine/Infrastructure/Configuration/LegacyV1/LegacyTemplateManagementConfigAdapter.cs index 4932eaaf..2ec3b889 100644 --- a/source/AAS.TwinEngine.DataEngine/Infrastructure/Configuration/LegacyV1/LegacyTemplateManagementConfigAdapter.cs +++ b/source/AAS.TwinEngine.DataEngine/Infrastructure/Configuration/LegacyV1/LegacyTemplateManagementConfigAdapter.cs @@ -25,6 +25,7 @@ public static void MapToConfig(IConfiguration configuration, TemplateManagementC { if (!LegacyConfigurationDetector.IsV1Configuration(configuration)) { + ApplyV1Overrides(configuration, options); return; } @@ -54,30 +55,79 @@ public static void MapToConfig(IConfiguration configuration, TemplateManagementC } // AasEnvironment base URLs → service endpoints + ApplyV1ServiceEndpointOverrides(configuration, options); + } + + /// + /// If V1-specific sections exist (e.g. from V1-style env vars), overrides the corresponding + /// V2 values. Called in both V1 and V2 modes so that legacy env vars work even when + /// appsettings.json already ships V2 sections. + /// + public static void ApplyV1Overrides(IConfiguration configuration, TemplateManagementConfig options) + { + // Top-level TemplateMappingRules (V1-only; in V2 it is nested under TemplateManagement) + var mappingRules = configuration.GetSection(TemplateMappingRules.Section).Get(); + if (mappingRules?.SubmodelTemplateMappings?.Count > 0 + || mappingRules?.ShellTemplateMappings?.Count > 0 + || mappingRules?.AasIdExtractionRules?.Count > 0) + { + options.TemplateMappingRules = mappingRules; + } + + // Semantics (V1: top-level "Semantics") + var semantics = configuration.GetSection(Semantics.Section).Get(); + if (semantics != null && !string.IsNullOrEmpty(semantics.InternalSemanticId)) + { + options.Semantics = new TemplateSemanticsConfig + { + InternalSemanticId = semantics.InternalSemanticId + }; + } + + // Resilience (V1: "HttpRetryPolicyOptions:TemplateProvider") + var retryPolicy = configuration.GetSection($"{HttpRetryPolicyOptions.Section}:{HttpRetryPolicyOptions.TemplateProvider}").Get(); + if (retryPolicy != null) + { + options.ResiliencePolicies.Retry.MaxRetryAttempts = retryPolicy.MaxRetryAttempts; + options.ResiliencePolicies.Retry.DelayInSeconds = retryPolicy.DelayInSeconds; + } + + // AasEnvironment → service endpoints + ApplyV1ServiceEndpointOverrides(configuration, options); + } + + private static void ApplyV1ServiceEndpointOverrides(IConfiguration configuration, TemplateManagementConfig options) + { var aasEnv = configuration.GetSection(AasEnvironmentConfig.Section).Get(); + if (aasEnv == null) + { + return; + } - // Header mappings from HeaderForwarding var headerForwarding = configuration.GetSection(HeaderForwardingOptions.Section).Get(); - if (aasEnv != null) + if (aasEnv.AasEnvironmentRepositoryBaseUrl != null) { - // V1 uses a single AasEnvironmentRepositoryBaseUrl for all template repositories. - // Map it to the TemplateRepository shorthand so the normalizer propagates - // the same URL and headers to Aas/Submodel/ConceptDescription template repositories. options.TemplateRepository = new ServiceEndpoint { Name = HttpClientNames.TemplateRepository, BaseUrl = aasEnv.AasEnvironmentRepositoryBaseUrl, HeaderMappings = headerForwarding?.HeaderMappings.TemplateRepository ?? [] }; + } + if (aasEnv.AasRegistryBaseUrl != null) + { options.AasTemplateRegistry = new ServiceEndpoint { Name = HttpClientNames.AasRegistry, BaseUrl = aasEnv.AasRegistryBaseUrl, HeaderMappings = headerForwarding?.HeaderMappings.TemplateRegistry ?? [] }; + } + if (aasEnv.SubModelRegistryBaseUrl != null) + { options.SubmodelTemplateRegistry = new ServiceEndpoint { Name = HttpClientNames.SubmodelRegistry, diff --git a/source/AAS.TwinEngine.DataEngine/ServiceConfiguration/InfrastructureDependencyInjectionExtensions.cs b/source/AAS.TwinEngine.DataEngine/ServiceConfiguration/InfrastructureDependencyInjectionExtensions.cs index 75b34590..75b13d76 100644 --- a/source/AAS.TwinEngine.DataEngine/ServiceConfiguration/InfrastructureDependencyInjectionExtensions.cs +++ b/source/AAS.TwinEngine.DataEngine/ServiceConfiguration/InfrastructureDependencyInjectionExtensions.cs @@ -56,19 +56,29 @@ public static void ConfigureInfrastructure(this IServiceCollection services, ICo .Bind(configuration.GetSection(RegistrySettingsConfig.Section)) .ValidateOnStart(); - // Normalizer: applies TemplateRepository shorthand to individual repository endpoints - _ = services.AddSingleton, TemplateManagementConfigNormalizer>(); - - // Validators - _ = services.AddSingleton, TemplateManagementConfigValidator>(); - _ = services.AddSingleton, RegistrySettingsConfigValidator>(); - // PluginsConfig: single registration via AddOptions to avoid double-binding of list properties _ = services.AddOptions() .Bind(configuration.GetSection(PluginsConfig.Section)) .ValidateOnStart(); _ = services.AddSingleton, PluginsConfigValidator>(); +#pragma warning disable CS0618 // Obsolete — intentional V1 backward-compat registration + _ = services.PostConfigure(options => + LegacyPluginsConfigAdapter.ApplyV1PluginInstanceOverrides(configuration, options)); + _ = services.PostConfigure(options => + LegacyGeneralConfigAdapter.ApplyV1Overrides(configuration, options)); + _ = services.PostConfigure(options => + LegacyTemplateManagementConfigAdapter.ApplyV1Overrides(configuration, options)); + _ = services.PostConfigure(options => + LegacyRegistrySettingsConfigAdapter.ApplyV1Overrides(configuration, options)); +#pragma warning restore CS0618 + + _ = services.AddSingleton, TemplateManagementConfigNormalizer>(); + + // Validators + _ = services.AddSingleton, TemplateManagementConfigValidator>(); + _ = services.AddSingleton, RegistrySettingsConfigValidator>(); + // ── Resolve config for HttpClient registration (no BuildServiceProvider) ── // Bind V2 sections, apply V1 adapter + normalizer manually. var templateManagement = new TemplateManagementConfig(); From f3a7a8da17000d0f5b8019b2430f270dddfc02bf Mon Sep 17 00:00:00 2001 From: Kevalkumar Date: Thu, 16 Apr 2026 03:48:34 +0530 Subject: [PATCH 35/71] remove warnings --- .../Configuration/LegacyV1/ConfigV1/AasEnvironmentConfig.cs | 1 + .../Configuration/LegacyV1/ConfigV1/PluginConfig.cs | 2 ++ 2 files changed, 3 insertions(+) diff --git a/source/AAS.TwinEngine.DataEngine/Infrastructure/Configuration/LegacyV1/ConfigV1/AasEnvironmentConfig.cs b/source/AAS.TwinEngine.DataEngine/Infrastructure/Configuration/LegacyV1/ConfigV1/AasEnvironmentConfig.cs index 830903b8..41a4446b 100644 --- a/source/AAS.TwinEngine.DataEngine/Infrastructure/Configuration/LegacyV1/ConfigV1/AasEnvironmentConfig.cs +++ b/source/AAS.TwinEngine.DataEngine/Infrastructure/Configuration/LegacyV1/ConfigV1/AasEnvironmentConfig.cs @@ -5,6 +5,7 @@ /// The constants have been moved to and . /// Only the URI properties and Section remain for V1 legacy adapter deserialization. /// +#pragma warning disable S1133 [Obsolete("V1 configuration is deprecated and will be removed in v2.0.0 version. Use HttpClientNames and ApiPaths instead.")] public class AasEnvironmentConfig { diff --git a/source/AAS.TwinEngine.DataEngine/Infrastructure/Configuration/LegacyV1/ConfigV1/PluginConfig.cs b/source/AAS.TwinEngine.DataEngine/Infrastructure/Configuration/LegacyV1/ConfigV1/PluginConfig.cs index da8844a5..ce23017e 100644 --- a/source/AAS.TwinEngine.DataEngine/Infrastructure/Configuration/LegacyV1/ConfigV1/PluginConfig.cs +++ b/source/AAS.TwinEngine.DataEngine/Infrastructure/Configuration/LegacyV1/ConfigV1/PluginConfig.cs @@ -7,6 +7,7 @@ namespace AAS.TwinEngine.DataEngine.Infrastructure.Configuration.LegacyV1; /// The constants have been moved to and . /// Only the Section, Plugins list, and Plugin child class remain for V1 legacy adapter deserialization. /// +#pragma warning disable S1133 [Obsolete("V1 configuration is deprecated and will be removed in v2.0.0 version. Use HttpClientNames and ApiPaths instead.")] public class PluginConfig { @@ -23,6 +24,7 @@ public class PluginConfig public required List Plugins { get; set; } } +#pragma warning disable S1133 [Obsolete("V1 configuration is deprecated and will be removed in v2.0.0 version. Use PluginInstance instead.")] public class Plugin { From 1b1a4a2e742a6dd8a2a871b0b15f44f423291768 Mon Sep 17 00:00:00 2001 From: Kevalkumar Date: Thu, 16 Apr 2026 15:36:11 +0530 Subject: [PATCH 36/71] conflict option refactyor with throwerror --- .../Services/Plugin/Config/MultiPluginConflictOptions.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/source/AAS.TwinEngine.DataEngine/ApplicationLogic/Services/Plugin/Config/MultiPluginConflictOptions.cs b/source/AAS.TwinEngine.DataEngine/ApplicationLogic/Services/Plugin/Config/MultiPluginConflictOptions.cs index a4d883f8..01361878 100644 --- a/source/AAS.TwinEngine.DataEngine/ApplicationLogic/Services/Plugin/Config/MultiPluginConflictOptions.cs +++ b/source/AAS.TwinEngine.DataEngine/ApplicationLogic/Services/Plugin/Config/MultiPluginConflictOptions.cs @@ -3,7 +3,7 @@ public class MultiPluginConflictOptions { public const string Section = "MultiPluginConflictOption"; - public MultiPluginConflictOption HandlingMode { get; set; } = MultiPluginConflictOption.TakeFirst; + public MultiPluginConflictOption HandlingMode { get; set; } = MultiPluginConflictOption.ThrowError; public enum MultiPluginConflictOption { From 4eb242d9d8bc97e4127af4e0b58f820276dc9efe Mon Sep 17 00:00:00 2001 From: Kevalkumar Date: Thu, 16 Apr 2026 15:53:57 +0530 Subject: [PATCH 37/71] Move MultiPluginConflictOptions class for handling plugin conflict configurations in LegacyV1 --- .../Services/PluginManifestConflictHandlerTests.cs | 2 +- .../LegacyV1/ConfigV1}/MultiPluginConflictOptions.cs | 5 ++++- .../Configuration/LegacyV1/LegacyGeneralConfigAdapter.cs | 3 +-- .../LegacyV1/LegacyTemplateManagementConfigAdapter.cs | 3 +-- .../Services/PluginManifestConflictHandler.cs | 2 +- .../InfrastructureDependencyInjectionExtensions.cs | 1 - 6 files changed, 8 insertions(+), 8 deletions(-) rename source/AAS.TwinEngine.DataEngine/{ApplicationLogic/Services/Plugin/Config => Infrastructure/Configuration/LegacyV1/ConfigV1}/MultiPluginConflictOptions.cs (79%) diff --git a/source/AAS.TwinEngine.DataEngine.UnitTests/Infrastructure/Providers/PluginDataProvider/Services/PluginManifestConflictHandlerTests.cs b/source/AAS.TwinEngine.DataEngine.UnitTests/Infrastructure/Providers/PluginDataProvider/Services/PluginManifestConflictHandlerTests.cs index c7f3e444..bc570776 100644 --- a/source/AAS.TwinEngine.DataEngine.UnitTests/Infrastructure/Providers/PluginDataProvider/Services/PluginManifestConflictHandlerTests.cs +++ b/source/AAS.TwinEngine.DataEngine.UnitTests/Infrastructure/Providers/PluginDataProvider/Services/PluginManifestConflictHandlerTests.cs @@ -1,6 +1,6 @@ using AAS.TwinEngine.DataEngine.ApplicationLogic.Exceptions.Application; -using AAS.TwinEngine.DataEngine.ApplicationLogic.Services.Plugin.Config; using AAS.TwinEngine.DataEngine.DomainModel.Plugin; +using AAS.TwinEngine.DataEngine.Infrastructure.Configuration.LegacyV1; using AAS.TwinEngine.DataEngine.Infrastructure.Providers.PluginDataProvider.Services; using Microsoft.Extensions.Logging; diff --git a/source/AAS.TwinEngine.DataEngine/ApplicationLogic/Services/Plugin/Config/MultiPluginConflictOptions.cs b/source/AAS.TwinEngine.DataEngine/Infrastructure/Configuration/LegacyV1/ConfigV1/MultiPluginConflictOptions.cs similarity index 79% rename from source/AAS.TwinEngine.DataEngine/ApplicationLogic/Services/Plugin/Config/MultiPluginConflictOptions.cs rename to source/AAS.TwinEngine.DataEngine/Infrastructure/Configuration/LegacyV1/ConfigV1/MultiPluginConflictOptions.cs index 01361878..db6559a5 100644 --- a/source/AAS.TwinEngine.DataEngine/ApplicationLogic/Services/Plugin/Config/MultiPluginConflictOptions.cs +++ b/source/AAS.TwinEngine.DataEngine/Infrastructure/Configuration/LegacyV1/ConfigV1/MultiPluginConflictOptions.cs @@ -1,4 +1,7 @@ -namespace AAS.TwinEngine.DataEngine.ApplicationLogic.Services.Plugin.Config; +namespace AAS.TwinEngine.DataEngine.Infrastructure.Configuration.LegacyV1; + +#pragma warning disable S1133 +[Obsolete("V1 configuration is deprecated and will be removed in v2.0.0 version.")] public class MultiPluginConflictOptions { diff --git a/source/AAS.TwinEngine.DataEngine/Infrastructure/Configuration/LegacyV1/LegacyGeneralConfigAdapter.cs b/source/AAS.TwinEngine.DataEngine/Infrastructure/Configuration/LegacyV1/LegacyGeneralConfigAdapter.cs index 086a18cb..2eaef289 100644 --- a/source/AAS.TwinEngine.DataEngine/Infrastructure/Configuration/LegacyV1/LegacyGeneralConfigAdapter.cs +++ b/source/AAS.TwinEngine.DataEngine/Infrastructure/Configuration/LegacyV1/LegacyGeneralConfigAdapter.cs @@ -1,5 +1,4 @@ -using AAS.TwinEngine.DataEngine.ApplicationLogic.Services.Plugin.Config; -using AAS.TwinEngine.DataEngine.Infrastructure.Http.Authorization.Config; +using AAS.TwinEngine.DataEngine.Infrastructure.Http.Authorization.Config; using AAS.TwinEngine.DataEngine.ServiceConfiguration.Config; using Microsoft.Extensions.Options; diff --git a/source/AAS.TwinEngine.DataEngine/Infrastructure/Configuration/LegacyV1/LegacyTemplateManagementConfigAdapter.cs b/source/AAS.TwinEngine.DataEngine/Infrastructure/Configuration/LegacyV1/LegacyTemplateManagementConfigAdapter.cs index 2ec3b889..d58563cf 100644 --- a/source/AAS.TwinEngine.DataEngine/Infrastructure/Configuration/LegacyV1/LegacyTemplateManagementConfigAdapter.cs +++ b/source/AAS.TwinEngine.DataEngine/Infrastructure/Configuration/LegacyV1/LegacyTemplateManagementConfigAdapter.cs @@ -1,5 +1,4 @@ -using AAS.TwinEngine.DataEngine.ApplicationLogic.Services.Plugin.Config; -using AAS.TwinEngine.DataEngine.Infrastructure.Http.Authorization.Config; +using AAS.TwinEngine.DataEngine.Infrastructure.Http.Authorization.Config; using AAS.TwinEngine.DataEngine.Infrastructure.Providers.TemplateProvider.Config; using AAS.TwinEngine.DataEngine.ServiceConfiguration.Config; diff --git a/source/AAS.TwinEngine.DataEngine/Infrastructure/Providers/PluginDataProvider/Services/PluginManifestConflictHandler.cs b/source/AAS.TwinEngine.DataEngine/Infrastructure/Providers/PluginDataProvider/Services/PluginManifestConflictHandler.cs index 358ff187..fa4459c7 100644 --- a/source/AAS.TwinEngine.DataEngine/Infrastructure/Providers/PluginDataProvider/Services/PluginManifestConflictHandler.cs +++ b/source/AAS.TwinEngine.DataEngine/Infrastructure/Providers/PluginDataProvider/Services/PluginManifestConflictHandler.cs @@ -1,7 +1,7 @@ using AAS.TwinEngine.DataEngine.ApplicationLogic.Exceptions.Application; using AAS.TwinEngine.DataEngine.ApplicationLogic.Services.Plugin; -using AAS.TwinEngine.DataEngine.ApplicationLogic.Services.Plugin.Config; using AAS.TwinEngine.DataEngine.DomainModel.Plugin; +using AAS.TwinEngine.DataEngine.Infrastructure.Configuration.LegacyV1; using Microsoft.Extensions.Options; diff --git a/source/AAS.TwinEngine.DataEngine/ServiceConfiguration/InfrastructureDependencyInjectionExtensions.cs b/source/AAS.TwinEngine.DataEngine/ServiceConfiguration/InfrastructureDependencyInjectionExtensions.cs index 75b13d76..e37df460 100644 --- a/source/AAS.TwinEngine.DataEngine/ServiceConfiguration/InfrastructureDependencyInjectionExtensions.cs +++ b/source/AAS.TwinEngine.DataEngine/ServiceConfiguration/InfrastructureDependencyInjectionExtensions.cs @@ -1,7 +1,6 @@ using AAS.TwinEngine.DataEngine.ApplicationLogic.Services.AasEnvironment.Providers; using AAS.TwinEngine.DataEngine.ApplicationLogic.Services.AasRegistry.Providers; using AAS.TwinEngine.DataEngine.ApplicationLogic.Services.Plugin; -using AAS.TwinEngine.DataEngine.ApplicationLogic.Services.Plugin.Config; using AAS.TwinEngine.DataEngine.ApplicationLogic.Services.Plugin.Helper; using AAS.TwinEngine.DataEngine.ApplicationLogic.Services.Plugin.Providers; using AAS.TwinEngine.DataEngine.ApplicationLogic.Services.Shared; From 66590d78af4ffaa8804f8ae80f1e511aa992ebcc Mon Sep 17 00:00:00 2001 From: Kevalkumar Date: Thu, 16 Apr 2026 16:31:19 +0530 Subject: [PATCH 38/71] Refactor configuration handling and remove deprecated classes for v2 compatibility --- .../AasRepositoryTemplateService.cs | 6 ++-- .../Config/AasxExportOptions.cs | 10 ------- .../SerializationService.cs | 11 ++++--- .../SubmodelTemplateService.cs | 4 +-- .../ConfigV1/HeaderForwardingOptions.cs | 29 +++++++++++++++++++ .../LegacyV1/LegacyGeneralConfigAdapter.cs | 3 +- .../LegacyV1/LegacyPluginsConfigAdapter.cs | 3 +- .../LegacyTemplateManagementConfigAdapter.cs | 2 +- .../Config/HeaderForwardingOptions.cs | 20 ------------- .../Services/ShellTemplateMappingProvider.cs | 2 +- 10 files changed, 47 insertions(+), 43 deletions(-) delete mode 100644 source/AAS.TwinEngine.DataEngine/ApplicationLogic/Services/SubmodelRepository/Config/AasxExportOptions.cs create mode 100644 source/AAS.TwinEngine.DataEngine/Infrastructure/Configuration/LegacyV1/ConfigV1/HeaderForwardingOptions.cs diff --git a/source/AAS.TwinEngine.DataEngine/ApplicationLogic/Services/AasRepository/AasRepositoryTemplateService.cs b/source/AAS.TwinEngine.DataEngine/ApplicationLogic/Services/AasRepository/AasRepositoryTemplateService.cs index 9f98054d..93e456d7 100644 --- a/source/AAS.TwinEngine.DataEngine/ApplicationLogic/Services/AasRepository/AasRepositoryTemplateService.cs +++ b/source/AAS.TwinEngine.DataEngine/ApplicationLogic/Services/AasRepository/AasRepositoryTemplateService.cs @@ -14,9 +14,9 @@ public class AasRepositoryTemplateService( IShellTemplateMappingProvider shellTemplateMappingProvider, IOptions generalConfig) : IAasRepositoryTemplateService { - private readonly ITemplateProvider _templateProvider = templateProvider ?? throw new ArgumentNullException(nameof(templateProvider)); - private readonly IShellTemplateMappingProvider _shellTemplateMappingProvider = shellTemplateMappingProvider ?? throw new ArgumentNullException(nameof(shellTemplateMappingProvider)); - private readonly Uri _customerDomainUrl = generalConfig.Value.CustomerDomainUrl ?? throw new ArgumentNullException(nameof(generalConfig), "CustomerDomainUrl is required."); + private readonly ITemplateProvider _templateProvider = templateProvider ?? throw new ArgumentNullException(); + private readonly IShellTemplateMappingProvider _shellTemplateMappingProvider = shellTemplateMappingProvider ?? throw new ArgumentNullException(); + private readonly Uri _customerDomainUrl = generalConfig.Value.CustomerDomainUrl ?? throw new ArgumentNullException(); private const string SubmodelUrlSegment = "submodel"; public async Task GetShellTemplateAsync(string aasIdentifier, CancellationToken cancellationToken) diff --git a/source/AAS.TwinEngine.DataEngine/ApplicationLogic/Services/SubmodelRepository/Config/AasxExportOptions.cs b/source/AAS.TwinEngine.DataEngine/ApplicationLogic/Services/SubmodelRepository/Config/AasxExportOptions.cs deleted file mode 100644 index 46263106..00000000 --- a/source/AAS.TwinEngine.DataEngine/ApplicationLogic/Services/SubmodelRepository/Config/AasxExportOptions.cs +++ /dev/null @@ -1,10 +0,0 @@ -namespace AAS.TwinEngine.DataEngine.ApplicationLogic.Services.SubmodelRepository.Config; - -public static class AasxExportOptions -{ - public const string Section = "AasxExportOptions"; - - public const string DefaultXmlFileName = "content.xml"; - - public const string RootFolder = "aasx"; -} diff --git a/source/AAS.TwinEngine.DataEngine/ApplicationLogic/Services/SubmodelRepository/SerializationService.cs b/source/AAS.TwinEngine.DataEngine/ApplicationLogic/Services/SubmodelRepository/SerializationService.cs index 2e5f8953..16637e1b 100644 --- a/source/AAS.TwinEngine.DataEngine/ApplicationLogic/Services/SubmodelRepository/SerializationService.cs +++ b/source/AAS.TwinEngine.DataEngine/ApplicationLogic/Services/SubmodelRepository/SerializationService.cs @@ -3,7 +3,6 @@ using AAS.TwinEngine.DataEngine.ApplicationLogic.Exceptions.Application; using AAS.TwinEngine.DataEngine.ApplicationLogic.Services.AasRepository; -using AAS.TwinEngine.DataEngine.ApplicationLogic.Services.SubmodelRepository.Config; using AasCore.Aas3.Package; using AasCore.Aas3_0; @@ -18,6 +17,10 @@ public class SerializationService( IConceptDescriptionService conceptDescriptionService, ILogger logger) : ISerializationService { + public const string DefaultXmlFileName = "content.xml"; + + public const string RootFolder = "aasx"; + public async Task GetAasxFileStreamAsync(IList aasIds, IList submodelIds, bool includeConceptDescriptions, @@ -192,13 +195,13 @@ private FileStream BuildAasxPackageStream(List shells /// and the file will be placed directly under the root folder. /// Otherwise, a subfolder with the IdShort as its name will be included in the path. /// - private string BuildRelativeXmlPath(string? idShort) + private static string BuildRelativeXmlPath(string? idShort) { var validName = GetValidFolderName(idShort ?? string.Empty); var includeFolder = validName == idShort; return includeFolder - ? $"/{AasxExportOptions.RootFolder}/{validName}/{validName}.xml" - : $"/{AasxExportOptions.RootFolder}/{AasxExportOptions.DefaultXmlFileName}"; + ? $"/{RootFolder}/{validName}/{validName}.xml" + : $"/{RootFolder}/{DefaultXmlFileName}"; } /// diff --git a/source/AAS.TwinEngine.DataEngine/ApplicationLogic/Services/SubmodelRepository/SubmodelTemplateService.cs b/source/AAS.TwinEngine.DataEngine/ApplicationLogic/Services/SubmodelRepository/SubmodelTemplateService.cs index 385d5ca6..2db8bf6f 100644 --- a/source/AAS.TwinEngine.DataEngine/ApplicationLogic/Services/SubmodelRepository/SubmodelTemplateService.cs +++ b/source/AAS.TwinEngine.DataEngine/ApplicationLogic/Services/SubmodelRepository/SubmodelTemplateService.cs @@ -15,10 +15,10 @@ public partial class SubmodelTemplateService( ITemplateProvider templateProvider, ISubmodelTemplateMappingProvider submodelTemplateMappingProvider) : ISubmodelTemplateService { - private readonly ITemplateProvider _templateProvider = templateProvider ?? throw new ArgumentNullException(nameof(templateProvider)); + private readonly ITemplateProvider _templateProvider = templateProvider ?? throw new ArgumentNullException(); private readonly ISubmodelTemplateMappingProvider _submodelTemplateMappingProvider = - submodelTemplateMappingProvider ?? throw new ArgumentNullException(nameof(submodelTemplateMappingProvider)); + submodelTemplateMappingProvider ?? throw new ArgumentNullException(); public async Task GetSubmodelTemplateAsync(string submodelId, CancellationToken cancellationToken) { diff --git a/source/AAS.TwinEngine.DataEngine/Infrastructure/Configuration/LegacyV1/ConfigV1/HeaderForwardingOptions.cs b/source/AAS.TwinEngine.DataEngine/Infrastructure/Configuration/LegacyV1/ConfigV1/HeaderForwardingOptions.cs new file mode 100644 index 00000000..f7c2bae9 --- /dev/null +++ b/source/AAS.TwinEngine.DataEngine/Infrastructure/Configuration/LegacyV1/ConfigV1/HeaderForwardingOptions.cs @@ -0,0 +1,29 @@ +using AAS.TwinEngine.DataEngine.Infrastructure.Http.Authorization.Config; + +using System.ComponentModel.DataAnnotations; + +namespace AAS.TwinEngine.DataEngine.Infrastructure.Configuration.LegacyV1.ConfigV1; + +#pragma warning disable S1133 +[Obsolete("V1 configuration is deprecated and will be removed in v2.0.0 version.")] +public class HeaderForwardingOptions +{ + public const string Section = "HeaderForwarding"; + + [Required] + public HeaderSanitizationOptions HeaderSanitization { get; set; } = new(); + + [Required] + public HeaderMappings HeaderMappings { get; set; } = new(); +} + +#pragma warning disable S1133 +[Obsolete("V1 configuration is deprecated and will be removed in v2.0.0 version.")] +public class HeaderMappings +{ + public IList TemplateRepository { get; } = []; + + public IList TemplateRegistry { get; } = []; + + public Dictionary> Plugins { get; init; } = new(StringComparer.OrdinalIgnoreCase); +} diff --git a/source/AAS.TwinEngine.DataEngine/Infrastructure/Configuration/LegacyV1/LegacyGeneralConfigAdapter.cs b/source/AAS.TwinEngine.DataEngine/Infrastructure/Configuration/LegacyV1/LegacyGeneralConfigAdapter.cs index 2eaef289..56c2c9a0 100644 --- a/source/AAS.TwinEngine.DataEngine/Infrastructure/Configuration/LegacyV1/LegacyGeneralConfigAdapter.cs +++ b/source/AAS.TwinEngine.DataEngine/Infrastructure/Configuration/LegacyV1/LegacyGeneralConfigAdapter.cs @@ -1,4 +1,5 @@ -using AAS.TwinEngine.DataEngine.Infrastructure.Http.Authorization.Config; +using AAS.TwinEngine.DataEngine.Infrastructure.Configuration.LegacyV1.ConfigV1; +using AAS.TwinEngine.DataEngine.Infrastructure.Http.Authorization.Config; using AAS.TwinEngine.DataEngine.ServiceConfiguration.Config; using Microsoft.Extensions.Options; diff --git a/source/AAS.TwinEngine.DataEngine/Infrastructure/Configuration/LegacyV1/LegacyPluginsConfigAdapter.cs b/source/AAS.TwinEngine.DataEngine/Infrastructure/Configuration/LegacyV1/LegacyPluginsConfigAdapter.cs index 1d5b1094..a264c5b7 100644 --- a/source/AAS.TwinEngine.DataEngine/Infrastructure/Configuration/LegacyV1/LegacyPluginsConfigAdapter.cs +++ b/source/AAS.TwinEngine.DataEngine/Infrastructure/Configuration/LegacyV1/LegacyPluginsConfigAdapter.cs @@ -1,4 +1,5 @@ -using AAS.TwinEngine.DataEngine.Infrastructure.Http.Authorization.Config; +using AAS.TwinEngine.DataEngine.Infrastructure.Configuration.LegacyV1.ConfigV1; +using AAS.TwinEngine.DataEngine.Infrastructure.Http.Authorization.Config; using AAS.TwinEngine.DataEngine.ServiceConfiguration.Config; using Microsoft.Extensions.Options; diff --git a/source/AAS.TwinEngine.DataEngine/Infrastructure/Configuration/LegacyV1/LegacyTemplateManagementConfigAdapter.cs b/source/AAS.TwinEngine.DataEngine/Infrastructure/Configuration/LegacyV1/LegacyTemplateManagementConfigAdapter.cs index d58563cf..d5af8f62 100644 --- a/source/AAS.TwinEngine.DataEngine/Infrastructure/Configuration/LegacyV1/LegacyTemplateManagementConfigAdapter.cs +++ b/source/AAS.TwinEngine.DataEngine/Infrastructure/Configuration/LegacyV1/LegacyTemplateManagementConfigAdapter.cs @@ -1,4 +1,4 @@ -using AAS.TwinEngine.DataEngine.Infrastructure.Http.Authorization.Config; +using AAS.TwinEngine.DataEngine.Infrastructure.Configuration.LegacyV1.ConfigV1; using AAS.TwinEngine.DataEngine.Infrastructure.Providers.TemplateProvider.Config; using AAS.TwinEngine.DataEngine.ServiceConfiguration.Config; diff --git a/source/AAS.TwinEngine.DataEngine/Infrastructure/Http/Authorization/Config/HeaderForwardingOptions.cs b/source/AAS.TwinEngine.DataEngine/Infrastructure/Http/Authorization/Config/HeaderForwardingOptions.cs index 49437b32..f5e06d9d 100644 --- a/source/AAS.TwinEngine.DataEngine/Infrastructure/Http/Authorization/Config/HeaderForwardingOptions.cs +++ b/source/AAS.TwinEngine.DataEngine/Infrastructure/Http/Authorization/Config/HeaderForwardingOptions.cs @@ -2,17 +2,6 @@ namespace AAS.TwinEngine.DataEngine.Infrastructure.Http.Authorization.Config; -public class HeaderForwardingOptions -{ - public const string Section = "HeaderForwarding"; - - [Required] - public HeaderSanitizationOptions HeaderSanitization { get; set; } = new(); - - [Required] - public HeaderMappings HeaderMappings { get; set; } = new(); -} - public class HeaderSanitizationOptions { [Range(1, int.MaxValue)] @@ -36,15 +25,6 @@ public class AllowedCharactersOptions public string HeaderValues { get; set; } = "^[\\x20-\\x7E]+$"; } -public class HeaderMappings -{ - public IList TemplateRepository { get; } = []; - - public IList TemplateRegistry { get; } = []; - - public Dictionary> Plugins { get; init; } = new(StringComparer.OrdinalIgnoreCase); -} - public class HeaderMappingRule { [Required] diff --git a/source/AAS.TwinEngine.DataEngine/Infrastructure/Providers/TemplateProvider/Services/ShellTemplateMappingProvider.cs b/source/AAS.TwinEngine.DataEngine/Infrastructure/Providers/TemplateProvider/Services/ShellTemplateMappingProvider.cs index 101dee1f..57b63a4b 100644 --- a/source/AAS.TwinEngine.DataEngine/Infrastructure/Providers/TemplateProvider/Services/ShellTemplateMappingProvider.cs +++ b/source/AAS.TwinEngine.DataEngine/Infrastructure/Providers/TemplateProvider/Services/ShellTemplateMappingProvider.cs @@ -11,7 +11,7 @@ namespace AAS.TwinEngine.DataEngine.Infrastructure.Providers.TemplateProvider.Se public class ShellTemplateMappingProvider(ILogger logger, IOptions options) : IShellTemplateMappingProvider { - private readonly ILogger _logger = logger ?? throw new ArgumentNullException(nameof(logger)); + private readonly ILogger _logger = logger ?? throw new ArgumentNullException(); private readonly IList _shellTemplateMappings = options.Value.TemplateMappingRules.ShellTemplateMappings ?? throw new ArgumentException("ShellTemplateMappings are missing in TemplateMappingRules"); private readonly IList _aasIdExtractionRules = options.Value.TemplateMappingRules.AasIdExtractionRules ?? throw new ArgumentException("AasIdExtractionRules are missing in TemplateMappingRules"); private readonly TimeSpan _regexTimeout = TimeSpan.FromSeconds(2); From 6f2862a07a5a8fa43f9f5d5b369b9c61abcfa790 Mon Sep 17 00:00:00 2001 From: Kevalkumar Date: Thu, 16 Apr 2026 16:34:06 +0530 Subject: [PATCH 39/71] Refactor configuration files to improve organization and add ResiliencePoliciesConfig class --- .../ServiceConfiguration/Config/PluginsConfig.cs | 9 +-------- .../Config/ResiliencePoliciesConfig.cs | 6 ++++++ .../Config/TemplateManagementConfig.cs | 1 + 3 files changed, 8 insertions(+), 8 deletions(-) create mode 100644 source/AAS.TwinEngine.DataEngine/ServiceConfiguration/Config/ResiliencePoliciesConfig.cs diff --git a/source/AAS.TwinEngine.DataEngine/ServiceConfiguration/Config/PluginsConfig.cs b/source/AAS.TwinEngine.DataEngine/ServiceConfiguration/Config/PluginsConfig.cs index a279e1aa..096ab298 100644 --- a/source/AAS.TwinEngine.DataEngine/ServiceConfiguration/Config/PluginsConfig.cs +++ b/source/AAS.TwinEngine.DataEngine/ServiceConfiguration/Config/PluginsConfig.cs @@ -1,4 +1,5 @@ using AAS.TwinEngine.DataEngine.Infrastructure.Http.Authorization.Config; +using AAS.TwinEngine.DataEngine.ServiceConfiguration.Config.Helpers; namespace AAS.TwinEngine.DataEngine.ServiceConfiguration.Config; @@ -38,14 +39,6 @@ public class PluginInstance public IList HeaderMappings { get; init; } = []; } -/// -/// Resilience policies shared within a domain (Plugins or TemplateManagement). -/// -public class ResiliencePoliciesConfig -{ - public RetryConfig Retry { get; set; } = new(); -} - public class RetryConfig { public int MaxRetryAttempts { get; set; } diff --git a/source/AAS.TwinEngine.DataEngine/ServiceConfiguration/Config/ResiliencePoliciesConfig.cs b/source/AAS.TwinEngine.DataEngine/ServiceConfiguration/Config/ResiliencePoliciesConfig.cs new file mode 100644 index 00000000..360e0bb9 --- /dev/null +++ b/source/AAS.TwinEngine.DataEngine/ServiceConfiguration/Config/ResiliencePoliciesConfig.cs @@ -0,0 +1,6 @@ +namespace AAS.TwinEngine.DataEngine.ServiceConfiguration.Config.Helpers; + +public class ResiliencePoliciesConfig +{ + public RetryConfig Retry { get; set; } = new(); +} diff --git a/source/AAS.TwinEngine.DataEngine/ServiceConfiguration/Config/TemplateManagementConfig.cs b/source/AAS.TwinEngine.DataEngine/ServiceConfiguration/Config/TemplateManagementConfig.cs index f1e1fd91..f0f40800 100644 --- a/source/AAS.TwinEngine.DataEngine/ServiceConfiguration/Config/TemplateManagementConfig.cs +++ b/source/AAS.TwinEngine.DataEngine/ServiceConfiguration/Config/TemplateManagementConfig.cs @@ -1,5 +1,6 @@ using AAS.TwinEngine.DataEngine.Infrastructure.Http.Authorization.Config; using AAS.TwinEngine.DataEngine.Infrastructure.Providers.TemplateProvider.Config; +using AAS.TwinEngine.DataEngine.ServiceConfiguration.Config.Helpers; namespace AAS.TwinEngine.DataEngine.ServiceConfiguration.Config; From 7a4e0baff07a4d192b8a18cc337dff176193e609 Mon Sep 17 00:00:00 2001 From: Kevalkumar Date: Fri, 17 Apr 2026 01:04:08 +0530 Subject: [PATCH 40/71] Refactor exception message in ShellTemplateMappingProvider for missing ShellTemplateMappings --- .../TemplateProvider/Services/ShellTemplateMappingProvider.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/source/AAS.TwinEngine.DataEngine/Infrastructure/Providers/TemplateProvider/Services/ShellTemplateMappingProvider.cs b/source/AAS.TwinEngine.DataEngine/Infrastructure/Providers/TemplateProvider/Services/ShellTemplateMappingProvider.cs index 57b63a4b..c8d2b6b2 100644 --- a/source/AAS.TwinEngine.DataEngine/Infrastructure/Providers/TemplateProvider/Services/ShellTemplateMappingProvider.cs +++ b/source/AAS.TwinEngine.DataEngine/Infrastructure/Providers/TemplateProvider/Services/ShellTemplateMappingProvider.cs @@ -12,7 +12,7 @@ namespace AAS.TwinEngine.DataEngine.Infrastructure.Providers.TemplateProvider.Se public class ShellTemplateMappingProvider(ILogger logger, IOptions options) : IShellTemplateMappingProvider { private readonly ILogger _logger = logger ?? throw new ArgumentNullException(); - private readonly IList _shellTemplateMappings = options.Value.TemplateMappingRules.ShellTemplateMappings ?? throw new ArgumentException("ShellTemplateMappings are missing in TemplateMappingRules"); + private readonly IList _shellTemplateMappings = options.Value.TemplateMappingRules.ShellTemplateMappings ?? throw new ArgumentException(); private readonly IList _aasIdExtractionRules = options.Value.TemplateMappingRules.AasIdExtractionRules ?? throw new ArgumentException("AasIdExtractionRules are missing in TemplateMappingRules"); private readonly TimeSpan _regexTimeout = TimeSpan.FromSeconds(2); From dbe750062a4899a979a005d7e2b0829bde21c1f5 Mon Sep 17 00:00:00 2001 From: Kevalkumar Date: Fri, 17 Apr 2026 01:23:58 +0530 Subject: [PATCH 41/71] Replace PluginInstance with ServiceInstance across the codebase - Modified legacy configuration classes to update deprecation messages and ensure consistency. - Introduced new HeaderMappingRule and HeaderSanitizationOptions classes for improved header management. --- .../HeaderForwardingHandlerTests.cs | 4 ++-- .../Headers/RequestHeaderMapperTests.cs | 12 +++++----- .../PluginAvailabilityHealthCheckTests.cs | 18 +++++++-------- .../Services/PluginManifestProviderTests.cs | 4 ++-- .../AasRepositoryTemplateService.cs | 6 ++--- .../SubmodelTemplateService.cs | 4 ++-- .../LegacyV1/ConfigV1/AasEnvironmentConfig.cs | 2 +- .../ConfigV1/AasRegistryPreComputed.cs | 2 +- .../ConfigV1/HeaderForwardingOptions.cs | 4 ++-- .../ConfigV1/HttpRetryPolicyOptions.cs | 2 +- .../ConfigV1/MultiLanguagePropertySettings.cs | 2 +- .../MultiLanguagePropertySettingsValidator.cs | 2 +- .../ConfigV1/MultiPluginConflictOptions.cs | 2 +- .../LegacyV1/ConfigV1/PluginConfig.cs | 4 ++-- .../LegacyV1/ConfigV1/Semantics.cs | 2 +- .../LegacyV1/LegacyConfigurationDetector.cs | 2 +- .../LegacyV1/LegacyGeneralConfigAdapter.cs | 2 +- .../LegacyV1/LegacyPluginsConfigAdapter.cs | 4 ++-- .../LegacyRegistrySettingsConfigAdapter.cs | 2 +- .../LegacyTemplateManagementConfigAdapter.cs | 8 +++---- .../LegacyV1ConfigurationExtensions.cs | 4 ++-- .../Authorization/Config/HeaderMappingRule.cs | 14 +++++++++++ ...ptions.cs => HeaderSanitizationOptions.cs} | 20 +--------------- .../PluginAvailabilityHealthCheck.cs | 4 ++-- .../Services/PluginManifestProvider.cs | 2 +- .../Services/ShellTemplateMappingProvider.cs | 4 ++-- .../TemplateManagementConfigNormalizer.cs | 2 +- .../TemplateManagementConfigValidator.cs | 2 +- .../Config/PluginsConfig.cs | 23 ++----------------- .../Config/ResiliencePoliciesConfig.cs | 9 +++++++- .../Config/TemplateManagementConfig.cs | 19 ++++++++------- ...astructureDependencyInjectionExtensions.cs | 5 +++- 32 files changed, 91 insertions(+), 105 deletions(-) create mode 100644 source/AAS.TwinEngine.DataEngine/Infrastructure/Http/Authorization/Config/HeaderMappingRule.cs rename source/AAS.TwinEngine.DataEngine/Infrastructure/Http/Authorization/Config/{HeaderForwardingOptions.cs => HeaderSanitizationOptions.cs} (53%) diff --git a/source/AAS.TwinEngine.DataEngine.UnitTests/Infrastructure/Http/Authorization/HeaderForwardingHandlerTests.cs b/source/AAS.TwinEngine.DataEngine.UnitTests/Infrastructure/Http/Authorization/HeaderForwardingHandlerTests.cs index e1aa7a89..4fb4987f 100644 --- a/source/AAS.TwinEngine.DataEngine.UnitTests/Infrastructure/Http/Authorization/HeaderForwardingHandlerTests.cs +++ b/source/AAS.TwinEngine.DataEngine.UnitTests/Infrastructure/Http/Authorization/HeaderForwardingHandlerTests.cs @@ -11,7 +11,7 @@ using NSubstitute; -namespace AAS.TwinEngine.DataEngine.UnitTests.Infrastructure.Http.Authorization.Middleware; +namespace AAS.TwinEngine.DataEngine.UnitTests.Infrastructure.Http.Authorization; public class HeaderForwardingHandlerTests { @@ -28,7 +28,7 @@ private static HeaderForwardingHandler CreateHandler(HttpContext httpContext, st { Instances = [ - new PluginInstance + new ServiceInstance { Name = "TestPlugin", BaseUrl = new Uri("http://example.com"), diff --git a/source/AAS.TwinEngine.DataEngine.UnitTests/Infrastructure/Http/Authorization/Headers/RequestHeaderMapperTests.cs b/source/AAS.TwinEngine.DataEngine.UnitTests/Infrastructure/Http/Authorization/Headers/RequestHeaderMapperTests.cs index 2697b3ca..00b011cd 100644 --- a/source/AAS.TwinEngine.DataEngine.UnitTests/Infrastructure/Http/Authorization/Headers/RequestHeaderMapperTests.cs +++ b/source/AAS.TwinEngine.DataEngine.UnitTests/Infrastructure/Http/Authorization/Headers/RequestHeaderMapperTests.cs @@ -75,7 +75,7 @@ public void ValidateIncomingHeaders_RequiredHeaderMissing_Throws() var config = DefaultConfig(); var templateConfig = new TemplateManagementConfig { - AasTemplateRepository = new ServiceEndpoint + AasTemplateRepository = new ServiceInstance { HeaderMappings = [ @@ -128,7 +128,7 @@ public void ApplyMappings_MapsAuthorizationHeader() var config = DefaultConfig(); var templateConfig = new TemplateManagementConfig { - AasTemplateRepository = new ServiceEndpoint + AasTemplateRepository = new ServiceInstance { HeaderMappings = [ @@ -156,7 +156,7 @@ public void ApplyMappings_RenamesHeader() var config = DefaultConfig(); var templateConfig = new TemplateManagementConfig { - AasTemplateRepository = new ServiceEndpoint + AasTemplateRepository = new ServiceInstance { HeaderMappings = [ @@ -183,7 +183,7 @@ public void ApplyMappings_OverwriteExistingHeader() var config = DefaultConfig(); var templateConfig = new TemplateManagementConfig { - AasTemplateRepository = new ServiceEndpoint + AasTemplateRepository = new ServiceInstance { HeaderMappings = [ @@ -212,7 +212,7 @@ public void ApplyMappings_MultipleValues_CombinedCorrectly() var config = DefaultConfig(); var templateConfig = new TemplateManagementConfig { - AasTemplateRepository = new ServiceEndpoint + AasTemplateRepository = new ServiceInstance { HeaderMappings = [ @@ -244,7 +244,7 @@ public void ApplyMappings_PluginMapping_Works() { Instances = [ - new PluginInstance + new ServiceInstance { Name = pluginName, BaseUrl = new Uri("http://test"), diff --git a/source/AAS.TwinEngine.DataEngine.UnitTests/Infrastructure/Monitoring/PluginAvailabilityHealthCheckTests.cs b/source/AAS.TwinEngine.DataEngine.UnitTests/Infrastructure/Monitoring/PluginAvailabilityHealthCheckTests.cs index 5f0c0657..0ae8d88f 100644 --- a/source/AAS.TwinEngine.DataEngine.UnitTests/Infrastructure/Monitoring/PluginAvailabilityHealthCheckTests.cs +++ b/source/AAS.TwinEngine.DataEngine.UnitTests/Infrastructure/Monitoring/PluginAvailabilityHealthCheckTests.cs @@ -76,8 +76,8 @@ public async Task CheckHealthAsync_Returns_Healthy_When_All_Plugins_Are_Healthy( { Instances = [ - new PluginInstance { Name = "Plugin1", BaseUrl = new Uri("http://localhost") }, - new PluginInstance { Name = "Plugin2", BaseUrl = new Uri("http://localhost") } + new ServiceInstance { Name = "Plugin1", BaseUrl = new Uri("http://localhost") }, + new ServiceInstance { Name = "Plugin2", BaseUrl = new Uri("http://localhost") } ] }); @@ -112,7 +112,7 @@ public async Task CheckHealthAsync_Returns_Unhealthy_When_Any_Plugin_Is_Unhealth { Instances = [ - new PluginInstance { Name = "Plugin1", BaseUrl = new Uri("http://localhost") } + new ServiceInstance { Name = "Plugin1", BaseUrl = new Uri("http://localhost") } ] }); @@ -145,7 +145,7 @@ public async Task CheckHealthAsync_Returns_Unhealthy_When_Plugin_Request_Throws_ { Instances = [ - new PluginInstance { Name = "Plugin1", BaseUrl = new Uri("http://localhost") } + new ServiceInstance { Name = "Plugin1", BaseUrl = new Uri("http://localhost") } ] }); @@ -178,7 +178,7 @@ public async Task CheckHealthAsync_Returns_Unhealthy_When_Plugin_Request_Throws_ { Instances = [ - new PluginInstance { Name = "Plugin1", BaseUrl = new Uri("http://localhost") } + new ServiceInstance { Name = "Plugin1", BaseUrl = new Uri("http://localhost") } ] }); @@ -211,7 +211,7 @@ public async Task CheckHealthAsync_Returns_Unhealthy_When_Plugin_Request_Throws_ { Instances = [ - new PluginInstance { Name = "Plugin1", BaseUrl = new Uri("http://localhost") } + new ServiceInstance { Name = "Plugin1", BaseUrl = new Uri("http://localhost") } ] }); @@ -241,8 +241,8 @@ public async Task CheckHealthAsync_Checks_All_Plugins_In_Parallel_Even_When_One_ { Instances = [ - new PluginInstance { Name = "Plugin1", BaseUrl = new Uri("http://localhost") }, - new PluginInstance { Name = "Plugin2", BaseUrl = new Uri("http://localhost") } + new ServiceInstance { Name = "Plugin1", BaseUrl = new Uri("http://localhost") }, + new ServiceInstance { Name = "Plugin2", BaseUrl = new Uri("http://localhost") } ] }); @@ -272,7 +272,7 @@ public async Task CheckHealthAsync_Uses_HealthCheck_Client_Names_Without_Retry_P { Instances = [ - new PluginInstance { Name = "Plugin1", BaseUrl = new Uri("http://localhost") } + new ServiceInstance { Name = "Plugin1", BaseUrl = new Uri("http://localhost") } ] }); diff --git a/source/AAS.TwinEngine.DataEngine.UnitTests/Infrastructure/Providers/PluginDataProvider/Services/PluginManifestProviderTests.cs b/source/AAS.TwinEngine.DataEngine.UnitTests/Infrastructure/Providers/PluginDataProvider/Services/PluginManifestProviderTests.cs index 393ed92a..0cd7f45b 100644 --- a/source/AAS.TwinEngine.DataEngine.UnitTests/Infrastructure/Providers/PluginDataProvider/Services/PluginManifestProviderTests.cs +++ b/source/AAS.TwinEngine.DataEngine.UnitTests/Infrastructure/Providers/PluginDataProvider/Services/PluginManifestProviderTests.cs @@ -18,9 +18,9 @@ public class PluginManifestProviderTests private readonly ICreateClient _clientFactory = Substitute.For(); private readonly IOptions _options; - private readonly List _plugins = + private readonly List _plugins = [ - new PluginInstance + new ServiceInstance { Name = "TestPlugin", BaseUrl = new Uri("https://plugin.url") diff --git a/source/AAS.TwinEngine.DataEngine/ApplicationLogic/Services/AasRepository/AasRepositoryTemplateService.cs b/source/AAS.TwinEngine.DataEngine/ApplicationLogic/Services/AasRepository/AasRepositoryTemplateService.cs index 93e456d7..9f98054d 100644 --- a/source/AAS.TwinEngine.DataEngine/ApplicationLogic/Services/AasRepository/AasRepositoryTemplateService.cs +++ b/source/AAS.TwinEngine.DataEngine/ApplicationLogic/Services/AasRepository/AasRepositoryTemplateService.cs @@ -14,9 +14,9 @@ public class AasRepositoryTemplateService( IShellTemplateMappingProvider shellTemplateMappingProvider, IOptions generalConfig) : IAasRepositoryTemplateService { - private readonly ITemplateProvider _templateProvider = templateProvider ?? throw new ArgumentNullException(); - private readonly IShellTemplateMappingProvider _shellTemplateMappingProvider = shellTemplateMappingProvider ?? throw new ArgumentNullException(); - private readonly Uri _customerDomainUrl = generalConfig.Value.CustomerDomainUrl ?? throw new ArgumentNullException(); + private readonly ITemplateProvider _templateProvider = templateProvider ?? throw new ArgumentNullException(nameof(templateProvider)); + private readonly IShellTemplateMappingProvider _shellTemplateMappingProvider = shellTemplateMappingProvider ?? throw new ArgumentNullException(nameof(shellTemplateMappingProvider)); + private readonly Uri _customerDomainUrl = generalConfig.Value.CustomerDomainUrl ?? throw new ArgumentNullException(nameof(generalConfig), "CustomerDomainUrl is required."); private const string SubmodelUrlSegment = "submodel"; public async Task GetShellTemplateAsync(string aasIdentifier, CancellationToken cancellationToken) diff --git a/source/AAS.TwinEngine.DataEngine/ApplicationLogic/Services/SubmodelRepository/SubmodelTemplateService.cs b/source/AAS.TwinEngine.DataEngine/ApplicationLogic/Services/SubmodelRepository/SubmodelTemplateService.cs index 2db8bf6f..385d5ca6 100644 --- a/source/AAS.TwinEngine.DataEngine/ApplicationLogic/Services/SubmodelRepository/SubmodelTemplateService.cs +++ b/source/AAS.TwinEngine.DataEngine/ApplicationLogic/Services/SubmodelRepository/SubmodelTemplateService.cs @@ -15,10 +15,10 @@ public partial class SubmodelTemplateService( ITemplateProvider templateProvider, ISubmodelTemplateMappingProvider submodelTemplateMappingProvider) : ISubmodelTemplateService { - private readonly ITemplateProvider _templateProvider = templateProvider ?? throw new ArgumentNullException(); + private readonly ITemplateProvider _templateProvider = templateProvider ?? throw new ArgumentNullException(nameof(templateProvider)); private readonly ISubmodelTemplateMappingProvider _submodelTemplateMappingProvider = - submodelTemplateMappingProvider ?? throw new ArgumentNullException(); + submodelTemplateMappingProvider ?? throw new ArgumentNullException(nameof(submodelTemplateMappingProvider)); public async Task GetSubmodelTemplateAsync(string submodelId, CancellationToken cancellationToken) { diff --git a/source/AAS.TwinEngine.DataEngine/Infrastructure/Configuration/LegacyV1/ConfigV1/AasEnvironmentConfig.cs b/source/AAS.TwinEngine.DataEngine/Infrastructure/Configuration/LegacyV1/ConfigV1/AasEnvironmentConfig.cs index 41a4446b..48a1d5ad 100644 --- a/source/AAS.TwinEngine.DataEngine/Infrastructure/Configuration/LegacyV1/ConfigV1/AasEnvironmentConfig.cs +++ b/source/AAS.TwinEngine.DataEngine/Infrastructure/Configuration/LegacyV1/ConfigV1/AasEnvironmentConfig.cs @@ -6,7 +6,7 @@ /// Only the URI properties and Section remain for V1 legacy adapter deserialization. /// #pragma warning disable S1133 -[Obsolete("V1 configuration is deprecated and will be removed in v2.0.0 version. Use HttpClientNames and ApiPaths instead.")] +[Obsolete("V1 configuration is deprecated and will be removed in next major release Use HttpClientNames and ApiPaths instead.")] public class AasEnvironmentConfig { public const string Section = "AasEnvironment"; diff --git a/source/AAS.TwinEngine.DataEngine/Infrastructure/Configuration/LegacyV1/ConfigV1/AasRegistryPreComputed.cs b/source/AAS.TwinEngine.DataEngine/Infrastructure/Configuration/LegacyV1/ConfigV1/AasRegistryPreComputed.cs index 06a0f3d1..cf738104 100644 --- a/source/AAS.TwinEngine.DataEngine/Infrastructure/Configuration/LegacyV1/ConfigV1/AasRegistryPreComputed.cs +++ b/source/AAS.TwinEngine.DataEngine/Infrastructure/Configuration/LegacyV1/ConfigV1/AasRegistryPreComputed.cs @@ -1,7 +1,7 @@ namespace AAS.TwinEngine.DataEngine.Infrastructure.Configuration.LegacyV1; #pragma warning disable S1133 -[Obsolete("V1 configuration is deprecated and will be removed in v2.0.0 version.")] +[Obsolete("V1 configuration is deprecated and will be removed in next major release")] public class AasRegistryPreComputed { public const string Section = "AasRegistryPreComputed"; diff --git a/source/AAS.TwinEngine.DataEngine/Infrastructure/Configuration/LegacyV1/ConfigV1/HeaderForwardingOptions.cs b/source/AAS.TwinEngine.DataEngine/Infrastructure/Configuration/LegacyV1/ConfigV1/HeaderForwardingOptions.cs index f7c2bae9..69454008 100644 --- a/source/AAS.TwinEngine.DataEngine/Infrastructure/Configuration/LegacyV1/ConfigV1/HeaderForwardingOptions.cs +++ b/source/AAS.TwinEngine.DataEngine/Infrastructure/Configuration/LegacyV1/ConfigV1/HeaderForwardingOptions.cs @@ -5,7 +5,7 @@ namespace AAS.TwinEngine.DataEngine.Infrastructure.Configuration.LegacyV1.ConfigV1; #pragma warning disable S1133 -[Obsolete("V1 configuration is deprecated and will be removed in v2.0.0 version.")] +[Obsolete("V1 configuration is deprecated and will be removed in next major release")] public class HeaderForwardingOptions { public const string Section = "HeaderForwarding"; @@ -18,7 +18,7 @@ public class HeaderForwardingOptions } #pragma warning disable S1133 -[Obsolete("V1 configuration is deprecated and will be removed in v2.0.0 version.")] +[Obsolete("V1 configuration is deprecated and will be removed in next major release")] public class HeaderMappings { public IList TemplateRepository { get; } = []; diff --git a/source/AAS.TwinEngine.DataEngine/Infrastructure/Configuration/LegacyV1/ConfigV1/HttpRetryPolicyOptions.cs b/source/AAS.TwinEngine.DataEngine/Infrastructure/Configuration/LegacyV1/ConfigV1/HttpRetryPolicyOptions.cs index ff08f343..b8974d28 100644 --- a/source/AAS.TwinEngine.DataEngine/Infrastructure/Configuration/LegacyV1/ConfigV1/HttpRetryPolicyOptions.cs +++ b/source/AAS.TwinEngine.DataEngine/Infrastructure/Configuration/LegacyV1/ConfigV1/HttpRetryPolicyOptions.cs @@ -3,7 +3,7 @@ namespace AAS.TwinEngine.DataEngine.Infrastructure.Configuration.LegacyV1; #pragma warning disable S1133 -[Obsolete("V1 configuration is deprecated and will be removed in v2.0.0 version.")] +[Obsolete("V1 configuration is deprecated and will be removed in next major release")] public class HttpRetryPolicyOptions { public const string Section = "HttpRetryPolicyOptions"; diff --git a/source/AAS.TwinEngine.DataEngine/Infrastructure/Configuration/LegacyV1/ConfigV1/MultiLanguagePropertySettings.cs b/source/AAS.TwinEngine.DataEngine/Infrastructure/Configuration/LegacyV1/ConfigV1/MultiLanguagePropertySettings.cs index d9cd7a5b..26ba2187 100644 --- a/source/AAS.TwinEngine.DataEngine/Infrastructure/Configuration/LegacyV1/ConfigV1/MultiLanguagePropertySettings.cs +++ b/source/AAS.TwinEngine.DataEngine/Infrastructure/Configuration/LegacyV1/ConfigV1/MultiLanguagePropertySettings.cs @@ -1,7 +1,7 @@ namespace AAS.TwinEngine.DataEngine.Infrastructure.Configuration.LegacyV1; #pragma warning disable S1133 -[Obsolete("V1 configuration is deprecated and will be removed in v2.0.0 version.")] +[Obsolete("V1 configuration is deprecated and will be removed in next major release")] public class MultiLanguagePropertySettings { public const string Section = "MultiLanguageProperty"; diff --git a/source/AAS.TwinEngine.DataEngine/Infrastructure/Configuration/LegacyV1/ConfigV1/MultiLanguagePropertySettingsValidator.cs b/source/AAS.TwinEngine.DataEngine/Infrastructure/Configuration/LegacyV1/ConfigV1/MultiLanguagePropertySettingsValidator.cs index e72822eb..d7a6051e 100644 --- a/source/AAS.TwinEngine.DataEngine/Infrastructure/Configuration/LegacyV1/ConfigV1/MultiLanguagePropertySettingsValidator.cs +++ b/source/AAS.TwinEngine.DataEngine/Infrastructure/Configuration/LegacyV1/ConfigV1/MultiLanguagePropertySettingsValidator.cs @@ -5,7 +5,7 @@ namespace AAS.TwinEngine.DataEngine.Infrastructure.Configuration.LegacyV1; #pragma warning disable S1133 -[Obsolete("V1 configuration is deprecated and will be removed in v2.0.0 version.")] +[Obsolete("V1 configuration is deprecated and will be removed in next major release")] public partial class MultiLanguagePropertySettingsValidator : IValidateOptions { public ValidateOptionsResult Validate(string? name, MultiLanguagePropertySettings options) diff --git a/source/AAS.TwinEngine.DataEngine/Infrastructure/Configuration/LegacyV1/ConfigV1/MultiPluginConflictOptions.cs b/source/AAS.TwinEngine.DataEngine/Infrastructure/Configuration/LegacyV1/ConfigV1/MultiPluginConflictOptions.cs index db6559a5..eaca2c12 100644 --- a/source/AAS.TwinEngine.DataEngine/Infrastructure/Configuration/LegacyV1/ConfigV1/MultiPluginConflictOptions.cs +++ b/source/AAS.TwinEngine.DataEngine/Infrastructure/Configuration/LegacyV1/ConfigV1/MultiPluginConflictOptions.cs @@ -1,7 +1,7 @@ namespace AAS.TwinEngine.DataEngine.Infrastructure.Configuration.LegacyV1; #pragma warning disable S1133 -[Obsolete("V1 configuration is deprecated and will be removed in v2.0.0 version.")] +[Obsolete("V1 configuration is deprecated and will be removed in next major release")] public class MultiPluginConflictOptions { diff --git a/source/AAS.TwinEngine.DataEngine/Infrastructure/Configuration/LegacyV1/ConfigV1/PluginConfig.cs b/source/AAS.TwinEngine.DataEngine/Infrastructure/Configuration/LegacyV1/ConfigV1/PluginConfig.cs index ce23017e..b45a3fec 100644 --- a/source/AAS.TwinEngine.DataEngine/Infrastructure/Configuration/LegacyV1/ConfigV1/PluginConfig.cs +++ b/source/AAS.TwinEngine.DataEngine/Infrastructure/Configuration/LegacyV1/ConfigV1/PluginConfig.cs @@ -8,7 +8,7 @@ namespace AAS.TwinEngine.DataEngine.Infrastructure.Configuration.LegacyV1; /// Only the Section, Plugins list, and Plugin child class remain for V1 legacy adapter deserialization. /// #pragma warning disable S1133 -[Obsolete("V1 configuration is deprecated and will be removed in v2.0.0 version. Use HttpClientNames and ApiPaths instead.")] +[Obsolete("V1 configuration is deprecated and will be removed in next major release Use HttpClientNames and ApiPaths instead.")] public class PluginConfig { public const string Section = "PluginConfig"; @@ -25,7 +25,7 @@ public class PluginConfig } #pragma warning disable S1133 -[Obsolete("V1 configuration is deprecated and will be removed in v2.0.0 version. Use PluginInstance instead.")] +[Obsolete("V1 configuration is deprecated and will be removed in next major release Use ServiceInstance instead.")] public class Plugin { public required string PluginName { get; set; } diff --git a/source/AAS.TwinEngine.DataEngine/Infrastructure/Configuration/LegacyV1/ConfigV1/Semantics.cs b/source/AAS.TwinEngine.DataEngine/Infrastructure/Configuration/LegacyV1/ConfigV1/Semantics.cs index 56e4450a..1f4f7802 100644 --- a/source/AAS.TwinEngine.DataEngine/Infrastructure/Configuration/LegacyV1/ConfigV1/Semantics.cs +++ b/source/AAS.TwinEngine.DataEngine/Infrastructure/Configuration/LegacyV1/ConfigV1/Semantics.cs @@ -1,7 +1,7 @@ namespace AAS.TwinEngine.DataEngine.Infrastructure.Configuration.LegacyV1; #pragma warning disable S1133 -[Obsolete("V1 configuration is deprecated and will be removed in v2.0.0 version.")] +[Obsolete("V1 configuration is deprecated and will be removed in next major release")] public class Semantics { public const string Section = "Semantics"; diff --git a/source/AAS.TwinEngine.DataEngine/Infrastructure/Configuration/LegacyV1/LegacyConfigurationDetector.cs b/source/AAS.TwinEngine.DataEngine/Infrastructure/Configuration/LegacyV1/LegacyConfigurationDetector.cs index 36b0e204..895eb413 100644 --- a/source/AAS.TwinEngine.DataEngine/Infrastructure/Configuration/LegacyV1/LegacyConfigurationDetector.cs +++ b/source/AAS.TwinEngine.DataEngine/Infrastructure/Configuration/LegacyV1/LegacyConfigurationDetector.cs @@ -7,7 +7,7 @@ namespace AAS.TwinEngine.DataEngine.Infrastructure.Configuration.LegacyV1; /// V2 is identified by the existence of "General", "Plugins:Instances", or "TemplateManagement" top-level sections. /// #pragma warning disable S1133 -[Obsolete("V1 configuration is deprecated and will be removed in v2.0.0 version.")] +[Obsolete("V1 configuration is deprecated and will be removed in next major release")] public static class LegacyConfigurationDetector { public static bool IsV1Configuration(IConfiguration configuration) diff --git a/source/AAS.TwinEngine.DataEngine/Infrastructure/Configuration/LegacyV1/LegacyGeneralConfigAdapter.cs b/source/AAS.TwinEngine.DataEngine/Infrastructure/Configuration/LegacyV1/LegacyGeneralConfigAdapter.cs index 56c2c9a0..855bb0ca 100644 --- a/source/AAS.TwinEngine.DataEngine/Infrastructure/Configuration/LegacyV1/LegacyGeneralConfigAdapter.cs +++ b/source/AAS.TwinEngine.DataEngine/Infrastructure/Configuration/LegacyV1/LegacyGeneralConfigAdapter.cs @@ -12,7 +12,7 @@ namespace AAS.TwinEngine.DataEngine.Infrastructure.Configuration.LegacyV1; /// merges these values before any consumer resolves IOptions<GeneralConfig>. /// #pragma warning disable S1133 -[Obsolete("V1 configuration is deprecated and will be removed in v2.0.0 version.")] +[Obsolete("V1 configuration is deprecated and will be removed in next major release")] public sealed class LegacyGeneralConfigAdapter(IConfiguration configuration) : IConfigureOptions { private readonly IConfiguration _configuration = configuration; diff --git a/source/AAS.TwinEngine.DataEngine/Infrastructure/Configuration/LegacyV1/LegacyPluginsConfigAdapter.cs b/source/AAS.TwinEngine.DataEngine/Infrastructure/Configuration/LegacyV1/LegacyPluginsConfigAdapter.cs index a264c5b7..735d6b8a 100644 --- a/source/AAS.TwinEngine.DataEngine/Infrastructure/Configuration/LegacyV1/LegacyPluginsConfigAdapter.cs +++ b/source/AAS.TwinEngine.DataEngine/Infrastructure/Configuration/LegacyV1/LegacyPluginsConfigAdapter.cs @@ -10,7 +10,7 @@ namespace AAS.TwinEngine.DataEngine.Infrastructure.Configuration.LegacyV1; /// Reads V1 flat config sections and maps them into the V2 shape. /// #pragma warning disable S1133 -[Obsolete("V1 configuration is deprecated and will be removed in v2.0.0 version.")] +[Obsolete("V1 configuration is deprecated and will be removed in next major release")] public sealed class LegacyPluginsConfigAdapter(IConfiguration configuration) : IConfigureOptions { private readonly IConfiguration _configuration = configuration; @@ -72,7 +72,7 @@ public static void ApplyV1PluginInstanceOverrides(IConfiguration configuration, if (pluginConfig?.Plugins != null && pluginConfig.Plugins.Count > 0) { - options.Instances = pluginConfig.Plugins.Select(plugin => new PluginInstance + options.Instances = pluginConfig.Plugins.Select(plugin => new ServiceInstance { Name = plugin.PluginName, BaseUrl = plugin.PluginUrl, diff --git a/source/AAS.TwinEngine.DataEngine/Infrastructure/Configuration/LegacyV1/LegacyRegistrySettingsConfigAdapter.cs b/source/AAS.TwinEngine.DataEngine/Infrastructure/Configuration/LegacyV1/LegacyRegistrySettingsConfigAdapter.cs index 4162e442..57f18498 100644 --- a/source/AAS.TwinEngine.DataEngine/Infrastructure/Configuration/LegacyV1/LegacyRegistrySettingsConfigAdapter.cs +++ b/source/AAS.TwinEngine.DataEngine/Infrastructure/Configuration/LegacyV1/LegacyRegistrySettingsConfigAdapter.cs @@ -8,7 +8,7 @@ namespace AAS.TwinEngine.DataEngine.Infrastructure.Configuration.LegacyV1; /// Reads V1 flat config sections and maps them into the V2 shape. /// #pragma warning disable S1133 -[Obsolete("V1 configuration is deprecated and will be removed in v2.0.0 version.")] +[Obsolete("V1 configuration is deprecated and will be removed in next major release")] public sealed class LegacyRegistrySettingsConfigAdapter(IConfiguration configuration) : IConfigureOptions { private readonly IConfiguration _configuration = configuration; diff --git a/source/AAS.TwinEngine.DataEngine/Infrastructure/Configuration/LegacyV1/LegacyTemplateManagementConfigAdapter.cs b/source/AAS.TwinEngine.DataEngine/Infrastructure/Configuration/LegacyV1/LegacyTemplateManagementConfigAdapter.cs index d5af8f62..dcf0c39f 100644 --- a/source/AAS.TwinEngine.DataEngine/Infrastructure/Configuration/LegacyV1/LegacyTemplateManagementConfigAdapter.cs +++ b/source/AAS.TwinEngine.DataEngine/Infrastructure/Configuration/LegacyV1/LegacyTemplateManagementConfigAdapter.cs @@ -10,7 +10,7 @@ namespace AAS.TwinEngine.DataEngine.Infrastructure.Configuration.LegacyV1; /// Reads V1 flat config sections and maps them into the V2 shape. /// #pragma warning disable S1133 -[Obsolete("V1 configuration is deprecated and will be removed in v2.0.0 version.")] +[Obsolete("V1 configuration is deprecated and will be removed in next major release")] public sealed class LegacyTemplateManagementConfigAdapter(IConfiguration configuration) : IConfigureOptions { private readonly IConfiguration _configuration = configuration; @@ -107,7 +107,7 @@ private static void ApplyV1ServiceEndpointOverrides(IConfiguration configuration if (aasEnv.AasEnvironmentRepositoryBaseUrl != null) { - options.TemplateRepository = new ServiceEndpoint + options.TemplateRepository = new ServiceInstance { Name = HttpClientNames.TemplateRepository, BaseUrl = aasEnv.AasEnvironmentRepositoryBaseUrl, @@ -117,7 +117,7 @@ private static void ApplyV1ServiceEndpointOverrides(IConfiguration configuration if (aasEnv.AasRegistryBaseUrl != null) { - options.AasTemplateRegistry = new ServiceEndpoint + options.AasTemplateRegistry = new ServiceInstance { Name = HttpClientNames.AasRegistry, BaseUrl = aasEnv.AasRegistryBaseUrl, @@ -127,7 +127,7 @@ private static void ApplyV1ServiceEndpointOverrides(IConfiguration configuration if (aasEnv.SubModelRegistryBaseUrl != null) { - options.SubmodelTemplateRegistry = new ServiceEndpoint + options.SubmodelTemplateRegistry = new ServiceInstance { Name = HttpClientNames.SubmodelRegistry, BaseUrl = aasEnv.SubModelRegistryBaseUrl, diff --git a/source/AAS.TwinEngine.DataEngine/Infrastructure/Configuration/LegacyV1/LegacyV1ConfigurationExtensions.cs b/source/AAS.TwinEngine.DataEngine/Infrastructure/Configuration/LegacyV1/LegacyV1ConfigurationExtensions.cs index 5ec72070..f25e8568 100644 --- a/source/AAS.TwinEngine.DataEngine/Infrastructure/Configuration/LegacyV1/LegacyV1ConfigurationExtensions.cs +++ b/source/AAS.TwinEngine.DataEngine/Infrastructure/Configuration/LegacyV1/LegacyV1ConfigurationExtensions.cs @@ -11,7 +11,7 @@ namespace AAS.TwinEngine.DataEngine.Infrastructure.Configuration.LegacyV1; /// When V2 config is present, the adapters are registered but short-circuit (no-op). /// #pragma warning disable S1133 -[Obsolete("V1 configuration is deprecated and will be removed in v2.0.0 version.")] +[Obsolete("V1 configuration is deprecated and will be removed in next major release")] public static class LegacyV1ConfigurationExtensions { /// @@ -20,7 +20,7 @@ public static class LegacyV1ConfigurationExtensions /// V2 section-bind (if present) overwrites the adapter-provided defaults. /// - [Obsolete("V1 configuration is deprecated and will be removed in v2.0.0 version.")] + [Obsolete("V1 configuration is deprecated and will be removed in next major release")] public static IServiceCollection AddLegacyV1ConfigurationAdapters(this IServiceCollection services) { _ = services.AddSingleton, LegacyGeneralConfigAdapter>(); diff --git a/source/AAS.TwinEngine.DataEngine/Infrastructure/Http/Authorization/Config/HeaderMappingRule.cs b/source/AAS.TwinEngine.DataEngine/Infrastructure/Http/Authorization/Config/HeaderMappingRule.cs new file mode 100644 index 00000000..822eded6 --- /dev/null +++ b/source/AAS.TwinEngine.DataEngine/Infrastructure/Http/Authorization/Config/HeaderMappingRule.cs @@ -0,0 +1,14 @@ +using System.ComponentModel.DataAnnotations; + +namespace AAS.TwinEngine.DataEngine.Infrastructure.Http.Authorization.Config; + +public class HeaderMappingRule +{ + [Required] + public string Source { get; set; } = null!; + + [Required] + public string Target { get; set; } = null!; + + public bool Required { get; set; } +} diff --git a/source/AAS.TwinEngine.DataEngine/Infrastructure/Http/Authorization/Config/HeaderForwardingOptions.cs b/source/AAS.TwinEngine.DataEngine/Infrastructure/Http/Authorization/Config/HeaderSanitizationOptions.cs similarity index 53% rename from source/AAS.TwinEngine.DataEngine/Infrastructure/Http/Authorization/Config/HeaderForwardingOptions.cs rename to source/AAS.TwinEngine.DataEngine/Infrastructure/Http/Authorization/Config/HeaderSanitizationOptions.cs index f5e06d9d..5b509536 100644 --- a/source/AAS.TwinEngine.DataEngine/Infrastructure/Http/Authorization/Config/HeaderForwardingOptions.cs +++ b/source/AAS.TwinEngine.DataEngine/Infrastructure/Http/Authorization/Config/HeaderSanitizationOptions.cs @@ -1,16 +1,11 @@ -using System.ComponentModel.DataAnnotations; - -namespace AAS.TwinEngine.DataEngine.Infrastructure.Http.Authorization.Config; +namespace AAS.TwinEngine.DataEngine.Infrastructure.Http.Authorization.Config; public class HeaderSanitizationOptions { - [Range(1, int.MaxValue)] public int MaxHeaderSize { get; set; } = 8192; - [Range(1, int.MaxValue)] public int MaxHeaderNameSize { get; set; } = 256; - [Required] public AllowedCharactersOptions AllowedCharacters { get; set; } = new(); public IList BlockedPatterns { get; init; } = ["\\r|\\n", "\\x00", " CheckHealthAsync(HealthCheckContext context : HealthCheckResult.Unhealthy(); } - private async Task CheckAllPluginsAsync(IList plugins, CancellationToken cancellationToken) + private async Task CheckAllPluginsAsync(IList plugins, CancellationToken cancellationToken) { var tasks = plugins.Select(plugin => CheckSinglePluginAsync(plugin, cancellationToken)).ToList(); var results = await Task.WhenAll(tasks).ConfigureAwait(false); return results.All(healthy => healthy); } - private async Task CheckSinglePluginAsync(PluginInstance plugin, CancellationToken cancellationToken) + private async Task CheckSinglePluginAsync(ServiceInstance plugin, CancellationToken cancellationToken) { try { diff --git a/source/AAS.TwinEngine.DataEngine/Infrastructure/Providers/PluginDataProvider/Services/PluginManifestProvider.cs b/source/AAS.TwinEngine.DataEngine/Infrastructure/Providers/PluginDataProvider/Services/PluginManifestProvider.cs index 8baec38e..dfa61597 100644 --- a/source/AAS.TwinEngine.DataEngine/Infrastructure/Providers/PluginDataProvider/Services/PluginManifestProvider.cs +++ b/source/AAS.TwinEngine.DataEngine/Infrastructure/Providers/PluginDataProvider/Services/PluginManifestProvider.cs @@ -17,7 +17,7 @@ public class PluginManifestProvider(ILogger logger, ICreateClient clientFactory) : IPluginManifestProvider { private const string ManifestEndpoint = "manifest"; - private readonly IList _plugins = pluginsConfig.Value.Instances; + private readonly IList _plugins = pluginsConfig.Value.Instances; public async Task> GetAllPluginManifestsAsync(CancellationToken cancellationToken) { diff --git a/source/AAS.TwinEngine.DataEngine/Infrastructure/Providers/TemplateProvider/Services/ShellTemplateMappingProvider.cs b/source/AAS.TwinEngine.DataEngine/Infrastructure/Providers/TemplateProvider/Services/ShellTemplateMappingProvider.cs index c8d2b6b2..101dee1f 100644 --- a/source/AAS.TwinEngine.DataEngine/Infrastructure/Providers/TemplateProvider/Services/ShellTemplateMappingProvider.cs +++ b/source/AAS.TwinEngine.DataEngine/Infrastructure/Providers/TemplateProvider/Services/ShellTemplateMappingProvider.cs @@ -11,8 +11,8 @@ namespace AAS.TwinEngine.DataEngine.Infrastructure.Providers.TemplateProvider.Se public class ShellTemplateMappingProvider(ILogger logger, IOptions options) : IShellTemplateMappingProvider { - private readonly ILogger _logger = logger ?? throw new ArgumentNullException(); - private readonly IList _shellTemplateMappings = options.Value.TemplateMappingRules.ShellTemplateMappings ?? throw new ArgumentException(); + private readonly ILogger _logger = logger ?? throw new ArgumentNullException(nameof(logger)); + private readonly IList _shellTemplateMappings = options.Value.TemplateMappingRules.ShellTemplateMappings ?? throw new ArgumentException("ShellTemplateMappings are missing in TemplateMappingRules"); private readonly IList _aasIdExtractionRules = options.Value.TemplateMappingRules.AasIdExtractionRules ?? throw new ArgumentException("AasIdExtractionRules are missing in TemplateMappingRules"); private readonly TimeSpan _regexTimeout = TimeSpan.FromSeconds(2); diff --git a/source/AAS.TwinEngine.DataEngine/ServiceConfiguration/Config/Helpers/TemplateManagementConfigNormalizer.cs b/source/AAS.TwinEngine.DataEngine/ServiceConfiguration/Config/Helpers/TemplateManagementConfigNormalizer.cs index 430d2734..2f0252a3 100644 --- a/source/AAS.TwinEngine.DataEngine/ServiceConfiguration/Config/Helpers/TemplateManagementConfigNormalizer.cs +++ b/source/AAS.TwinEngine.DataEngine/ServiceConfiguration/Config/Helpers/TemplateManagementConfigNormalizer.cs @@ -23,7 +23,7 @@ public void PostConfigure(string? name, TemplateManagementConfig options) ApplyFallback(options.ConceptDescriptionTemplateRepository, fallback); } - private static void ApplyFallback(ServiceEndpoint target, ServiceEndpoint fallback) + private static void ApplyFallback(ServiceInstance target, ServiceInstance fallback) { target.BaseUrl ??= fallback.BaseUrl; diff --git a/source/AAS.TwinEngine.DataEngine/ServiceConfiguration/Config/Helpers/TemplateManagementConfigValidator.cs b/source/AAS.TwinEngine.DataEngine/ServiceConfiguration/Config/Helpers/TemplateManagementConfigValidator.cs index cbb4262f..f83b34ff 100644 --- a/source/AAS.TwinEngine.DataEngine/ServiceConfiguration/Config/Helpers/TemplateManagementConfigValidator.cs +++ b/source/AAS.TwinEngine.DataEngine/ServiceConfiguration/Config/Helpers/TemplateManagementConfigValidator.cs @@ -4,7 +4,7 @@ namespace AAS.TwinEngine.DataEngine.ServiceConfiguration.Config.Helpers; public class TemplateManagementConfigValidator : IValidateOptions { - private static readonly (string Name, Func Accessor)[] Endpoints = + private static readonly (string Name, Func Accessor)[] Endpoints = [ ("AasTemplateRepository", c => c.AasTemplateRepository), ("SubmodelTemplateRepository", c => c.SubmodelTemplateRepository), diff --git a/source/AAS.TwinEngine.DataEngine/ServiceConfiguration/Config/PluginsConfig.cs b/source/AAS.TwinEngine.DataEngine/ServiceConfiguration/Config/PluginsConfig.cs index 096ab298..33e3a345 100644 --- a/source/AAS.TwinEngine.DataEngine/ServiceConfiguration/Config/PluginsConfig.cs +++ b/source/AAS.TwinEngine.DataEngine/ServiceConfiguration/Config/PluginsConfig.cs @@ -1,5 +1,4 @@ -using AAS.TwinEngine.DataEngine.Infrastructure.Http.Authorization.Config; -using AAS.TwinEngine.DataEngine.ServiceConfiguration.Config.Helpers; +using AAS.TwinEngine.DataEngine.ServiceConfiguration.Config.Helpers; namespace AAS.TwinEngine.DataEngine.ServiceConfiguration.Config; @@ -14,7 +13,7 @@ public class PluginsConfig public string SubmodelElementIndexContextPrefix { get; set; } = "_aastwinengineindex_"; public PluginMultiLanguagePropertyConfig MultiLanguageProperty { get; set; } = new(); public ResiliencePoliciesConfig ResiliencePolicies { get; set; } = new(); - public IList Instances { get; set; } = []; + public IList Instances { get; set; } = []; } /// @@ -27,21 +26,3 @@ public class PluginMultiLanguagePropertyConfig public IList? DefaultLanguages { get; init; } public string SemanticPostfixSeparator { get; set; } = "_"; } - -/// -/// A single plugin instance. Replaces old Plugin class with renamed properties -/// and co-located header mappings. -/// -public class PluginInstance -{ - public required string Name { get; set; } - public required Uri BaseUrl { get; set; } - public IList HeaderMappings { get; init; } = []; -} - -public class RetryConfig -{ - public int MaxRetryAttempts { get; set; } - public int DelayInSeconds { get; set; } - public double BackoffMultiplier { get; set; } = 2.0; -} diff --git a/source/AAS.TwinEngine.DataEngine/ServiceConfiguration/Config/ResiliencePoliciesConfig.cs b/source/AAS.TwinEngine.DataEngine/ServiceConfiguration/Config/ResiliencePoliciesConfig.cs index 360e0bb9..bc8fbef5 100644 --- a/source/AAS.TwinEngine.DataEngine/ServiceConfiguration/Config/ResiliencePoliciesConfig.cs +++ b/source/AAS.TwinEngine.DataEngine/ServiceConfiguration/Config/ResiliencePoliciesConfig.cs @@ -1,6 +1,13 @@ -namespace AAS.TwinEngine.DataEngine.ServiceConfiguration.Config.Helpers; +namespace AAS.TwinEngine.DataEngine.ServiceConfiguration.Config; public class ResiliencePoliciesConfig { public RetryConfig Retry { get; set; } = new(); } + +public class RetryConfig +{ + public int MaxRetryAttempts { get; set; } + public int DelayInSeconds { get; set; } + public double BackoffMultiplier { get; set; } = 2.0; +} diff --git a/source/AAS.TwinEngine.DataEngine/ServiceConfiguration/Config/TemplateManagementConfig.cs b/source/AAS.TwinEngine.DataEngine/ServiceConfiguration/Config/TemplateManagementConfig.cs index f0f40800..cae920ca 100644 --- a/source/AAS.TwinEngine.DataEngine/ServiceConfiguration/Config/TemplateManagementConfig.cs +++ b/source/AAS.TwinEngine.DataEngine/ServiceConfiguration/Config/TemplateManagementConfig.cs @@ -1,6 +1,5 @@ using AAS.TwinEngine.DataEngine.Infrastructure.Http.Authorization.Config; using AAS.TwinEngine.DataEngine.Infrastructure.Providers.TemplateProvider.Config; -using AAS.TwinEngine.DataEngine.ServiceConfiguration.Config.Helpers; namespace AAS.TwinEngine.DataEngine.ServiceConfiguration.Config; @@ -16,12 +15,12 @@ public class TemplateManagementConfig public TemplateMappingRules TemplateMappingRules { get; set; } = new(); public TemplateSemanticsConfig Semantics { get; set; } = new(); - public ServiceEndpoint? TemplateRepository { get; set; } - public ServiceEndpoint AasTemplateRepository { get; set; } = new(); - public ServiceEndpoint SubmodelTemplateRepository { get; set; } = new(); - public ServiceEndpoint ConceptDescriptionTemplateRepository { get; set; } = new(); - public ServiceEndpoint AasTemplateRegistry { get; set; } = new(); - public ServiceEndpoint SubmodelTemplateRegistry { get; set; } = new(); + public ServiceInstance? TemplateRepository { get; set; } + public ServiceInstance AasTemplateRepository { get; set; } = new(); + public ServiceInstance SubmodelTemplateRepository { get; set; } = new(); + public ServiceInstance ConceptDescriptionTemplateRepository { get; set; } = new(); + public ServiceInstance AasTemplateRegistry { get; set; } = new(); + public ServiceInstance SubmodelTemplateRegistry { get; set; } = new(); } /// @@ -33,10 +32,10 @@ public class TemplateSemanticsConfig } /// -/// A named service endpoint with a base URL and co-located header mappings. -/// Used for template repositories and registries in the V2 schema. +/// A named service instance with a base URL and co-located header mappings. +/// Used for both plugin instances and template service endpoints. /// -public class ServiceEndpoint +public class ServiceInstance { public string Name { get; set; } = string.Empty; public Uri? BaseUrl { get; set; } diff --git a/source/AAS.TwinEngine.DataEngine/ServiceConfiguration/InfrastructureDependencyInjectionExtensions.cs b/source/AAS.TwinEngine.DataEngine/ServiceConfiguration/InfrastructureDependencyInjectionExtensions.cs index e37df460..f7a7ec7e 100644 --- a/source/AAS.TwinEngine.DataEngine/ServiceConfiguration/InfrastructureDependencyInjectionExtensions.cs +++ b/source/AAS.TwinEngine.DataEngine/ServiceConfiguration/InfrastructureDependencyInjectionExtensions.cs @@ -44,7 +44,10 @@ public static void ConfigureInfrastructure(this IServiceCollection services, ICo #pragma warning restore CS0618 // ── V2 POCO registrations (section-bind overwrites adapter defaults when V2 JSON exists) ── - _ = services.Configure(configuration.GetSection(GeneralConfig.Section)); + _ = services.AddOptions() + .Bind(configuration.GetSection(GeneralConfig.Section)) + .ValidateDataAnnotations() + .ValidateOnStart(); // MultiPluginConflictOptions: V1 config binds the old section value; V2 has no section → default ThrowError _ = services.Configure(configuration.GetSection(MultiPluginConflictOptions.Section)); From 7bf491c9ffef6c30b8e8f01af60a072f7698b14c Mon Sep 17 00:00:00 2001 From: Kevalkumar Date: Fri, 17 Apr 2026 01:35:50 +0530 Subject: [PATCH 42/71] removed unsued usings in TemplateProviderTests --- .../Http/Authorization/Headers/RequestHeaderMapperTests.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/source/AAS.TwinEngine.DataEngine.UnitTests/Infrastructure/Http/Authorization/Headers/RequestHeaderMapperTests.cs b/source/AAS.TwinEngine.DataEngine.UnitTests/Infrastructure/Http/Authorization/Headers/RequestHeaderMapperTests.cs index 00b011cd..8e4cbc6f 100644 --- a/source/AAS.TwinEngine.DataEngine.UnitTests/Infrastructure/Http/Authorization/Headers/RequestHeaderMapperTests.cs +++ b/source/AAS.TwinEngine.DataEngine.UnitTests/Infrastructure/Http/Authorization/Headers/RequestHeaderMapperTests.cs @@ -1,5 +1,4 @@ using AAS.TwinEngine.DataEngine.ApplicationLogic.Exceptions.Application; -using AAS.TwinEngine.DataEngine.ServiceConfiguration.Config; using AAS.TwinEngine.DataEngine.Infrastructure.Http.Authorization.Config; using AAS.TwinEngine.DataEngine.Infrastructure.Http.Authorization.Headers; using AAS.TwinEngine.DataEngine.ServiceConfiguration.Config; From a94fd6fcee7b2021284e488a05bb46c08858dddf Mon Sep 17 00:00:00 2001 From: Kevalkumar Date: Fri, 17 Apr 2026 01:49:43 +0530 Subject: [PATCH 43/71] fix: update expected test data for BaSyx SNAPSHOT compatibility The HandoverDocumentation submodel descriptor expected data had supplementalSemanticId as an array, but newer BaSyx 2.0.0-SNAPSHOT builds no longer propagate this field to registry descriptors. Updated expected value from array to null to match current upstream behavior and fix Playwright test failures in CI. --- ...odelDescriptorById_HandoverDocumentation_Expected.json | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/source/AAS.TwinEngine.Plugin.TestPlugin.PlaywrightTests/SubmodelRegistry/TestData/GetSubmodelDescriptorById_HandoverDocumentation_Expected.json b/source/AAS.TwinEngine.Plugin.TestPlugin.PlaywrightTests/SubmodelRegistry/TestData/GetSubmodelDescriptorById_HandoverDocumentation_Expected.json index fe43afdb..91f10035 100644 --- a/source/AAS.TwinEngine.Plugin.TestPlugin.PlaywrightTests/SubmodelRegistry/TestData/GetSubmodelDescriptorById_HandoverDocumentation_Expected.json +++ b/source/AAS.TwinEngine.Plugin.TestPlugin.PlaywrightTests/SubmodelRegistry/TestData/GetSubmodelDescriptorById_HandoverDocumentation_Expected.json @@ -21,13 +21,7 @@ "referredSemanticId": null, "keys": null }, - "supplementalSemanticId": [ - { - "type": "ExternalReference", - "referredSemanticId": null, - "keys": null - } - ], + "supplementalSemanticId": null, "endpoints": [ { "interface": "SUBMODEL-3.0", From 7360173b297428c8f1b6975b499acc03a7f00559 Mon Sep 17 00:00:00 2001 From: Kevalkumar Date: Fri, 17 Apr 2026 17:16:53 +0530 Subject: [PATCH 44/71] refactor: update template management configuration to support multiple repositories --- .../TestData/v2-config/appsettings.json | 14 ++++++- ...figurationBackwardCompatibilityE2ETests.cs | 10 +++-- ...acyTemplateManagementConfigAdapterTests.cs | 23 +++++------ .../LegacyTemplateManagementConfigAdapter.cs | 20 ++++++++-- .../TemplateManagementConfigNormalizer.cs | 38 ------------------- .../Config/HttpClientNames.cs | 2 - .../Config/TemplateManagementConfig.cs | 1 - ...astructureDependencyInjectionExtensions.cs | 3 -- .../appsettings.development.json | 38 +++++++++++++++++-- .../appsettings.json | 14 ++++++- .../Example/docker-compose.yml | 20 +++++++--- 11 files changed, 107 insertions(+), 76 deletions(-) delete mode 100644 source/AAS.TwinEngine.DataEngine/ServiceConfiguration/Config/Helpers/TemplateManagementConfigNormalizer.cs diff --git a/source/AAS.TwinEngine.DataEngine.ModuleTests/TestData/v2-config/appsettings.json b/source/AAS.TwinEngine.DataEngine.ModuleTests/TestData/v2-config/appsettings.json index 6d7a5292..54188a20 100644 --- a/source/AAS.TwinEngine.DataEngine.ModuleTests/TestData/v2-config/appsettings.json +++ b/source/AAS.TwinEngine.DataEngine.ModuleTests/TestData/v2-config/appsettings.json @@ -119,8 +119,8 @@ "Semantics": { "InternalSemanticId": "InternalSemanticId" }, - "TemplateRepository": { - "Name": "TemplateRepository", + "AasTemplateRepository": { + "Name": "AasTemplateRepository", "baseUrl": "http://localhost:8081", "headerMappings": [ { @@ -135,6 +135,16 @@ } ] }, + "SubmodelTemplateRepository": { + "Name": "SubmodelTemplateRepository", + "baseUrl": "http://localhost:8081", + "headerMappings": [] + }, + "ConceptDescriptionTemplateRepository": { + "Name": "ConceptDescriptionTemplateRepository", + "baseUrl": "http://localhost:8081", + "headerMappings": [] + }, "AasTemplateRegistry": { "Name": "AasTemplateRegistry", "baseUrl": "http://localhost:8082", diff --git a/source/AAS.TwinEngine.DataEngine.UnitTests/ServiceConfiguration/ConfigurationMigration/ConfigurationBackwardCompatibilityE2ETests.cs b/source/AAS.TwinEngine.DataEngine.UnitTests/ServiceConfiguration/ConfigurationMigration/ConfigurationBackwardCompatibilityE2ETests.cs index 25c9f074..b01b3866 100644 --- a/source/AAS.TwinEngine.DataEngine.UnitTests/ServiceConfiguration/ConfigurationMigration/ConfigurationBackwardCompatibilityE2ETests.cs +++ b/source/AAS.TwinEngine.DataEngine.UnitTests/ServiceConfiguration/ConfigurationMigration/ConfigurationBackwardCompatibilityE2ETests.cs @@ -1,6 +1,5 @@ using AAS.TwinEngine.DataEngine.Infrastructure.Configuration.LegacyV1; using AAS.TwinEngine.DataEngine.ServiceConfiguration.Config; -using AAS.TwinEngine.DataEngine.ServiceConfiguration.Config.Helpers; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; @@ -245,6 +244,12 @@ public void V1Config_AasEnvironmentUrls_MapToCorrectV2ServiceEndpoints() // AasEnvironmentRepositoryBaseUrl → TemplateManagement:AasTemplateRepository Assert.Equal(new Uri("http://localhost:8081"), tmConfig.AasTemplateRepository.BaseUrl); + // AasEnvironmentRepositoryBaseUrl → TemplateManagement:SubmodelTemplateRepository + Assert.Equal(new Uri("http://localhost:8081"), tmConfig.SubmodelTemplateRepository.BaseUrl); + + // AasEnvironmentRepositoryBaseUrl → TemplateManagement:ConceptDescriptionTemplateRepository + Assert.Equal(new Uri("http://localhost:8081"), tmConfig.ConceptDescriptionTemplateRepository.BaseUrl); + // AasRegistryBaseUrl → TemplateManagement:AasTemplateRegistry Assert.Equal(new Uri("http://localhost:8082"), tmConfig.AasTemplateRegistry.BaseUrl); @@ -283,9 +288,6 @@ private static ServiceProvider BuildServiceProvider(IConfiguration configuration services.Configure(configuration.GetSection(TemplateManagementConfig.Section)); services.Configure(configuration.GetSection(RegistrySettingsConfig.Section)); - // Register the normalizer that propagates TemplateRepository shorthand to individual repos - services.AddSingleton, TemplateManagementConfigNormalizer>(); - return services.BuildServiceProvider(); } diff --git a/source/AAS.TwinEngine.DataEngine.UnitTests/ServiceConfiguration/ConfigurationMigration/LegacyTemplateManagementConfigAdapterTests.cs b/source/AAS.TwinEngine.DataEngine.UnitTests/ServiceConfiguration/ConfigurationMigration/LegacyTemplateManagementConfigAdapterTests.cs index bf464290..3fb5380b 100644 --- a/source/AAS.TwinEngine.DataEngine.UnitTests/ServiceConfiguration/ConfigurationMigration/LegacyTemplateManagementConfigAdapterTests.cs +++ b/source/AAS.TwinEngine.DataEngine.UnitTests/ServiceConfiguration/ConfigurationMigration/LegacyTemplateManagementConfigAdapterTests.cs @@ -70,10 +70,9 @@ public void Configure_WithV1Config_MapsAasTemplateRepository() adapter.Configure(options); - // V1 maps AasEnvironmentRepositoryBaseUrl to the TemplateRepository shorthand - Assert.NotNull(options.TemplateRepository); - Assert.Equal(HttpClientNames.TemplateRepository, options.TemplateRepository!.Name); - Assert.Equal(new Uri("http://localhost:8081"), options.TemplateRepository.BaseUrl); + // V1 maps AasEnvironmentRepositoryBaseUrl to all 3 repository endpoints directly + Assert.Equal(HttpClientNames.AasTemplateRepository, options.AasTemplateRepository.Name); + Assert.Equal(new Uri("http://localhost:8081"), options.AasTemplateRepository.BaseUrl); } [Fact] @@ -111,10 +110,9 @@ public void Configure_WithV1Config_MapsSubmodelTemplateRepository() adapter.Configure(options); - // SubmodelTemplateRepository is now populated via TemplateRepository shorthand + normalizer; - // the adapter sets the shorthand, not individual repos directly. - Assert.NotNull(options.TemplateRepository); - Assert.Equal(new Uri("http://localhost:8081"), options.TemplateRepository!.BaseUrl); + // SubmodelTemplateRepository is populated directly from AasEnvironmentRepositoryBaseUrl + Assert.Equal(HttpClientNames.SubmodelTemplateRepository, options.SubmodelTemplateRepository.Name); + Assert.Equal(new Uri("http://localhost:8081"), options.SubmodelTemplateRepository.BaseUrl); } [Fact] @@ -126,11 +124,10 @@ public void Configure_WithV1Config_MapsTemplateRepositoryHeaders() adapter.Configure(options); - // Headers now live on the TemplateRepository shorthand - Assert.NotNull(options.TemplateRepository); - Assert.Equal(2, options.TemplateRepository!.HeaderMappings.Count); - Assert.Equal("Authorization", options.TemplateRepository.HeaderMappings[0].Source); - Assert.Equal("Authorization", options.TemplateRepository.HeaderMappings[0].Target); + // Headers now live on each repository endpoint directly + Assert.Equal(2, options.AasTemplateRepository.HeaderMappings.Count); + Assert.Equal("Authorization", options.AasTemplateRepository.HeaderMappings[0].Source); + Assert.Equal("Authorization", options.AasTemplateRepository.HeaderMappings[0].Target); } [Fact] diff --git a/source/AAS.TwinEngine.DataEngine/Infrastructure/Configuration/LegacyV1/LegacyTemplateManagementConfigAdapter.cs b/source/AAS.TwinEngine.DataEngine/Infrastructure/Configuration/LegacyV1/LegacyTemplateManagementConfigAdapter.cs index dcf0c39f..48d61ab7 100644 --- a/source/AAS.TwinEngine.DataEngine/Infrastructure/Configuration/LegacyV1/LegacyTemplateManagementConfigAdapter.cs +++ b/source/AAS.TwinEngine.DataEngine/Infrastructure/Configuration/LegacyV1/LegacyTemplateManagementConfigAdapter.cs @@ -107,11 +107,25 @@ private static void ApplyV1ServiceEndpointOverrides(IConfiguration configuration if (aasEnv.AasEnvironmentRepositoryBaseUrl != null) { - options.TemplateRepository = new ServiceInstance + var repoHeaders = headerForwarding?.HeaderMappings.TemplateRepository ?? []; + + options.AasTemplateRepository = new ServiceInstance + { + Name = HttpClientNames.AasTemplateRepository, + BaseUrl = aasEnv.AasEnvironmentRepositoryBaseUrl, + HeaderMappings = repoHeaders + }; + options.SubmodelTemplateRepository = new ServiceInstance + { + Name = HttpClientNames.SubmodelTemplateRepository, + BaseUrl = aasEnv.AasEnvironmentRepositoryBaseUrl, + HeaderMappings = repoHeaders + }; + options.ConceptDescriptionTemplateRepository = new ServiceInstance { - Name = HttpClientNames.TemplateRepository, + Name = HttpClientNames.ConceptDescriptorTemplateRepository, BaseUrl = aasEnv.AasEnvironmentRepositoryBaseUrl, - HeaderMappings = headerForwarding?.HeaderMappings.TemplateRepository ?? [] + HeaderMappings = repoHeaders }; } diff --git a/source/AAS.TwinEngine.DataEngine/ServiceConfiguration/Config/Helpers/TemplateManagementConfigNormalizer.cs b/source/AAS.TwinEngine.DataEngine/ServiceConfiguration/Config/Helpers/TemplateManagementConfigNormalizer.cs deleted file mode 100644 index 2f0252a3..00000000 --- a/source/AAS.TwinEngine.DataEngine/ServiceConfiguration/Config/Helpers/TemplateManagementConfigNormalizer.cs +++ /dev/null @@ -1,38 +0,0 @@ -using Microsoft.Extensions.Options; - -namespace AAS.TwinEngine.DataEngine.ServiceConfiguration.Config.Helpers; - -/// -/// Post-configuration step that applies the -/// shorthand: when TemplateRepository is provided, its BaseUrl and HeaderMappings are used -/// as defaults for AasTemplateRepository, SubmodelTemplateRepository, and -/// ConceptDescriptionTemplateRepository — unless they already have their own BaseUrl. -/// -public sealed class TemplateManagementConfigNormalizer : IPostConfigureOptions -{ - public void PostConfigure(string? name, TemplateManagementConfig options) - { - var fallback = options.TemplateRepository; - if (fallback?.BaseUrl is null) - { - return; - } - - ApplyFallback(options.AasTemplateRepository, fallback); - ApplyFallback(options.SubmodelTemplateRepository, fallback); - ApplyFallback(options.ConceptDescriptionTemplateRepository, fallback); - } - - private static void ApplyFallback(ServiceInstance target, ServiceInstance fallback) - { - target.BaseUrl ??= fallback.BaseUrl; - - if (target.HeaderMappings.Count == 0 && fallback.HeaderMappings.Count > 0) - { - foreach (var mapping in fallback.HeaderMappings) - { - target.HeaderMappings.Add(mapping); - } - } - } -} diff --git a/source/AAS.TwinEngine.DataEngine/ServiceConfiguration/Config/HttpClientNames.cs b/source/AAS.TwinEngine.DataEngine/ServiceConfiguration/Config/HttpClientNames.cs index ff6c9f3f..9853f99a 100644 --- a/source/AAS.TwinEngine.DataEngine/ServiceConfiguration/Config/HttpClientNames.cs +++ b/source/AAS.TwinEngine.DataEngine/ServiceConfiguration/Config/HttpClientNames.cs @@ -7,7 +7,6 @@ namespace AAS.TwinEngine.DataEngine.ServiceConfiguration.Config; /// public static class HttpClientNames { - public const string TemplateRepository = "template-repository"; public const string AasRegistry = "aas-registry"; public const string SubmodelRegistry = "submodel-registry"; public const string SubmodelTemplateRepository = "submodel-template-repository"; @@ -23,7 +22,6 @@ public static class HttpClientNames public static string GetHealthCheckName(string clientName) => $"{clientName}{HealthCheckSuffix}"; - public static string TemplateRepositoryHealthCheck => GetHealthCheckName(TemplateRepository); public static string AasRegistryHealthCheck => GetHealthCheckName(AasRegistry); public static string SubmodelRegistryHealthCheck => GetHealthCheckName(SubmodelRegistry); public static string SubmodelTemplateRepositoryHealthCheck => GetHealthCheckName(SubmodelTemplateRepository); diff --git a/source/AAS.TwinEngine.DataEngine/ServiceConfiguration/Config/TemplateManagementConfig.cs b/source/AAS.TwinEngine.DataEngine/ServiceConfiguration/Config/TemplateManagementConfig.cs index cae920ca..f796d723 100644 --- a/source/AAS.TwinEngine.DataEngine/ServiceConfiguration/Config/TemplateManagementConfig.cs +++ b/source/AAS.TwinEngine.DataEngine/ServiceConfiguration/Config/TemplateManagementConfig.cs @@ -15,7 +15,6 @@ public class TemplateManagementConfig public TemplateMappingRules TemplateMappingRules { get; set; } = new(); public TemplateSemanticsConfig Semantics { get; set; } = new(); - public ServiceInstance? TemplateRepository { get; set; } public ServiceInstance AasTemplateRepository { get; set; } = new(); public ServiceInstance SubmodelTemplateRepository { get; set; } = new(); public ServiceInstance ConceptDescriptionTemplateRepository { get; set; } = new(); diff --git a/source/AAS.TwinEngine.DataEngine/ServiceConfiguration/InfrastructureDependencyInjectionExtensions.cs b/source/AAS.TwinEngine.DataEngine/ServiceConfiguration/InfrastructureDependencyInjectionExtensions.cs index f7a7ec7e..f6702dab 100644 --- a/source/AAS.TwinEngine.DataEngine/ServiceConfiguration/InfrastructureDependencyInjectionExtensions.cs +++ b/source/AAS.TwinEngine.DataEngine/ServiceConfiguration/InfrastructureDependencyInjectionExtensions.cs @@ -75,8 +75,6 @@ public static void ConfigureInfrastructure(this IServiceCollection services, ICo LegacyRegistrySettingsConfigAdapter.ApplyV1Overrides(configuration, options)); #pragma warning restore CS0618 - _ = services.AddSingleton, TemplateManagementConfigNormalizer>(); - // Validators _ = services.AddSingleton, TemplateManagementConfigValidator>(); _ = services.AddSingleton, RegistrySettingsConfigValidator>(); @@ -88,7 +86,6 @@ public static void ConfigureInfrastructure(this IServiceCollection services, ICo #pragma warning disable CS0618 // Obsolete — intentional V1 backward-compat mapping LegacyTemplateManagementConfigAdapter.MapToConfig(configuration, templateManagement); #pragma warning restore CS0618 - new TemplateManagementConfigNormalizer().PostConfigure(Options.DefaultName, templateManagement); var pluginsConfig = new PluginsConfig(); configuration.GetSection(PluginsConfig.Section).Bind(pluginsConfig); diff --git a/source/AAS.TwinEngine.DataEngine/appsettings.development.json b/source/AAS.TwinEngine.DataEngine/appsettings.development.json index d11834c2..f7d821ad 100644 --- a/source/AAS.TwinEngine.DataEngine/appsettings.development.json +++ b/source/AAS.TwinEngine.DataEngine/appsettings.development.json @@ -1,4 +1,4 @@ -{ + { "General": { "Logging": { "LogLevel": { @@ -166,8 +166,40 @@ "Semantics": { "InternalSemanticId": "InternalSemanticId" }, - "TemplateRepository": { - "Name": "TemplateRepository", + "AasTemplateRepository": { + "Name": "AasTemplateRepository", + "baseUrl": "http://localhost:8081", + "headerMappings": [ + { + "source": "Authorization", + "target": "Authorization", + "required": false + }, + { + "source": "X-User-Roles", + "target": "X-Template-Access-Roles", + "required": false + } + ] + }, + "SubmodelTemplateRepository": { + "Name": "SubmodelTemplateRepository", + "baseUrl": "http://localhost:8081", + "headerMappings": [ + { + "source": "Authorization", + "target": "Authorization", + "required": false + }, + { + "source": "X-User-Roles", + "target": "X-Template-Access-Roles", + "required": false + } + ] + }, + "ConceptDescriptionTemplateRepository": { + "Name": "ConceptDescriptionTemplateRepository", "baseUrl": "http://localhost:8081", "headerMappings": [ { diff --git a/source/AAS.TwinEngine.DataEngine/appsettings.json b/source/AAS.TwinEngine.DataEngine/appsettings.json index 2913cd1b..88378977 100644 --- a/source/AAS.TwinEngine.DataEngine/appsettings.json +++ b/source/AAS.TwinEngine.DataEngine/appsettings.json @@ -113,8 +113,18 @@ "Semantics": { "InternalSemanticId": "InternalSemanticId" }, - "TemplateRepository": { - "Name": "TemplateRepository", + "AasTemplateRepository": { + "Name": "AasTemplateRepository", + "baseUrl": "http://localhost:8081", + "headerMappings": [] + }, + "SubmodelTemplateRepository": { + "Name": "SubmodelTemplateRepository", + "baseUrl": "http://localhost:8081", + "headerMappings": [] + }, + "ConceptDescriptionTemplateRepository": { + "Name": "ConceptDescriptionTemplateRepository", "baseUrl": "http://localhost:8081", "headerMappings": [] }, diff --git a/source/AAS.TwinEngine.Plugin.TestPlugin/Example/docker-compose.yml b/source/AAS.TwinEngine.Plugin.TestPlugin/Example/docker-compose.yml index 7dc61eb6..f6967016 100644 --- a/source/AAS.TwinEngine.Plugin.TestPlugin/Example/docker-compose.yml +++ b/source/AAS.TwinEngine.Plugin.TestPlugin/Example/docker-compose.yml @@ -56,11 +56,21 @@ services: - TemplateManagement__TemplateMappingRules__AasIdExtractionRules__0__Pattern=Regex - TemplateManagement__TemplateMappingRules__AasIdExtractionRules__0__Index=6 - TemplateManagement__TemplateMappingRules__AasIdExtractionRules__0__Separator=/ - - TemplateManagement__TemplateRepository__Name=AasTemplateRepository - - TemplateManagement__TemplateRepository__baseUrl=http://template-repository:8081 - - TemplateManagement__TemplateRepository__headerMappings__0__source=Authorization - - TemplateManagement__TemplateRepository__headerMappings__0__target=Authorization - - TemplateManagement__TemplateRepository__headerMappings__0__required=false + - TemplateManagement__AasTemplateRepository__Name=AasTemplateRepository + - TemplateManagement__AasTemplateRepository__baseUrl=http://template-repository:8081 + - TemplateManagement__AasTemplateRepository__headerMappings__0__source=Authorization + - TemplateManagement__AasTemplateRepository__headerMappings__0__target=Authorization + - TemplateManagement__AasTemplateRepository__headerMappings__0__required=false + - TemplateManagement__SubmodelTemplateRepository__Name=SubmodelTemplateRepository + - TemplateManagement__SubmodelTemplateRepository__baseUrl=http://template-repository:8081 + - TemplateManagement__SubmodelTemplateRepository__headerMappings__0__source=Authorization + - TemplateManagement__SubmodelTemplateRepository__headerMappings__0__target=Authorization + - TemplateManagement__SubmodelTemplateRepository__headerMappings__0__required=false + - TemplateManagement__ConceptDescriptionTemplateRepository__Name=ConceptDescriptionTemplateRepository + - TemplateManagement__ConceptDescriptionTemplateRepository__baseUrl=http://template-repository:8081 + - TemplateManagement__ConceptDescriptionTemplateRepository__headerMappings__0__source=Authorization + - TemplateManagement__ConceptDescriptionTemplateRepository__headerMappings__0__target=Authorization + - TemplateManagement__ConceptDescriptionTemplateRepository__headerMappings__0__required=false - TemplateManagement__AasTemplateRegistry__Name=AasTemplateRegistry - TemplateManagement__AasTemplateRegistry__baseUrl=http://aas-template-registry:8080 - TemplateManagement__AasTemplateRegistry__headerMappings__0__source=Authorization From fedc79a51e7cdfa2e8109ff9c78938c33d11bba6 Mon Sep 17 00:00:00 2001 From: Kevalkumar Date: Fri, 17 Apr 2026 17:19:17 +0530 Subject: [PATCH 45/71] refactor: update template management configuration to support multiple template repositories --- example/docker-compose.yml | 20 +++++++++++++++----- 1 file changed, 15 insertions(+), 5 deletions(-) diff --git a/example/docker-compose.yml b/example/docker-compose.yml index 7d24114f..6e588a27 100644 --- a/example/docker-compose.yml +++ b/example/docker-compose.yml @@ -55,11 +55,21 @@ services: - TemplateManagement__TemplateMappingRules__AasIdExtractionRules__0__Pattern=Regex - TemplateManagement__TemplateMappingRules__AasIdExtractionRules__0__Index=6 - TemplateManagement__TemplateMappingRules__AasIdExtractionRules__0__Separator=/ - - TemplateManagement__TemplateRepository__Name=TemplateRepository - - TemplateManagement__TemplateRepository__baseUrl=http://template-repository:8081 - - TemplateManagement__TemplateRepository__headerMappings__0__source=Authorization - - TemplateManagement__TemplateRepository__headerMappings__0__target=Authorization - - TemplateManagement__TemplateRepository__headerMappings__0__required=false + - TemplateManagement__AasTemplateRepository__Name=AasTemplateRepository + - TemplateManagement__AasTemplateRepository__baseUrl=http://template-repository:8081 + - TemplateManagement__AasTemplateRepository__headerMappings__0__source=Authorization + - TemplateManagement__AasTemplateRepository__headerMappings__0__target=Authorization + - TemplateManagement__AasTemplateRepository__headerMappings__0__required=false + - TemplateManagement__SubmodelTemplateRepository__Name=SubmodelTemplateRepository + - TemplateManagement__SubmodelTemplateRepository__baseUrl=http://template-repository:8081 + - TemplateManagement__SubmodelTemplateRepository__headerMappings__0__source=Authorization + - TemplateManagement__SubmodelTemplateRepository__headerMappings__0__target=Authorization + - TemplateManagement__SubmodelTemplateRepository__headerMappings__0__required=false + - TemplateManagement__ConceptDescriptionTemplateRepository__Name=ConceptDescriptionTemplateRepository + - TemplateManagement__ConceptDescriptionTemplateRepository__baseUrl=http://template-repository:8081 + - TemplateManagement__ConceptDescriptionTemplateRepository__headerMappings__0__source=Authorization + - TemplateManagement__ConceptDescriptionTemplateRepository__headerMappings__0__target=Authorization + - TemplateManagement__ConceptDescriptionTemplateRepository__headerMappings__0__required=false - TemplateManagement__AasTemplateRegistry__Name=AasTemplateRegistry - TemplateManagement__AasTemplateRegistry__baseUrl=http://aas-template-registry:8080 - TemplateManagement__AasTemplateRegistry__headerMappings__0__source=Authorization From 80c6543ba2ff29173ea13b3648896908512f16e2 Mon Sep 17 00:00:00 2001 From: Kevalkumar Date: Mon, 20 Apr 2026 01:28:16 +0530 Subject: [PATCH 46/71] Refactor AasIdExtractionRules to support Split strategy and enhance validation --- example/docker-compose.yml | 5 +- .../Api/Services/AasRepository/TestData.cs | 24 +- .../TestData/v1-config/appsettings.json | 2 +- .../TestData/v2-config/appsettings.json | 5 +- .../TemplateMappingRulesValidatorTests.cs | 269 +++++++++ .../ShellTemplateMappingProviderTests.cs | 515 ++++++++++++++---- ...figurationBackwardCompatibilityE2ETests.cs | 4 +- ...acyTemplateManagementConfigAdapterTests.cs | 7 +- .../V2DirectBindingTests.cs | 4 +- .../LegacyTemplateManagementConfigAdapter.cs | 30 + .../Config/TemplateMappingRules.cs | 27 +- .../Config/TemplateMappingRulesValidator.cs | 82 +++ .../Services/ShellTemplateMappingProvider.cs | 63 ++- ...astructureDependencyInjectionExtensions.cs | 4 + .../appsettings.development.json | 26 +- .../appsettings.json | 11 +- .../Example/docker-compose.yml | 5 +- 17 files changed, 920 insertions(+), 163 deletions(-) create mode 100644 source/AAS.TwinEngine.DataEngine.UnitTests/Infrastructure/Providers/TemplateProvider/Config/TemplateMappingRulesValidatorTests.cs create mode 100644 source/AAS.TwinEngine.DataEngine/Infrastructure/Providers/TemplateProvider/Config/TemplateMappingRulesValidator.cs diff --git a/example/docker-compose.yml b/example/docker-compose.yml index 7d24114f..803a2af1 100644 --- a/example/docker-compose.yml +++ b/example/docker-compose.yml @@ -52,9 +52,10 @@ services: - TemplateManagement__TemplateMappingRules__SubmodelTemplateMappings__4__pattern__0=HandoverDocumentation - TemplateManagement__TemplateMappingRules__ShellTemplateMappings__0__templateId=https://mm-software.com/aas/aasTemplate - TemplateManagement__TemplateMappingRules__ShellTemplateMappings__0__pattern__0= - - TemplateManagement__TemplateMappingRules__AasIdExtractionRules__0__Pattern=Regex + - TemplateManagement__TemplateMappingRules__AasIdExtractionRules__0__Strategy=Split + - TemplateManagement__TemplateMappingRules__AasIdExtractionRules__0__Pattern=/ - TemplateManagement__TemplateMappingRules__AasIdExtractionRules__0__Index=6 - - TemplateManagement__TemplateMappingRules__AasIdExtractionRules__0__Separator=/ + - TemplateManagement__TemplateMappingRules__AasIdExtractionRules__0__Description=Default split by '/' segment 6 - TemplateManagement__TemplateRepository__Name=TemplateRepository - TemplateManagement__TemplateRepository__baseUrl=http://template-repository:8081 - TemplateManagement__TemplateRepository__headerMappings__0__source=Authorization diff --git a/source/AAS.TwinEngine.DataEngine.ModuleTests/Api/Services/AasRepository/TestData.cs b/source/AAS.TwinEngine.DataEngine.ModuleTests/Api/Services/AasRepository/TestData.cs index eedb170c..b6db7171 100644 --- a/source/AAS.TwinEngine.DataEngine.ModuleTests/Api/Services/AasRepository/TestData.cs +++ b/source/AAS.TwinEngine.DataEngine.ModuleTests/Api/Services/AasRepository/TestData.cs @@ -1,7 +1,6 @@ using System.Text.Json.Nodes; using AAS.TwinEngine.DataEngine.DomainModel.Plugin; -using AAS.TwinEngine.DataEngine.Infrastructure.Providers.TemplateProvider.Config; using AasCore.Aas3_0; @@ -201,25 +200,12 @@ public static List CreateSubmodelRefs() public static string? GetProductIdFromRule(string aasIdentifier, int index) { - var aasIdExtractionRules = new List + var parts = aasIdentifier?.Split("/"); + if (parts is null || parts.Length < index || index < 1) { - new() { - Pattern = "Regex", - Index = index, - Separator = "/" - } - }; - - var productId = aasIdExtractionRules - .Select(rule => new - { - Rule = rule, - Parts = aasIdentifier?.Split(rule.Separator) - }) - .Where(x => x.Parts is { Length: >= 1 } && x.Rule.Index > 0 && x.Parts.Length >= x.Rule.Index) - .Select(x => x.Parts![x.Rule.Index - 1]) - .FirstOrDefault(); + return null; + } - return productId; + return parts[index - 1]; } } diff --git a/source/AAS.TwinEngine.DataEngine.ModuleTests/TestData/v1-config/appsettings.json b/source/AAS.TwinEngine.DataEngine.ModuleTests/TestData/v1-config/appsettings.json index d4cd7a1f..9c01b356 100644 --- a/source/AAS.TwinEngine.DataEngine.ModuleTests/TestData/v1-config/appsettings.json +++ b/source/AAS.TwinEngine.DataEngine.ModuleTests/TestData/v1-config/appsettings.json @@ -62,7 +62,7 @@ ], "AasIdExtractionRules": [ { - "Pattern": "Regex", + "Pattern": "Split", "Index": 6, "Separator": "/" } diff --git a/source/AAS.TwinEngine.DataEngine.ModuleTests/TestData/v2-config/appsettings.json b/source/AAS.TwinEngine.DataEngine.ModuleTests/TestData/v2-config/appsettings.json index 6d7a5292..46943105 100644 --- a/source/AAS.TwinEngine.DataEngine.ModuleTests/TestData/v2-config/appsettings.json +++ b/source/AAS.TwinEngine.DataEngine.ModuleTests/TestData/v2-config/appsettings.json @@ -110,9 +110,10 @@ ], "AasIdExtractionRules": [ { - "Pattern": "Regex", + "Strategy": "Split", + "Pattern": "/", "Index": 6, - "Separator": "/" + "Description": "Default: split by '/' and take segment 6" } ] }, diff --git a/source/AAS.TwinEngine.DataEngine.UnitTests/Infrastructure/Providers/TemplateProvider/Config/TemplateMappingRulesValidatorTests.cs b/source/AAS.TwinEngine.DataEngine.UnitTests/Infrastructure/Providers/TemplateProvider/Config/TemplateMappingRulesValidatorTests.cs new file mode 100644 index 00000000..8e43161a --- /dev/null +++ b/source/AAS.TwinEngine.DataEngine.UnitTests/Infrastructure/Providers/TemplateProvider/Config/TemplateMappingRulesValidatorTests.cs @@ -0,0 +1,269 @@ +using AAS.TwinEngine.DataEngine.Infrastructure.Providers.TemplateProvider.Config; +using AAS.TwinEngine.DataEngine.ServiceConfiguration.Config; + +using Microsoft.Extensions.Options; + +namespace AAS.TwinEngine.DataEngine.UnitTests.Infrastructure.Providers.TemplateProvider.Config; + +public class TemplateMappingRulesValidatorTests +{ + private readonly TemplateMappingRulesValidator _sut = new(); + + private static TemplateManagementConfig CreateConfig(params AasIdExtractionRule[] rules) + { + return new TemplateManagementConfig + { + TemplateMappingRules = new TemplateMappingRules + { + AasIdExtractionRules = rules + } + }; + } + + // ── Rule 1: At least one rule is required ── + + [Fact] + public void Validate_ZeroRules_Fails() + { + var config = CreateConfig(); + + var result = _sut.Validate(null, config); + + Assert.True(result.Failed); + Assert.Contains("At least one AasIdExtractionRule is required", result.FailureMessage); + } + + // ── Rule 2: Single rule, no ValidationPattern → succeeds ── + + [Fact] + public void Validate_SingleRule_NoValidationPattern_Succeeds() + { + var config = CreateConfig( + new AasIdExtractionRule + { + Strategy = ExtractionStrategy.Split, + Pattern = "/", + Index = 6 + }); + + var result = _sut.Validate(null, config); + + Assert.True(result.Succeeded); + } + + // ── Rule 3: Multiple rules, all have ValidationPattern → succeeds ── + + [Fact] + public void Validate_MultipleRules_AllHaveValidationPattern_Succeeds() + { + var config = CreateConfig( + new AasIdExtractionRule + { + Strategy = ExtractionStrategy.Regex, + Pattern = @"^https?://[^/]+/ids/submodel/([^/]+/[^/]+)(?:/|$)", + Index = 1, + ValidationPattern = @"^[0-9\-/]+$" + }, + new AasIdExtractionRule + { + Strategy = ExtractionStrategy.Regex, + Pattern = @"^https?://[^/]+/ids/submodel/([^/]+)(?:/|$)", + Index = 1, + ValidationPattern = @"^[0-9\-]+$" + }); + + var result = _sut.Validate(null, config); + + Assert.True(result.Succeeded); + } + + // ── Rule 3: Multiple rules, one missing ValidationPattern → fails ── + + [Fact] + public void Validate_MultipleRules_OneMissingValidationPattern_Fails() + { + var config = CreateConfig( + new AasIdExtractionRule + { + Strategy = ExtractionStrategy.Regex, + Pattern = @"^https?://[^/]+/ids/submodel/([^/]+)(?:/|$)", + Index = 1, + ValidationPattern = @"^[0-9\-]+$" + }, + new AasIdExtractionRule + { + Strategy = ExtractionStrategy.Split, + Pattern = "/", + Index = 6 + // Missing ValidationPattern + }); + + var result = _sut.Validate(null, config); + + Assert.True(result.Failed); + Assert.Contains("ValidationPattern is required when multiple extraction rules are configured", result.FailureMessage); + } + + // ── Regex with invalid pattern → fails ── + + [Fact] + public void Validate_Regex_InvalidPattern_Fails() + { + var config = CreateConfig( + new AasIdExtractionRule + { + Strategy = ExtractionStrategy.Regex, + Pattern = "[invalid", + Index = 1 + }); + + var result = _sut.Validate(null, config); + + Assert.True(result.Failed); + Assert.Contains("invalid regex Pattern", result.FailureMessage); + } + + // ── Split with empty separator → fails ── + + [Fact] + public void Validate_Split_EmptyPattern_Fails() + { + var config = CreateConfig( + new AasIdExtractionRule + { + Strategy = ExtractionStrategy.Split, + Pattern = "", + Index = 6 + }); + + var result = _sut.Validate(null, config); + + Assert.True(result.Failed); + Assert.Contains("empty Pattern", result.FailureMessage); + } + + // ── Index < 1 → fails ── + + [Fact] + public void Validate_IndexLessThanOne_Fails() + { + var config = CreateConfig( + new AasIdExtractionRule + { + Strategy = ExtractionStrategy.Split, + Pattern = "/", + Index = 0 + }); + + var result = _sut.Validate(null, config); + + Assert.True(result.Failed); + Assert.Contains("Index must be >= 1", result.FailureMessage); + } + + // ── EndIndex < Index → fails ── + + [Fact] + public void Validate_Split_EndIndexLessThanIndex_Fails() + { + var config = CreateConfig( + new AasIdExtractionRule + { + Strategy = ExtractionStrategy.Split, + Pattern = "/", + Index = 5, + EndIndex = 3 + }); + + var result = _sut.Validate(null, config); + + Assert.True(result.Failed); + Assert.Contains("EndIndex (3) must be >= Index (5)", result.FailureMessage); + } + + // ── Invalid ValidationPattern regex → fails ── + + [Fact] + public void Validate_InvalidValidationPattern_Fails() + { + var config = CreateConfig( + new AasIdExtractionRule + { + Strategy = ExtractionStrategy.Split, + Pattern = "/", + Index = 6, + ValidationPattern = "[broken" + }); + + var result = _sut.Validate(null, config); + + Assert.True(result.Failed); + Assert.Contains("invalid ValidationPattern", result.FailureMessage); + } + + // ── Null options → throws ── + + [Fact] + public void Validate_NullOptions_ThrowsArgumentNullException() + { + Assert.Throws(() => _sut.Validate(null, null!)); + } + + // ── Uses Description in error message when available ── + + [Fact] + public void Validate_UsesDescriptionInErrorMessage() + { + var config = CreateConfig( + new AasIdExtractionRule + { + Strategy = ExtractionStrategy.Split, + Pattern = "/", + Index = 0, + Description = "My broken rule" + }); + + var result = _sut.Validate(null, config); + + Assert.True(result.Failed); + Assert.Contains("My broken rule", result.FailureMessage); + } + + // ── Valid Regex strategy rule → succeeds ── + + [Fact] + public void Validate_ValidRegexRule_Succeeds() + { + var config = CreateConfig( + new AasIdExtractionRule + { + Strategy = ExtractionStrategy.Regex, + Pattern = @"^https?://[^/]+/ids/submodel/([^/]+)(?:/|$)", + Index = 1, + Description = "Single-segment" + }); + + var result = _sut.Validate(null, config); + + Assert.True(result.Succeeded); + } + + // ── Valid Split with EndIndex → succeeds ── + + [Fact] + public void Validate_ValidSplitWithEndIndex_Succeeds() + { + var config = CreateConfig( + new AasIdExtractionRule + { + Strategy = ExtractionStrategy.Split, + Pattern = "/", + Index = 5, + EndIndex = 6 + }); + + var result = _sut.Validate(null, config); + + Assert.True(result.Succeeded); + } +} diff --git a/source/AAS.TwinEngine.DataEngine.UnitTests/Infrastructure/Providers/TemplateProvider/Services/ShellTemplateMappingProviderTests.cs b/source/AAS.TwinEngine.DataEngine.UnitTests/Infrastructure/Providers/TemplateProvider/Services/ShellTemplateMappingProviderTests.cs index e986c541..3384c85d 100644 --- a/source/AAS.TwinEngine.DataEngine.UnitTests/Infrastructure/Providers/TemplateProvider/Services/ShellTemplateMappingProviderTests.cs +++ b/source/AAS.TwinEngine.DataEngine.UnitTests/Infrastructure/Providers/TemplateProvider/Services/ShellTemplateMappingProviderTests.cs @@ -13,183 +13,490 @@ namespace AAS.TwinEngine.DataEngine.UnitTests.Infrastructure.Providers.TemplateP public class ShellTemplateMappingProviderTests { private readonly ILogger _logger = Substitute.For>(); - private readonly IOptions _options = Substitute.For>(); - private ShellTemplateMappingProvider _sut; - public ShellTemplateMappingProviderTests() + private ShellTemplateMappingProvider CreateSut( + IList rules, + IList? shellMappings = null) { - var settings = new TemplateManagementConfig + var options = Substitute.For>(); + var config = new TemplateManagementConfig { TemplateMappingRules = new TemplateMappingRules { - ShellTemplateMappings = - [ - new ShellTemplateMappings - { - Pattern = ["shell1"], - TemplateId = "template1" - }, - new ShellTemplateMappings - { - Pattern = ["shell2"], - TemplateId = "template2" - } - ], - AasIdExtractionRules = - [ - new AasIdExtractionRules - { - Pattern = ".*", - Index = 3, - Separator = ":" - }, - new AasIdExtractionRules - { - Pattern = ".*", - Index = 2, - Separator = "-" - } - ] + ShellTemplateMappings = shellMappings ?? + [ + new ShellTemplateMappings { Pattern = [".*"], TemplateId = "default-template" } + ], + AasIdExtractionRules = rules } }; + options.Value.Returns(config); + return new ShellTemplateMappingProvider(_logger, options); + } + + // ── Regex Strategy: Single-segment extraction ── - _options.Value.Returns(settings); - _sut = new ShellTemplateMappingProvider(_logger, _options); + [Fact] + public void GetProductIdFromRule_Regex_SingleSegment_ExtractsCorrectly() + { + var sut = CreateSut( + [ + new AasIdExtractionRule + { + Strategy = ExtractionStrategy.Regex, + Pattern = @"^https?://[^/]+/ids/submodel/([^/]+)(?:/|$)", + Index = 1 + } + ]); + + var result = sut.GetProductIdFromRule("https://wago.com/ids/submodel/2000-2201/ContactInformation"); + Assert.Equal("2000-2201", result); } + // ── Regex Strategy: Multi-segment extraction (two parts) ── + [Fact] - public void GetTemplateId_ValidInput_MatchesFirstRule_ReturnsTemplate1() + public void GetProductIdFromRule_Regex_MultiSegment_TwoParts_ExtractsCorrectly() { - var result = _sut.GetTemplateId("one:two:shell1:four"); - Assert.Equal("template1", result); + var sut = CreateSut( + [ + new AasIdExtractionRule + { + Strategy = ExtractionStrategy.Regex, + Pattern = @"^https?://[^/]+/ids/submodel/([^/]+/[^/]+)(?:/|$)", + Index = 1, + ValidationPattern = @"^[0-9\-/]+$" + } + ]); + + var result = sut.GetProductIdFromRule("https://wago.com/ids/submodel/2000-2201/353-000/ContactInformation"); + Assert.Equal("2000-2201/353-000", result); } + // ── Regex Strategy: Multi-segment extraction (three parts) ── + [Fact] - public void GetTemplateId_ValidInput_MatchesSecondRule_ReturnsTemplate2() + public void GetProductIdFromRule_Regex_MultiSegment_ThreeParts_ExtractsCorrectly() { - var result = _sut.GetTemplateId("part1-shell2-part3"); - Assert.Equal("template2", result); + var sut = CreateSut( + [ + new AasIdExtractionRule + { + Strategy = ExtractionStrategy.Regex, + Pattern = @"^https?://[^/]+/ids/submodel/([^/]+/[^/]+/[^/]+)(?:/|$)", + Index = 1, + ValidationPattern = @"^[0-9\-/v]+$" + } + ]); + + var result = sut.GetProductIdFromRule("https://wago.com/ids/submodel/2000-2201/353-000/v2/ContactInformation"); + Assert.Equal("2000-2201/353-000/v2", result); } + // ── Regex Strategy: Validation pattern rejects — falls through ── + [Fact] - public void GetTemplateId_ValidInput_MultipleMatches_ReturnsFirstMatchingTemplate() + public void GetProductIdFromRule_Regex_ValidationFails_FallsToNextRule() { - var result = _sut.GetTemplateId("x:y:shell2:z"); - Assert.Equal("template2", result); + var sut = CreateSut( + [ + new AasIdExtractionRule + { + Strategy = ExtractionStrategy.Regex, + Pattern = @"^https?://[^/]+/ids/submodel/([^/]+/[^/]+)(?:/|$)", + Index = 1, + ValidationPattern = @"^[0-9\-/]+$", + Description = "Multi-segment (digits only)" + }, + new AasIdExtractionRule + { + Strategy = ExtractionStrategy.Regex, + Pattern = @"^https?://[^/]+/ids/submodel/([^/]+)(?:/|$)", + Index = 1, + ValidationPattern = @"^[a-zA-Z0-9\-]+$", + Description = "Single-segment" + } + ]); + + // "2000-2201/ContactInformation" matches first regex, extracts "2000-2201/ContactInformation" + // but validation "^[0-9\-/]+$" fails (letters in "ContactInformation") + // Falls to second regex which extracts "2000-2201" and validates OK + var result = sut.GetProductIdFromRule("https://wago.com/ids/submodel/2000-2201/ContactInformation"); + Assert.Equal("2000-2201", result); } + // ── Regex Strategy: No match at all ── + [Fact] - public void GetTemplateId_InputDoesNotContainEnoughParts_SkipsRule() + public void GetProductIdFromRule_Regex_NoMatch_ThrowsResourceNotFoundException() { - const string AasId = "x:y"; - Assert.Throws(() => _sut.GetTemplateId(AasId)); + var sut = CreateSut( + [ + new AasIdExtractionRule + { + Strategy = ExtractionStrategy.Regex, + Pattern = @"^https?://[^/]+/ids/submodel/([^/]+)(?:/|$)", + Index = 1 + } + ]); + + Assert.Throws(() => sut.GetProductIdFromRule("random-garbage")); } + // ── Regex Strategy: Invalid capture group index ── + [Fact] - public void GetTemplateId_ValidFormatButNoPatternMatch_ThrowsResourceNotFoundException() + public void GetProductIdFromRule_Regex_InvalidGroupIndex_SkipsRule() { - const string AasId = "one:two:nomatch:four"; - Assert.Throws(() => _sut.GetTemplateId(AasId)); + var sut = CreateSut( + [ + new AasIdExtractionRule + { + Strategy = ExtractionStrategy.Regex, + Pattern = @"^https?://[^/]+/ids/submodel/([^/]+)(?:/|$)", + Index = 5 // only 1 capture group exists + } + ]); + + Assert.Throws(() => + sut.GetProductIdFromRule("https://wago.com/ids/submodel/2000-2201/CI")); } + // ── Split Strategy: Single segment (no EndIndex = old IndexSplit) ── + [Fact] - public void GetTemplateId_EmptyAasIdentifier_ThrowsResourceNotFoundException() + public void GetProductIdFromRule_Split_SingleSegment_ExtractsCorrectly() { - const string AasId = ""; - Assert.Throws(() => _sut.GetTemplateId(AasId)); + var sut = CreateSut( + [ + new AasIdExtractionRule + { + Strategy = ExtractionStrategy.Split, + Pattern = "/", + Index = 6 + } + ]); + + // Split by "/": ["https:", "", "wago.com", "ids", "submodel", "2000-2201", "ContactInformation"] + // indices: 1 2 3 4 5 6 7 + var result = sut.GetProductIdFromRule("https://wago.com/ids/submodel/2000-2201/ContactInformation"); + Assert.Equal("2000-2201", result); + } + + // ── Split Strategy: Range (with EndIndex = old RangeSplit) ── + + [Fact] + public void GetProductIdFromRule_Split_Range_ExtractsMultipleSegments() + { + var sut = CreateSut( + [ + new AasIdExtractionRule + { + Strategy = ExtractionStrategy.Split, + Pattern = "/", + Index = 6, + EndIndex = 7 + } + ]); + + // Split: ["https:", "", "wago.com", "ids", "submodel", "2000-2201", "353-000", "ContactInformation"] + // 1 2 3 4 5 6 7 8 + var result = sut.GetProductIdFromRule("https://wago.com/ids/submodel/2000-2201/353-000/ContactInformation"); + Assert.Equal("2000-2201/353-000", result); + } + + // ── Split Strategy: Out of bounds ── + + [Fact] + public void GetProductIdFromRule_Split_IndexOutOfBounds_SkipsRule() + { + var sut = CreateSut( + [ + new AasIdExtractionRule + { + Strategy = ExtractionStrategy.Split, + Pattern = "/", + Index = 99 + } + ]); + + Assert.Throws(() => + sut.GetProductIdFromRule("https://wago.com/ids/submodel/2000-2201/CI")); } + // ── Split Strategy: EndIndex out of bounds ── + + [Fact] + public void GetProductIdFromRule_Split_EndIndexOutOfBounds_SkipsRule() + { + var sut = CreateSut( + [ + new AasIdExtractionRule + { + Strategy = ExtractionStrategy.Split, + Pattern = "/", + Index = 6, + EndIndex = 99 + } + ]); + + Assert.Throws(() => + sut.GetProductIdFromRule("https://wago.com/ids/submodel/2000-2201/CI")); + } + + // ── Split Strategy: Validation pattern rejects ── + + [Fact] + public void GetProductIdFromRule_Split_ValidationFails_SkipsRule() + { + var sut = CreateSut( + [ + new AasIdExtractionRule + { + Strategy = ExtractionStrategy.Split, + Pattern = "/", + Index = 6, + ValidationPattern = @"^[0-9]+$" // digits only + } + ]); + + // Segment 6 = "2000-2201" which contains hyphens → fails validation + Assert.Throws(() => + sut.GetProductIdFromRule("https://wago.com/ids/submodel/2000-2201/CI")); + } + + // ── First rule matches — stops (does not try second) ── + + [Fact] + public void GetProductIdFromRule_FirstRuleMatches_StopsImmediately() + { + var sut = CreateSut( + [ + new AasIdExtractionRule + { + Strategy = ExtractionStrategy.Regex, + Pattern = @"^https?://[^/]+/ids/submodel/([^/]+/[^/]+)(?:/|$)", + Index = 1, + ValidationPattern = @"^[0-9\-/]+$" + }, + new AasIdExtractionRule + { + Strategy = ExtractionStrategy.Regex, + Pattern = @"^https?://[^/]+/ids/submodel/([^/]+)(?:/|$)", + Index = 1, + ValidationPattern = @"^[0-9\-]+$" + } + ]); + + var result = sut.GetProductIdFromRule("https://wago.com/ids/submodel/2000-2201/353-000/ContactInformation"); + Assert.Equal("2000-2201/353-000", result); + } + + // ── Mixed strategies: Regex then Split fallback ── + + [Fact] + public void GetProductIdFromRule_RegexFails_FallsToSplit() + { + var sut = CreateSut( + [ + new AasIdExtractionRule + { + Strategy = ExtractionStrategy.Regex, + Pattern = @"^urn:([^:]+)$", + Index = 1, + ValidationPattern = @"^[a-z]+$" + }, + new AasIdExtractionRule + { + Strategy = ExtractionStrategy.Split, + Pattern = "/", + Index = 6, + ValidationPattern = @"^[0-9\-]+$" + } + ]); + + var result = sut.GetProductIdFromRule("https://wago.com/ids/submodel/2000-2201/ContactInformation"); + Assert.Equal("2000-2201", result); + } + + // ── Single rule without ValidationPattern works ── + + [Fact] + public void GetProductIdFromRule_SingleRule_NoValidationPattern_Works() + { + var sut = CreateSut( + [ + new AasIdExtractionRule + { + Strategy = ExtractionStrategy.Split, + Pattern = "/", + Index = 6 + // No ValidationPattern — allowed for single rule + } + ]); + + var result = sut.GetProductIdFromRule("https://wago.com/ids/submodel/2000-2201/ContactInformation"); + Assert.Equal("2000-2201", result); + } + + // ── Split with colon separator ── + + [Fact] + public void GetProductIdFromRule_Split_ColonSeparator_ExtractsCorrectly() + { + var sut = CreateSut( + [ + new AasIdExtractionRule + { + Strategy = ExtractionStrategy.Split, + Pattern = ":", + Index = 3 + } + ]); + + var result = sut.GetProductIdFromRule("one:two:shell1:four"); + Assert.Equal("shell1", result); + } + + // ── GetTemplateId: end-to-end with Regex extraction + template matching ── + + [Fact] + public void GetTemplateId_RegexExtraction_MatchesTemplate() + { + var sut = CreateSut( + rules: + [ + new AasIdExtractionRule + { + Strategy = ExtractionStrategy.Regex, + Pattern = @"^https?://[^/]+/ids/submodel/([^/]+)(?:/|$)", + Index = 1 + } + ], + shellMappings: + [ + new ShellTemplateMappings { Pattern = ["2000.*"], TemplateId = "wago-template" } + ]); + + var result = sut.GetTemplateId("https://wago.com/ids/submodel/2000-2201/ContactInformation"); + Assert.Equal("wago-template", result); + } + + // ── GetTemplateId: case-insensitive template matching ── + + [Fact] + public void GetTemplateId_CaseInsensitive_MatchesTemplate() + { + var sut = CreateSut( + rules: + [ + new AasIdExtractionRule + { + Strategy = ExtractionStrategy.Split, + Pattern = ":", + Index = 3 + } + ], + shellMappings: + [ + new ShellTemplateMappings { Pattern = ["SHELL1"], TemplateId = "template1" } + ]); + + var result = sut.GetTemplateId("A:B:shell1:D"); + Assert.Equal("template1", result); + } + + // ── GetTemplateId: no matching template ── + + [Fact] + public void GetTemplateId_NoMatchingTemplate_ThrowsResourceNotFoundException() + { + var sut = CreateSut( + rules: + [ + new AasIdExtractionRule + { + Strategy = ExtractionStrategy.Split, + Pattern = ":", + Index = 3 + } + ], + shellMappings: + [ + new ShellTemplateMappings { Pattern = ["nomatch"], TemplateId = "template1" } + ]); + + Assert.Throws(() => sut.GetTemplateId("A:B:shell1:D")); + } + + // ── Constructor: null logger ── + [Fact] public void Constructor_NullLogger_ThrowsArgumentNullException() { + var options = Substitute.For>(); + options.Value.Returns(new TemplateManagementConfig()); + var ex = Assert.Throws(() => - new ShellTemplateMappingProvider(null!, _options)); + new ShellTemplateMappingProvider(null!, options)); Assert.Equal("logger", ex.ParamName); } + // ── Constructor: null ShellTemplateMappings ── + [Fact] public void Constructor_NullShellTemplateMappings_ThrowsArgumentException() { - var config = new TemplateManagementConfig + var options = Substitute.For>(); + options.Value.Returns(new TemplateManagementConfig { TemplateMappingRules = new TemplateMappingRules { ShellTemplateMappings = null!, AasIdExtractionRules = [] } - }; - - _options.Value.Returns(config); + }); var ex = Assert.Throws(() => - new ShellTemplateMappingProvider(_logger, _options)); - Assert.Contains("ShellTemplateMappings are missing in TemplateMappingRules", ex.Message, StringComparison.Ordinal); + new ShellTemplateMappingProvider(_logger, options)); + Assert.Contains("ShellTemplateMappings are missing", ex.Message, StringComparison.Ordinal); } + // ── Constructor: null AasIdExtractionRules ── + [Fact] public void Constructor_NullAasIdExtractionRules_ThrowsArgumentException() { - var config = new TemplateManagementConfig + var options = Substitute.For>(); + options.Value.Returns(new TemplateManagementConfig { TemplateMappingRules = new TemplateMappingRules { ShellTemplateMappings = [], AasIdExtractionRules = null! } - }; - - _options.Value.Returns(config); + }); var ex = Assert.Throws(() => - new ShellTemplateMappingProvider(_logger, _options)); + new ShellTemplateMappingProvider(_logger, options)); Assert.Contains("AasIdExtractionRules are missing", ex.Message, StringComparison.Ordinal); } - [Fact] - public void GetTemplateId_WithExactMatchButCaseInsensitive_ReturnsMatch() - { - var result = _sut.GetTemplateId("A:B:ShElL1:D"); - Assert.Equal("template1", result); - } + // ── Empty identifier ── [Fact] - public void GetTemplateId_PatternIsRegex_WildcardMatch_ReturnsMatch() + public void GetProductIdFromRule_EmptyIdentifier_ThrowsResourceNotFoundException() { - var config = new TemplateManagementConfig - { - TemplateMappingRules = new TemplateMappingRules + var sut = CreateSut( + [ + new AasIdExtractionRule { - ShellTemplateMappings = - [ - new ShellTemplateMappings { Pattern = ["shell.*"], TemplateId = "template-wild" } - ], - AasIdExtractionRules = - [ - new AasIdExtractionRules { Pattern = ".*", Index = 2, Separator = "-" } - ] + Strategy = ExtractionStrategy.Split, + Pattern = "/", + Index = 6 } - }; - - _options.Value.Returns(config); - _sut = new ShellTemplateMappingProvider(_logger, _options); - - var result = _sut.GetTemplateId("aaa-shellX-ccc"); - Assert.Equal("template-wild", result); - } - - [Fact] - public void GetTemplateId_NoMatchingTemplate_LogsError() - { - const string AasId = "aaa-bbb-ccc"; - - Assert.Throws(() => _sut.GetTemplateId(AasId)); + ]); - _logger.Received().Log( - LogLevel.Error, - Arg.Any(), - Arg.Is(o => o.ToString()!.Contains("No matching template found for shell: aaa-bbb-ccc")), - null, - Arg.Any>() - ); + Assert.Throws(() => sut.GetProductIdFromRule("")); } } diff --git a/source/AAS.TwinEngine.DataEngine.UnitTests/ServiceConfiguration/ConfigurationMigration/ConfigurationBackwardCompatibilityE2ETests.cs b/source/AAS.TwinEngine.DataEngine.UnitTests/ServiceConfiguration/ConfigurationMigration/ConfigurationBackwardCompatibilityE2ETests.cs index ba934861..0d63d018 100644 --- a/source/AAS.TwinEngine.DataEngine.UnitTests/ServiceConfiguration/ConfigurationMigration/ConfigurationBackwardCompatibilityE2ETests.cs +++ b/source/AAS.TwinEngine.DataEngine.UnitTests/ServiceConfiguration/ConfigurationMigration/ConfigurationBackwardCompatibilityE2ETests.cs @@ -329,9 +329,9 @@ private static ServiceProvider BuildServiceProvider(IConfiguration configuration ["TemplateManagement:TemplateMappingRules:SubmodelTemplateMappings:0:pattern:0"] = "Nameplate", ["TemplateManagement:TemplateMappingRules:ShellTemplateMappings:0:templateId"] = "https://mm-software.com/aas/aasTemplate", ["TemplateManagement:TemplateMappingRules:ShellTemplateMappings:0:pattern:0"] = "", - ["TemplateManagement:TemplateMappingRules:AasIdExtractionRules:0:Pattern"] = "Regex", + ["TemplateManagement:TemplateMappingRules:AasIdExtractionRules:0:Strategy"] = "Split", + ["TemplateManagement:TemplateMappingRules:AasIdExtractionRules:0:Pattern"] = "/", ["TemplateManagement:TemplateMappingRules:AasIdExtractionRules:0:Index"] = "6", - ["TemplateManagement:TemplateMappingRules:AasIdExtractionRules:0:Separator"] = "/", ["TemplateManagement:AasTemplateRepository:Name"] = "AasTemplateRepository", ["TemplateManagement:AasTemplateRepository:baseUrl"] = "http://localhost:8081", ["TemplateManagement:AasTemplateRepository:headerMappings:0:source"] = "Authorization", diff --git a/source/AAS.TwinEngine.DataEngine.UnitTests/ServiceConfiguration/ConfigurationMigration/LegacyTemplateManagementConfigAdapterTests.cs b/source/AAS.TwinEngine.DataEngine.UnitTests/ServiceConfiguration/ConfigurationMigration/LegacyTemplateManagementConfigAdapterTests.cs index 83dbf9bc..34ba80ee 100644 --- a/source/AAS.TwinEngine.DataEngine.UnitTests/ServiceConfiguration/ConfigurationMigration/LegacyTemplateManagementConfigAdapterTests.cs +++ b/source/AAS.TwinEngine.DataEngine.UnitTests/ServiceConfiguration/ConfigurationMigration/LegacyTemplateManagementConfigAdapterTests.cs @@ -1,5 +1,6 @@ using AAS.TwinEngine.DataEngine.ApplicationLogic.Services.Plugin.Config; using AAS.TwinEngine.DataEngine.Infrastructure.Configuration.LegacyV1; +using AAS.TwinEngine.DataEngine.Infrastructure.Providers.TemplateProvider.Config; using AAS.TwinEngine.DataEngine.ServiceConfiguration.Config; using Microsoft.Extensions.Configuration; @@ -157,9 +158,9 @@ public void Configure_WithV1Config_MapsAasIdExtractionRules() adapter.Configure(options); Assert.Single(options.TemplateMappingRules.AasIdExtractionRules); - Assert.Equal("Regex", options.TemplateMappingRules.AasIdExtractionRules[0].Pattern); + Assert.Equal("/", options.TemplateMappingRules.AasIdExtractionRules[0].Pattern); Assert.Equal(6, options.TemplateMappingRules.AasIdExtractionRules[0].Index); - Assert.Equal("/", options.TemplateMappingRules.AasIdExtractionRules[0].Separator); + Assert.Equal(ExtractionStrategy.Split, options.TemplateMappingRules.AasIdExtractionRules[0].Strategy); } [Fact] @@ -192,7 +193,7 @@ private static IConfiguration BuildV1Config() ["TemplateMappingRules:SubmodelTemplateMappings:1:pattern:0"] = "CarbonFootprint", ["TemplateMappingRules:ShellTemplateMappings:0:templateId"] = "https://mm-software.com/aas/aasTemplate", ["TemplateMappingRules:ShellTemplateMappings:0:pattern:0"] = "", - ["TemplateMappingRules:AasIdExtractionRules:0:Pattern"] = "Regex", + ["TemplateMappingRules:AasIdExtractionRules:0:Pattern"] = "Split", ["TemplateMappingRules:AasIdExtractionRules:0:Index"] = "6", ["TemplateMappingRules:AasIdExtractionRules:0:Separator"] = "/", ["HttpRetryPolicyOptions:TemplateProvider:MaxRetryAttempts"] = "3", diff --git a/source/AAS.TwinEngine.DataEngine.UnitTests/ServiceConfiguration/ConfigurationMigration/V2DirectBindingTests.cs b/source/AAS.TwinEngine.DataEngine.UnitTests/ServiceConfiguration/ConfigurationMigration/V2DirectBindingTests.cs index c2c5c284..50f0c607 100644 --- a/source/AAS.TwinEngine.DataEngine.UnitTests/ServiceConfiguration/ConfigurationMigration/V2DirectBindingTests.cs +++ b/source/AAS.TwinEngine.DataEngine.UnitTests/ServiceConfiguration/ConfigurationMigration/V2DirectBindingTests.cs @@ -213,9 +213,9 @@ private static IConfiguration BuildV2Config() ["TemplateManagement:TemplateMappingRules:SubmodelTemplateMappings:0:pattern:0"] = "Nameplate", ["TemplateManagement:TemplateMappingRules:ShellTemplateMappings:0:templateId"] = "https://mm-software.com/aas/aasTemplate", ["TemplateManagement:TemplateMappingRules:ShellTemplateMappings:0:pattern:0"] = "", - ["TemplateManagement:TemplateMappingRules:AasIdExtractionRules:0:Pattern"] = "Regex", + ["TemplateManagement:TemplateMappingRules:AasIdExtractionRules:0:Strategy"] = "Split", + ["TemplateManagement:TemplateMappingRules:AasIdExtractionRules:0:Pattern"] = "/", ["TemplateManagement:TemplateMappingRules:AasIdExtractionRules:0:Index"] = "6", - ["TemplateManagement:TemplateMappingRules:AasIdExtractionRules:0:Separator"] = "/", ["TemplateManagement:AasTemplateRepository:Name"] = "AasTemplateRepository", ["TemplateManagement:AasTemplateRepository:baseUrl"] = "http://localhost:8081", ["TemplateManagement:AasTemplateRepository:headerMappings:0:source"] = "Authorization", diff --git a/source/AAS.TwinEngine.DataEngine/Infrastructure/Configuration/LegacyV1/LegacyTemplateManagementConfigAdapter.cs b/source/AAS.TwinEngine.DataEngine/Infrastructure/Configuration/LegacyV1/LegacyTemplateManagementConfigAdapter.cs index 132a0ce2..3d49fa80 100644 --- a/source/AAS.TwinEngine.DataEngine/Infrastructure/Configuration/LegacyV1/LegacyTemplateManagementConfigAdapter.cs +++ b/source/AAS.TwinEngine.DataEngine/Infrastructure/Configuration/LegacyV1/LegacyTemplateManagementConfigAdapter.cs @@ -37,6 +37,7 @@ public void Configure(TemplateManagementConfig options) var mappingRules = _configuration.GetSection(TemplateMappingRules.Section).Get(); if (mappingRules != null) { + RemapLegacyExtractionRules(mappingRules); options.TemplateMappingRules = mappingRules; } @@ -81,4 +82,33 @@ public void Configure(TemplateManagementConfig options) }; } } + + /// + /// V1 used { "Pattern": "Split", "Separator": "/", "Index": 6 } + /// where Pattern held the strategy name and Separator held the actual delimiter. + /// Detects this by checking for a "Separator" key in the raw config and remaps accordingly. + /// + private void RemapLegacyExtractionRules(TemplateMappingRules rules) + { + var rulesSection = _configuration.GetSection($"{TemplateMappingRules.Section}:AasIdExtractionRules"); + + for (var i = 0; i < rules.AasIdExtractionRules.Count; i++) + { + var separator = rulesSection.GetSection(i.ToString())["Separator"]; + + if (string.IsNullOrEmpty(separator)) + { + continue; + } + + var rule = rules.AasIdExtractionRules[i]; + // In old config, "Pattern" was the strategy name (e.g. "Split") + if (Enum.TryParse(rule.Pattern, ignoreCase: true, out var strategy)) + { + rule.Strategy = strategy; + } + + rule.Pattern = separator; + } + } } diff --git a/source/AAS.TwinEngine.DataEngine/Infrastructure/Providers/TemplateProvider/Config/TemplateMappingRules.cs b/source/AAS.TwinEngine.DataEngine/Infrastructure/Providers/TemplateProvider/Config/TemplateMappingRules.cs index ba1f92df..9c5e276b 100644 --- a/source/AAS.TwinEngine.DataEngine/Infrastructure/Providers/TemplateProvider/Config/TemplateMappingRules.cs +++ b/source/AAS.TwinEngine.DataEngine/Infrastructure/Providers/TemplateProvider/Config/TemplateMappingRules.cs @@ -1,11 +1,13 @@ -namespace AAS.TwinEngine.DataEngine.Infrastructure.Providers.TemplateProvider.Config; +using System.ComponentModel.DataAnnotations; + +namespace AAS.TwinEngine.DataEngine.Infrastructure.Providers.TemplateProvider.Config; public class TemplateMappingRules { public const string Section = "TemplateMappingRules"; public IList SubmodelTemplateMappings { get; init; } = []; public IList ShellTemplateMappings { get; init; } = []; - public IList AasIdExtractionRules { get; init; } = []; + public IList AasIdExtractionRules { get; init; } = []; } public class SubmodelTemplateMappings @@ -20,9 +22,26 @@ public class ShellTemplateMappings public IList Pattern { get; init; } = []; } -public class AasIdExtractionRules +public enum ExtractionStrategy { + Regex, + Split +} + +public class AasIdExtractionRule +{ + [Required] + public ExtractionStrategy Strategy { get; set; } + + [Required] public string Pattern { get; set; } = string.Empty; + + [Required] public int Index { get; set; } - public string Separator { get; set; } = string.Empty; + + public int? EndIndex { get; set; } + + public string? ValidationPattern { get; set; } + + public string? Description { get; set; } } diff --git a/source/AAS.TwinEngine.DataEngine/Infrastructure/Providers/TemplateProvider/Config/TemplateMappingRulesValidator.cs b/source/AAS.TwinEngine.DataEngine/Infrastructure/Providers/TemplateProvider/Config/TemplateMappingRulesValidator.cs new file mode 100644 index 00000000..43b842af --- /dev/null +++ b/source/AAS.TwinEngine.DataEngine/Infrastructure/Providers/TemplateProvider/Config/TemplateMappingRulesValidator.cs @@ -0,0 +1,82 @@ +using System.Text.RegularExpressions; + +using AAS.TwinEngine.DataEngine.ServiceConfiguration.Config; + +using Microsoft.Extensions.Options; + +namespace AAS.TwinEngine.DataEngine.Infrastructure.Providers.TemplateProvider.Config; + +public class TemplateMappingRulesValidator : IValidateOptions +{ + public ValidateOptionsResult Validate(string? name, TemplateManagementConfig options) + { + ArgumentNullException.ThrowIfNull(options); + + var rules = options.TemplateMappingRules.AasIdExtractionRules; + + if (rules == null || rules.Count == 0) + { + return ValidateOptionsResult.Fail("At least one AasIdExtractionRule is required."); + } + + var requireValidationPattern = rules.Count > 1; + + for (var i = 0; i < rules.Count; i++) + { + var rule = rules[i]; + var label = !string.IsNullOrEmpty(rule.Description) ? rule.Description : $"Rule[{i}]"; + + if (string.IsNullOrWhiteSpace(rule.Pattern)) + { + return ValidateOptionsResult.Fail($"AasIdExtractionRules: {label} has an empty Pattern."); + } + + if (rule.Index < 1) + { + return ValidateOptionsResult.Fail($"AasIdExtractionRules: {label} Index must be >= 1."); + } + + if (rule.Strategy == ExtractionStrategy.Regex) + { + if (!TryCompileRegex(rule.Pattern, out var error)) + { + return ValidateOptionsResult.Fail($"AasIdExtractionRules: {label} has an invalid regex Pattern: {error}"); + } + } + + if (rule.Strategy == ExtractionStrategy.Split && rule.EndIndex.HasValue && rule.EndIndex.Value < rule.Index) + { + return ValidateOptionsResult.Fail($"AasIdExtractionRules: {label} EndIndex ({rule.EndIndex}) must be >= Index ({rule.Index})."); + } + + if (requireValidationPattern && string.IsNullOrWhiteSpace(rule.ValidationPattern)) + { + return ValidateOptionsResult.Fail( + $"AasIdExtractionRules: {label} is missing ValidationPattern. " + + "ValidationPattern is required when multiple extraction rules are configured."); + } + + if (!string.IsNullOrWhiteSpace(rule.ValidationPattern) && !TryCompileRegex(rule.ValidationPattern, out var vpError)) + { + return ValidateOptionsResult.Fail($"AasIdExtractionRules: {label} has an invalid ValidationPattern: {vpError}"); + } + } + + return ValidateOptionsResult.Success; + } + + private static bool TryCompileRegex(string pattern, out string? error) + { + try + { + _ = new Regex(pattern, RegexOptions.None, TimeSpan.FromSeconds(2)); + error = null; + return true; + } + catch (ArgumentException ex) + { + error = ex.Message; + return false; + } + } +} diff --git a/source/AAS.TwinEngine.DataEngine/Infrastructure/Providers/TemplateProvider/Services/ShellTemplateMappingProvider.cs b/source/AAS.TwinEngine.DataEngine/Infrastructure/Providers/TemplateProvider/Services/ShellTemplateMappingProvider.cs index 101dee1f..b26ec615 100644 --- a/source/AAS.TwinEngine.DataEngine/Infrastructure/Providers/TemplateProvider/Services/ShellTemplateMappingProvider.cs +++ b/source/AAS.TwinEngine.DataEngine/Infrastructure/Providers/TemplateProvider/Services/ShellTemplateMappingProvider.cs @@ -13,7 +13,7 @@ public class ShellTemplateMappingProvider(ILogger { private readonly ILogger _logger = logger ?? throw new ArgumentNullException(nameof(logger)); private readonly IList _shellTemplateMappings = options.Value.TemplateMappingRules.ShellTemplateMappings ?? throw new ArgumentException("ShellTemplateMappings are missing in TemplateMappingRules"); - private readonly IList _aasIdExtractionRules = options.Value.TemplateMappingRules.AasIdExtractionRules ?? throw new ArgumentException("AasIdExtractionRules are missing in TemplateMappingRules"); + private readonly IList _aasIdExtractionRules = options.Value.TemplateMappingRules.AasIdExtractionRules ?? throw new ArgumentException("AasIdExtractionRules are missing in TemplateMappingRules"); private readonly TimeSpan _regexTimeout = TimeSpan.FromSeconds(2); public string? GetTemplateId(string aasIdentifier) @@ -36,22 +36,59 @@ public class ShellTemplateMappingProvider(ILogger public string GetProductIdFromRule(string aasIdentifier) { - var productId = _aasIdExtractionRules - .Select(rule => new - { - Rule = rule, - Parts = aasIdentifier?.Split(rule.Separator) - }) - .Where(x => x.Parts is { Length: >= 1 } && x.Rule.Index > 0 && x.Parts.Length >= x.Rule.Index) - .Select(x => x.Parts![x.Rule.Index - 1]) - .FirstOrDefault(extractedId => !string.Equals(extractedId, aasIdentifier, StringComparison.Ordinal)); - - if (!string.IsNullOrEmpty(productId)) + foreach (var rule in _aasIdExtractionRules) { - return productId; + var extracted = rule.Strategy switch + { + ExtractionStrategy.Regex => TryExtractWithRegex(aasIdentifier, rule), + ExtractionStrategy.Split => TryExtractWithSplit(aasIdentifier, rule), + _ => null + }; + + if (string.IsNullOrEmpty(extracted)) + { + continue; + } + + if (!string.IsNullOrEmpty(rule.ValidationPattern)) + { + if (!Regex.IsMatch(extracted, rule.ValidationPattern, RegexOptions.None, _regexTimeout)) + { + continue; + } + } + + return extracted; } _logger.LogError("ProductId could not be extracted from the provided aas Identifier."); throw new ResourceNotFoundException(); } + + private string? TryExtractWithRegex(string input, AasIdExtractionRule rule) + { + var match = Regex.Match(input, rule.Pattern, RegexOptions.None, _regexTimeout); + if (!match.Success || rule.Index < 1 || rule.Index >= match.Groups.Count) + { + return null; + } + + var value = match.Groups[rule.Index].Value; + return string.IsNullOrEmpty(value) ? null : value; + } + + private static string? TryExtractWithSplit(string input, AasIdExtractionRule rule) + { + var parts = input.Split(rule.Pattern); + var startIndex = rule.Index - 1; // convert 1-based to 0-based + var endIndex = (rule.EndIndex ?? rule.Index) - 1; + + if (startIndex < 0 || endIndex < startIndex || endIndex >= parts.Length) + { + return null; + } + + var extracted = string.Join(rule.Pattern, parts[startIndex..(endIndex + 1)]); + return string.IsNullOrEmpty(extracted) ? null : extracted; + } } diff --git a/source/AAS.TwinEngine.DataEngine/ServiceConfiguration/InfrastructureDependencyInjectionExtensions.cs b/source/AAS.TwinEngine.DataEngine/ServiceConfiguration/InfrastructureDependencyInjectionExtensions.cs index 913616f4..f4ecc774 100644 --- a/source/AAS.TwinEngine.DataEngine/ServiceConfiguration/InfrastructureDependencyInjectionExtensions.cs +++ b/source/AAS.TwinEngine.DataEngine/ServiceConfiguration/InfrastructureDependencyInjectionExtensions.cs @@ -17,6 +17,7 @@ using AAS.TwinEngine.DataEngine.Infrastructure.Providers.PluginDataProvider.Helper; using AAS.TwinEngine.DataEngine.Infrastructure.Providers.PluginDataProvider.Services; using AAS.TwinEngine.DataEngine.Infrastructure.Providers.SubmodelRegistryProvider.Services; +using AAS.TwinEngine.DataEngine.Infrastructure.Providers.TemplateProvider.Config; using AAS.TwinEngine.DataEngine.Infrastructure.Providers.TemplateProvider.Services; using AAS.TwinEngine.DataEngine.Infrastructure.Shared; using AAS.TwinEngine.DataEngine.ServiceConfiguration.Config; @@ -56,6 +57,9 @@ public static void ConfigureInfrastructure(this IServiceCollection services, ICo // Normalizer: applies TemplateRepository shorthand to individual repository endpoints _ = services.AddSingleton, TemplateManagementConfigNormalizer>(); + // Validator: enforces AasIdExtractionRules constraints at startup + _ = services.AddSingleton, TemplateMappingRulesValidator>(); + // PluginsConfig: single registration via AddOptions to avoid double-binding of list properties _ = services.AddOptions() .Bind(configuration.GetSection(PluginsConfig.Section)) diff --git a/source/AAS.TwinEngine.DataEngine/appsettings.development.json b/source/AAS.TwinEngine.DataEngine/appsettings.development.json index d11834c2..c8bf178b 100644 --- a/source/AAS.TwinEngine.DataEngine/appsettings.development.json +++ b/source/AAS.TwinEngine.DataEngine/appsettings.development.json @@ -152,14 +152,26 @@ ], "AasIdExtractionRules": [ { - "Pattern": "Regex", - "Index": 3, - "Separator": ":" + "Strategy": "Regex", + "Pattern": "^https?://[^/]+/ids/submodel/([^/]+/[^/]+)(?:/|$)", + "Index": 1, + "ValidationPattern": "^[0-9\\-/]+$", + "Description": "Multi-segment IDs like '2000-2201/353-000'" }, { - "Pattern": "Regex", - "Index": 6, - "Separator": "/" + "Strategy": "Regex", + "Pattern": "^https?://[^/]+/ids/submodel/([^/]+)(?:/|$)", + "Index": 1, + "ValidationPattern": "^[0-9\\-]+$", + "Description": "Single-segment IDs like '2000-2201'" + }, + { + "Strategy": "Split", + "Pattern": "/", + "Index": 5, + "EndIndex": 6, + "ValidationPattern": "^[0-9\\-/]+$", + "Description": "Fallback: split by '/' and join segments 5-6" } ] }, @@ -207,7 +219,7 @@ }, "RegistrySettings": { "PreComputed": { - "Enabled": false, + "Enabled": true, "Schedule": "0 */3 * * * *" } } diff --git a/source/AAS.TwinEngine.DataEngine/appsettings.json b/source/AAS.TwinEngine.DataEngine/appsettings.json index 2913cd1b..a04f9369 100644 --- a/source/AAS.TwinEngine.DataEngine/appsettings.json +++ b/source/AAS.TwinEngine.DataEngine/appsettings.json @@ -108,7 +108,14 @@ "pattern": [ "" ] } ], - "AasIdExtractionRules": [] + "AasIdExtractionRules": [ + { + "Strategy": "Split", + "Pattern": "/", + "Index": 6, + "Description": "Default: split by '/' and take segment 6" + } + ] }, "Semantics": { "InternalSemanticId": "InternalSemanticId" @@ -131,7 +138,7 @@ }, "RegistrySettings": { "PreComputed": { - "Enabled": false, + "Enabled": true, "Schedule": "0 */3 * * * *" } } diff --git a/source/AAS.TwinEngine.Plugin.TestPlugin/Example/docker-compose.yml b/source/AAS.TwinEngine.Plugin.TestPlugin/Example/docker-compose.yml index 7dc61eb6..5a5e5a0e 100644 --- a/source/AAS.TwinEngine.Plugin.TestPlugin/Example/docker-compose.yml +++ b/source/AAS.TwinEngine.Plugin.TestPlugin/Example/docker-compose.yml @@ -53,9 +53,10 @@ services: - TemplateManagement__TemplateMappingRules__SubmodelTemplateMappings__2__pattern__0=CustomSubmodel - TemplateManagement__TemplateMappingRules__ShellTemplateMappings__0__templateId=https://mm-software.com/aas/aasTemplate - TemplateManagement__TemplateMappingRules__ShellTemplateMappings__0__pattern__0= - - TemplateManagement__TemplateMappingRules__AasIdExtractionRules__0__Pattern=Regex + - TemplateManagement__TemplateMappingRules__AasIdExtractionRules__0__Strategy=Split + - TemplateManagement__TemplateMappingRules__AasIdExtractionRules__0__Pattern=/ - TemplateManagement__TemplateMappingRules__AasIdExtractionRules__0__Index=6 - - TemplateManagement__TemplateMappingRules__AasIdExtractionRules__0__Separator=/ + - TemplateManagement__TemplateMappingRules__AasIdExtractionRules__0__Description=Default split by '/' segment 6 - TemplateManagement__TemplateRepository__Name=AasTemplateRepository - TemplateManagement__TemplateRepository__baseUrl=http://template-repository:8081 - TemplateManagement__TemplateRepository__headerMappings__0__source=Authorization From a2e6133c83009901b09acc7e8de5618756e1cb2f Mon Sep 17 00:00:00 2001 From: Kevalkumar Date: Mon, 20 Apr 2026 01:32:23 +0530 Subject: [PATCH 47/71] merge with parent branch --- example/docker-compose.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/example/docker-compose.yml b/example/docker-compose.yml index 8804424f..be95275e 100644 --- a/example/docker-compose.yml +++ b/example/docker-compose.yml @@ -55,7 +55,7 @@ services: - TemplateManagement__TemplateMappingRules__AasIdExtractionRules__0__Strategy=Split - TemplateManagement__TemplateMappingRules__AasIdExtractionRules__0__Pattern=/ - TemplateManagement__TemplateMappingRules__AasIdExtractionRules__0__Index=6 - TemplateManagement__TemplateMappingRules__AasIdExtractionRules__0__Description=Default split by '/' segment 6 + - TemplateManagement__TemplateMappingRules__AasIdExtractionRules__0__Description=Default split by '/' segment 6 - TemplateManagement__AasTemplateRepository__Name=AasTemplateRepository - TemplateManagement__AasTemplateRepository__baseUrl=http://template-repository:8081 - TemplateManagement__AasTemplateRepository__headerMappings__0__source=Authorization From d967891028a962129b3ffa208509c3f83e0ea165 Mon Sep 17 00:00:00 2001 From: Kevalkumar Date: Mon, 20 Apr 2026 19:39:45 +0530 Subject: [PATCH 48/71] Enhace var names in module tests --- .../ShellDescriptorControllerTests.cs | 12 +++------ .../AasRepositoryControllerTests.cs | 26 +++++++------------ .../SubmodelDescriptorControllerTests.cs | 10 ++----- .../SerializationControllerTests.cs | 10 ++----- .../SubmodelRepositoryControllerTests.cs | 12 +++------ .../LegacyTemplateManagementConfigAdapter.cs | 10 +++---- 6 files changed, 24 insertions(+), 56 deletions(-) diff --git a/source/AAS.TwinEngine.DataEngine.ModuleTests/Api/Services/AasRegistry/ShellDescriptorControllerTests.cs b/source/AAS.TwinEngine.DataEngine.ModuleTests/Api/Services/AasRegistry/ShellDescriptorControllerTests.cs index da2d0058..9ac48055 100644 --- a/source/AAS.TwinEngine.DataEngine.ModuleTests/Api/Services/AasRegistry/ShellDescriptorControllerTests.cs +++ b/source/AAS.TwinEngine.DataEngine.ModuleTests/Api/Services/AasRegistry/ShellDescriptorControllerTests.cs @@ -1,4 +1,4 @@ -using System.Net; +using System.Net; using System.Net.Http.Json; using System.Text; using System.Text.Json.Nodes; @@ -346,15 +346,9 @@ private static string EncodeBase64Url(string plainText) } } -public class ShellDescriptorControllerTests_V1Config : ShellDescriptorControllerTestsBase -{ - public ShellDescriptorControllerTests_V1Config() : base("v1-config") { } -} +public class ShellDescriptorControllerTestsV1Config() : ShellDescriptorControllerTestsBase("v1-config"); -public class ShellDescriptorControllerTests_V2Config : ShellDescriptorControllerTestsBase -{ - public ShellDescriptorControllerTests_V2Config() : base("v2-config") { } -} +public class ShellDescriptorControllerTestsV2Config() : ShellDescriptorControllerTestsBase("v2-config"); public class FakeHttpMessageHandler(Func> send) : HttpMessageHandler { diff --git a/source/AAS.TwinEngine.DataEngine.ModuleTests/Api/Services/AasRepository/AasRepositoryControllerTests.cs b/source/AAS.TwinEngine.DataEngine.ModuleTests/Api/Services/AasRepository/AasRepositoryControllerTests.cs index 4e32094b..f6b7dfcd 100644 --- a/source/AAS.TwinEngine.DataEngine.ModuleTests/Api/Services/AasRepository/AasRepositoryControllerTests.cs +++ b/source/AAS.TwinEngine.DataEngine.ModuleTests/Api/Services/AasRepository/AasRepositoryControllerTests.cs @@ -1,4 +1,4 @@ -using System.Net; +using System.Net; using System.Net.Http.Json; using System.Text; using System.Text.Json.Nodes; @@ -23,14 +23,14 @@ namespace AAS.TwinEngine.DataEngine.ModuleTests.Api.Services.AasRepository; -public abstract class AasRepositoryControllerTestsBase : IDisposable +public abstract class AasRepositoryControllerTests : IDisposable { private readonly ConfigTestFactory _factory; private readonly ITemplateProvider _mockTemplateProvider; private readonly HttpClient _client; private readonly ICreateClient _httpClientFactory; - protected AasRepositoryControllerTestsBase(string configDir) + protected AasRepositoryControllerTests(string configDir) { _mockTemplateProvider = Substitute.For(); var mockPluginManifestProvider = Substitute.For(); @@ -60,7 +60,7 @@ public void Dispose() public async Task GetShellByIdAsync_ReturnsOkAsync() { // Arrange - var aasIdentifier = "aHR0cHM6Ly9leGFtcGxlLmNvbS9pZHMvYWFzLzExNzBfMTE2MF8zMDUyXzY1NjgvdGVzdC9hYXM="; + const string AasIdentifier = "aHR0cHM6Ly9leGFtcGxlLmNvbS9pZHMvYWFzLzExNzBfMTE2MF8zMDUyXzY1NjgvdGVzdC9hYXM="; var mockShellTemplate = TestData.CreateShellTemplate(); var mockAssetInformationTemplate = TestData.CreateAssetInformationTemplate(); using var messageHandler = new FakeHttpMessageHandler((_, _) => Task.FromResult(new HttpResponseMessage @@ -80,7 +80,7 @@ public async Task GetShellByIdAsync_ReturnsOkAsync() _ = _mockTemplateProvider.GetAssetInformationTemplateAsync(Arg.Any(), Arg.Any()).Returns(mockAssetInformationTemplate); // Act - var response = await _client.GetAsync($"/shells/{aasIdentifier}"); + var response = await _client.GetAsync($"/shells/{AasIdentifier}"); // Assert Assert.Equal(HttpStatusCode.OK, response.StatusCode); @@ -93,7 +93,7 @@ public async Task GetShellByIdAsync_ReturnsOkAsync() var expectedShell = TestData.CreateShellResponse(); Assert.Equal(shellResponse, expectedShell); var productId = TestData.GetProductIdFromRule(shell.Submodels!.FirstOrDefault()?.Keys.FirstOrDefault()!.Value!, 5); - var expectedProductId = TestData.GetProductIdFromRule(aasIdentifier.DecodeBase64Url(), 6); + var expectedProductId = TestData.GetProductIdFromRule(AasIdentifier.DecodeBase64Url(), 6); Assert.Equal(productId, expectedProductId); } @@ -101,7 +101,7 @@ public async Task GetShellByIdAsync_ReturnsOkAsync() public async Task GetShellByIdAsync_ReturnsNotFoundAsync_WhenErrorWhileExtractionOfProductIdAsync() { // Arrange - var aasIdentifier = "aHR0cHM6Ly9leGFtcGxlLmNvbS9pZHMvYWFz"; + const string AasIdentifier = "aHR0cHM6Ly9leGFtcGxlLmNvbS9pZHMvYWFz"; var mockShellTemplate = TestData.CreateShellTemplate(); var mockAssetInformationTemplate = TestData.CreateAssetInformationTemplate(); using var messageHandler = new FakeHttpMessageHandler((_, _) => Task.FromResult(new HttpResponseMessage @@ -121,7 +121,7 @@ public async Task GetShellByIdAsync_ReturnsNotFoundAsync_WhenErrorWhileExtractio _ = _mockTemplateProvider.GetAssetInformationTemplateAsync(Arg.Any(), Arg.Any()).Returns(mockAssetInformationTemplate); // Act - var response = await _client.GetAsync($"/shells/{aasIdentifier}"); + var response = await _client.GetAsync($"/shells/{AasIdentifier}"); // Assert Assert.Equal(HttpStatusCode.NotFound, response.StatusCode); @@ -341,15 +341,9 @@ private static string EncodeBase64Url(string plainText) } } -public class AasRepositoryControllerTests_V1Config : AasRepositoryControllerTestsBase -{ - public AasRepositoryControllerTests_V1Config() : base("v1-config") { } -} +public class AasRepositoryControllerTestsV1Config() : AasRepositoryControllerTests("v1-config"); -public class AasRepositoryControllerTests_V2Config : AasRepositoryControllerTestsBase -{ - public AasRepositoryControllerTests_V2Config() : base("v2-config") { } -} +public class AasRepositoryControllerTestsV2Config() : AasRepositoryControllerTests("v2-config"); public class FakeHttpMessageHandler(Func> send) : HttpMessageHandler { diff --git a/source/AAS.TwinEngine.DataEngine.ModuleTests/Api/Services/SubmodelRegistry/SubmodelDescriptorControllerTests.cs b/source/AAS.TwinEngine.DataEngine.ModuleTests/Api/Services/SubmodelRegistry/SubmodelDescriptorControllerTests.cs index 2ec00a1a..89572a80 100644 --- a/source/AAS.TwinEngine.DataEngine.ModuleTests/Api/Services/SubmodelRegistry/SubmodelDescriptorControllerTests.cs +++ b/source/AAS.TwinEngine.DataEngine.ModuleTests/Api/Services/SubmodelRegistry/SubmodelDescriptorControllerTests.cs @@ -186,12 +186,6 @@ private static string EncodeBase64Url(string plainText) } } -public class SubmodelDescriptorControllerTests_V1Config : SubmodelDescriptorControllerTestsBase -{ - public SubmodelDescriptorControllerTests_V1Config() : base("v1-config") { } -} +public class SubmodelDescriptorControllerTestsV1Config() : SubmodelDescriptorControllerTestsBase("v1-config"); -public class SubmodelDescriptorControllerTests_V2Config : SubmodelDescriptorControllerTestsBase -{ - public SubmodelDescriptorControllerTests_V2Config() : base("v2-config") { } -} +public class SubmodelDescriptorControllerTestsV2Config() : SubmodelDescriptorControllerTestsBase("v2-config"); diff --git a/source/AAS.TwinEngine.DataEngine.ModuleTests/Api/Services/SubmodelRepository/SerializationControllerTests.cs b/source/AAS.TwinEngine.DataEngine.ModuleTests/Api/Services/SubmodelRepository/SerializationControllerTests.cs index 62f3e203..2b273aea 100644 --- a/source/AAS.TwinEngine.DataEngine.ModuleTests/Api/Services/SubmodelRepository/SerializationControllerTests.cs +++ b/source/AAS.TwinEngine.DataEngine.ModuleTests/Api/Services/SubmodelRepository/SerializationControllerTests.cs @@ -242,12 +242,6 @@ private static string EncodeBase64Url(string plainText) } } -public class SerializationControllerTests_V1Config : SerializationControllerTestsBase -{ - public SerializationControllerTests_V1Config() : base("v1-config") { } -} +public class SerializationControllerTestsV1Config() : SerializationControllerTestsBase("v1-config"); -public class SerializationControllerTests_V2Config : SerializationControllerTestsBase -{ - public SerializationControllerTests_V2Config() : base("v2-config") { } -} +public class SerializationControllerTestsV2Config() : SerializationControllerTestsBase("v2-config"); diff --git a/source/AAS.TwinEngine.DataEngine.ModuleTests/Api/Services/SubmodelRepository/SubmodelRepositoryControllerTests.cs b/source/AAS.TwinEngine.DataEngine.ModuleTests/Api/Services/SubmodelRepository/SubmodelRepositoryControllerTests.cs index 491954de..9c9fb950 100644 --- a/source/AAS.TwinEngine.DataEngine.ModuleTests/Api/Services/SubmodelRepository/SubmodelRepositoryControllerTests.cs +++ b/source/AAS.TwinEngine.DataEngine.ModuleTests/Api/Services/SubmodelRepository/SubmodelRepositoryControllerTests.cs @@ -1,4 +1,4 @@ -using System.Net; +using System.Net; using System.Net.Http.Json; using System.Text; using System.Text.Json.Nodes; @@ -328,15 +328,9 @@ private static string CreateSubmodelElementPath(string submodelIdentifier, strin => $"/submodels/{submodelIdentifier}/submodel-elements/{Uri.EscapeDataString(idShortPath)}"; } -public class SubmodelRepositoryControllerTests_V1Config : SubmodelRepositoryControllerTestsBase -{ - public SubmodelRepositoryControllerTests_V1Config() : base("v1-config") { } -} +public class SubmodelRepositoryControllerTestsV1Config() : SubmodelRepositoryControllerTestsBase("v1-config"); -public class SubmodelRepositoryControllerTests_V2Config : SubmodelRepositoryControllerTestsBase -{ - public SubmodelRepositoryControllerTests_V2Config() : base("v2-config") { } -} +public class SubmodelRepositoryControllerTestsV2Config() : SubmodelRepositoryControllerTestsBase("v2-config"); public class FakeHttpMessageHandler(Func> send) : HttpMessageHandler { diff --git a/source/AAS.TwinEngine.DataEngine/Infrastructure/Configuration/LegacyV1/LegacyTemplateManagementConfigAdapter.cs b/source/AAS.TwinEngine.DataEngine/Infrastructure/Configuration/LegacyV1/LegacyTemplateManagementConfigAdapter.cs index 755a0a81..216fab31 100644 --- a/source/AAS.TwinEngine.DataEngine/Infrastructure/Configuration/LegacyV1/LegacyTemplateManagementConfigAdapter.cs +++ b/source/AAS.TwinEngine.DataEngine/Infrastructure/Configuration/LegacyV1/LegacyTemplateManagementConfigAdapter.cs @@ -13,9 +13,7 @@ namespace AAS.TwinEngine.DataEngine.Infrastructure.Configuration.LegacyV1; [Obsolete("V1 configuration is deprecated and will be removed in next major release")] public sealed class LegacyTemplateManagementConfigAdapter(IConfiguration configuration) : IConfigureOptions { - private readonly IConfiguration _configuration = configuration; - - public void Configure(TemplateManagementConfig options) => MapToConfig(_configuration, options); + public void Configure(TemplateManagementConfig options) => MapToConfig(configuration, options); /// /// Static entry point used during DI registration to apply V1 mapping without BuildServiceProvider(). @@ -42,7 +40,7 @@ public static void MapToConfig(IConfiguration configuration, TemplateManagementC var mappingRules = configuration.GetSection(TemplateMappingRules.Section).Get(); if (mappingRules != null) { - RemapLegacyExtractionRules(mappingRules); + RemapLegacyExtractionRules(configuration, mappingRules); options.TemplateMappingRules = mappingRules; } @@ -156,9 +154,9 @@ private static void ApplyV1ServiceEndpointOverrides(IConfiguration configuration /// where Pattern held the strategy name and Separator held the actual delimiter. /// Detects this by checking for a "Separator" key in the raw config and remaps accordingly. /// - private void RemapLegacyExtractionRules(TemplateMappingRules rules) + private static void RemapLegacyExtractionRules(IConfiguration configuration, TemplateMappingRules rules) { - var rulesSection = _configuration.GetSection($"{TemplateMappingRules.Section}:AasIdExtractionRules"); + var rulesSection = configuration.GetSection($"{TemplateMappingRules.Section}:AasIdExtractionRules"); for (var i = 0; i < rules.AasIdExtractionRules.Count; i++) { From 7c3411417f478cfacf64cb0b3262def83c3b1ef2 Mon Sep 17 00:00:00 2001 From: Kevalkumar Date: Tue, 21 Apr 2026 01:50:15 +0530 Subject: [PATCH 49/71] Refactor extraction rules handling in configuration and update unit tests for exception types --- example/docker-compose.yml | 5 +++-- .../ShellTemplateMappingProviderTests.cs | 18 ++++++------------ .../LegacyTemplateManagementConfigAdapter.cs | 1 + 3 files changed, 10 insertions(+), 14 deletions(-) diff --git a/example/docker-compose.yml b/example/docker-compose.yml index be95275e..ee123992 100644 --- a/example/docker-compose.yml +++ b/example/docker-compose.yml @@ -111,9 +111,10 @@ services: - ExtractionRules__SubmodelNameExtractionRules__4__pattern__0=CarbonFootprint - ExtractionRules__SubmodelNameExtractionRules__4__pattern__1=Footprint - ExtractionRules__SubmodelNameExtractionRules__4__pattern__2=Carbon - - ExtractionRules__ProductIdExtractionRules__0__Pattern=Regex + - ExtractionRules__ProductIdExtractionRules__0__Pattern=/ - ExtractionRules__ProductIdExtractionRules__0__Index=5 - - ExtractionRules__ProductIdExtractionRules__0__Separator=/ + - ExtractionRules__ProductIdExtractionRules__0__Strategy=Split + - ExtractionRules__ProductIdExtractionRules__0__Description=Default split by '/' segment 6 networks: - twinengine-network diff --git a/source/AAS.TwinEngine.DataEngine.UnitTests/Infrastructure/Providers/TemplateProvider/Services/ShellTemplateMappingProviderTests.cs b/source/AAS.TwinEngine.DataEngine.UnitTests/Infrastructure/Providers/TemplateProvider/Services/ShellTemplateMappingProviderTests.cs index 209e55c8..6c63b15d 100644 --- a/source/AAS.TwinEngine.DataEngine.UnitTests/Infrastructure/Providers/TemplateProvider/Services/ShellTemplateMappingProviderTests.cs +++ b/source/AAS.TwinEngine.DataEngine.UnitTests/Infrastructure/Providers/TemplateProvider/Services/ShellTemplateMappingProviderTests.cs @@ -433,20 +433,18 @@ public void GetTemplateId_NoMatchingTemplate_ThrowsResourceNotFoundException() // ── Constructor: null logger ── [Fact] - public void Constructor_NullLogger_ThrowsArgumentNullException() + public void Constructor_NullLogger_ThrowsInvalidDependencyException() { var options = Substitute.For>(); options.Value.Returns(new TemplateManagementConfig()); - var ex = Assert.Throws(() => - new ShellTemplateMappingProvider(null!, options)); - Assert.Equal("logger", ex.ParamName); + var ex = Assert.Throws(() => new ShellTemplateMappingProvider(null!, options)); } // ── Constructor: null ShellTemplateMappings ── [Fact] - public void Constructor_NullShellTemplateMappings_ThrowsArgumentException() + public void Constructor_NullShellTemplateMappings_ThrowsInvalidDependencyException() { var options = Substitute.For>(); options.Value.Returns(new TemplateManagementConfig @@ -458,15 +456,13 @@ public void Constructor_NullShellTemplateMappings_ThrowsArgumentException() } }); - var ex = Assert.Throws(() => - new ShellTemplateMappingProvider(_logger, options)); - Assert.Contains("ShellTemplateMappings are missing", ex.Message, StringComparison.Ordinal); + var ex = Assert.Throws(() => new ShellTemplateMappingProvider(_logger, options)); } // ── Constructor: null AasIdExtractionRules ── [Fact] - public void Constructor_NullAasIdExtractionRules_ThrowsArgumentException() + public void Constructor_NullAasIdExtractionRules_ThrowsInvalidDependencyException() { var options = Substitute.For>(); options.Value.Returns(new TemplateManagementConfig @@ -478,9 +474,7 @@ public void Constructor_NullAasIdExtractionRules_ThrowsArgumentException() } }); - var ex = Assert.Throws(() => - new ShellTemplateMappingProvider(_logger, options)); - Assert.Contains("AasIdExtractionRules are missing", ex.Message, StringComparison.Ordinal); + var ex = Assert.Throws(() => new ShellTemplateMappingProvider(_logger, options)); } // ── Empty identifier ── diff --git a/source/AAS.TwinEngine.DataEngine/Infrastructure/Configuration/LegacyV1/LegacyTemplateManagementConfigAdapter.cs b/source/AAS.TwinEngine.DataEngine/Infrastructure/Configuration/LegacyV1/LegacyTemplateManagementConfigAdapter.cs index 216fab31..42897607 100644 --- a/source/AAS.TwinEngine.DataEngine/Infrastructure/Configuration/LegacyV1/LegacyTemplateManagementConfigAdapter.cs +++ b/source/AAS.TwinEngine.DataEngine/Infrastructure/Configuration/LegacyV1/LegacyTemplateManagementConfigAdapter.cs @@ -69,6 +69,7 @@ public static void ApplyV1Overrides(IConfiguration configuration, TemplateManage || mappingRules?.ShellTemplateMappings?.Count > 0 || mappingRules?.AasIdExtractionRules?.Count > 0) { + RemapLegacyExtractionRules(configuration, mappingRules); options.TemplateMappingRules = mappingRules; } From f7b1268d1bb6234ff3617e22e80721ce7f2ab246 Mon Sep 17 00:00:00 2001 From: Kevalkumar Date: Tue, 21 Apr 2026 02:02:02 +0530 Subject: [PATCH 50/71] Refactor validation logic in TemplateMappingRulesValidator for improved clarity and maintainability --- .../Config/TemplateMappingRulesValidator.cs | 127 +++++++++++++----- 1 file changed, 94 insertions(+), 33 deletions(-) diff --git a/source/AAS.TwinEngine.DataEngine/Infrastructure/Providers/TemplateProvider/Config/TemplateMappingRulesValidator.cs b/source/AAS.TwinEngine.DataEngine/Infrastructure/Providers/TemplateProvider/Config/TemplateMappingRulesValidator.cs index 43b842af..6beb9c34 100644 --- a/source/AAS.TwinEngine.DataEngine/Infrastructure/Providers/TemplateProvider/Config/TemplateMappingRulesValidator.cs +++ b/source/AAS.TwinEngine.DataEngine/Infrastructure/Providers/TemplateProvider/Config/TemplateMappingRulesValidator.cs @@ -1,4 +1,4 @@ -using System.Text.RegularExpressions; +using System.Text.RegularExpressions; using AAS.TwinEngine.DataEngine.ServiceConfiguration.Config; @@ -14,55 +14,116 @@ public ValidateOptionsResult Validate(string? name, TemplateManagementConfig opt var rules = options.TemplateMappingRules.AasIdExtractionRules; - if (rules == null || rules.Count == 0) + var basicValidation = ValidateRulesExist(rules); + if (basicValidation != null) { - return ValidateOptionsResult.Fail("At least one AasIdExtractionRule is required."); + return basicValidation; } - var requireValidationPattern = rules.Count > 1; + var requireValidationPattern = rules!.Count > 1; for (var i = 0; i < rules.Count; i++) { var rule = rules[i]; - var label = !string.IsNullOrEmpty(rule.Description) ? rule.Description : $"Rule[{i}]"; + var label = GetLabel(rule, i); - if (string.IsNullOrWhiteSpace(rule.Pattern)) - { - return ValidateOptionsResult.Fail($"AasIdExtractionRules: {label} has an empty Pattern."); - } + var result = + ValidatePattern(rule, label) ?? + ValidateIndex(rule, label) ?? + ValidateRegex(rule, label) ?? + ValidateSplit(rule, label) ?? + ValidateValidationPattern(rule, label, requireValidationPattern); - if (rule.Index < 1) + if (result != null) { - return ValidateOptionsResult.Fail($"AasIdExtractionRules: {label} Index must be >= 1."); + return result; } + } - if (rule.Strategy == ExtractionStrategy.Regex) - { - if (!TryCompileRegex(rule.Pattern, out var error)) - { - return ValidateOptionsResult.Fail($"AasIdExtractionRules: {label} has an invalid regex Pattern: {error}"); - } - } + return ValidateOptionsResult.Success; + } - if (rule.Strategy == ExtractionStrategy.Split && rule.EndIndex.HasValue && rule.EndIndex.Value < rule.Index) - { - return ValidateOptionsResult.Fail($"AasIdExtractionRules: {label} EndIndex ({rule.EndIndex}) must be >= Index ({rule.Index})."); - } + private static ValidateOptionsResult? ValidateRulesExist(IList? rules) + { + if (rules == null || rules.Count == 0) + { + return ValidateOptionsResult.Fail("At least one AasIdExtractionRule is required."); + } - if (requireValidationPattern && string.IsNullOrWhiteSpace(rule.ValidationPattern)) - { - return ValidateOptionsResult.Fail( - $"AasIdExtractionRules: {label} is missing ValidationPattern. " + - "ValidationPattern is required when multiple extraction rules are configured."); - } + return null; + } - if (!string.IsNullOrWhiteSpace(rule.ValidationPattern) && !TryCompileRegex(rule.ValidationPattern, out var vpError)) - { - return ValidateOptionsResult.Fail($"AasIdExtractionRules: {label} has an invalid ValidationPattern: {vpError}"); - } + private static string GetLabel(AasIdExtractionRule rule, int index) => + !string.IsNullOrEmpty(rule.Description) ? rule.Description : $"Rule[{index}]"; + + private static ValidateOptionsResult? ValidatePattern(AasIdExtractionRule rule, string label) + { + if (string.IsNullOrWhiteSpace(rule.Pattern)) + { + return ValidateOptionsResult.Fail($"AasIdExtractionRules: {label} has an empty Pattern."); } - return ValidateOptionsResult.Success; + return null; + } + + private static ValidateOptionsResult? ValidateIndex(AasIdExtractionRule rule, string label) + { + if (rule.Index < 1) + { + return ValidateOptionsResult.Fail($"AasIdExtractionRules: {label} Index must be >= 1."); + } + + return null; + } + + private static ValidateOptionsResult? ValidateRegex(AasIdExtractionRule rule, string label) + { + if (rule.Strategy != ExtractionStrategy.Regex) + { + return null; + } + + if (!TryCompileRegex(rule.Pattern, out var error)) + { + return ValidateOptionsResult.Fail($"AasIdExtractionRules: {label} has an invalid regex Pattern: {error}"); + } + + return null; + } + + private static ValidateOptionsResult? ValidateSplit(AasIdExtractionRule rule, string label) + { + if (rule.Strategy == ExtractionStrategy.Split && + rule.EndIndex.HasValue && + rule.EndIndex.Value < rule.Index) + { + return ValidateOptionsResult.Fail( + $"AasIdExtractionRules: {label} EndIndex ({rule.EndIndex}) must be >= Index ({rule.Index})."); + } + + return null; + } + + private static ValidateOptionsResult? ValidateValidationPattern( + AasIdExtractionRule rule, + string label, + bool requireValidationPattern) + { + if (requireValidationPattern && string.IsNullOrWhiteSpace(rule.ValidationPattern)) + { + return ValidateOptionsResult.Fail( + $"AasIdExtractionRules: {label} is missing ValidationPattern. " + + "ValidationPattern is required when multiple extraction rules are configured."); + } + + if (!string.IsNullOrWhiteSpace(rule.ValidationPattern) && + !TryCompileRegex(rule.ValidationPattern, out var error)) + { + return ValidateOptionsResult.Fail( + $"AasIdExtractionRules: {label} has an invalid ValidationPattern: {error}"); + } + + return null; } private static bool TryCompileRegex(string pattern, out string? error) From f8284574cb0a8729affa6ea33a36ee9a86b958cb Mon Sep 17 00:00:00 2001 From: Kevalkumar Date: Tue, 21 Apr 2026 02:11:48 +0530 Subject: [PATCH 51/71] Update extraction rules description and refactor test class names for consistency --- example/docker-compose.yml | 2 +- .../ShellDescriptorControllerTests.cs | 20 +++++-------------- .../SubmodelDescriptorControllerTests.cs | 18 ++++------------- .../SerializationControllerTests.cs | 18 ++++------------- .../SubmodelRepositoryControllerTests.cs | 10 +++++----- .../Services/ShellTemplateMappingProvider.cs | 8 +++----- .../appsettings.development.json | 1 + 7 files changed, 23 insertions(+), 54 deletions(-) diff --git a/example/docker-compose.yml b/example/docker-compose.yml index ee123992..5d3c27dc 100644 --- a/example/docker-compose.yml +++ b/example/docker-compose.yml @@ -114,7 +114,7 @@ services: - ExtractionRules__ProductIdExtractionRules__0__Pattern=/ - ExtractionRules__ProductIdExtractionRules__0__Index=5 - ExtractionRules__ProductIdExtractionRules__0__Strategy=Split - - ExtractionRules__ProductIdExtractionRules__0__Description=Default split by '/' segment 6 + - ExtractionRules__ProductIdExtractionRules__0__Description=Default split by '/' segment 5 networks: - twinengine-network diff --git a/source/AAS.TwinEngine.DataEngine.ModuleTests/Api/Services/AasRegistry/ShellDescriptorControllerTests.cs b/source/AAS.TwinEngine.DataEngine.ModuleTests/Api/Services/AasRegistry/ShellDescriptorControllerTests.cs index 694caab5..1a0186e0 100644 --- a/source/AAS.TwinEngine.DataEngine.ModuleTests/Api/Services/AasRegistry/ShellDescriptorControllerTests.cs +++ b/source/AAS.TwinEngine.DataEngine.ModuleTests/Api/Services/AasRegistry/ShellDescriptorControllerTests.cs @@ -1,4 +1,4 @@ -using System.Net; +using System.Net; using System.Net.Http.Json; using System.Text; using System.Text.Json.Nodes; @@ -20,14 +20,14 @@ namespace AAS.TwinEngine.DataEngine.ModuleTests.Api.Services.AasRegistry; -public abstract class ShellDescriptorControllerTestsBase : IDisposable +public abstract class ShellDescriptorControllerTests : IDisposable { private readonly ConfigTestFactory _factory; private readonly ITemplateProvider _mockTemplateProvider; private readonly HttpClient _client; private readonly ICreateClient _httpClientFactory; - protected ShellDescriptorControllerTestsBase(string configDir) + protected ShellDescriptorControllerTests(string configDir) { _mockTemplateProvider = Substitute.For(); var mockPluginManifestProvider = Substitute.For(); @@ -346,19 +346,9 @@ private static string EncodeBase64Url(string plainText) } } -public class ShellDescriptorControllerTests_V1Config : ShellDescriptorControllerTestsBase -{ - public ShellDescriptorControllerTests_V1Config() : base("v1-config") { } -} - -public class ShellDescriptorControllerTests_V2Config : ShellDescriptorControllerTestsBase -{ - public ShellDescriptorControllerTests_V2Config() : base("v2-config") { } -} - -public class ShellDescriptorControllerTestsV1Config() : ShellDescriptorControllerTestsBase("v1-config"); +public class ShellDescriptorControllerTestsV1Config() : ShellDescriptorControllerTests("v1-config"); -public class ShellDescriptorControllerTestsV2Config() : ShellDescriptorControllerTestsBase("v2-config"); +public class ShellDescriptorControllerTestsV2Config() : ShellDescriptorControllerTests("v2-config"); public class FakeHttpMessageHandler(Func> send) : HttpMessageHandler { diff --git a/source/AAS.TwinEngine.DataEngine.ModuleTests/Api/Services/SubmodelRegistry/SubmodelDescriptorControllerTests.cs b/source/AAS.TwinEngine.DataEngine.ModuleTests/Api/Services/SubmodelRegistry/SubmodelDescriptorControllerTests.cs index 77b73ddb..11297bcf 100644 --- a/source/AAS.TwinEngine.DataEngine.ModuleTests/Api/Services/SubmodelRegistry/SubmodelDescriptorControllerTests.cs +++ b/source/AAS.TwinEngine.DataEngine.ModuleTests/Api/Services/SubmodelRegistry/SubmodelDescriptorControllerTests.cs @@ -16,13 +16,13 @@ namespace AAS.TwinEngine.DataEngine.ModuleTests.Api.Services.SubmodelRegistry; -public abstract class SubmodelDescriptorControllerTestsBase : IDisposable +public abstract class SubmodelDescriptorControllerTests : IDisposable { private readonly ConfigTestFactory _factory; private readonly ISubmodelDescriptorProvider _mockSubmodelDescriptorProvider; private readonly HttpClient _client; - protected SubmodelDescriptorControllerTestsBase(string configDir) + protected SubmodelDescriptorControllerTests(string configDir) { _mockSubmodelDescriptorProvider = Substitute.For(); var mockPluginManifestProvider = Substitute.For(); @@ -186,16 +186,6 @@ private static string EncodeBase64Url(string plainText) } } -public class SubmodelDescriptorControllerTests_V1Config : SubmodelDescriptorControllerTestsBase -{ - public SubmodelDescriptorControllerTests_V1Config() : base("v1-config") { } -} - -public class SubmodelDescriptorControllerTests_V2Config : SubmodelDescriptorControllerTestsBase -{ - public SubmodelDescriptorControllerTests_V2Config() : base("v2-config") { } -} - -public class SubmodelDescriptorControllerTestsV1Config() : SubmodelDescriptorControllerTestsBase("v1-config"); +public class SubmodelDescriptorControllerTestsV1Config() : SubmodelDescriptorControllerTests("v1-config"); -public class SubmodelDescriptorControllerTestsV2Config() : SubmodelDescriptorControllerTestsBase("v2-config"); +public class SubmodelDescriptorControllerTestsV2Config() : SubmodelDescriptorControllerTests("v2-config"); diff --git a/source/AAS.TwinEngine.DataEngine.ModuleTests/Api/Services/SubmodelRepository/SerializationControllerTests.cs b/source/AAS.TwinEngine.DataEngine.ModuleTests/Api/Services/SubmodelRepository/SerializationControllerTests.cs index 47f33d9f..02226d25 100644 --- a/source/AAS.TwinEngine.DataEngine.ModuleTests/Api/Services/SubmodelRepository/SerializationControllerTests.cs +++ b/source/AAS.TwinEngine.DataEngine.ModuleTests/Api/Services/SubmodelRepository/SerializationControllerTests.cs @@ -15,7 +15,7 @@ namespace AAS.TwinEngine.DataEngine.ModuleTests.Api.Services.SubmodelRepository; -public abstract class SerializationControllerTestsBase : IDisposable +public abstract class SerializationControllerTests : IDisposable { private readonly ConfigTestFactory _factory; private readonly IAasRepositoryService _mockAasRepositoryService; @@ -23,7 +23,7 @@ public abstract class SerializationControllerTestsBase : IDisposable private readonly IConceptDescriptionService _mockConceptDescriptionService; private readonly HttpClient _client; - protected SerializationControllerTestsBase(string configDir) + protected SerializationControllerTests(string configDir) { _mockAasRepositoryService = Substitute.For(); _mockSubmodelRepositoryService = Substitute.For(); @@ -242,16 +242,6 @@ private static string EncodeBase64Url(string plainText) } } -public class SerializationControllerTests_V1Config : SerializationControllerTestsBase -{ - public SerializationControllerTests_V1Config() : base("v1-config") { } -} - -public class SerializationControllerTests_V2Config : SerializationControllerTestsBase -{ - public SerializationControllerTests_V2Config() : base("v2-config") { } -} - -public class SerializationControllerTestsV1Config() : SerializationControllerTestsBase("v1-config"); +public class SerializationControllerTestsV1Config() : SerializationControllerTests("v1-config"); -public class SerializationControllerTestsV2Config() : SerializationControllerTestsBase("v2-config"); +public class SerializationControllerTestsV2Config() : SerializationControllerTests("v2-config"); diff --git a/source/AAS.TwinEngine.DataEngine.ModuleTests/Api/Services/SubmodelRepository/SubmodelRepositoryControllerTests.cs b/source/AAS.TwinEngine.DataEngine.ModuleTests/Api/Services/SubmodelRepository/SubmodelRepositoryControllerTests.cs index 3897437d..afd65479 100644 --- a/source/AAS.TwinEngine.DataEngine.ModuleTests/Api/Services/SubmodelRepository/SubmodelRepositoryControllerTests.cs +++ b/source/AAS.TwinEngine.DataEngine.ModuleTests/Api/Services/SubmodelRepository/SubmodelRepositoryControllerTests.cs @@ -1,4 +1,4 @@ -using System.Net; +using System.Net; using System.Net.Http.Json; using System.Text; using System.Text.Json.Nodes; @@ -20,14 +20,14 @@ namespace AAS.TwinEngine.DataEngine.ModuleTests.Api.Services.SubmodelRepository; -public abstract class SubmodelRepositoryControllerTestsBase : IDisposable +public abstract class SubmodelRepositoryControllerTests : IDisposable { private readonly ConfigTestFactory _factory; private readonly ITemplateProvider _mockTemplateProvider; private readonly HttpClient _client; private readonly ICreateClient _httpClientFactory; - protected SubmodelRepositoryControllerTestsBase(string configDir) + protected SubmodelRepositoryControllerTests(string configDir) { _mockTemplateProvider = Substitute.For(); var mockPluginManifestProvider = Substitute.For(); @@ -328,9 +328,9 @@ private static string CreateSubmodelElementPath(string submodelIdentifier, strin => $"/submodels/{submodelIdentifier}/submodel-elements/{Uri.EscapeDataString(idShortPath)}"; } -public class SubmodelRepositoryControllerTestsV1Config() : SubmodelRepositoryControllerTestsBase("v1-config"); +public class SubmodelRepositoryControllerTestsV1Config() : SubmodelRepositoryControllerTests("v1-config"); -public class SubmodelRepositoryControllerTestsV2Config() : SubmodelRepositoryControllerTestsBase("v2-config"); +public class SubmodelRepositoryControllerTestsV2Config() : SubmodelRepositoryControllerTests("v2-config"); public class FakeHttpMessageHandler(Func> send) : HttpMessageHandler { diff --git a/source/AAS.TwinEngine.DataEngine/Infrastructure/Providers/TemplateProvider/Services/ShellTemplateMappingProvider.cs b/source/AAS.TwinEngine.DataEngine/Infrastructure/Providers/TemplateProvider/Services/ShellTemplateMappingProvider.cs index c3826d23..04013afb 100644 --- a/source/AAS.TwinEngine.DataEngine/Infrastructure/Providers/TemplateProvider/Services/ShellTemplateMappingProvider.cs +++ b/source/AAS.TwinEngine.DataEngine/Infrastructure/Providers/TemplateProvider/Services/ShellTemplateMappingProvider.cs @@ -51,12 +51,10 @@ public string GetProductIdFromRule(string aasIdentifier) continue; } - if (!string.IsNullOrEmpty(rule.ValidationPattern)) + if (!string.IsNullOrEmpty(rule.ValidationPattern) && + !Regex.IsMatch(extracted, rule.ValidationPattern, RegexOptions.None, _regexTimeout)) { - if (!Regex.IsMatch(extracted, rule.ValidationPattern, RegexOptions.None, _regexTimeout)) - { - continue; - } + continue; } return extracted; diff --git a/source/AAS.TwinEngine.DataEngine/appsettings.development.json b/source/AAS.TwinEngine.DataEngine/appsettings.development.json index 01174ade..0e3d59f3 100644 --- a/source/AAS.TwinEngine.DataEngine/appsettings.development.json +++ b/source/AAS.TwinEngine.DataEngine/appsettings.development.json @@ -7,6 +7,7 @@ } }, "CustomerDomainUrl": "https://mm-software.com", + "DataEngineRepositoryBaseUrl": "http://localhost:5059", "HeaderSanitization": { "MaxHeaderSize": 8192, "MaxHeaderNameSize": 256, From 9d02adcce7dba382e2243f1191d454815f65499a Mon Sep 17 00:00:00 2001 From: Kevalkumar Date: Tue, 21 Apr 2026 02:18:53 +0530 Subject: [PATCH 52/71] Add check to skip extraction if identifier matches extracted value --- .../Services/ShellTemplateMappingProvider.cs | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/source/AAS.TwinEngine.DataEngine/Infrastructure/Providers/TemplateProvider/Services/ShellTemplateMappingProvider.cs b/source/AAS.TwinEngine.DataEngine/Infrastructure/Providers/TemplateProvider/Services/ShellTemplateMappingProvider.cs index 04013afb..8f76d015 100644 --- a/source/AAS.TwinEngine.DataEngine/Infrastructure/Providers/TemplateProvider/Services/ShellTemplateMappingProvider.cs +++ b/source/AAS.TwinEngine.DataEngine/Infrastructure/Providers/TemplateProvider/Services/ShellTemplateMappingProvider.cs @@ -51,6 +51,11 @@ public string GetProductIdFromRule(string aasIdentifier) continue; } + if (string.Equals(extracted, aasIdentifier, StringComparison.Ordinal)) + { + continue; + } + if (!string.IsNullOrEmpty(rule.ValidationPattern) && !Regex.IsMatch(extracted, rule.ValidationPattern, RegexOptions.None, _regexTimeout)) { From 8293d97637138581fbef5191ab5a8a9f8e821d31 Mon Sep 17 00:00:00 2001 From: Kevalkumar Date: Tue, 21 Apr 2026 02:40:57 +0530 Subject: [PATCH 53/71] Remove unused using directives and delete IBaseUrlProvider interface and HttpRequestBaseUrlProvider class --- .../ShellDescriptorMapperProfileTests.cs | 1 - .../AasRepositoryTemplateServiceTests.cs | 3 - .../SubmodelDescriptorServiceTests.cs | 1 - .../SubmodelTemplateServiceTests.cs | 1 - .../Services/PluginDataHandlerTests.cs | 3 +- .../Services/Shared/IBaseUrlProvider.cs | 11 ---- .../SubmodelTemplateService.cs | 1 - .../Shared/HttpRequestBaseUrlProvider.cs | 57 ------------------- ...astructureDependencyInjectionExtensions.cs | 1 - 9 files changed, 1 insertion(+), 78 deletions(-) delete mode 100644 source/AAS.TwinEngine.DataEngine/ApplicationLogic/Services/Shared/IBaseUrlProvider.cs delete mode 100644 source/AAS.TwinEngine.DataEngine/Infrastructure/Shared/HttpRequestBaseUrlProvider.cs diff --git a/source/AAS.TwinEngine.DataEngine.UnitTests/Api/AasRegistry/MappingProfiles/ShellDescriptorMapperProfileTests.cs b/source/AAS.TwinEngine.DataEngine.UnitTests/Api/AasRegistry/MappingProfiles/ShellDescriptorMapperProfileTests.cs index 275d3079..610a3b3f 100644 --- a/source/AAS.TwinEngine.DataEngine.UnitTests/Api/AasRegistry/MappingProfiles/ShellDescriptorMapperProfileTests.cs +++ b/source/AAS.TwinEngine.DataEngine.UnitTests/Api/AasRegistry/MappingProfiles/ShellDescriptorMapperProfileTests.cs @@ -1,5 +1,4 @@ using AAS.TwinEngine.DataEngine.Api.AasRegistry.MappingProfiles; -using AAS.TwinEngine.DataEngine.DomainModel.AasRegistry; using AAS.TwinEngine.DataEngine.UnitTests.Api.Shared.MappingProfiles; using AasCore.Aas3_0; diff --git a/source/AAS.TwinEngine.DataEngine.UnitTests/ApplicationLogic/Services/AasRepository/AasRepositoryTemplateServiceTests.cs b/source/AAS.TwinEngine.DataEngine.UnitTests/ApplicationLogic/Services/AasRepository/AasRepositoryTemplateServiceTests.cs index 0c801be9..361ba4b5 100644 --- a/source/AAS.TwinEngine.DataEngine.UnitTests/ApplicationLogic/Services/AasRepository/AasRepositoryTemplateServiceTests.cs +++ b/source/AAS.TwinEngine.DataEngine.UnitTests/ApplicationLogic/Services/AasRepository/AasRepositoryTemplateServiceTests.cs @@ -2,13 +2,10 @@ using AAS.TwinEngine.DataEngine.ApplicationLogic.Exceptions.Infrastructure; using AAS.TwinEngine.DataEngine.ApplicationLogic.Services.AasEnvironment.Providers; using AAS.TwinEngine.DataEngine.ApplicationLogic.Services.AasRepository; -using AAS.TwinEngine.DataEngine.ApplicationLogic.Services.SubmodelRepository.SemanticId.FillOut; using AAS.TwinEngine.DataEngine.ServiceConfiguration.Config; using AasCore.Aas3_0; -using Castle.Core.Logging; - using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; diff --git a/source/AAS.TwinEngine.DataEngine.UnitTests/ApplicationLogic/Services/SubmodelRegistry/SubmodelDescriptorServiceTests.cs b/source/AAS.TwinEngine.DataEngine.UnitTests/ApplicationLogic/Services/SubmodelRegistry/SubmodelDescriptorServiceTests.cs index 8da428ae..ff748136 100644 --- a/source/AAS.TwinEngine.DataEngine.UnitTests/ApplicationLogic/Services/SubmodelRegistry/SubmodelDescriptorServiceTests.cs +++ b/source/AAS.TwinEngine.DataEngine.UnitTests/ApplicationLogic/Services/SubmodelRegistry/SubmodelDescriptorServiceTests.cs @@ -1,7 +1,6 @@ using AAS.TwinEngine.DataEngine.ApplicationLogic.Exceptions.Application; using AAS.TwinEngine.DataEngine.ApplicationLogic.Exceptions.Infrastructure; using AAS.TwinEngine.DataEngine.ApplicationLogic.Services.AasEnvironment.Providers; -using AAS.TwinEngine.DataEngine.ApplicationLogic.Services.AasRepository; using AAS.TwinEngine.DataEngine.ApplicationLogic.Services.SubmodelRegistry; using AAS.TwinEngine.DataEngine.ApplicationLogic.Services.SubmodelRegistry.Providers; using AAS.TwinEngine.DataEngine.DomainModel.Shared; diff --git a/source/AAS.TwinEngine.DataEngine.UnitTests/ApplicationLogic/Services/SubmodelRepository/SubmodelTemplateServiceTests.cs b/source/AAS.TwinEngine.DataEngine.UnitTests/ApplicationLogic/Services/SubmodelRepository/SubmodelTemplateServiceTests.cs index 5e213474..9e84b150 100644 --- a/source/AAS.TwinEngine.DataEngine.UnitTests/ApplicationLogic/Services/SubmodelRepository/SubmodelTemplateServiceTests.cs +++ b/source/AAS.TwinEngine.DataEngine.UnitTests/ApplicationLogic/Services/SubmodelRepository/SubmodelTemplateServiceTests.cs @@ -2,7 +2,6 @@ using AAS.TwinEngine.DataEngine.ApplicationLogic.Exceptions.Base; using AAS.TwinEngine.DataEngine.ApplicationLogic.Exceptions.Infrastructure; using AAS.TwinEngine.DataEngine.ApplicationLogic.Services.AasEnvironment.Providers; -using AAS.TwinEngine.DataEngine.ApplicationLogic.Services.AasRepository; using AAS.TwinEngine.DataEngine.ApplicationLogic.Services.SubmodelRepository; using AasCore.Aas3_0; diff --git a/source/AAS.TwinEngine.DataEngine.UnitTests/Infrastructure/Providers/PluginDataProvider/Services/PluginDataHandlerTests.cs b/source/AAS.TwinEngine.DataEngine.UnitTests/Infrastructure/Providers/PluginDataProvider/Services/PluginDataHandlerTests.cs index b6ff471b..103dcd00 100644 --- a/source/AAS.TwinEngine.DataEngine.UnitTests/Infrastructure/Providers/PluginDataProvider/Services/PluginDataHandlerTests.cs +++ b/source/AAS.TwinEngine.DataEngine.UnitTests/Infrastructure/Providers/PluginDataProvider/Services/PluginDataHandlerTests.cs @@ -1,4 +1,4 @@ -using System.Net; +using System.Net; using System.Net.Http.Json; using System.Text; using System.Text.Json; @@ -8,7 +8,6 @@ using AAS.TwinEngine.DataEngine.ApplicationLogic.Services.Plugin; using AAS.TwinEngine.DataEngine.ApplicationLogic.Services.Plugin.Helper; using AAS.TwinEngine.DataEngine.ApplicationLogic.Services.Plugin.Providers; -using AAS.TwinEngine.DataEngine.ApplicationLogic.Services.Shared; using AAS.TwinEngine.DataEngine.DomainModel.AasRegistry; using AAS.TwinEngine.DataEngine.DomainModel.AasRepository; using AAS.TwinEngine.DataEngine.DomainModel.Plugin; diff --git a/source/AAS.TwinEngine.DataEngine/ApplicationLogic/Services/Shared/IBaseUrlProvider.cs b/source/AAS.TwinEngine.DataEngine/ApplicationLogic/Services/Shared/IBaseUrlProvider.cs deleted file mode 100644 index ab9f67fd..00000000 --- a/source/AAS.TwinEngine.DataEngine/ApplicationLogic/Services/Shared/IBaseUrlProvider.cs +++ /dev/null @@ -1,11 +0,0 @@ -namespace AAS.TwinEngine.DataEngine.ApplicationLogic.Services.Shared; - -/// -/// Provides the base URL of the DataEngine's own repository. -/// V2 config: extracted from the current HTTP request. -/// V1 config: falls back to the configured DataEngineRepositoryBaseUrl value. -/// -public interface IBaseUrlProvider -{ - Uri GetBaseUrl(); -} diff --git a/source/AAS.TwinEngine.DataEngine/ApplicationLogic/Services/SubmodelRepository/SubmodelTemplateService.cs b/source/AAS.TwinEngine.DataEngine/ApplicationLogic/Services/SubmodelRepository/SubmodelTemplateService.cs index f44d9851..d60cc973 100644 --- a/source/AAS.TwinEngine.DataEngine/ApplicationLogic/Services/SubmodelRepository/SubmodelTemplateService.cs +++ b/source/AAS.TwinEngine.DataEngine/ApplicationLogic/Services/SubmodelRepository/SubmodelTemplateService.cs @@ -5,7 +5,6 @@ using AAS.TwinEngine.DataEngine.ApplicationLogic.Exceptions.Base; using AAS.TwinEngine.DataEngine.ApplicationLogic.Exceptions.Infrastructure; using AAS.TwinEngine.DataEngine.ApplicationLogic.Services.AasEnvironment.Providers; -using AAS.TwinEngine.DataEngine.ApplicationLogic.Services.AasRepository; using AAS.TwinEngine.DataEngine.DomainModel.SubmodelRepository; using AasCore.Aas3_0; diff --git a/source/AAS.TwinEngine.DataEngine/Infrastructure/Shared/HttpRequestBaseUrlProvider.cs b/source/AAS.TwinEngine.DataEngine/Infrastructure/Shared/HttpRequestBaseUrlProvider.cs deleted file mode 100644 index 3515195e..00000000 --- a/source/AAS.TwinEngine.DataEngine/Infrastructure/Shared/HttpRequestBaseUrlProvider.cs +++ /dev/null @@ -1,57 +0,0 @@ -using AAS.TwinEngine.DataEngine.ApplicationLogic.Services.Shared; -using AAS.TwinEngine.DataEngine.ServiceConfiguration.Config; - -using Microsoft.Extensions.Options; - -namespace AAS.TwinEngine.DataEngine.Infrastructure.Shared; - -/// -/// Resolves the DataEngine's own base URL. -/// -/// V1 (old config): If was populated -/// by the legacy adapter, that value is used. -/// V2 (new config): The property is null, so the URL is derived from the incoming -/// HTTP request (Scheme://Host). -/// -/// The Host header is validated against to prevent -/// Host Header Injection attacks (OWASP A05:2021). -/// -public class HttpRequestBaseUrlProvider( - IHttpContextAccessor httpContextAccessor, - IOptions generalConfig) : IBaseUrlProvider -{ - private readonly IHttpContextAccessor _httpContextAccessor = httpContextAccessor; - private readonly Uri? _configuredBaseUrl = generalConfig.Value.DataEngineRepositoryBaseUrl; - private readonly string _allowedHosts = generalConfig.Value.AllowedHosts; - - public Uri GetBaseUrl() - { - if (_configuredBaseUrl != null) - { - return _configuredBaseUrl; - } - - var request = _httpContextAccessor.HttpContext?.Request - ?? throw new InvalidOperationException("No HTTP request context available — cannot derive base URL."); - - if (!IsHostAllowed(request.Host.Host)) - { - throw new InvalidOperationException( - $"Host header '{request.Host}' is not in the configured AllowedHosts."); - } - - var baseUrl = $"{request.Scheme}://{request.Host}"; - return new Uri(baseUrl, UriKind.Absolute); - } - - private bool IsHostAllowed(string host) - { - if (string.IsNullOrEmpty(_allowedHosts) || _allowedHosts == "*") - { - return true; - } - - var allowed = _allowedHosts.Split(';', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries); - return allowed.Any(a => string.Equals(a, host, StringComparison.OrdinalIgnoreCase)); - } -} diff --git a/source/AAS.TwinEngine.DataEngine/ServiceConfiguration/InfrastructureDependencyInjectionExtensions.cs b/source/AAS.TwinEngine.DataEngine/ServiceConfiguration/InfrastructureDependencyInjectionExtensions.cs index 8f238e14..c079ee7b 100644 --- a/source/AAS.TwinEngine.DataEngine/ServiceConfiguration/InfrastructureDependencyInjectionExtensions.cs +++ b/source/AAS.TwinEngine.DataEngine/ServiceConfiguration/InfrastructureDependencyInjectionExtensions.cs @@ -3,7 +3,6 @@ using AAS.TwinEngine.DataEngine.ApplicationLogic.Services.Plugin; using AAS.TwinEngine.DataEngine.ApplicationLogic.Services.Plugin.Helper; using AAS.TwinEngine.DataEngine.ApplicationLogic.Services.Plugin.Providers; -using AAS.TwinEngine.DataEngine.ApplicationLogic.Services.Shared; using AAS.TwinEngine.DataEngine.ApplicationLogic.Services.SubmodelRegistry.Providers; using AAS.TwinEngine.DataEngine.Infrastructure.Configuration.LegacyV1; using AAS.TwinEngine.DataEngine.Infrastructure.Http.Authorization.Headers; From 24f95913b8d3f6211f08a03dbbda2f6463d73ddc Mon Sep 17 00:00:00 2001 From: Kevalkumar Date: Tue, 21 Apr 2026 02:46:54 +0530 Subject: [PATCH 54/71] Remove HttpRequestBaseUrlProviderTests class and associated unit tests --- .../Shared/HttpRequestBaseUrlProviderTests.cs | 144 ------------------ 1 file changed, 144 deletions(-) delete mode 100644 source/AAS.TwinEngine.DataEngine.UnitTests/Infrastructure/Shared/HttpRequestBaseUrlProviderTests.cs diff --git a/source/AAS.TwinEngine.DataEngine.UnitTests/Infrastructure/Shared/HttpRequestBaseUrlProviderTests.cs b/source/AAS.TwinEngine.DataEngine.UnitTests/Infrastructure/Shared/HttpRequestBaseUrlProviderTests.cs deleted file mode 100644 index 46efc122..00000000 --- a/source/AAS.TwinEngine.DataEngine.UnitTests/Infrastructure/Shared/HttpRequestBaseUrlProviderTests.cs +++ /dev/null @@ -1,144 +0,0 @@ -using AAS.TwinEngine.DataEngine.Infrastructure.Shared; -using AAS.TwinEngine.DataEngine.ServiceConfiguration.Config; - -using Microsoft.AspNetCore.Http; -using Microsoft.Extensions.Options; - -using NSubstitute; - -namespace AAS.TwinEngine.DataEngine.UnitTests.Infrastructure.Shared; - -public class HttpRequestBaseUrlProviderTests -{ - private readonly IHttpContextAccessor _httpContextAccessor = Substitute.For(); - - private static IOptions CreateOptions(Uri? baseUrl, string allowedHosts = "*") - { - return Options.Create(new GeneralConfig - { - DataEngineRepositoryBaseUrl = baseUrl, - AllowedHosts = allowedHosts - }); - } - - private static DefaultHttpContext CreateHttpContext(string scheme = "https", string host = "example.com") - { - var context = new DefaultHttpContext(); - context.Request.Scheme = scheme; - context.Request.Host = new HostString(host); - return context; - } - - [Fact] - public void GetBaseUrl_WithConfiguredUrl_ReturnsConfiguredValue() - { - // Arrange - var configuredUrl = new Uri("https://configured.com/"); - var options = CreateOptions(configuredUrl); - - var sut = new HttpRequestBaseUrlProvider(_httpContextAccessor, options); - - // Act - var result = sut.GetBaseUrl(); - - // Assert - Assert.Equal(configuredUrl, result); - } - - [Fact] - public void GetBaseUrl_WithDynamicUrl_ExtractsFromHttpRequest() - { - // Arrange - var context = CreateHttpContext("https", "mydomain.com"); - _httpContextAccessor.HttpContext.Returns(context); - - var options = CreateOptions(null); - var sut = new HttpRequestBaseUrlProvider(_httpContextAccessor, options); - - // Act - var result = sut.GetBaseUrl(); - - // Assert - Assert.Equal(new Uri("https://mydomain.com"), result); - } - - [Fact] - public void GetBaseUrl_WithNullHttpContext_ThrowsInvalidOperationException() - { - // Arrange - _httpContextAccessor.HttpContext.Returns((HttpContext?)null); - - var options = CreateOptions(null); - var sut = new HttpRequestBaseUrlProvider(_httpContextAccessor, options); - - // Act & Assert - var ex = Assert.Throws(() => sut.GetBaseUrl()); - Assert.Contains("No HTTP request context", ex.Message); - } - - [Fact] - public void GetBaseUrl_WithMaliciousHostHeader_ThrowsWhenAllowedHostsConfigured() - { - // Arrange - var context = CreateHttpContext("https", "evil.com"); - _httpContextAccessor.HttpContext.Returns(context); - - var options = CreateOptions(null, allowedHosts: "example.com;mydomain.com"); - var sut = new HttpRequestBaseUrlProvider(_httpContextAccessor, options); - - // Act & Assert - var ex = Assert.Throws(() => sut.GetBaseUrl()); - Assert.Contains("not in the configured AllowedHosts", ex.Message); - } - - [Fact] - public void GetBaseUrl_WithAllowedHost_ReturnsUrl() - { - // Arrange - var context = CreateHttpContext("https", "mydomain.com"); - _httpContextAccessor.HttpContext.Returns(context); - - var options = CreateOptions(null, allowedHosts: "example.com;mydomain.com"); - var sut = new HttpRequestBaseUrlProvider(_httpContextAccessor, options); - - // Act - var result = sut.GetBaseUrl(); - - // Assert - Assert.Equal(new Uri("https://mydomain.com"), result); - } - - [Fact] - public void GetBaseUrl_WithWildcardAllowedHosts_PermitsAnyHost() - { - // Arrange - var context = CreateHttpContext("https", "anyhost.com"); - _httpContextAccessor.HttpContext.Returns(context); - - var options = CreateOptions(null, allowedHosts: "*"); - var sut = new HttpRequestBaseUrlProvider(_httpContextAccessor, options); - - // Act - var result = sut.GetBaseUrl(); - - // Assert - Assert.Equal(new Uri("https://anyhost.com"), result); - } - - [Fact] - public void GetBaseUrl_WithDifferentCaseHost_PermitsHost() - { - // Arrange - var context = CreateHttpContext("https", "MyDomain.COM"); - _httpContextAccessor.HttpContext.Returns(context); - - var options = CreateOptions(null, allowedHosts: "mydomain.com"); - var sut = new HttpRequestBaseUrlProvider(_httpContextAccessor, options); - - // Act - var result = sut.GetBaseUrl(); - - // Assert - Assert.Equal(new Uri("https://MyDomain.COM"), result); - } -} From 6f05c535b98d121fae81ed9c98fe4e1ff0a819d7 Mon Sep 17 00:00:00 2001 From: Kevalkumar Date: Tue, 21 Apr 2026 03:00:47 +0530 Subject: [PATCH 55/71] Refactor exception handling in validation and extraction logic to throw InvalidDependencyException for null inputs --- ...iLanguagePropertySettingsValidatorTests.cs | 7 +++--- .../Extraction/SemanticTreeExtractorTests.cs | 14 +++++++---- .../SemanticIdHandlerTests.cs | 6 +++-- .../TemplateMappingRulesValidatorTests.cs | 6 ++--- .../LegacyConfigurationDetectorTests.cs | 5 ++-- .../Extraction/SemanticTreeExtractor.cs | 24 +++++++++++++++---- .../MultiLanguagePropertySettingsValidator.cs | 9 +++++-- .../LegacyV1/LegacyConfigurationDetector.cs | 8 +++++-- .../Config/TemplateMappingRulesValidator.cs | 8 +++++-- 9 files changed, 60 insertions(+), 27 deletions(-) diff --git a/source/AAS.TwinEngine.DataEngine.UnitTests/ApplicationLogic/Services/SubmodelRepository/Config/Helper/MultiLanguagePropertySettingsValidatorTests.cs b/source/AAS.TwinEngine.DataEngine.UnitTests/ApplicationLogic/Services/SubmodelRepository/Config/Helper/MultiLanguagePropertySettingsValidatorTests.cs index 34cde844..034bf19f 100644 --- a/source/AAS.TwinEngine.DataEngine.UnitTests/ApplicationLogic/Services/SubmodelRepository/Config/Helper/MultiLanguagePropertySettingsValidatorTests.cs +++ b/source/AAS.TwinEngine.DataEngine.UnitTests/ApplicationLogic/Services/SubmodelRepository/Config/Helper/MultiLanguagePropertySettingsValidatorTests.cs @@ -1,4 +1,5 @@ -using AAS.TwinEngine.DataEngine.Infrastructure.Configuration.LegacyV1; +using AAS.TwinEngine.DataEngine.ApplicationLogic.Exceptions.Application; +using AAS.TwinEngine.DataEngine.Infrastructure.Configuration.LegacyV1; namespace AAS.TwinEngine.DataEngine.UnitTests.ApplicationLogic.Services.SubmodelRepository.Config.Helper; @@ -122,8 +123,8 @@ public void Validate_WhitespaceInList_ReturnsFail() } [Fact] - public void Validate_NullOptions_ThrowsArgumentNullException() + public void Validate_NullOptions_ThrowsInValidDependencyException() { - Assert.Throws(() => _validator.Validate(null, null!)); + Assert.Throws(() => _validator.Validate(null, null!)); } } diff --git a/source/AAS.TwinEngine.DataEngine.UnitTests/ApplicationLogic/Services/SubmodelRepository/SemanticId/Extraction/SemanticTreeExtractorTests.cs b/source/AAS.TwinEngine.DataEngine.UnitTests/ApplicationLogic/Services/SubmodelRepository/SemanticId/Extraction/SemanticTreeExtractorTests.cs index 1e732063..26451e10 100644 --- a/source/AAS.TwinEngine.DataEngine.UnitTests/ApplicationLogic/Services/SubmodelRepository/SemanticId/Extraction/SemanticTreeExtractorTests.cs +++ b/source/AAS.TwinEngine.DataEngine.UnitTests/ApplicationLogic/Services/SubmodelRepository/SemanticId/Extraction/SemanticTreeExtractorTests.cs @@ -6,6 +6,8 @@ using AasCore.Aas3_0; +using Microsoft.Extensions.Logging; + using NSubstitute; using static Xunit.Assert; @@ -17,18 +19,20 @@ public class SemanticTreeExtractorTests private readonly SemanticTreeExtractor _sut; private readonly ISemanticIdResolver _resolver; private readonly ISubmodelElementHelper _elementHelper; + private readonly ILogger _logger; private readonly List _handlers; public SemanticTreeExtractorTests() { _resolver = Substitute.For(); + _logger = Substitute.For>(); _elementHelper = Substitute.For(); _handlers = []; - _sut = new SemanticTreeExtractor(_resolver, _elementHelper, _handlers); + _sut = new SemanticTreeExtractor(_resolver, _elementHelper, _handlers, _logger); } [Fact] - public void Extract_NullSubmodel_ThrowsArgumentNullException() => Throws(() => _sut.Extract(null!)); + public void Extract_NullSubmodel_ThrowsInvalidDependencyException() => Throws(() => _sut.Extract(null!)); [Fact] public void Extract_SubmodelWithNoElements_ReturnsRootNodeWithNoChildren() @@ -90,13 +94,13 @@ public void Extract_ElementWithNoHandler_CreatesLeafNodeFallback() } [Fact] - public void Extract_ByIdShortPath_NullSubmodel_ThrowsArgumentNullException() => Throws(() => _sut.Extract(null!, "path")); + public void Extract_ByIdShortPath_NullSubmodel_ThrowsInvalidDependencyException() => Throws(() => _sut.Extract(null!, "path")); [Fact] - public void Extract_ByIdShortPath_NullPath_ThrowsArgumentNullException() + public void Extract_ByIdShortPath_NullPath_ThrowsInvalidDependencyException() { var submodel = Substitute.For(); - Throws(() => _sut.Extract(submodel, null!)); + Throws(() => _sut.Extract(submodel, null!)); } [Fact] diff --git a/source/AAS.TwinEngine.DataEngine.UnitTests/ApplicationLogic/Services/SubmodelRepository/SemanticIdHandlerTests.cs b/source/AAS.TwinEngine.DataEngine.UnitTests/ApplicationLogic/Services/SubmodelRepository/SemanticIdHandlerTests.cs index 75c9b97f..184eb2e5 100644 --- a/source/AAS.TwinEngine.DataEngine.UnitTests/ApplicationLogic/Services/SubmodelRepository/SemanticIdHandlerTests.cs +++ b/source/AAS.TwinEngine.DataEngine.UnitTests/ApplicationLogic/Services/SubmodelRepository/SemanticIdHandlerTests.cs @@ -28,10 +28,12 @@ public class SemanticIdHandlerTests private readonly ILogger _fillerLogger; private readonly IOptions _pluginsConfig; private readonly IOptions _templateManagementConfig; + private readonly ILogger _logger; public SemanticIdHandlerTests() { _fillerLogger = Substitute.For>(); + _logger = Substitute.For>(); _pluginsConfig = Options.Create(new PluginsConfig { MultiLanguageProperty = new PluginMultiLanguagePropertyConfig @@ -681,7 +683,7 @@ public void FillOutTemplate_ShouldPreserveElement_WhenCollectionIsNull() } [Fact] - public void FillOutTemplate_ThrowsArgumentException_WhenElementTypeIsUnsupported() + public void FillOutTemplate_ThrowsInternalDataProcessingException_WhenElementTypeIsUnsupported() { var unsupportedElement = new Operation { @@ -868,7 +870,7 @@ private SemanticIdHandler CreateSut(IOptions pluginsConfig, IOpti new RelationshipElementHandler(resolver, referenceHelper), }; - var extractor = new SemanticTreeExtractor(resolver, helper, handlers); + var extractor = new SemanticTreeExtractor(resolver, helper, handlers, _logger); var filler = new SubmodelFiller(resolver, helper, handlers, _fillerLogger); return new SemanticIdHandler(extractor, filler); } diff --git a/source/AAS.TwinEngine.DataEngine.UnitTests/Infrastructure/Providers/TemplateProvider/Config/TemplateMappingRulesValidatorTests.cs b/source/AAS.TwinEngine.DataEngine.UnitTests/Infrastructure/Providers/TemplateProvider/Config/TemplateMappingRulesValidatorTests.cs index 8e43161a..ec41cd9d 100644 --- a/source/AAS.TwinEngine.DataEngine.UnitTests/Infrastructure/Providers/TemplateProvider/Config/TemplateMappingRulesValidatorTests.cs +++ b/source/AAS.TwinEngine.DataEngine.UnitTests/Infrastructure/Providers/TemplateProvider/Config/TemplateMappingRulesValidatorTests.cs @@ -1,3 +1,4 @@ +using AAS.TwinEngine.DataEngine.ApplicationLogic.Exceptions.Application; using AAS.TwinEngine.DataEngine.Infrastructure.Providers.TemplateProvider.Config; using AAS.TwinEngine.DataEngine.ServiceConfiguration.Config; @@ -204,10 +205,7 @@ public void Validate_InvalidValidationPattern_Fails() // ── Null options → throws ── [Fact] - public void Validate_NullOptions_ThrowsArgumentNullException() - { - Assert.Throws(() => _sut.Validate(null, null!)); - } + public void Validate_NullOptions_ThrowsInvalidDependencyException() => Assert.Throws(() => _sut.Validate(null, null!)); // ── Uses Description in error message when available ── diff --git a/source/AAS.TwinEngine.DataEngine.UnitTests/ServiceConfiguration/ConfigurationMigration/LegacyConfigurationDetectorTests.cs b/source/AAS.TwinEngine.DataEngine.UnitTests/ServiceConfiguration/ConfigurationMigration/LegacyConfigurationDetectorTests.cs index 6519848f..b9ea1d88 100644 --- a/source/AAS.TwinEngine.DataEngine.UnitTests/ServiceConfiguration/ConfigurationMigration/LegacyConfigurationDetectorTests.cs +++ b/source/AAS.TwinEngine.DataEngine.UnitTests/ServiceConfiguration/ConfigurationMigration/LegacyConfigurationDetectorTests.cs @@ -1,4 +1,5 @@ -using AAS.TwinEngine.DataEngine.Infrastructure.Configuration.LegacyV1; +using AAS.TwinEngine.DataEngine.ApplicationLogic.Exceptions.Application; +using AAS.TwinEngine.DataEngine.Infrastructure.Configuration.LegacyV1; using Microsoft.Extensions.Configuration; @@ -63,7 +64,7 @@ public void IsV1Configuration_EmptyConfig_ReturnsTrue() } [Fact] - public void IsV1Configuration_NullConfig_ThrowsArgumentNullException() => Assert.Throws(() => LegacyConfigurationDetector.IsV1Configuration(null!)); + public void IsV1Configuration_NullConfig_ThrowsInvalidDependencyException() => Assert.Throws(() => LegacyConfigurationDetector.IsV1Configuration(null!)); private static IConfiguration BuildConfig(Dictionary values) { diff --git a/source/AAS.TwinEngine.DataEngine/ApplicationLogic/Services/SubmodelRepository/SemanticId/Extraction/SemanticTreeExtractor.cs b/source/AAS.TwinEngine.DataEngine/ApplicationLogic/Services/SubmodelRepository/SemanticId/Extraction/SemanticTreeExtractor.cs index a3b53514..961dc940 100644 --- a/source/AAS.TwinEngine.DataEngine/ApplicationLogic/Services/SubmodelRepository/SemanticId/Extraction/SemanticTreeExtractor.cs +++ b/source/AAS.TwinEngine.DataEngine/ApplicationLogic/Services/SubmodelRepository/SemanticId/Extraction/SemanticTreeExtractor.cs @@ -10,11 +10,15 @@ namespace AAS.TwinEngine.DataEngine.ApplicationLogic.Services.SubmodelRepository public class SemanticTreeExtractor( ISemanticIdResolver semanticIdResolver, ISubmodelElementHelper elementHelper, - IEnumerable handlers) : ISemanticTreeExtractor + IEnumerable handlers, + ILogger logger) : ISemanticTreeExtractor { public SemanticTreeNode Extract(ISubmodel submodelTemplate) { - ArgumentNullException.ThrowIfNull(submodelTemplate); + if (submodelTemplate == null) + { + throw new InvalidDependencyException(nameof(submodelTemplate), logger); + } var rootNode = new SemanticBranchNode(semanticIdResolver.ResolveSemanticId(submodelTemplate, submodelTemplate.IdShort!), Cardinality.Unknown); var childNodes = submodelTemplate.SubmodelElements! @@ -32,8 +36,15 @@ public SemanticTreeNode Extract(ISubmodel submodelTemplate) public ISubmodelElement Extract(ISubmodel submodelTemplate, string idShortPath) { - ArgumentNullException.ThrowIfNull(submodelTemplate); - ArgumentNullException.ThrowIfNull(idShortPath); + if (submodelTemplate == null) + { + throw new InvalidDependencyException(nameof(submodelTemplate), logger); + } + + if (idShortPath == null) + { + throw new InvalidDependencyException(nameof(idShortPath), logger); + } var currentSubmodelElements = submodelTemplate.SubmodelElements; var idShortPathSegments = idShortPath.Split('.'); @@ -58,7 +69,10 @@ public ISubmodelElement Extract(ISubmodel submodelTemplate, string idShortPath) public SemanticTreeNode? ExtractElement(ISubmodelElement element) { - ArgumentNullException.ThrowIfNull(element); + if (element == null) + { + throw new InvalidDependencyException(nameof(element), logger); + } var handler = handlers.FirstOrDefault(h => h.CanHandle(element)); return handler != null diff --git a/source/AAS.TwinEngine.DataEngine/Infrastructure/Configuration/LegacyV1/ConfigV1/MultiLanguagePropertySettingsValidator.cs b/source/AAS.TwinEngine.DataEngine/Infrastructure/Configuration/LegacyV1/ConfigV1/MultiLanguagePropertySettingsValidator.cs index d7a6051e..8d98f89b 100644 --- a/source/AAS.TwinEngine.DataEngine/Infrastructure/Configuration/LegacyV1/ConfigV1/MultiLanguagePropertySettingsValidator.cs +++ b/source/AAS.TwinEngine.DataEngine/Infrastructure/Configuration/LegacyV1/ConfigV1/MultiLanguagePropertySettingsValidator.cs @@ -1,5 +1,7 @@ using System.Text.RegularExpressions; +using AAS.TwinEngine.DataEngine.ApplicationLogic.Exceptions.Application; + using Microsoft.Extensions.Options; namespace AAS.TwinEngine.DataEngine.Infrastructure.Configuration.LegacyV1; @@ -10,7 +12,10 @@ public partial class MultiLanguagePropertySettingsValidator : IValidateOptions 0) { return ValidateOptionsResult.Fail( - $"Invalid BCP-47 language tag(s) in {MultiLanguagePropertySettings.Section}.DefaultLanguages: " + + $"Invalid BCP-47 or Null language tag(s) in {MultiLanguagePropertySettings.Section}.DefaultLanguages: " + $"{string.Join(", ", invalidLanguages)}. Note: Use hyphens (-) not underscores (_)."); } diff --git a/source/AAS.TwinEngine.DataEngine/Infrastructure/Configuration/LegacyV1/LegacyConfigurationDetector.cs b/source/AAS.TwinEngine.DataEngine/Infrastructure/Configuration/LegacyV1/LegacyConfigurationDetector.cs index 895eb413..25c20f3e 100644 --- a/source/AAS.TwinEngine.DataEngine/Infrastructure/Configuration/LegacyV1/LegacyConfigurationDetector.cs +++ b/source/AAS.TwinEngine.DataEngine/Infrastructure/Configuration/LegacyV1/LegacyConfigurationDetector.cs @@ -1,4 +1,5 @@ -using AAS.TwinEngine.DataEngine.ServiceConfiguration.Config; +using AAS.TwinEngine.DataEngine.ApplicationLogic.Exceptions.Application; +using AAS.TwinEngine.DataEngine.ServiceConfiguration.Config; namespace AAS.TwinEngine.DataEngine.Infrastructure.Configuration.LegacyV1; @@ -12,7 +13,10 @@ public static class LegacyConfigurationDetector { public static bool IsV1Configuration(IConfiguration configuration) { - ArgumentNullException.ThrowIfNull(configuration); + if (configuration == null) + { + throw new InvalidDependencyException(nameof(configuration)); + } // V2 introduces these grouped top-level sections; if any exists → V2 var isV2 = configuration.GetSection(GeneralConfig.Section).Exists() diff --git a/source/AAS.TwinEngine.DataEngine/Infrastructure/Providers/TemplateProvider/Config/TemplateMappingRulesValidator.cs b/source/AAS.TwinEngine.DataEngine/Infrastructure/Providers/TemplateProvider/Config/TemplateMappingRulesValidator.cs index 6beb9c34..583ca032 100644 --- a/source/AAS.TwinEngine.DataEngine/Infrastructure/Providers/TemplateProvider/Config/TemplateMappingRulesValidator.cs +++ b/source/AAS.TwinEngine.DataEngine/Infrastructure/Providers/TemplateProvider/Config/TemplateMappingRulesValidator.cs @@ -1,5 +1,6 @@ using System.Text.RegularExpressions; +using AAS.TwinEngine.DataEngine.ApplicationLogic.Exceptions.Application; using AAS.TwinEngine.DataEngine.ServiceConfiguration.Config; using Microsoft.Extensions.Options; @@ -10,7 +11,10 @@ public class TemplateMappingRulesValidator : IValidateOptions Date: Tue, 21 Apr 2026 10:18:51 +0530 Subject: [PATCH 56/71] Update exception handling in TemplateMappingRulesValidator to catch ArgumentException instead of InvalidDependencyException --- .../Config/TemplateMappingRulesValidatorTests.cs | 2 -- .../TemplateProvider/Config/TemplateMappingRulesValidator.cs | 2 +- 2 files changed, 1 insertion(+), 3 deletions(-) diff --git a/source/AAS.TwinEngine.DataEngine.UnitTests/Infrastructure/Providers/TemplateProvider/Config/TemplateMappingRulesValidatorTests.cs b/source/AAS.TwinEngine.DataEngine.UnitTests/Infrastructure/Providers/TemplateProvider/Config/TemplateMappingRulesValidatorTests.cs index ec41cd9d..83907170 100644 --- a/source/AAS.TwinEngine.DataEngine.UnitTests/Infrastructure/Providers/TemplateProvider/Config/TemplateMappingRulesValidatorTests.cs +++ b/source/AAS.TwinEngine.DataEngine.UnitTests/Infrastructure/Providers/TemplateProvider/Config/TemplateMappingRulesValidatorTests.cs @@ -2,8 +2,6 @@ using AAS.TwinEngine.DataEngine.Infrastructure.Providers.TemplateProvider.Config; using AAS.TwinEngine.DataEngine.ServiceConfiguration.Config; -using Microsoft.Extensions.Options; - namespace AAS.TwinEngine.DataEngine.UnitTests.Infrastructure.Providers.TemplateProvider.Config; public class TemplateMappingRulesValidatorTests diff --git a/source/AAS.TwinEngine.DataEngine/Infrastructure/Providers/TemplateProvider/Config/TemplateMappingRulesValidator.cs b/source/AAS.TwinEngine.DataEngine/Infrastructure/Providers/TemplateProvider/Config/TemplateMappingRulesValidator.cs index 583ca032..240d2caa 100644 --- a/source/AAS.TwinEngine.DataEngine/Infrastructure/Providers/TemplateProvider/Config/TemplateMappingRulesValidator.cs +++ b/source/AAS.TwinEngine.DataEngine/Infrastructure/Providers/TemplateProvider/Config/TemplateMappingRulesValidator.cs @@ -138,7 +138,7 @@ private static bool TryCompileRegex(string pattern, out string? error) error = null; return true; } - catch (InvalidDependencyException ex) + catch (ArgumentException ex) { error = ex.Message; return false; From 1f0658e0f32a2552582a857f042e66ccde88f7a4 Mon Sep 17 00:00:00 2001 From: Kevalkumar Date: Tue, 21 Apr 2026 11:11:43 +0530 Subject: [PATCH 57/71] Update AasIdExtractionRules to adjust index for split strategy --- source/AAS.TwinEngine.DataEngine/appsettings.development.json | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/source/AAS.TwinEngine.DataEngine/appsettings.development.json b/source/AAS.TwinEngine.DataEngine/appsettings.development.json index 0e3d59f3..83985cbd 100644 --- a/source/AAS.TwinEngine.DataEngine/appsettings.development.json +++ b/source/AAS.TwinEngine.DataEngine/appsettings.development.json @@ -169,9 +169,7 @@ { "Strategy": "Split", "Pattern": "/", - "Index": 5, - "EndIndex": 6, - "ValidationPattern": "^[0-9\\-/]+$", + "Index": 6, "Description": "Fallback: split by '/' and join segments 5-6" } ] From faba9f2c2328bf8242e33d5fb1a6b5e4a653b48c Mon Sep 17 00:00:00 2001 From: Hardi Shah Date: Fri, 24 Apr 2026 10:06:40 +0530 Subject: [PATCH 58/71] Upgrade JsonSchema.Net to v9, support JSON Schema 2020-12 - Update JsonSchema.Net to v9 and migrate all schema handling to JSON Schema Draft 2020-12 (`$defs`, new evaluation API, etc.) - Update all dependencies to latest versions (xunit, Serilog, OpenTelemetry, NSwag, FluentValidation, etc.) - Refactor schema generation and validation logic for new draft and library API - Remove obsolete schema registry logic; improve normalization and error handling - Expand and modernize test suites, add coverage for edge cases and deep nesting - Use System.Text.Json.Nodes for schema parsing and traversal - Clean up code, improve assertion style, and ensure compatibility with new libraries --- ...S.TwinEngine.DataEngine.ModuleTests.csproj | 16 +- ...AAS.TwinEngine.DataEngine.UnitTests.csproj | 18 +- .../SemanticIdHandlerTests.cs | 15 +- .../CleanArchitectureTests.cs | 43 +- .../Helper/JsonSchemaGeneratorTests.cs | 369 ++++++++++------- .../Helper/JsonSchemaValidatorTests.cs | 251 ++++++++++-- .../AAS.TwinEngine.DataEngine.csproj | 43 +- .../Helper/JsonSchemaGenerator.cs | 90 ++-- .../Helper/JsonSchemaValidator.cs | 137 +++---- ...nEngine.Plugin.TestPlugin.UnitTests.csproj | 18 +- .../Services/JsonSchemaParserTests.cs | 387 +++++++----------- .../Services/JsonSchemaValidatorTests.cs | 379 ++++++++++++++--- .../CleanArchitectureTests.cs | 38 +- .../AAS.TwinEngine.Plugin.TestPlugin.csproj | 30 +- .../Api/Submodel/Services/JsonSchemaParser.cs | 191 +++++---- .../Submodel/Services/JsonSchemaValidator.cs | 102 +++-- 16 files changed, 1289 insertions(+), 838 deletions(-) diff --git a/source/AAS.TwinEngine.DataEngine.ModuleTests/AAS.TwinEngine.DataEngine.ModuleTests.csproj b/source/AAS.TwinEngine.DataEngine.ModuleTests/AAS.TwinEngine.DataEngine.ModuleTests.csproj index 291f284e..5e6f690b 100644 --- a/source/AAS.TwinEngine.DataEngine.ModuleTests/AAS.TwinEngine.DataEngine.ModuleTests.csproj +++ b/source/AAS.TwinEngine.DataEngine.ModuleTests/AAS.TwinEngine.DataEngine.ModuleTests.csproj @@ -10,12 +10,18 @@ - - - + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + - - + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + diff --git a/source/AAS.TwinEngine.DataEngine.UnitTests/AAS.TwinEngine.DataEngine.UnitTests.csproj b/source/AAS.TwinEngine.DataEngine.UnitTests/AAS.TwinEngine.DataEngine.UnitTests.csproj index 379fcad9..e06a0d83 100644 --- a/source/AAS.TwinEngine.DataEngine.UnitTests/AAS.TwinEngine.DataEngine.UnitTests.csproj +++ b/source/AAS.TwinEngine.DataEngine.UnitTests/AAS.TwinEngine.DataEngine.UnitTests.csproj @@ -1,4 +1,4 @@ - + net8.0 @@ -15,12 +15,18 @@ - - + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + - - - + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + diff --git a/source/AAS.TwinEngine.DataEngine.UnitTests/ApplicationLogic/Services/SubmodelRepository/SemanticIdHandlerTests.cs b/source/AAS.TwinEngine.DataEngine.UnitTests/ApplicationLogic/Services/SubmodelRepository/SemanticIdHandlerTests.cs index 184eb2e5..58ff65a3 100644 --- a/source/AAS.TwinEngine.DataEngine.UnitTests/ApplicationLogic/Services/SubmodelRepository/SemanticIdHandlerTests.cs +++ b/source/AAS.TwinEngine.DataEngine.UnitTests/ApplicationLogic/Services/SubmodelRepository/SemanticIdHandlerTests.cs @@ -12,8 +12,6 @@ using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; -using MongoDB.Bson; - using NSubstitute; using static Xunit.Assert; @@ -611,7 +609,7 @@ public void FillOutTemplate_ShouldNotChange_RelationShipElementWhenExternalRefer Single(result!.SubmodelElements!); IsType(result!.SubmodelElements![0]); var relationShipElement = result.SubmodelElements[0] as RelationshipElement; - Equal(TestData.CreateRelationshipElementWithBothExternalReference().ToJson(), relationShipElement!.ToJson()); + AssertElementsJsonEquivalent(TestData.CreateRelationshipElementWithBothExternalReference(), relationShipElement!); } [Fact] @@ -628,7 +626,7 @@ public void FillOutTemplate_RelationShipElementHaveOneModelReference_ProvidesAll Single(result!.SubmodelElements!); IsType(result!.SubmodelElements![0]); var relationShipElement = result.SubmodelElements[0] as RelationshipElement; - Equal(TestData.CreateFilledRelationshipElementWithOneExternalReferenceAndOneModelReference().ToJson(), relationShipElement!.ToJson()); + AssertElementsJsonEquivalent(TestData.CreateFilledRelationshipElementWithOneExternalReferenceAndOneModelReference(), relationShipElement!); } [Fact] @@ -645,7 +643,7 @@ public void FillOutTemplate_RelationShipElementBothModelReference_HandlesMissing Single(result!.SubmodelElements!); IsType(result!.SubmodelElements![0]); var relationShipElement = result.SubmodelElements[0] as RelationshipElement; - Equal(TestData.CreateFilledRelationshipElementWithBothModelReference().ToJson(), relationShipElement!.ToJson()); + AssertElementsJsonEquivalent(TestData.CreateFilledRelationshipElementWithBothModelReference(), relationShipElement!); } [Fact] @@ -889,5 +887,12 @@ private static IOptions CreatePluginsConfigWithMlp(List? SubmodelElementIndexContextPrefix = "_aastwinengineindex_" }); } + + private static void AssertElementsJsonEquivalent(IClass expected, IClass actual) + { + var expectedJson = Jsonization.Serialize.ToJsonObject(expected).ToJsonString(); + var actualJson = Jsonization.Serialize.ToJsonObject(actual).ToJsonString(); + Equal(expectedJson, actualJson); + } } diff --git a/source/AAS.TwinEngine.DataEngine.UnitTests/CleanArchitectureTests.cs b/source/AAS.TwinEngine.DataEngine.UnitTests/CleanArchitectureTests.cs index 205a3995..8a2a6587 100644 --- a/source/AAS.TwinEngine.DataEngine.UnitTests/CleanArchitectureTests.cs +++ b/source/AAS.TwinEngine.DataEngine.UnitTests/CleanArchitectureTests.cs @@ -14,26 +14,17 @@ public class CleanArchitectureTests { private const string BaseNamespace = "AAS.TwinEngine.DataEngine"; - private readonly Architecture _architecture; - private readonly IObjectProvider _apiLayer; - private readonly IObjectProvider _applicationLogicLayer; - private readonly IObjectProvider _domainModelLayer; - private readonly IObjectProvider _infrastructureLayer; - - public CleanArchitectureTests() - { - _architecture = new ArchLoader().LoadAssemblies(System.Reflection.Assembly.Load(BaseNamespace)).Build(); - - _apiLayer = Types().That().ResideInNamespace($"{BaseNamespace}.Api.*", true).As("Api"); - _applicationLogicLayer = Types().That().ResideInNamespace($"{BaseNamespace}.ApplicationLogic.*", true).As("ApplicationLogic"); - _domainModelLayer = Types().That().ResideInNamespace($"{BaseNamespace}.DomainModel*", true).As("DomainModel"); - _infrastructureLayer = Types().That().ResideInNamespace($"{BaseNamespace}.Infrastructure.*", true).As("Infrastructure"); - } + private readonly Architecture _architecture = new ArchLoader().LoadAssemblies(System.Reflection.Assembly.Load(BaseNamespace)).Build(); + private readonly IObjectProvider _apiLayer = Types().That().ResideInNamespaceMatching($"{BaseNamespace}\\.Api($|\\..*)").As("Api"); + private readonly IObjectProvider _applicationLogicLayer = Types().That().ResideInNamespaceMatching($"{BaseNamespace}\\.ApplicationLogic($|\\..*)").As("ApplicationLogic"); + private readonly IObjectProvider _domainModelLayer = Types().That().ResideInNamespaceMatching($"{BaseNamespace}\\.DomainModel.*").As("DomainModel"); + private readonly IObjectProvider _infrastructureLayer = Types().That().ResideInNamespaceMatching($"{BaseNamespace}\\.Infrastructure($|\\..*)").As("Infrastructure"); [Fact] public void DomainModelShallNotHaveExternalDependencies() { var forbiddenTypes = new List(); + forbiddenTypes.AddRange(_infrastructureLayer.GetObjects(_architecture)); forbiddenTypes.AddRange(_apiLayer.GetObjects(_architecture)); forbiddenTypes.AddRange(_applicationLogicLayer.GetObjects(_architecture)); @@ -41,6 +32,7 @@ public void DomainModelShallNotHaveExternalDependencies() Types().That().Are(_domainModelLayer) .Should() .NotDependOnAny(Types().That().Are(forbiddenTypes)) + .WithoutRequiringPositiveResults() .Check(_architecture); } @@ -50,6 +42,7 @@ public void ApplicationLogicShallNotHaveDependenciesToInfrastructure() Types().That().Are(_applicationLogicLayer) .Should() .NotDependOnAny(Types().That().Are(_infrastructureLayer)) + .WithoutRequiringPositiveResults() .Check(_architecture); } @@ -59,6 +52,7 @@ public void ApplicationLogicShallNotHaveDependenciesToApi() Types().That().Are(_applicationLogicLayer) .Should() .NotDependOnAny(Types().That().Are(_apiLayer)) + .WithoutRequiringPositiveResults() .Check(_architecture); } @@ -68,6 +62,7 @@ public void InfrastructureShallNotHaveDependenciesToApi() Types().That().Are(_infrastructureLayer) .Should() .NotDependOnAny(Types().That().Are(_apiLayer)) + .WithoutRequiringPositiveResults() .Check(_architecture); } @@ -75,16 +70,17 @@ public void InfrastructureShallNotHaveDependenciesToApi() public void ApiShallNotHaveDependenciesToInfrastructure() { Types().That().Are(_apiLayer) - .Should() - .NotDependOnAny(Types().That().Are(_infrastructureLayer)) - .Check(_architecture); + .Should() + .NotDependOnAny(Types().That().Are(_infrastructureLayer)) + .WithoutRequiringPositiveResults() + .Check(_architecture); } [Fact] public void RepositoryClassesShallBeInCorrectNamespace() { Classes().That().HaveNameEndingWith("Repository").Should() - .ResideInNamespace($"{BaseNamespace}.Infrastructure.Providers*", true) + .ResideInNamespaceMatching($"{BaseNamespace}\\.Infrastructure\\.DataAccess.*") .WithoutRequiringPositiveResults() .Check(_architecture); } @@ -97,7 +93,7 @@ public void RepositoryInterfacesShallBeInCorrectNamespace() .And() .DoNotHaveFullName($"{BaseNamespace}.Infrastructure.DataAccess.GenericRepository.IMongoDbRepository") .Should() - .ResideInNamespace($"{BaseNamespace}.ApplicationLogic.*", true) + .ResideInNamespaceMatching($"{BaseNamespace}\\.ApplicationLogic($|\\..*).") .WithoutRequiringPositiveResults() .Check(_architecture); } @@ -106,7 +102,7 @@ public void RepositoryInterfacesShallBeInCorrectNamespace() public void ServicesShallBeInCorrectNamespace() { Classes().That().HaveNameEndingWith("Service").Should() - .ResideInNamespace($"{BaseNamespace}.ApplicationLogic.Service.*", true) + .ResideInNamespaceMatching($"{BaseNamespace}\\.ApplicationLogic\\.Services($|\\..*).") .Check(_architecture); } @@ -115,7 +111,7 @@ public void ServiceInterfacesShallBeInCorrectNamespace() { Interfaces().That().HaveNameEndingWith("Service") .Should() - .ResideInNamespace($"{BaseNamespace}.ApplicationLogic.Service.*", true) + .ResideInNamespaceMatching($"{BaseNamespace}\\.ApplicationLogic\\.Services($|\\..*).") .Check(_architecture); } @@ -123,7 +119,8 @@ public void ServiceInterfacesShallBeInCorrectNamespace() public void ControllerShallBeInCorrectNamespace() { Classes().That().HaveNameEndingWith("Controller").Should() - .ResideInNamespace($"{BaseNamespace}.Api.*", true) + .ResideInNamespaceMatching($"{BaseNamespace}\\.Api($|\\..*).") .Check(_architecture); } } + diff --git a/source/AAS.TwinEngine.DataEngine.UnitTests/Infrastructure/Providers/PluginDataProvider/Helper/JsonSchemaGeneratorTests.cs b/source/AAS.TwinEngine.DataEngine.UnitTests/Infrastructure/Providers/PluginDataProvider/Helper/JsonSchemaGeneratorTests.cs index 1c88f6d7..57e1db6e 100644 --- a/source/AAS.TwinEngine.DataEngine.UnitTests/Infrastructure/Providers/PluginDataProvider/Helper/JsonSchemaGeneratorTests.cs +++ b/source/AAS.TwinEngine.DataEngine.UnitTests/Infrastructure/Providers/PluginDataProvider/Helper/JsonSchemaGeneratorTests.cs @@ -1,4 +1,7 @@ -using AAS.TwinEngine.DataEngine.ApplicationLogic.Exceptions.Application; +using System.Text.Json; +using System.Text.Json.Nodes; + +using AAS.TwinEngine.DataEngine.ApplicationLogic.Exceptions.Application; using AAS.TwinEngine.DataEngine.DomainModel.SubmodelRepository; using AAS.TwinEngine.DataEngine.Infrastructure.Providers.PluginDataProvider.Helper; @@ -12,39 +15,31 @@ public class JsonSchemaGeneratorTests public void ConvertToJsonSchema_LeafNode_ReturnSchema() { const string SemanticId = "http://example.com/idta/digital-nameplate/contact-list/Name"; - var leaf = new SemanticLeafNode( - semanticId: "http://example.com/idta/digital-nameplate/contact-list/Name", - value: "", - dataType: DataType.String, - cardinality: Cardinality.One - ); + var leaf = new SemanticLeafNode(SemanticId, "", DataType.String, Cardinality.One); var result = JsonSchemaGenerator.ConvertToJsonSchema(leaf); - var typeKeyword = result.Keywords!.OfType().SingleOrDefault(); - Assert.Equal(SchemaValueType.Object, typeKeyword!.Type); - var propKeyword = result.Keywords!.OfType().SingleOrDefault(); - Assert.True(propKeyword!.Properties.ContainsKey(SemanticId)); - var leafSchema = propKeyword.Properties[SemanticId]; - var leafTypeKeyword = leafSchema.Keywords!.OfType().SingleOrDefault(); - Assert.NotNull(leafTypeKeyword); - Assert.Equal(SchemaValueType.String, leafTypeKeyword.Type); + var json = ToJson(result); + Assert.Equal("object", json["type"]!.ToString()); + var props = json["properties"]!; + Assert.NotNull(props[SemanticId]); + var leafSchema = props[SemanticId]; + Assert.Equal("string", leafSchema!["type"]!.ToString()); } [Fact] public void ConvertToJsonSchema_OptionalLeafNode_IsNotRequired() { const string SemanticId = "http://example.com/optional"; + var leaf = new SemanticLeafNode(SemanticId, "", DataType.String, Cardinality.ZeroToOne); var schema = JsonSchemaGenerator.ConvertToJsonSchema(leaf); + var json = ToJson(schema); - var propKeyword = schema.Keywords!.OfType().SingleOrDefault(); - Assert.NotNull(propKeyword); - Assert.True(propKeyword.Properties.ContainsKey(SemanticId)); - - var requiredKeyword = schema.Keywords!.OfType().SingleOrDefault(); - Assert.Null(requiredKeyword); + var props = json["properties"]!; + Assert.NotNull(props[SemanticId]); + Assert.Null(json["required"]); } [Fact] @@ -53,44 +48,25 @@ public void ConvertToJsonSchema_BranchNodeWithOneCardinality_ReturnsObjectSchema const string SemanticId = "http://example.com/idta/digital-nameplate/contact-list"; const string NameId = "http://example.com/idta/digital-nameplate/contact-list/Name"; const string WeightId = "http://example.com/idta/digital-nameplate/contact-list/Weight"; - var branch = new SemanticBranchNode( - semanticId: "http://example.com/idta/digital-nameplate/contact-list", - cardinality: Cardinality.One - ); - branch.AddChild(new SemanticLeafNode( - "http://example.com/idta/digital-nameplate/contact-list/Name", "", DataType.String, Cardinality.One)); - branch.AddChild(new SemanticLeafNode( - "http://example.com/idta/digital-nameplate/contact-list/Weight", null!, DataType.Integer, Cardinality.ZeroToOne)); + var branch = new SemanticBranchNode(SemanticId, Cardinality.One); + branch.AddChild(new SemanticLeafNode(NameId, "", DataType.String, Cardinality.One)); + branch.AddChild(new SemanticLeafNode(WeightId, null!, DataType.Integer, Cardinality.ZeroToOne)); var result = JsonSchemaGenerator.ConvertToJsonSchema(branch); - var typeKeyword = result.Keywords!.OfType().SingleOrDefault(); - Assert.Equal(SchemaValueType.Object, typeKeyword!.Type); - var propKeyword = result.Keywords!.OfType().SingleOrDefault(); - Assert.True(propKeyword!.Properties.ContainsKey(SemanticId)); - var branchSchema = propKeyword.Properties[SemanticId]; - var branchType = branchSchema.Keywords!.OfType().SingleOrDefault(); - Assert.Equal(SchemaValueType.Object, branchType!.Type); - - var branchProps = branchSchema.Keywords!.OfType().SingleOrDefault(); - Assert.NotNull(branchProps); - Assert.True(branchProps.Properties.ContainsKey(NameId)); - Assert.True(branchProps.Properties.ContainsKey(WeightId)); - - var nameSchema = branchProps.Properties[NameId]; - var nameType = nameSchema.Keywords!.OfType().SingleOrDefault(); - Assert.NotNull(nameType); - Assert.Equal(SchemaValueType.String, nameType.Type); - - var weightSchema = branchProps.Properties[WeightId]; - var weightType = weightSchema.Keywords!.OfType().SingleOrDefault(); - Assert.NotNull(weightType); - Assert.Equal(SchemaValueType.Integer, weightType.Type); - - var requiredKeyword = branchSchema.Keywords!.OfType().SingleOrDefault(); - Assert.NotNull(requiredKeyword); - Assert.Contains(NameId, requiredKeyword.Properties); - Assert.DoesNotContain(WeightId, requiredKeyword.Properties); + var json = ToJson(result); + Assert.Equal("object", json["type"]!.ToString()); + var rootProps = json["properties"]!; + var branchSchema = rootProps[SemanticId]!; + Assert.Equal("object", branchSchema["type"]!.ToString()); + var branchProps = branchSchema["properties"]!; + Assert.NotNull(branchProps[NameId]); + Assert.NotNull(branchProps[WeightId]); + Assert.Equal("string", branchProps[NameId]!["type"]!.ToString()); + Assert.Equal("integer", branchProps[WeightId]!["type"]!.ToString()); + var required = branchSchema["required"]!.AsArray(); + Assert.Contains(NameId, required.Select(x => x!.ToString())); + Assert.DoesNotContain(WeightId, required.Select(x => x!.ToString())); } [Fact] @@ -98,97 +74,57 @@ public void ConvertToJsonSchema_BranchNodeWithZeroToManyCardinality_ReturnsArray { const string SemanticId = "http://example.com/idta/digital-nameplate/contact-list"; const string NameId = "http://example.com/idta/digital-nameplate/contact-list/Name"; - var branchNode = new SemanticBranchNode("http://example.com/idta/digital-nameplate/contact-list", Cardinality.ZeroToMany); - branchNode.AddChild(new SemanticLeafNode( - "http://example.com/idta/digital-nameplate/contact-list/Name", "", DataType.String, Cardinality.One)); + var branchNode = new SemanticBranchNode(SemanticId, Cardinality.ZeroToMany); + branchNode.AddChild(new SemanticLeafNode(NameId, "", DataType.String, Cardinality.One)); var result = JsonSchemaGenerator.ConvertToJsonSchema(branchNode); - var typeKeyword = result.Keywords!.OfType().SingleOrDefault(); - Assert.Equal(SchemaValueType.Object, typeKeyword!.Type); - - var propKeyword = result.Keywords!.OfType().SingleOrDefault(); - Assert.True(propKeyword!.Properties.ContainsKey(SemanticId)); - - var arraySchema = propKeyword.Properties[SemanticId]; - var arrayType = arraySchema.Keywords!.OfType().SingleOrDefault(); - Assert.Equal(SchemaValueType.Array, arrayType!.Type); - - var arraySchemaProps = arraySchema.Keywords!.OfType().SingleOrDefault(); - Assert.True(arraySchemaProps!.Properties.ContainsKey(NameId)); - var nameSchema = arraySchemaProps.Properties[NameId]; - var nameType = nameSchema.Keywords!.OfType().SingleOrDefault(); - Assert.NotNull(nameType); - Assert.Equal(SchemaValueType.String, nameType.Type); - - var arrayRequiredProps = arraySchema.Keywords!.OfType().SingleOrDefault(); - var contains = arrayRequiredProps!.Properties.Contains(NameId); - - Assert.True(contains); + var json = ToJson(result); + Assert.Equal("object", json["type"]!.ToString()); + var rootProps = json["properties"]!; + var arraySchema = rootProps[SemanticId]!; + Assert.Equal("array", arraySchema["type"]!.ToString()); + var items = arraySchema["items"]!; + var props = items["properties"]!; + Assert.True(props[NameId] != null); + Assert.Equal("string", props[NameId]!["type"]!.ToString()); + var required = items["required"]!.AsArray(); + Assert.Contains(NameId, required.Select(x => x!.ToString())); } [Fact] - public void ConvertToJsonSchema_NestedBranchNodes_UsesRefAndDefinitionsCorrectly() + public void ConvertToJsonSchema_NestedBranchNodes_UsesRefAndDefsCorrectly() { const string RootSemanticId = "http://example.com/idta/digital-nameplate"; const string BranchSemanticId = "http://example.com/idta/digital-nameplate/contact-list"; const string NameId = "http://example.com/idta/digital-nameplate/contact-list/Name"; - var root = new SemanticBranchNode("http://example.com/idta/digital-nameplate", Cardinality.Unknown); - var childBranch = new SemanticBranchNode("http://example.com/idta/digital-nameplate/contact-list", Cardinality.One); - childBranch.AddChild(new SemanticLeafNode("http://example.com/idta/digital-nameplate/contact-list/Name", "", DataType.String, Cardinality.One)); + var root = new SemanticBranchNode(RootSemanticId, Cardinality.Unknown); + var childBranch = new SemanticBranchNode(BranchSemanticId, Cardinality.One); + childBranch.AddChild(new SemanticLeafNode(NameId, "", DataType.String, Cardinality.One)); root.AddChild(childBranch); var schema = JsonSchemaGenerator.ConvertToJsonSchema(root); - - var typeKeyword = schema.Keywords!.OfType().SingleOrDefault(); - Assert.NotNull(typeKeyword); - Assert.Equal(SchemaValueType.Object, typeKeyword.Type); - - var propKeyword = schema.Keywords!.OfType().SingleOrDefault(); - Assert.NotNull(propKeyword); - Assert.True(propKeyword.Properties.ContainsKey(RootSemanticId)); - - var rootPropSchema = propKeyword.Properties[RootSemanticId]; - var rootPropType = rootPropSchema.Keywords!.OfType().SingleOrDefault(); - Assert.NotNull(rootPropType); - Assert.Equal(SchemaValueType.Object, rootPropType.Type); - - var rootProps = rootPropSchema.Keywords!.OfType().SingleOrDefault(); - Assert.NotNull(rootProps); - Assert.True(rootProps.Properties.ContainsKey(BranchSemanticId)); - - var branchRefSchema = rootProps.Properties[BranchSemanticId]; - var refKeyword = branchRefSchema.Keywords!.OfType().SingleOrDefault(); - Assert.NotNull(refKeyword); - Assert.Equal($"#/definitions/{BranchSemanticId}", refKeyword.Reference.ToString()); - - var definitionsKeyword = schema.Keywords!.OfType().SingleOrDefault(); - Assert.NotNull(definitionsKeyword); - Assert.True(definitionsKeyword.Definitions.ContainsKey(BranchSemanticId)); - - var branchDefSchema = definitionsKeyword.Definitions[BranchSemanticId]; - var branchType = branchDefSchema.Keywords!.OfType().SingleOrDefault(); - Assert.NotNull(branchType); - Assert.Equal(SchemaValueType.Object, branchType.Type); - - var branchProps = branchDefSchema.Keywords!.OfType().SingleOrDefault(); - Assert.NotNull(branchProps); - Assert.True(branchProps.Properties.ContainsKey(NameId)); - - var nameSchema = branchProps.Properties[NameId]; - var nameType = nameSchema.Keywords!.OfType().SingleOrDefault(); - Assert.NotNull(nameType); - Assert.Equal(SchemaValueType.String, nameType.Type); - - var requiredKeyword = branchDefSchema.Keywords!.OfType().SingleOrDefault(); - Assert.NotNull(requiredKeyword); - Assert.Contains(NameId, requiredKeyword.Properties); + var json = ToJson(schema); + + Assert.Equal("object", json["type"]!.ToString()); + var rootProps = json["properties"]![RootSemanticId]!["properties"]!; + var branchRef = rootProps[BranchSemanticId]!["$ref"]!.ToString(); + Assert.Equal($"#/$defs/{BranchSemanticId}", branchRef); + var defs = json["$defs"]!; + Assert.NotNull(defs[BranchSemanticId]); + var branchDef = defs[BranchSemanticId]; + Assert.Equal("object", branchDef!["type"]!.ToString()); + var branchProps = branchDef["properties"]!; + Assert.NotNull(branchProps[NameId]); + var required = branchDef["required"]!.AsArray(); + Assert.Contains(NameId, required.Select(x => x!.ToString())); } [Fact] public void ConvertToJsonSchema_DataTypeMapping_ConvertsCorrectly() { var branch = new SemanticBranchNode("http://example.com/schema/data-types", Cardinality.One); + branch.AddChild(new SemanticLeafNode("string", "", DataType.String, Cardinality.One)); branch.AddChild(new SemanticLeafNode("integer", null!, DataType.Integer, Cardinality.One)); branch.AddChild(new SemanticLeafNode("number", null!, DataType.Number, Cardinality.One)); @@ -196,26 +132,30 @@ public void ConvertToJsonSchema_DataTypeMapping_ConvertsCorrectly() branch.AddChild(new SemanticLeafNode("unknown", null!, DataType.Unknown, Cardinality.One)); var schema = JsonSchemaGenerator.ConvertToJsonSchema(branch); + var json = ToJson(schema); + + var rootProps = json["properties"]!["http://example.com/schema/data-types"]!["properties"]!; - var typeKeyword = schema.Keywords!.OfType().SingleOrDefault(); - Assert.NotNull(typeKeyword); - Assert.Equal(SchemaValueType.Object, typeKeyword.Type); - - var propKeyword = schema.Keywords!.OfType().SingleOrDefault(); - Assert.NotNull(propKeyword); - Assert.True(propKeyword.Properties.ContainsKey("http://example.com/schema/data-types")); - - var rootPropSchema = propKeyword.Properties["http://example.com/schema/data-types"]; - var rootType = rootPropSchema.Keywords!.OfType().SingleOrDefault(); - Assert.NotNull(rootType); - Assert.Equal(SchemaValueType.Object, rootType.Type); - var rootProps = rootPropSchema.Keywords!.OfType().SingleOrDefault(); - Assert.NotNull(rootProps); - Assert.Equal(SchemaValueType.String, GetTypeForProperty(rootProps, "string")); - Assert.Equal(SchemaValueType.Integer, GetTypeForProperty(rootProps, "integer")); - Assert.Equal(SchemaValueType.Number, GetTypeForProperty(rootProps, "number")); - Assert.Equal(SchemaValueType.Boolean, GetTypeForProperty(rootProps, "boolean")); - Assert.Equal(SchemaValueType.String, GetTypeForProperty(rootProps, "unknown")); + Assert.Equal("string", rootProps["string"]!["type"]!.ToString()); + Assert.Equal("integer", rootProps["integer"]!["type"]!.ToString()); + Assert.Equal("number", rootProps["number"]!["type"]!.ToString()); + Assert.Equal("boolean", rootProps["boolean"]!["type"]!.ToString()); + Assert.Equal("string", rootProps["unknown"]!["type"]!.ToString()); + } + + [Fact] + public void ConvertToJsonSchema_ArraySchema_UsesItemsKeyword() + { + var branch = new SemanticBranchNode("root", Cardinality.ZeroToMany); + branch.AddChild(new SemanticLeafNode("child", "", DataType.String, Cardinality.One)); + + var schema = JsonSchemaGenerator.ConvertToJsonSchema(branch); + var json = ToJson(schema); + + var rootProps = json["properties"]!["root"]!; + + Assert.Equal("array", rootProps["type"]!.ToString()); + Assert.NotNull(rootProps["items"]); } [Fact] @@ -223,17 +163,136 @@ public void ConvertToJsonSchema_UnsupportedNode_ThrowsException() { var unsupportedNode = new UnsupportedSemanticNode("unsupported", Cardinality.One); - var ex = Assert.Throws(() => - JsonSchemaGenerator.ConvertToJsonSchema(unsupportedNode)); + Assert.Throws(() => + JsonSchemaGenerator.ConvertToJsonSchema(unsupportedNode)); } - private sealed class UnsupportedSemanticNode(string semanticId, Cardinality cardinality) : SemanticTreeNode(semanticId, cardinality); + [Fact] + public void ConvertToJsonSchema_EmptyBranchNode_ReturnsEmptyObjectSchema() + { + var branch = new SemanticBranchNode("root", Cardinality.One); + + var schema = JsonSchemaGenerator.ConvertToJsonSchema(branch); + var json = ToJson(schema); + + var rootProps = json["properties"]!["root"]!; + Assert.Equal("object", rootProps["type"]!.ToString()); + Assert.NotNull(rootProps["properties"]); + Assert.Empty(rootProps["properties"]!.AsObject()); + } - private static SchemaValueType GetTypeForProperty(PropertiesKeyword props, string key) + [Fact] + public void ConvertToJsonSchema_ArrayWithOptionalChildren_DoesNotContainRequired() { - var schema = props.Properties[key]; - var typeKeyword = schema.Keywords!.OfType().SingleOrDefault(); - Assert.NotNull(typeKeyword); - return typeKeyword.Type; + var branch = new SemanticBranchNode("root", Cardinality.ZeroToMany); + branch.AddChild(new SemanticLeafNode("child", "", DataType.String, Cardinality.ZeroToOne)); + + var schema = JsonSchemaGenerator.ConvertToJsonSchema(branch); + var json = ToJson(schema); + + var items = json["properties"]!["root"]!["items"]!; + Assert.Null(items["required"]); } + + [Fact] + public void ConvertToJsonSchema_ReusedBranch_UsesSingleDefinition() + { + var shared = new SemanticBranchNode("shared", Cardinality.One); + shared.AddChild(new SemanticLeafNode("name", "", DataType.String, Cardinality.One)); + var root = new SemanticBranchNode("root", Cardinality.One); + root.AddChild(shared); + root.AddChild(shared); + + var schema = JsonSchemaGenerator.ConvertToJsonSchema(root); + + var json = ToJson(schema); + var defs = json["$defs"]!; + Assert.Single(defs.AsObject()); + var rootProps = json["properties"]!["root"]!["properties"]!; + Assert.Equal("#/$defs/shared", rootProps["shared"]!["$ref"]!.ToString()); + } + + [Fact] + public void ConvertToJsonSchema_DeepNestedStructure_WorksCorrectly() + { + var level3 = new SemanticBranchNode("level3", Cardinality.One); + level3.AddChild(new SemanticLeafNode("leaf", "", DataType.String, Cardinality.One)); + var level2 = new SemanticBranchNode("level2", Cardinality.One); + level2.AddChild(level3); + var level1 = new SemanticBranchNode("level1", Cardinality.One); + level1.AddChild(level2); + + var schema = JsonSchemaGenerator.ConvertToJsonSchema(level1); + + var json = ToJson(schema); + var defs = json["$defs"]!; + Assert.NotNull(defs["level2"]); + Assert.NotNull(defs["level3"]); + } + + [Fact] + public void ConvertToJsonSchema_MixedCardinality_WorksCorrectly() + { + var root = new SemanticBranchNode("root", Cardinality.One); + var objectChild = new SemanticBranchNode("objectChild", Cardinality.One); + objectChild.AddChild(new SemanticLeafNode("name", "", DataType.String, Cardinality.One)); + var arrayChild = new SemanticBranchNode("arrayChild", Cardinality.ZeroToMany); + arrayChild.AddChild(new SemanticLeafNode("value", "", DataType.Number, Cardinality.One)); + root.AddChild(objectChild); + root.AddChild(arrayChild); + + var schema = JsonSchemaGenerator.ConvertToJsonSchema(root); + + var json = ToJson(schema); + var props = json["properties"]!["root"]!["properties"]!; + var objectRef = props["objectChild"]!["$ref"]!.ToString(); + Assert.Equal("#/$defs/objectChild", objectRef); + var arrayRef = props["arrayChild"]!["$ref"]!.ToString(); + Assert.Equal("#/$defs/arrayChild", arrayRef); + var defs = json["$defs"]!; + var objectDef = defs["objectChild"]!; + Assert.Equal("object", objectDef["type"]!.ToString()); + var arrayDef = defs["arrayChild"]!; + Assert.Equal("array", arrayDef["type"]!.ToString()); + Assert.NotNull(arrayDef["items"]); + } + + [Fact] + public void ConvertToJsonSchema_UnknownCardinality_TreatedAsObject() + { + var branch = new SemanticBranchNode("root", Cardinality.Unknown); + branch.AddChild(new SemanticLeafNode("name", "", DataType.String, Cardinality.One)); + + var schema = JsonSchemaGenerator.ConvertToJsonSchema(branch); + + var json = ToJson(schema); + var root = json["properties"]!["root"]!; + Assert.Equal("object", root["type"]!.ToString()); + } + + [Fact] + public void ConvertToJsonSchema_MultipleRequiredFields_AllIncluded() + { + var branch = new SemanticBranchNode("root", Cardinality.One); + branch.AddChild(new SemanticLeafNode("a", "", DataType.String, Cardinality.One)); + branch.AddChild(new SemanticLeafNode("b", "", DataType.String, Cardinality.One)); + + var schema = JsonSchemaGenerator.ConvertToJsonSchema(branch); + + var json = ToJson(schema); + var required = json["properties"]!["root"]!["required"]!.AsArray(); + Assert.Contains("a", required.Select(x => x!.ToString())); + Assert.Contains("b", required.Select(x => x!.ToString())); + } + + private sealed class UnsupportedSemanticNode(string semanticId, Cardinality cardinality) + : SemanticTreeNode(semanticId, cardinality); + + private static readonly JsonSerializerOptions SerializerOption = new() + { + WriteIndented = false + }; + + private static JsonNode ToJson(JsonSchema schema) + => JsonSerializer.SerializeToNode(schema, SerializerOption)!; } diff --git a/source/AAS.TwinEngine.DataEngine.UnitTests/Infrastructure/Providers/PluginDataProvider/Helper/JsonSchemaValidatorTests.cs b/source/AAS.TwinEngine.DataEngine.UnitTests/Infrastructure/Providers/PluginDataProvider/Helper/JsonSchemaValidatorTests.cs index 82a285a1..0c2f2c46 100644 --- a/source/AAS.TwinEngine.DataEngine.UnitTests/Infrastructure/Providers/PluginDataProvider/Helper/JsonSchemaValidatorTests.cs +++ b/source/AAS.TwinEngine.DataEngine.UnitTests/Infrastructure/Providers/PluginDataProvider/Helper/JsonSchemaValidatorTests.cs @@ -1,4 +1,5 @@ using AAS.TwinEngine.DataEngine.ApplicationLogic.Exceptions.Application; +using AAS.TwinEngine.DataEngine.ApplicationLogic.Exceptions.Base; using AAS.TwinEngine.DataEngine.Infrastructure.Providers.PluginDataProvider.Helper; using AAS.TwinEngine.DataEngine.ServiceConfiguration.Config; @@ -33,38 +34,26 @@ public JsonSchemaValidatorTests() { SubmodelElementIndexContextPrefix = "_aastwinengine_" }); + var logger = Substitute.For>(); _sut = new JsonSchemaValidator(pluginsConfig, logger); } [Fact] - public void ValidateRequestSchema_NullSchema_ThrowsBadRequest() => Assert.Throws(() => _sut.ValidateRequestSchema(null!)); - - [Fact] - public void ValidateRequestSchema_WithInvalidJson_ThrowsParseError() - { - var schema = new JsonSchemaBuilder() - .Type(SchemaValueType.Object) - .Properties(new Dictionary - { - ["invalid"] = null! - }) - .Build(); - - Assert.Throws(() => _sut.ValidateRequestSchema(schema)); - } + public void ValidateRequestSchema_NullSchema_ThrowsBadRequest() + => Assert.Throws(() => _sut.ValidateRequestSchema(null!)); [Fact] public void ValidateRequestSchema_ValidSchema_DoesNotThrow() { var schema = new JsonSchemaBuilder() - .Schema("http://json-schema.org/draft-07/schema#") - .Type(SchemaValueType.Object) - .Properties(new Dictionary - { - ["name"] = new JsonSchemaBuilder().Type(SchemaValueType.String).Build() - }) - .Build(); + .Schema("https://json-schema.org/draft/2020-12/schema") + .Type(SchemaValueType.Object) + .Properties(new Dictionary + { + ["name"] = new JsonSchemaBuilder().Type(SchemaValueType.String) + }) + .Build(); _sut.ValidateRequestSchema(schema); } @@ -72,7 +61,9 @@ public void ValidateRequestSchema_ValidSchema_DoesNotThrow() [Fact] public void ValidateResponseContent_EmptyResponse_ThrowsBadRequest() { - var schema = new JsonSchemaBuilder().Type(SchemaValueType.Object).Build(); + var schema = new JsonSchemaBuilder() + .Type(SchemaValueType.Object) + .Build(); Assert.Throws(() => _sut.ValidateResponseContent("", schema)); } @@ -81,13 +72,13 @@ public void ValidateResponseContent_EmptyResponse_ThrowsBadRequest() public void ValidateResponseContent_ValidateJsonSchemaRemovePrefix_DoesNotThrow() { var schema = new JsonSchemaBuilder() - .Type(SchemaValueType.Object) - .Properties(new Dictionary - { - ["ContactInformation_aastwinengine_00"] = new JsonSchemaBuilder().Type(SchemaValueType.Object).Build() - }) - .Required("ContactInformation_aastwinengine_00") - .Build(); + .Type(SchemaValueType.Object) + .Properties(new Dictionary + { + ["ContactInformation_aastwinengine_00"] = new JsonSchemaBuilder().Type(SchemaValueType.Object) + }) + .Required("ContactInformation_aastwinengine_00") + .Build(); const string Json = "{\"ContactInformation\": {}}"; @@ -98,13 +89,13 @@ public void ValidateResponseContent_ValidateJsonSchemaRemovePrefix_DoesNotThrow( public void ValidateResponseContent_ValidJsonAndSchema_DoesNotThrow() { var schema = new JsonSchemaBuilder() - .Type(SchemaValueType.Object) - .Properties(new Dictionary - { - ["name"] = new JsonSchemaBuilder().Type(SchemaValueType.String).Build() - }) - .Required("name") - .Build(); + .Type(SchemaValueType.Object) + .Properties(new Dictionary + { + ["name"] = new JsonSchemaBuilder().Type(SchemaValueType.String) + }) + .Required("name") + .Build(); const string Json = "{\"name\": \"Test\"}"; @@ -120,12 +111,13 @@ public void ValidateResponseContent_InvalidValueType_ThrowsBadRequest( { var schema = new JsonSchemaBuilder() .Type(SchemaValueType.Object) - .Properties(new Dictionary + .Properties(new Dictionary { - [property] = new JsonSchemaBuilder().Type(expectedType).Build() + [property] = new JsonSchemaBuilder().Type(expectedType) }) .Required(property) .Build(); + var json = $"{{\"{property}\": {rawValue} }}"; Assert.Throws(() => _sut.ValidateResponseContent(json, schema)); @@ -136,9 +128,9 @@ public void ValidateResponseContent_SchemaMismatch_ThrowsBadRequest() { var schema = new JsonSchemaBuilder() .Type(SchemaValueType.Object) - .Properties(new Dictionary + .Properties(new Dictionary { - ["name"] = new JsonSchemaBuilder().Type(SchemaValueType.String).Build() + ["name"] = new JsonSchemaBuilder().Type(SchemaValueType.String) }) .Required("name") .Build(); @@ -154,8 +146,185 @@ public void ValidateResponseContent_InvalidJson_ThrowsBadRequest() var schema = new JsonSchemaBuilder() .Type(SchemaValueType.Object) .Build(); + const string BadJson = "{ not valid json }"; Assert.Throws(() => _sut.ValidateResponseContent(BadJson, schema)); } + + [Fact] + public void ValidateResponseContent_NullResponse_ThrowsException() + { + var schema = new JsonSchemaBuilder() + .Type(SchemaValueType.Object) + .Build(); + + Assert.Throws(() => _sut.ValidateResponseContent(null!, schema)); + } + + [Fact] + public void ValidateResponseContent_WhitespaceOnlyResponse_ThrowsException() + { + var schema = new JsonSchemaBuilder() + .Type(SchemaValueType.Object) + .Build(); + + Assert.Throws(() => _sut.ValidateResponseContent(" ", schema)); + } + + [Fact] + public void ValidateResponseContent_MalformedJson_ThrowsException() + { + var schema = new JsonSchemaBuilder() + .Type(SchemaValueType.Object) + .Build(); + + Assert.Throws(() => _sut.ValidateResponseContent("{\"key\": }", schema)); + } + [Fact] + public void ValidateResponseContent_ArrayOfObjects_ValidatesCorrectly() + { + var schema = new JsonSchemaBuilder() + .Type(SchemaValueType.Object) + .Properties(new Dictionary + { + ["users"] = new JsonSchemaBuilder() + .Type(SchemaValueType.Array) + .Items(new JsonSchemaBuilder() + .Type(SchemaValueType.Object) + .Properties(new Dictionary + { + ["name_aastwinengine_01"] = new JsonSchemaBuilder().Type(SchemaValueType.String) + })) + }) + .Build(); + + const string Json = "{\"users\": [{\"name\": \"Alice\"}, {\"name\": \"Bob\"}]}"; + + _sut.ValidateResponseContent(Json, schema); + } + + [Fact] + public void ValidateResponseContent_WithDefsReference_DoesNotThrow() + { + const string schemaJson = @"{ + ""$schema"": ""https://json-schema.org/draft/2020-12/schema"", + ""type"": ""object"", + ""properties"": { + ""item"": { ""$ref"": ""#/$defs/MyType"" } + }, + ""$defs"": { + ""MyType"": { + ""type"": ""object"", + ""properties"": { + ""name"": { ""type"": ""string"" } + } + } + } + }"; + + var schema = JsonSchema.FromText(schemaJson); + + const string json = @"{ ""item"": { ""name"": ""test"" } }"; + + _sut.ValidateResponseContent(json, schema); + } + + [Fact] + public void ValidateResponseContent_BrokenRef_Throws() + { + const string schemaJson = @"{ + ""$schema"": ""https://json-schema.org/draft/2020-12/schema"", + ""type"": ""object"", + ""properties"": { + ""item"": { ""$ref"": ""#/$defs/UnknownType"" } + }, + ""$defs"": {} + }"; + + var schema = JsonSchema.FromText(schemaJson); + + const string json = @"{ ""item"": {} }"; + + Assert.Throws(() => + _sut.ValidateResponseContent(json, schema)); + } + + [Fact] + public void ValidateResponseContent_ArrayItemInvalid_Throws() + { + var schema = new JsonSchemaBuilder() + .Type(SchemaValueType.Object) + .Properties(new Dictionary + { + ["users"] = new JsonSchemaBuilder() + .Type(SchemaValueType.Array) + .Items(new JsonSchemaBuilder() + .Type(SchemaValueType.Object) + .Properties(new Dictionary + { + ["name"] = new JsonSchemaBuilder().Type(SchemaValueType.String) + }) + .Required("name")) + }) + .Build(); + + const string json = @"{ ""users"": [{}] }"; + + Assert.Throws(() => + _sut.ValidateResponseContent(json, schema)); + } + + [Fact] + public void ValidateResponseContent_NestedDefsReference_Works() + { + const string schemaJson = @"{ + ""$schema"": ""https://json-schema.org/draft/2020-12/schema"", + ""type"": ""object"", + ""properties"": { + ""item"": { ""$ref"": ""#/$defs/Level1"" } + }, + ""$defs"": { + ""Level1"": { + ""type"": ""object"", + ""properties"": { + ""child"": { ""$ref"": ""#/$defs/Level2"" } + } + }, + ""Level2"": { + ""type"": ""string"" + } + } + }"; + var schema = JsonSchema.FromText(schemaJson); + + const string json = @"{ ""item"": { ""child"": ""ok"" } }"; + + _sut.ValidateResponseContent(json, schema); + } + + [Fact] + public void ValidateResponseContent_ArrayMissingRequiredField_Throws() + { + var schema = new JsonSchemaBuilder() + .Type(SchemaValueType.Object) + .Properties(new Dictionary + { + ["items"] = new JsonSchemaBuilder() + .Type(SchemaValueType.Array) + .Items(new JsonSchemaBuilder() + .Type(SchemaValueType.Object) + .Properties(new Dictionary + { + ["name"] = new JsonSchemaBuilder().Type(SchemaValueType.String) + }) + .Required("name")) + }) + .Build(); + + const string json = @"{ ""items"": [{}] }"; + + Assert.Throws(() => + _sut.ValidateResponseContent(json, schema)); + } } diff --git a/source/AAS.TwinEngine.DataEngine/AAS.TwinEngine.DataEngine.csproj b/source/AAS.TwinEngine.DataEngine/AAS.TwinEngine.DataEngine.csproj index 398019d7..445f3c1b 100644 --- a/source/AAS.TwinEngine.DataEngine/AAS.TwinEngine.DataEngine.csproj +++ b/source/AAS.TwinEngine.DataEngine/AAS.TwinEngine.DataEngine.csproj @@ -1,4 +1,4 @@ - + net8.0 @@ -19,29 +19,28 @@ - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + - + diff --git a/source/AAS.TwinEngine.DataEngine/Infrastructure/Providers/PluginDataProvider/Helper/JsonSchemaGenerator.cs b/source/AAS.TwinEngine.DataEngine/Infrastructure/Providers/PluginDataProvider/Helper/JsonSchemaGenerator.cs index 623f0dcb..ab45b52d 100644 --- a/source/AAS.TwinEngine.DataEngine/Infrastructure/Providers/PluginDataProvider/Helper/JsonSchemaGenerator.cs +++ b/source/AAS.TwinEngine.DataEngine/Infrastructure/Providers/PluginDataProvider/Helper/JsonSchemaGenerator.cs @@ -9,25 +9,28 @@ namespace AAS.TwinEngine.DataEngine.Infrastructure.Providers.PluginDataProvider. public static class JsonSchemaGenerator { - private const string DefinitionsRefPrefix = "#/definitions/"; + private const string DefinitionsRefPrefix = "#/$defs/"; public static JsonSchema ConvertToJsonSchema(SemanticTreeNode rootNode) { - var definitions = new Dictionary(); + var definitions = new Dictionary(); var rootSchema = BuildNode(rootNode, definitions, isRoot: true); return new JsonSchemaBuilder() - .Schema(MetaSchemas.Draft7Id) + .Schema(MetaSchemas.Draft202012Id) .Type(SchemaValueType.Object) - .Properties(new Dictionary + .Properties(new Dictionary { - [rootNode?.SemanticId!] = rootSchema + [rootNode!.SemanticId] = rootSchema }) - .Definitions(definitions) + .Defs(definitions) .Build(); } - private static JsonSchema BuildNode(SemanticTreeNode node, Dictionary definitions, bool isRoot = false) + private static JsonSchemaBuilder BuildNode( + SemanticTreeNode node, + Dictionary definitions, + bool isRoot = false) { return node switch { @@ -37,7 +40,10 @@ private static JsonSchema BuildNode(SemanticTreeNode node, Dictionary definitions, bool isRoot = false) + private static JsonSchemaBuilder BuildBranch( + SemanticBranchNode branch, + Dictionary definitions, + bool isRoot = false) { if (!isRoot && definitions.ContainsKey(branch.SemanticId)) { @@ -45,31 +51,62 @@ private static JsonSchema BuildBranch(SemanticBranchNode branch, Dictionary(); - var children = BuildChildren(branch.Children, definitions, requiredProperties); - var isArray = IsArrayCardinality(branch.Cardinality); - - var schemaBuilder = new JsonSchemaBuilder() - .Type(isArray ? SchemaValueType.Array : SchemaValueType.Object) - .Properties(children) - .Required(requiredProperties); + var schemaBuilder = IsArrayCardinality(branch.Cardinality) + ? BuildArraySchema(children, requiredProperties) + : BuildObjectSchema(children, requiredProperties); if (isRoot) { - return schemaBuilder.Build(); + return schemaBuilder; } - definitions[branch.SemanticId] = schemaBuilder.Build(); + definitions[branch.SemanticId] = schemaBuilder; + return CreateRefSchema(branch.SemanticId); } - private static Dictionary BuildChildren( + private static JsonSchemaBuilder BuildObjectSchema( + Dictionary properties, + List requiredProperties) + { + var schemaBuilder = new JsonSchemaBuilder() + .Type(SchemaValueType.Object) + .Properties(properties); + + if (requiredProperties.Count > 0) + { + schemaBuilder = schemaBuilder.Required(requiredProperties); + } + + return schemaBuilder; + } + + private static JsonSchemaBuilder BuildArraySchema( + Dictionary properties, + List requiredProperties) + { + var itemBuilder = new JsonSchemaBuilder() + .Type(SchemaValueType.Object) + .Properties(properties); + + if (requiredProperties.Count > 0) + { + itemBuilder = itemBuilder.Required(requiredProperties); + } + + return new JsonSchemaBuilder() + .Type(SchemaValueType.Array) + .Items(itemBuilder); + } + + private static Dictionary BuildChildren( ReadOnlyCollection children, - Dictionary definitions, + Dictionary definitions, List required) { - var properties = new Dictionary(); + var properties = new Dictionary(); foreach (var child in children) { @@ -91,7 +128,7 @@ private static Dictionary BuildChildren( return properties; } - private static JsonSchema BuildLeaf(SemanticLeafNode leaf) + private static JsonSchemaBuilder BuildLeaf(SemanticLeafNode leaf) { var type = leaf.DataType switch { @@ -103,12 +140,15 @@ private static JsonSchema BuildLeaf(SemanticLeafNode leaf) _ => SchemaValueType.String }; - return new JsonSchemaBuilder().Type(type).Build(); + return new JsonSchemaBuilder().Type(type); } - private static bool IsArrayCardinality(Cardinality cardinality) => cardinality is Cardinality.ZeroToMany or Cardinality.OneToMany; + private static bool IsArrayCardinality(Cardinality cardinality) + => cardinality is Cardinality.ZeroToMany or Cardinality.OneToMany; - private static bool IsRequiredCardinality(Cardinality cardinality) => cardinality is Cardinality.One or Cardinality.OneToMany; + private static bool IsRequiredCardinality(Cardinality cardinality) + => cardinality is Cardinality.One or Cardinality.OneToMany; - private static JsonSchema CreateRefSchema(string semanticId) => new JsonSchemaBuilder().Ref($"{DefinitionsRefPrefix}{semanticId}").Build(); + private static JsonSchemaBuilder CreateRefSchema(string semanticId) + => new JsonSchemaBuilder().Ref($"{DefinitionsRefPrefix}{semanticId}"); } diff --git a/source/AAS.TwinEngine.DataEngine/Infrastructure/Providers/PluginDataProvider/Helper/JsonSchemaValidator.cs b/source/AAS.TwinEngine.DataEngine/Infrastructure/Providers/PluginDataProvider/Helper/JsonSchemaValidator.cs index 5ab61739..326a237d 100644 --- a/source/AAS.TwinEngine.DataEngine/Infrastructure/Providers/PluginDataProvider/Helper/JsonSchemaValidator.cs +++ b/source/AAS.TwinEngine.DataEngine/Infrastructure/Providers/PluginDataProvider/Helper/JsonSchemaValidator.cs @@ -10,12 +10,16 @@ using Microsoft.Extensions.Options; -namespace AAS.TwinEngine.DataEngine.Infrastructure.Providers.PluginDataProvider.Helper; - public class JsonSchemaValidator(IOptions pluginsConfig, ILogger logger) : IJsonSchemaValidator { private readonly string _contextPrefix = pluginsConfig.Value.SubmodelElementIndexContextPrefix; - private const string DefinitionsPrefix = "#/definitions/"; + + private const string DefsPrefix = "#/$defs/"; + + private readonly EvaluationOptions _evaluationOptions = new() + { + OutputFormat = OutputFormat.List + }; public void ValidateRequestSchema(JsonSchema schema) { @@ -41,15 +45,18 @@ public void ValidateRequestSchema(JsonSchema schema) try { - var result = MetaSchemas.Draft7.Evaluate(schemaNode, new EvaluationOptions { OutputFormat = OutputFormat.List }); + var jsonElement = JsonDocument.Parse(schemaNode.ToJsonString()).RootElement; + + var result = MetaSchemas.Draft202012.Evaluate(jsonElement, _evaluationOptions); + if (!result.IsValid) { - LogAndThrowException("Schema is not valid against Draft-7."); + LogAndThrowException("Schema is not valid against Draft 2020-12."); } } catch (Exception ex) { - LogAndThrowException("Draft-7 evaluation failed.", ex); + LogAndThrowException("Draft 2020-12 evaluation failed.", ex); } } @@ -70,15 +77,12 @@ public void ValidateResponseContent(string responseJson, JsonSchema requestSchem LogAndThrowException($"Failed to normalize request schema: {normalizeError}"); } - if (!TryRegisterJsonSchema(normalizedSchema, out var registerError)) - { - LogAndThrowException($"Failed to register schema: {registerError}"); - } - try { var schema = JsonSchema.FromText(normalizedSchema.ToJsonString()); - var result = schema.Evaluate(responseDoc!.RootElement, new EvaluationOptions { OutputFormat = OutputFormat.List }); + + var result = schema.Evaluate(responseDoc!.RootElement, _evaluationOptions); + if (!result.IsValid) { LogAndThrowException("Response did not validate against schema."); @@ -125,6 +129,7 @@ private static bool TryParseSchemaNode(string schemaText, out JsonNode? node, ou { error = null; node = null; + try { node = JsonNode.Parse(schemaText); @@ -163,15 +168,12 @@ private bool TryNormalizeSchema(JsonSchema schema, out JsonObject normalized, ou { var json = JsonSerializer.Serialize(schema, JsonSerializationOptions.SerializationWithEnum); - normalized = JsonNode.Parse(json)?.AsObject(); + normalized = JsonNode.Parse(json)?.AsObject() + ?? throw new InvalidDependencyException(nameof(normalized), logger); EscapeJsonReferencePointers(normalized); - if (normalized == null) - { - throw new InvalidDependencyException(nameof(normalized), logger); - } - normalized["$id"] = normalized["$id"]?.GetValue() ?? $"urn:uuid:{Guid.NewGuid():D}"; + normalized["$schema"] ??= "https://json-schema.org/draft/2020-12/schema"; return true; } @@ -182,38 +184,19 @@ private bool TryNormalizeSchema(JsonSchema schema, out JsonObject normalized, ou } } - private static bool TryRegisterJsonSchema(JsonObject schemaJsonObject, out string? registrationErrorMessage) - { - registrationErrorMessage = null; - - try - { - var jsonSchema = JsonSchema.FromText(schemaJsonObject.ToJsonString()); - var schemaIdentifierUri = new Uri(schemaJsonObject["$id"]!.GetValue()!); - SchemaRegistry.Global.Register(schemaIdentifierUri, jsonSchema); - return true; - } - catch (Exception exception) - { - registrationErrorMessage = $"Schema registration failed: {exception.Message}"; - return false; - } - } - private void EscapeJsonReferencePointers(JsonNode? currentNode) { switch (currentNode) { - case JsonObject jsonObjectNode: - ProcessJsonObjectForEscaping(jsonObjectNode); + case JsonObject obj: + ProcessJsonObjectForEscaping(obj); break; - case JsonArray jsonArrayNode: - foreach (var arrayElement in jsonArrayNode) + case JsonArray array: + foreach (var item in array) { - EscapeJsonReferencePointers(arrayElement); + EscapeJsonReferencePointers(item); } - break; } } @@ -221,78 +204,84 @@ private void EscapeJsonReferencePointers(JsonNode? currentNode) private void ProcessJsonObjectForEscaping(JsonObject jsonObject) { var propertiesToRename = jsonObject - .Select(property => property.Key) - .Select(propertyName => (originalName: propertyName, strippedName: RemoveContextSuffix(propertyName))) - .Where(namePair => namePair.strippedName != namePair.originalName) + .Select(p => p.Key) + .Select(name => (original: name, stripped: RemoveContextSuffix(name))) + .Where(x => x.original != x.stripped) .ToList(); - foreach (var (originalName, strippedName) in propertiesToRename) + foreach (var (original, stripped) in propertiesToRename) { - RenameJsonProperty(jsonObject, originalName, strippedName); + RenameJsonProperty(jsonObject, original, stripped); } - if (jsonObject.TryGetPropertyValue("required", out var requiredPropertiesNode) && - requiredPropertiesNode is JsonArray requiredPropertiesArray) + if (jsonObject.TryGetPropertyValue("required", out var requiredNode) && + requiredNode is JsonArray requiredArray) { - RemoveContextSuffixFromRequiredProperties(requiredPropertiesArray); + RemoveContextSuffixFromRequiredProperties(requiredArray); } foreach (var property in jsonObject.ToList()) { - var propertyName = property.Key; - var propertyValue = property.Value; - - if (propertyName == "$ref" && - propertyValue is JsonValue referenceValue && - referenceValue.TryGetValue(out var referenceString) && - referenceString.StartsWith(DefinitionsPrefix, StringComparison.OrdinalIgnoreCase)) + if (property.Key == "$ref" && + property.Value is JsonValue value && + value.TryGetValue(out var reference)) { - jsonObject["$ref"] = BuildEscapedReferencePath(referenceString); + if (reference.StartsWith(DefsPrefix, StringComparison.OrdinalIgnoreCase)) + { + jsonObject["$ref"] = BuildEscapedReferencePath(reference); + } } else { - EscapeJsonReferencePointers(propertyValue); + EscapeJsonReferencePointers(property.Value); } } } private void RemoveContextSuffixFromRequiredProperties(JsonArray requiredProperties) { - for (var index = 0; index < requiredProperties.Count; index++) + for (var i = 0; i < requiredProperties.Count; i++) { - if (requiredProperties[index]?.GetValue() is { } propertyName) + if (requiredProperties[i]?.GetValue() is { } name) { - requiredProperties[index] = RemoveContextSuffix(propertyName); + requiredProperties[i] = RemoveContextSuffix(name); } } } - private string BuildEscapedReferencePath(string originalReferencePath) + private string BuildEscapedReferencePath(string reference) { - var referenceWithoutPrefix = originalReferencePath[DefinitionsPrefix.Length..]; + if (!reference.StartsWith(DefsPrefix, StringComparison.OrdinalIgnoreCase)) + { + return reference; + } + + var body = reference[DefsPrefix.Length..]; - var strippedReference = RemoveContextSuffix(referenceWithoutPrefix); + var stripped = RemoveContextSuffix(body); - var escapedReference = strippedReference.Replace("~", "~0", StringComparison.OrdinalIgnoreCase).Replace("/", "~1", StringComparison.OrdinalIgnoreCase); + var escaped = stripped + .Replace("~", "~0", StringComparison.OrdinalIgnoreCase) + .Replace("/", "~1", StringComparison.OrdinalIgnoreCase); - return DefinitionsPrefix + escapedReference; + return DefsPrefix + escaped; } private string RemoveContextSuffix(string propertyName) { - var suffixIndex = propertyName.IndexOf(_contextPrefix, StringComparison.Ordinal); - return suffixIndex >= 0 ? propertyName[..suffixIndex] : propertyName; + var index = propertyName.IndexOf(_contextPrefix, StringComparison.Ordinal); + return index >= 0 ? propertyName[..index] : propertyName; } - private static void RenameJsonProperty(JsonObject jsonObject, string oldPropertyName, string newPropertyName) + private static void RenameJsonProperty(JsonObject jsonObject, string oldName, string newName) { - if (oldPropertyName == newPropertyName) + if (oldName == newName) { return; } - var propertyValue = jsonObject[oldPropertyName]; - _ = jsonObject.Remove(oldPropertyName); - jsonObject[newPropertyName] = propertyValue!; + var value = jsonObject[oldName]; + _ = jsonObject.Remove(oldName); + jsonObject[newName] = value!; } } diff --git a/source/AAS.TwinEngine.Plugin.TestPlugin.UnitTests/AAS.TwinEngine.Plugin.TestPlugin.UnitTests.csproj b/source/AAS.TwinEngine.Plugin.TestPlugin.UnitTests/AAS.TwinEngine.Plugin.TestPlugin.UnitTests.csproj index 617925c6..8e1a44c8 100644 --- a/source/AAS.TwinEngine.Plugin.TestPlugin.UnitTests/AAS.TwinEngine.Plugin.TestPlugin.UnitTests.csproj +++ b/source/AAS.TwinEngine.Plugin.TestPlugin.UnitTests/AAS.TwinEngine.Plugin.TestPlugin.UnitTests.csproj @@ -14,13 +14,19 @@ - - - + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + - - - + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + diff --git a/source/AAS.TwinEngine.Plugin.TestPlugin.UnitTests/Api/Submodel/Services/JsonSchemaParserTests.cs b/source/AAS.TwinEngine.Plugin.TestPlugin.UnitTests/Api/Submodel/Services/JsonSchemaParserTests.cs index a0513cdd..bfb6453a 100644 --- a/source/AAS.TwinEngine.Plugin.TestPlugin.UnitTests/Api/Submodel/Services/JsonSchemaParserTests.cs +++ b/source/AAS.TwinEngine.Plugin.TestPlugin.UnitTests/Api/Submodel/Services/JsonSchemaParserTests.cs @@ -27,90 +27,93 @@ public class JsonSchemaParserTests } }; - private readonly string _invalidJson = @"{""Invalid json"": {}}"; - private const string ValidationFailSchemaString = @"{ ""type"" : ""null"" }"; private const string NoPropertiesSchemaString = @"{ - ""$schema"": ""http://json-schema.org/draft-07/schema#"", - ""type"": ""object"" - }"; + ""$schema"": ""https://json-schema.org/draft/2020-12/schema"", + ""type"": ""object"" + }"; private const string SimpleSchemaString = @"{ - ""$schema"": ""http://json-schema.org/draft-07/schema#"", - ""type"": ""object"", - ""properties"": { - ""foo"": { ""type"": ""string"" } - }}"; + ""$schema"": ""https://json-schema.org/draft/2020-12/schema"", + ""type"": ""object"", + ""properties"": { + ""foo"": { ""type"": ""string"" } + }}"; private const string NestedSchemaString = @"{ - ""$schema"": ""http://json-schema.org/draft-07/schema#"", - ""type"": ""object"", - ""properties"": { - ""parent"": { - ""type"": ""object"", - ""properties"": { - ""child"": { ""type"": ""number"" } - }}}}"; + ""$schema"": ""https://json-schema.org/draft/2020-12/schema"", + ""type"": ""object"", + ""properties"": { + ""parent"": { + ""type"": ""object"", + ""properties"": { + ""child"": { ""type"": ""number"" } + }}}}"; private const string ArraySchemaString = @"{ - ""$schema"": ""http://json-schema.org/draft-07/schema#"", - ""type"": ""object"", - ""properties"": { - ""list"": { - ""type"": ""array"", - ""properties"": { ""id"": { ""type"": ""integer"" } } - }}}"; - - private const string ArrayWithRefSchemaString = @"{ - ""$schema"": ""http://json-schema.org/draft-07/schema#"", - ""type"": ""object"", - ""properties"": { + ""$schema"": ""https://json-schema.org/draft/2020-12/schema"", + ""type"": ""object"", + ""properties"": { + ""list"": { + ""type"": ""array"", ""items"": { - ""type"": ""array"", - ""$ref"": ""#/definitions/ItemDef"" - } - }, - ""definitions"": { - ""ItemDef"": { ""type"": ""object"", - ""properties"": { ""val"": { ""type"": ""integer"" } } + ""properties"": { ""id"": { ""type"": ""integer"" } } } + }}}"; + + private const string ArrayWithRefSchemaString = @"{ + ""$schema"": ""https://json-schema.org/draft/2020-12/schema"", + ""type"": ""object"", + ""properties"": { + ""items"": { + ""type"": ""array"", + ""items"": { ""$ref"": ""#/$defs/ItemDef"" } } - }"; + }, + ""$defs"": { + ""ItemDef"": { + ""type"": ""object"", + ""properties"": { ""val"": { ""type"": ""integer"" } } + } + } + }"; private const string AllDataTypesSchemaWithRefString = @"{ - ""$schema"": ""http://json-schema.org/draft-07/schema#"", - ""type"": ""object"", - ""properties"": { - ""root"" :{ - ""type"" : ""array"", - ""properties"" : { - ""stringField"": { ""type"": ""string"" }, - ""numberField"": { ""type"": ""number"" }, - ""integerField"": { ""type"": ""integer"" }, - ""booleanField"": { ""type"": ""boolean"" }, - ""arrayField"": { - ""$ref"" : ""#/definitions/itemField"" - }, - ""objectField"": { - ""type"": ""object"", - ""properties"": { - ""nestedProp"": { ""type"": ""string"" } + ""$schema"": ""https://json-schema.org/draft/2020-12/schema"", + ""type"": ""object"", + ""properties"": { + ""root"" :{ + ""type"" : ""array"", + ""items"" : { + ""type"": ""object"", + ""properties"" : { + ""stringField"": { ""type"": ""string"" }, + ""numberField"": { ""type"": ""number"" }, + ""integerField"": { ""type"": ""integer"" }, + ""booleanField"": { ""type"": ""boolean"" }, + ""arrayField"": { + ""$ref"" : ""#/$defs/itemField"" + }, + ""objectField"": { + ""type"": ""object"", + ""properties"": { + ""nestedProp"": { ""type"": ""string"" } + } + } + } + } } }, - ""nullField"": { ""type"": ""null"" } - } + ""$defs"" : { + ""itemField"":{ + ""type"":""array"", + ""items"": { + ""type"": ""string"" + } + } } - }, - ""definitions"" : { - ""itemField"":{ - ""type"":""array"", - ""properties"": { - ""items"": { ""type"": ""string"" } - } - } - } }"; private readonly ILogger _logger; @@ -122,268 +125,162 @@ public JsonSchemaParserTests() _sut = new JsonSchemaParser(_logger); } - [Fact] - public void ParseJsonSchema_InvalidJson_ThrowsBadRequestException() - { - var InvalidJsonSchema = JsonSerializer.Deserialize(_invalidJson, _options); - - Assert.Throws(() => _sut.ParseJsonSchema(InvalidJsonSchema)); - } - [Fact] public void ParseJsonSchema_SchemaValidationFails_ThrowsBadRequestException() { - var ValidationFailSchema = JsonSerializer.Deserialize(ValidationFailSchemaString, _options); - - Assert.Throws(() => _sut.ParseJsonSchema(ValidationFailSchema)); + var schema = JsonSerializer.Deserialize(ValidationFailSchemaString, _options); + Assert.Throws(() => _sut.ParseJsonSchema(schema)); } [Fact] public void ParseJsonSchema_NoRootProperties_ThrowsBadRequestException() { - var NoPropertiesSchema = JsonSerializer.Deserialize(NoPropertiesSchemaString, _options); - - Assert.Throws(() => _sut.ParseJsonSchema(NoPropertiesSchema)); + var schema = JsonSerializer.Deserialize(NoPropertiesSchemaString, _options); + Assert.Throws(() => _sut.ParseJsonSchema(schema)); } [Fact] public void ParseJsonSchema_SimpleSchema_ReturnsLeafNode() { - var SimpleSchema = JsonSerializer.Deserialize(SimpleSchemaString, _options); + var schema = JsonSerializer.Deserialize(SimpleSchemaString, _options); - var node = _sut.ParseJsonSchema(SimpleSchema); + var node = _sut.ParseJsonSchema(schema); - Assert.NotNull(node); - Assert.IsType(node); - var leaf = (SemanticLeafNode)node; + var leaf = Assert.IsType(node); Assert.Equal("foo", leaf.SemanticId); - Assert.Equal(string.Empty, leaf.Value); } [Fact] public void ParseJsonSchema_NestedObject_ReturnsBranchNodeWithChild() { - var NestedSchema = JsonSerializer.Deserialize(NestedSchemaString, _options); + var schema = JsonSerializer.Deserialize(NestedSchemaString, _options); - var node = _sut.ParseJsonSchema(NestedSchema); + var node = _sut.ParseJsonSchema(schema); - Assert.NotNull(node); - Assert.IsType(node); - var branch = (SemanticBranchNode)node; + var branch = Assert.IsType(node); Assert.Equal("parent", branch.SemanticId); - Assert.Single(branch.Children); - var child = branch.Children[0] as SemanticLeafNode; - Assert.NotNull(child); + + var child = Assert.IsType(branch.Children[0]); Assert.Equal("child", child.SemanticId); } [Fact] public void ParseJsonSchema_ArrayOfObjects_ReturnsBranchNodeWithLeafChild() { - var arraySchema = JsonSerializer.Deserialize(ArraySchemaString, _options); + var schema = JsonSerializer.Deserialize(ArraySchemaString, _options); - var node = _sut.ParseJsonSchema(arraySchema!); + var node = _sut.ParseJsonSchema(schema); - Assert.NotNull(node); - Assert.IsType(node); - var branch = (SemanticBranchNode)node; + var branch = Assert.IsType(node); Assert.Equal("list", branch.SemanticId); - Assert.Single(branch.Children); - var child = branch.Children[0] as SemanticLeafNode; - Assert.NotNull(child); + + var child = Assert.IsType(branch.Children[0]); Assert.Equal("id", child.SemanticId); } [Fact] public void ParseJsonSchema_ArrayWithRef_ReturnsBranchNodeWithLeafChild() { - var arrayWithRefSchema = JsonSerializer.Deserialize(ArrayWithRefSchemaString, _options); + var schema = JsonSerializer.Deserialize(ArrayWithRefSchemaString, _options); - var node = _sut.ParseJsonSchema(arrayWithRefSchema!); + var node = _sut.ParseJsonSchema(schema); - Assert.NotNull(node); - Assert.IsType(node); - var branch = (SemanticBranchNode)node; + var branch = Assert.IsType(node); Assert.Equal("items", branch.SemanticId); - Assert.Single(branch.Children); - var child = branch.Children[0] as SemanticLeafNode; - Assert.NotNull(child); + + var child = Assert.IsType(branch.Children[0]); Assert.Equal("val", child.SemanticId); } [Fact] - public void ParseJsonSchema_AllDataTypeSchemaWithRef_ReturnsBranchNodeWithLeafChild() + public void ParseJsonSchema_AllDataTypeSchemaWithRef_ReturnsBranchNode() { - var allDataTypesSchemaWithRef = JsonSerializer.Deserialize(AllDataTypesSchemaWithRefString, _options); + var schema = JsonSerializer.Deserialize(AllDataTypesSchemaWithRefString, _options); - var node = _sut.ParseJsonSchema(allDataTypesSchemaWithRef!); + var node = _sut.ParseJsonSchema(schema); - Assert.NotNull(node); - Assert.IsType(node); - var branch = (SemanticBranchNode)node; + var branch = Assert.IsType(node); Assert.Equal("root", branch.SemanticId); Assert.Equal(DataType.Array, branch.DataType); - var child1 = branch.Children[0] as SemanticLeafNode; - Assert.Equal("stringField", child1!.SemanticId); - Assert.Equal(DataType.String, child1.DataType); - var child2 = branch.Children[1] as SemanticLeafNode; - Assert.Equal("numberField", child2!.SemanticId); - Assert.Equal(DataType.Number, child2.DataType); - var child3 = branch.Children[2] as SemanticLeafNode; - Assert.Equal("integerField", child3!.SemanticId); - Assert.Equal(DataType.Integer, child3.DataType); - var child4 = branch.Children[3] as SemanticLeafNode; - Assert.Equal("booleanField", child4!.SemanticId); - Assert.Equal(DataType.Boolean, child4.DataType); - var branch1 = branch.Children[4] as SemanticBranchNode; - Assert.Equal("arrayField", branch1?.SemanticId); - Assert.Equal(DataType.Array, branch1?.DataType); - var leaf1 = branch1!.Children[0] as SemanticLeafNode; - Assert.Equal("items", leaf1!.SemanticId); - Assert.Equal(DataType.String, leaf1.DataType); } [Fact] public void ParseJsonSchema_ReferenceNotFound_ReturnsLeafNode() { - const string SchemaString = @"{ - ""$schema"": ""http://json-schema.org/draft-07/schema#"", - ""type"": ""object"", - ""properties"": { - ""mystery"": { ""$ref"": ""#/definitions/DoesNotExist"" } - } - }"; - var schema = JsonSerializer.Deserialize(SchemaString, _options); - - var node = _sut.ParseJsonSchema(schema!); - - Assert.NotNull(node); - Assert.IsType(node); - var leaf = (SemanticLeafNode)node; - Assert.Equal("mystery", leaf.SemanticId); - Assert.Equal(DataType.Unknown, leaf.DataType); - } - - [Fact] - public void ParseJsonSchema_ReferenceToObjectDefinition_ReturnsBranchNode() - { - const string SchemaString = @"{ - ""$schema"": ""http://json-schema.org/draft-07/schema#"", - ""type"": ""object"", - ""properties"": { - ""person"": { ""$ref"": ""#/definitions/Person"" } - }, - ""definitions"": { - ""Person"": { - ""type"": ""object"", - ""properties"": { - ""name"": { ""type"": ""string"" }, - ""age"": { ""type"": ""integer"" } - } + const string Schema = @"{ + ""type"": ""object"", + ""properties"": { + ""mystery"": { ""$ref"": ""#/$defs/DoesNotExist"" } } - } - }"; - var schema = JsonSerializer.Deserialize(SchemaString, _options); + }"; + + var schema = JsonSerializer.Deserialize(Schema, _options); var node = _sut.ParseJsonSchema(schema); - Assert.NotNull(node); - Assert.IsType(node); - var branch = (SemanticBranchNode)node; - Assert.Equal("person", branch.SemanticId); - Assert.Equal(DataType.Object, branch.DataType); - Assert.Collection(branch.Children, - child => - { - var leaf = Assert.IsType(child); - Assert.Equal("name", leaf.SemanticId); - Assert.Equal(DataType.String, leaf.DataType); - }, - child => - { - var leaf = Assert.IsType(child); - Assert.Equal("age", leaf.SemanticId); - Assert.Equal(DataType.Integer, leaf.DataType); - }); + var leaf = Assert.IsType(node); + Assert.Equal("mystery", leaf.SemanticId); + Assert.Equal(DataType.Unknown, leaf.DataType); } [Fact] - public void ParseJsonSchema_ReferenceToArrayDefinition_ReturnsBranchNode() + public void ParseJsonSchema_MissingType_DefaultsToString() { - const string SchemaString = @"{ - ""$schema"": ""http://json-schema.org/draft-07/schema#"", - ""type"": ""object"", - ""properties"": { - ""primes"": { ""$ref"": ""#/definitions/PrimeList"" } - }, - ""definitions"": { - ""PrimeList"": { - ""type"": ""array"" - } + var schema = JsonSchema.FromText(@"{ + ""type"": ""object"", + ""properties"": { + ""unknown"": {} } - }"; - var schema = JsonSerializer.Deserialize(SchemaString, _options); + }"); var node = _sut.ParseJsonSchema(schema); - Assert.NotNull(node); - Assert.IsType(node); - var branch = (SemanticBranchNode)node; - Assert.Equal("primes", branch.SemanticId); - Assert.Equal(DataType.Array, branch.DataType); - Assert.Empty(branch.Children); + var leaf = Assert.IsType(node); + Assert.Equal(DataType.String, leaf.DataType); } [Fact] - public void ParseJsonSchema_ReferenceToLeafNodeDefinition_ReturnsLeafNode() + public void ParseJsonSchema_InvalidRef_ReturnsUnknownLeaf() { - const string SchemaString = @"{ - ""$schema"": ""http://json-schema.org/draft-07/schema#"", - ""type"": ""object"", - ""properties"": { - ""person"": { ""$ref"": ""#/definitions/Person"" } - }, - ""definitions"": { - ""Person"": { - ""type"": ""string"" + var schema = JsonSchema.FromText(@"{ + ""type"": ""object"", + ""properties"": { + ""bad"": { ""$ref"": ""#/$defs/Unknown"" } } - } - }"; - var schema = JsonSerializer.Deserialize(SchemaString, _options); + }"); var node = _sut.ParseJsonSchema(schema); - Assert.NotNull(node); - Assert.IsType(node); - var branch = (SemanticLeafNode)node; - Assert.Equal("person", branch.SemanticId); - Assert.Equal(DataType.String, branch.DataType); + var leaf = Assert.IsType(node); + Assert.Equal(DataType.Unknown, leaf.DataType); } [Fact] - public void ParseJsonSchema_InlineObjectWithNoProperties_CoversProcessObjectFallback() + public void ParseJsonSchema_DeepNestedDefs_ResolvesCorrectly() { - const string SchemaString = @"{ - ""$schema"": ""http://json-schema.org/draft-07/schema#"", - ""type"": ""object"", - ""properties"": { - ""outer"": { - ""type"": ""object"", - ""properties"": { - ""inner"": { ""type"": ""object"" } + var schema = JsonSchema.FromText(@"{ + ""type"": ""object"", + ""properties"": { + ""root"": { ""$ref"": ""#/$defs/A"" } + }, + ""$defs"": { + ""A"": { + ""type"": ""object"", + ""properties"": { + ""child"": { ""$ref"": ""#/$defs/B"" } + } + }, + ""B"": { + ""type"": ""string"" } } - } - }"; - var schema = JsonSerializer.Deserialize(SchemaString, _options); + }"); - var root = _sut.ParseJsonSchema(schema); + var node = _sut.ParseJsonSchema(schema); - var outerBranch = Assert.IsType(root); - Assert.Equal("outer", outerBranch.SemanticId); - Assert.Single(outerBranch.Children); - var innerBranch = Assert.IsType(outerBranch.Children[0]); - Assert.Equal("inner", innerBranch.SemanticId); - Assert.Empty(innerBranch.Children); + var branch = Assert.IsType(node); + var child = Assert.IsType(branch.Children[0]); + Assert.Equal("child", child.SemanticId); + Assert.Equal(DataType.String, child.DataType); } } diff --git a/source/AAS.TwinEngine.Plugin.TestPlugin.UnitTests/Api/Submodel/Services/JsonSchemaValidatorTests.cs b/source/AAS.TwinEngine.Plugin.TestPlugin.UnitTests/Api/Submodel/Services/JsonSchemaValidatorTests.cs index 808a4104..123ee722 100644 --- a/source/AAS.TwinEngine.Plugin.TestPlugin.UnitTests/Api/Submodel/Services/JsonSchemaValidatorTests.cs +++ b/source/AAS.TwinEngine.Plugin.TestPlugin.UnitTests/Api/Submodel/Services/JsonSchemaValidatorTests.cs @@ -49,13 +49,13 @@ public void ValidateResponseContent_EmptyResponse_ThrowsBadRequest() public void ValidateResponseContent_ValidateJsonSchemaRemovePrefix_DoesNotThrow() { var schema = new JsonSchemaBuilder() - .Type(SchemaValueType.Object) - .Properties(new Dictionary - { - ["ContactInformation_aastwinengine_00"] = new JsonSchemaBuilder().Type(SchemaValueType.Object).Build() - }) - .Required("ContactInformation_aastwinengine_00") - .Build(); + .Type(SchemaValueType.Object) + .Properties(new Dictionary + { + ["ContactInformation_aastwinengine_00"] = new JsonSchemaBuilder().Type(SchemaValueType.Object) + }) + .Required("ContactInformation_aastwinengine_00") + .Build(); const string Json = "{\"ContactInformation\": {}}"; @@ -66,13 +66,13 @@ public void ValidateResponseContent_ValidateJsonSchemaRemovePrefix_DoesNotThrow( public void ValidateResponseContent_ValidJsonAndSchema_DoesNotThrow() { var schema = new JsonSchemaBuilder() - .Type(SchemaValueType.Object) - .Properties(new Dictionary - { - ["name"] = new JsonSchemaBuilder().Type(SchemaValueType.String).Build() - }) - .Required("name") - .Build(); + .Type(SchemaValueType.Object) + .Properties(new Dictionary + { + ["name"] = new JsonSchemaBuilder().Type(SchemaValueType.String) + }) + .Required("name") + .Build(); const string Json = "{\"name\": \"Test\"}"; @@ -88,78 +88,220 @@ public void ValidateResponseContent_InvalidValueType_ThrowsBadRequest( { var schema = new JsonSchemaBuilder() .Type(SchemaValueType.Object) - .Properties(new Dictionary + .Properties(new Dictionary { - [property] = new JsonSchemaBuilder().Type(expectedType).Build() + [property] = new JsonSchemaBuilder().Type(expectedType) }) .Required(property) .Build(); + var json = $"{{\"{property}\": {rawValue} }}"; Assert.Throws(() => _sut.ValidateResponseContent(json, schema)); } [Fact] - public void ValidateResponseContent_PropertyTypeStringOrArray_WithString_DoesNotThrow() + public void ValidateResponseContent_SchemaMismatch_ThrowsBadRequest() + { + var schema = new JsonSchemaBuilder() + .Type(SchemaValueType.Object) + .Properties(new Dictionary + { + ["name"] = new JsonSchemaBuilder().Type(SchemaValueType.String) + }) + .Required("name") + .Build(); + + const string Json = "{}"; + + Assert.Throws(() => _sut.ValidateResponseContent(Json, schema)); + } + + [Fact] + public void ValidateResponseContent_InvalidJson_ThrowsBadRequest() { var schema = new JsonSchemaBuilder() - .Type(SchemaValueType.Object) - .Properties(new Dictionary - { - ["value"] = new JsonSchemaBuilder().Type(SchemaValueType.String, SchemaValueType.Array).Build() - }) - .Required("value") - .Build(); + .Type(SchemaValueType.Object) + .Build(); + + const string BadJson = "{ not valid json }"; + + Assert.Throws(() => _sut.ValidateResponseContent(BadJson, schema)); + } - const string Json = "{\"value\": \"hello\"}"; + [Fact] + public void ValidateResponseContent_NullResponse_ThrowsException() + { + var schema = new JsonSchemaBuilder() + .Type(SchemaValueType.Object) + .Build(); + + Assert.Throws(() => _sut.ValidateResponseContent(null!, schema)); + } + + [Fact] + public void ValidateResponseContent_WhitespaceOnlyResponse_ThrowsException() + { + var schema = new JsonSchemaBuilder() + .Type(SchemaValueType.Object) + .Build(); + + Assert.Throws(() => _sut.ValidateResponseContent(" ", schema)); + } + + [Fact] + public void ValidateResponseContent_MalformedJson_ThrowsException() + { + var schema = new JsonSchemaBuilder() + .Type(SchemaValueType.Object) + .Build(); + + Assert.Throws(() => _sut.ValidateResponseContent("{\"key\": }", schema)); + } + + [Fact] + public void ValidateResponseContent_PropertyWithSuffix_RemovesSuffix() + { + var schema = new JsonSchemaBuilder() + .Type(SchemaValueType.Object) + .Properties(new Dictionary + { + ["PropertyName_aastwinengine_123"] = new JsonSchemaBuilder().Type(SchemaValueType.String) + }) + .Required("PropertyName_aastwinengine_123") + .Build(); + + const string Json = "{\"PropertyName\": \"value\"}"; _sut.ValidateResponseContent(Json, schema); } [Fact] - public void ValidateResponseContent_PropertyTypeStringOrArray_WithArray_DoesNotThrow() + public void ValidateResponseContent_PropertyWithoutSuffix_DoesNotModify() { var schema = new JsonSchemaBuilder() - .Type(SchemaValueType.Object) - .Properties(new Dictionary - { - ["value"] = new JsonSchemaBuilder().Type(SchemaValueType.String, SchemaValueType.Array).Build() - }) - .Required("value") - .Build(); + .Type(SchemaValueType.Object) + .Properties(new Dictionary + { + ["PropertyWithoutSuffix"] = new JsonSchemaBuilder().Type(SchemaValueType.String) + }) + .Required("PropertyWithoutSuffix") + .Build(); - const string Json = "{\"value\": [\"one\", \"two\"]}"; + const string Json = "{\"PropertyWithoutSuffix\": \"value\"}"; _sut.ValidateResponseContent(Json, schema); } [Fact] - public void ValidateResponseContent_PropertyTypeStringOrArray_WithNumber_ThrowsBadRequest() + public void ValidateResponseContent_NestedObjectWithSuffixedProperties_RemovesSuffixes() { + var nestedSchema = new JsonSchemaBuilder() + .Type(SchemaValueType.Object) + .Properties(new Dictionary + { + ["nestedField_aastwinengine_01"] = new JsonSchemaBuilder().Type(SchemaValueType.String) + }); + var schema = new JsonSchemaBuilder() - .Type(SchemaValueType.Object) - .Properties(new Dictionary - { - ["value"] = new JsonSchemaBuilder().Type(SchemaValueType.String, SchemaValueType.Array).Build() - }) - .Required("value") - .Build(); + .Type(SchemaValueType.Object) + .Properties(new Dictionary + { + ["parent_aastwinengine_00"] = nestedSchema + }) + .Build(); - const string Json = "{\"value\": 123}"; + const string Json = "{\"parent\": {\"nestedField\": \"value\"}}"; - Assert.Throws(() => _sut.ValidateResponseContent(Json, schema)); + _sut.ValidateResponseContent(Json, schema); } [Fact] - public void ValidateResponseContent_SchemaMismatch_ThrowsBadRequest() + public void ValidateResponseContent_ArrayWithSuffixedItems_RemovesSuffixes() { var schema = new JsonSchemaBuilder() .Type(SchemaValueType.Object) - .Properties(new Dictionary + .Properties(new Dictionary { - ["name"] = new JsonSchemaBuilder().Type(SchemaValueType.String).Build() + ["items_aastwinengine_01"] = new JsonSchemaBuilder() + .Type(SchemaValueType.Array) + .Items(new JsonSchemaBuilder().Type(SchemaValueType.String)) + }) + .Build(); + + const string Json = "{\"items\": [\"item1\", \"item2\"]}"; + + _sut.ValidateResponseContent(Json, schema); + } + + [Fact] + public void ValidateResponseContent_ArrayOfObjects_ValidatesCorrectly() + { + var schema = new JsonSchemaBuilder() + .Type(SchemaValueType.Object) + .Properties(new Dictionary + { + ["users"] = new JsonSchemaBuilder() + .Type(SchemaValueType.Array) + .Items(new JsonSchemaBuilder() + .Type(SchemaValueType.Object) + .Properties(new Dictionary + { + ["name_aastwinengine_01"] = new JsonSchemaBuilder().Type(SchemaValueType.String) + })) }) - .Required("name") + .Build(); + + const string Json = "{\"users\": [{\"name\": \"Alice\"}, {\"name\": \"Bob\"}]}"; + + _sut.ValidateResponseContent(Json, schema); + } + + [Fact] + public void ValidateResponseContent_EmptyArray_ValidatesCorrectly() + { + var schema = new JsonSchemaBuilder() + .Type(SchemaValueType.Object) + .Properties(new Dictionary + { + ["items"] = new JsonSchemaBuilder() + .Type(SchemaValueType.Array) + .Items(new JsonSchemaBuilder().Type(SchemaValueType.String)) + }) + .Build(); + + const string Json = "{\"items\": []}"; + + _sut.ValidateResponseContent(Json, schema); + } + + [Fact] + public void ValidateResponseContent_RequiredPropertyWithSuffix_ValidatesAfterRemoval() + { + var schema = new JsonSchemaBuilder() + .Type(SchemaValueType.Object) + .Properties(new Dictionary + { + ["requiredField_aastwinengine_01"] = new JsonSchemaBuilder().Type(SchemaValueType.String) + }) + .Required("requiredField_aastwinengine_01") + .Build(); + + const string Json = "{\"requiredField\": \"value\"}"; + + _sut.ValidateResponseContent(Json, schema); + } + + [Fact] + public void ValidateResponseContent_MissingRequiredProperty_ThrowsException() + { + var schema = new JsonSchemaBuilder() + .Type(SchemaValueType.Object) + .Properties(new Dictionary + { + ["requiredField_aastwinengine_01"] = new JsonSchemaBuilder().Type(SchemaValueType.String) + }) + .Required("requiredField_aastwinengine_01") .Build(); const string Json = "{}"; @@ -168,13 +310,148 @@ public void ValidateResponseContent_SchemaMismatch_ThrowsBadRequest() } [Fact] - public void ValidateResponseContent_InvalidJson_ThrowsBadRequest() + public void ValidateResponseContent_SchemaWithoutId_ValidatesCorrectly() { var schema = new JsonSchemaBuilder() .Type(SchemaValueType.Object) + .Properties(new Dictionary + { + ["field"] = new JsonSchemaBuilder().Type(SchemaValueType.String) + }) .Build(); - const string BadJson = "{ not valid json }"; - Assert.Throws(() => _sut.ValidateResponseContent(BadJson, schema)); + const string Json = "{\"field\": \"value\"}"; + + _sut.ValidateResponseContent(Json, schema); + } + + [Fact] + public void ValidateResponseContent_PropertyNameWithHyphen_HandlesCorrectly() + { + var schema = new JsonSchemaBuilder() + .Type(SchemaValueType.Object) + .Properties(new Dictionary + { + ["field-name_aastwinengine_01"] = new JsonSchemaBuilder().Type(SchemaValueType.String) + }) + .Build(); + + const string Json = "{\"field-name\": \"value\"}"; + + _sut.ValidateResponseContent(Json, schema); + } + + [Fact] + public void ValidateResponseContent_PropertyNameWithDot_HandlesCorrectly() + { + var schema = new JsonSchemaBuilder() + .Type(SchemaValueType.Object) + .Properties(new Dictionary + { + ["field.name_aastwinengine_01"] = new JsonSchemaBuilder().Type(SchemaValueType.String) + }) + .Build(); + + const string Json = "{\"field.name\": \"value\"}"; + + _sut.ValidateResponseContent(Json, schema); + } + + [Fact] + public void ValidateResponseContent_PropertyNameWithUnicode_HandlesCorrectly() + { + var schema = new JsonSchemaBuilder() + .Type(SchemaValueType.Object) + .Properties(new Dictionary + { + ["field名前_aastwinengine_01"] = new JsonSchemaBuilder().Type(SchemaValueType.String) + }) + .Build(); + + const string Json = "{\"field名前\": \"value\"}"; + + _sut.ValidateResponseContent(Json, schema); + } + + [Fact] + public void ValidateResponseContent_DeeplyNestedStructure_ValidatesCorrectly() + { + var level3 = new JsonSchemaBuilder() + .Type(SchemaValueType.Object) + .Properties(new Dictionary + { + ["deepField_aastwinengine_03"] = new JsonSchemaBuilder().Type(SchemaValueType.String) + }); + + var level2 = new JsonSchemaBuilder() + .Type(SchemaValueType.Object) + .Properties(new Dictionary + { + ["midField_aastwinengine_02"] = level3 + }); + + var schema = new JsonSchemaBuilder() + .Type(SchemaValueType.Object) + .Properties(new Dictionary + { + ["topField_aastwinengine_01"] = level2 + }) + .Build(); + + const string Json = "{\"topField\": {\"midField\": {\"deepField\": \"value\"}}}"; + + _sut.ValidateResponseContent(Json, schema); + } + + [Fact] + public void ValidateResponseContent_MixedArrayAndObjectNesting_ValidatesCorrectly() + { + var objectSchema = new JsonSchemaBuilder() + .Type(SchemaValueType.Object) + .Properties(new Dictionary + { + ["items_aastwinengine_02"] = new JsonSchemaBuilder() + .Type(SchemaValueType.Array) + .Items(new JsonSchemaBuilder().Type(SchemaValueType.String)) + }); + + var schema = new JsonSchemaBuilder() + .Type(SchemaValueType.Object) + .Properties(new Dictionary + { + ["data_aastwinengine_01"] = new JsonSchemaBuilder() + .Type(SchemaValueType.Array) + .Items(objectSchema) + }) + .Build(); + + const string Json = "{\"data\": [{\"items\": [\"a\", \"b\"]}, {\"items\": [\"c\"]}]}"; + + _sut.ValidateResponseContent(Json, schema); + } + + [Fact] + public void ValidateResponseContent_WithDefsReference_DoesNotThrow() + { + const string schemaJson = @"{ + ""type"": ""object"", + ""properties"": { + ""item"": { ""$ref"": ""#/$defs/MyType"" } + }, + ""$defs"": { + ""MyType"": { + ""type"": ""object"", + ""properties"": { + ""name"": { ""type"": ""string"" } + } + } + } + }"; + + var schema = JsonSchema.FromText(schemaJson); + + const string json = @"{ ""item"": { ""name"": ""test"" } }"; + + _sut.ValidateResponseContent(json, schema); } } diff --git a/source/AAS.TwinEngine.Plugin.TestPlugin.UnitTests/CleanArchitectureTests.cs b/source/AAS.TwinEngine.Plugin.TestPlugin.UnitTests/CleanArchitectureTests.cs index c2f60b8b..525d5146 100644 --- a/source/AAS.TwinEngine.Plugin.TestPlugin.UnitTests/CleanArchitectureTests.cs +++ b/source/AAS.TwinEngine.Plugin.TestPlugin.UnitTests/CleanArchitectureTests.cs @@ -14,26 +14,17 @@ public class CleanArchitectureTests { private const string BaseNamespace = "AAS.TwinEngine.Plugin.TestPlugin"; - private readonly Architecture _architecture; - private readonly IObjectProvider _apiLayer; - private readonly IObjectProvider _applicationLogicLayer; - private readonly IObjectProvider _domainModelLayer; - private readonly IObjectProvider _infrastructureLayer; - - public CleanArchitectureTests() - { - _architecture = new ArchLoader().LoadAssemblies(System.Reflection.Assembly.Load(BaseNamespace)).Build(); - - _apiLayer = Types().That().ResideInNamespace($"{BaseNamespace}.Api.*", true).As("Api"); - _applicationLogicLayer = Types().That().ResideInNamespace($"{BaseNamespace}.ApplicationLogic.*", true).As("ApplicationLogic"); - _domainModelLayer = Types().That().ResideInNamespace($"{BaseNamespace}.DomainModel*", true).As("DomainModel"); - _infrastructureLayer = Types().That().ResideInNamespace($"{BaseNamespace}.Infrastructure.*", true).As("Infrastructure"); - } + private readonly Architecture _architecture = new ArchLoader().LoadAssemblies(System.Reflection.Assembly.Load(BaseNamespace)).Build(); + private readonly IObjectProvider _apiLayer = Types().That().ResideInNamespaceMatching($"{BaseNamespace}\\.Api($|\\..*)").As("Api"); + private readonly IObjectProvider _applicationLogicLayer = Types().That().ResideInNamespaceMatching($"{BaseNamespace}\\.ApplicationLogic($|\\..*)").As("ApplicationLogic"); + private readonly IObjectProvider _domainModelLayer = Types().That().ResideInNamespaceMatching($"{BaseNamespace}\\.DomainModel.*").As("DomainModel"); + private readonly IObjectProvider _infrastructureLayer = Types().That().ResideInNamespaceMatching($"{BaseNamespace}\\.Infrastructure($|\\..*)").As("Infrastructure"); [Fact] public void DomainModelShallNotHaveExternalDependencies() { var forbiddenTypes = new List(); + forbiddenTypes.AddRange(_infrastructureLayer.GetObjects(_architecture)); forbiddenTypes.AddRange(_apiLayer.GetObjects(_architecture)); forbiddenTypes.AddRange(_applicationLogicLayer.GetObjects(_architecture)); @@ -41,6 +32,7 @@ public void DomainModelShallNotHaveExternalDependencies() Types().That().Are(_domainModelLayer) .Should() .NotDependOnAny(Types().That().Are(forbiddenTypes)) + .WithoutRequiringPositiveResults() .Check(_architecture); } @@ -50,6 +42,7 @@ public void ApplicationLogicShallNotHaveDependenciesToInfrastructure() Types().That().Are(_applicationLogicLayer) .Should() .NotDependOnAny(Types().That().Are(_infrastructureLayer)) + .WithoutRequiringPositiveResults() .Check(_architecture); } @@ -59,6 +52,7 @@ public void ApplicationLogicShallNotHaveDependenciesToApi() Types().That().Are(_applicationLogicLayer) .Should() .NotDependOnAny(Types().That().Are(_apiLayer)) + .WithoutRequiringPositiveResults() .Check(_architecture); } @@ -68,6 +62,7 @@ public void InfrastructureShallNotHaveDependenciesToApi() Types().That().Are(_infrastructureLayer) .Should() .NotDependOnAny(Types().That().Are(_apiLayer)) + .WithoutRequiringPositiveResults() .Check(_architecture); } @@ -77,6 +72,7 @@ public void ApiShallNotHaveDependenciesToInfrastructure() Types().That().Are(_apiLayer) .Should() .NotDependOnAny(Types().That().Are(_infrastructureLayer)) + .WithoutRequiringPositiveResults() .Check(_architecture); } @@ -84,7 +80,7 @@ public void ApiShallNotHaveDependenciesToInfrastructure() public void RepositoryClassesShallBeInCorrectNamespace() { Classes().That().HaveNameEndingWith("Repository").Should() - .ResideInNamespace($"{BaseNamespace}.Infrastructure.DataAccess*", true) + .ResideInNamespaceMatching($"{BaseNamespace}\\.Infrastructure\\.DataAccess.*") .WithoutRequiringPositiveResults() .Check(_architecture); } @@ -97,7 +93,7 @@ public void RepositoryInterfacesShallBeInCorrectNamespace() .And() .DoNotHaveFullName($"{BaseNamespace}.Infrastructure.DataAccess.GenericRepository.IMongoDbRepository") .Should() - .ResideInNamespace($"{BaseNamespace}.ApplicationLogic.*", true) + .ResideInNamespaceMatching($"{BaseNamespace}\\.ApplicationLogic($|\\..*).") .WithoutRequiringPositiveResults() .Check(_architecture); } @@ -106,7 +102,7 @@ public void RepositoryInterfacesShallBeInCorrectNamespace() public void ServicesShallBeInCorrectNamespace() { Classes().That().HaveNameEndingWith("Service").Should() - .ResideInNamespace($"{BaseNamespace}.ApplicationLogic.Service.*", true) + .ResideInNamespaceMatching($"{BaseNamespace}\\.ApplicationLogic\\.Services($|\\..*).") .Check(_architecture); } @@ -115,7 +111,7 @@ public void ServiceInterfacesShallBeInCorrectNamespace() { Interfaces().That().HaveNameEndingWith("Service") .Should() - .ResideInNamespace($"{BaseNamespace}.ApplicationLogic.Service.*", true) + .ResideInNamespaceMatching($"{BaseNamespace}\\.ApplicationLogic\\.Services($|\\..*).") .Check(_architecture); } @@ -123,7 +119,7 @@ public void ServiceInterfacesShallBeInCorrectNamespace() public void ControllerShallBeInCorrectNamespace() { Classes().That().HaveNameEndingWith("Controller").Should() - .ResideInNamespace($"{BaseNamespace}.Api.*", true) + .ResideInNamespaceMatching($"{BaseNamespace}\\.Api($|\\..*).") .Check(_architecture); } -} \ No newline at end of file +} diff --git a/source/AAS.TwinEngine.Plugin.TestPlugin/AAS.TwinEngine.Plugin.TestPlugin.csproj b/source/AAS.TwinEngine.Plugin.TestPlugin/AAS.TwinEngine.Plugin.TestPlugin.csproj index 4f165651..615935a6 100644 --- a/source/AAS.TwinEngine.Plugin.TestPlugin/AAS.TwinEngine.Plugin.TestPlugin.csproj +++ b/source/AAS.TwinEngine.Plugin.TestPlugin/AAS.TwinEngine.Plugin.TestPlugin.csproj @@ -1,4 +1,4 @@ - + net8.0 @@ -19,20 +19,20 @@ - - + + - - - - - - - - - - - - + + + + + + + + + + + + diff --git a/source/AAS.TwinEngine.Plugin.TestPlugin/Api/Submodel/Services/JsonSchemaParser.cs b/source/AAS.TwinEngine.Plugin.TestPlugin/Api/Submodel/Services/JsonSchemaParser.cs index f346f229..923cdf79 100644 --- a/source/AAS.TwinEngine.Plugin.TestPlugin/Api/Submodel/Services/JsonSchemaParser.cs +++ b/source/AAS.TwinEngine.Plugin.TestPlugin/Api/Submodel/Services/JsonSchemaParser.cs @@ -1,4 +1,5 @@ using System.Text.Json; +using System.Text.Json.Nodes; using AAS.TwinEngine.Plugin.TestPlugin.ApplicationLogic.Constants; using AAS.TwinEngine.Plugin.TestPlugin.ApplicationLogic.Exceptions; @@ -20,147 +21,161 @@ private void ValidateRequest(JsonSchema jsonSchema) { try { - var node = JsonSerializer.SerializeToNode(jsonSchema); - var result = MetaSchemas.Draft7.Evaluate(node, new EvaluationOptions { OutputFormat = OutputFormat.List }); + var json = JsonSerializer.SerializeToNode(jsonSchema); + var element = JsonDocument.Parse(json!.ToJsonString()).RootElement; + + var result = MetaSchemas.Draft202012.Evaluate(element, new EvaluationOptions + { + OutputFormat = OutputFormat.List + }); + if (!result.IsValid) { - logger.LogError("Requested schema is not validate"); + logger.LogError("Requested schema is not valid"); throw new BadRequestException(ExceptionMessages.RequestBodyInvalid); } } catch (JsonException) { - logger.LogError("Requested schema is not validate"); + logger.LogError("Requested schema is not valid"); throw new BadRequestException(ExceptionMessages.FailedParsingJsonSchema); } } private SemanticTreeNode CreateSemanticTree(JsonSchema jsonSchema) { - var propertiesKeyword = jsonSchema.GetKeyword(); - if (propertiesKeyword == null || !propertiesKeyword.Properties.Any()) + var json = JsonSerializer.SerializeToNode(jsonSchema)!.AsObject(); + + if (!json.TryGetPropertyValue("properties", out var propsNode) || + propsNode is not JsonObject props || + !props.Any()) { throw new BadRequestException(ExceptionMessages.InvalidJsonSchemaRootElement); } - var rootProperty = propertiesKeyword.Properties.First(); - return ProcessProperty(rootProperty.Key, rootProperty.Value, jsonSchema.GetKeyword()); + var rootProperty = props.First(); + return ProcessProperty(rootProperty.Key, rootProperty.Value!, json); } - private SemanticTreeNode ProcessProperty(string schemaPropertyName, JsonSchema property, DefinitionsKeyword definitions) + private SemanticTreeNode ProcessProperty(string name, JsonNode propertyNode, JsonObject root) { - var refKeyword = property.GetKeyword(); - if (refKeyword != null) - { - return HandleReference(schemaPropertyName, property, definitions); - } + var property = propertyNode.AsObject(); - var typeKeyword = property.GetKeyword(); - if (typeKeyword == null) + if (property.TryGetPropertyValue("$ref", out var refNode)) { - return new SemanticLeafNode(schemaPropertyName, DataType.String, ""); + return HandleReference(name, refNode!.GetValue(), root); } - var schemaType = GetSchemaType(typeKeyword); - if (schemaType is DataType.Object or DataType.Array) - { - return BuildObjectNode(schemaPropertyName, schemaType, property, definitions); - } + var type = GetType(property); - return new SemanticLeafNode(schemaPropertyName, schemaType, ""); + return type switch + { + DataType.Object => BuildObjectBranch(name, propertyNode, root), + DataType.Array => BuildArrayBranch(name, propertyNode, root), + _ => new SemanticLeafNode(name, type, "") + }; } - private SemanticTreeNode HandleReference(string schemaPropertyName, JsonSchema property, DefinitionsKeyword definitions) + private SemanticTreeNode HandleReference(string name, string reference, JsonObject root) { - var refKeyword = property.GetKeyword(); - if (refKeyword == null) + if (!reference.StartsWith("#/$defs/", StringComparison.OrdinalIgnoreCase)) { - return new SemanticLeafNode(schemaPropertyName, DataType.String, ""); + return new SemanticLeafNode(name, DataType.Unknown, ""); } - var definitionKey = refKeyword.Reference.ToString().Replace("#/definitions/", ""); - if (definitions == null || !definitions.Definitions.TryGetValue(definitionKey, out var def)) - { - return new SemanticLeafNode(schemaPropertyName, DataType.Unknown, ""); - } + var key = reference.Replace("#/$defs/", "", StringComparison.OrdinalIgnoreCase); - var defTypeKeyword = def.GetKeyword(); - if (defTypeKeyword == null) + if (!root.TryGetPropertyValue("$defs", out var defsNode) || + defsNode is not JsonObject defs || + !defs.TryGetPropertyValue(key, out var defNode)) { - return new SemanticLeafNode(schemaPropertyName, DataType.String, ""); + return new SemanticLeafNode(name, DataType.Unknown, ""); } - var schemaType = GetSchemaType(defTypeKeyword); - if (schemaType is DataType.Object or DataType.Array) + return ProcessProperty(name, defNode!, root); + } + + private SemanticBranchNode BuildObjectBranch(string name, JsonNode node, JsonObject root) + { + var obj = node.AsObject(); + var branch = new SemanticBranchNode(name, DataType.Object); + + if (obj.TryGetPropertyValue("properties", out var propsNode) && + propsNode is JsonObject props) { - return BuildObjectNode(schemaPropertyName, schemaType, def, definitions); + foreach (var prop in props) + { + branch.AddChild(ProcessProperty(prop.Key, prop.Value!, root)); + } } - return new SemanticLeafNode(schemaPropertyName, schemaType, ""); + return branch; } - private SemanticBranchNode BuildObjectNode(string schemaPropertyName, DataType dataType, JsonSchema schema, DefinitionsKeyword definitions) + private SemanticBranchNode BuildArrayBranch(string name, JsonNode node, JsonObject root) { - var branchNode = new SemanticBranchNode(schemaPropertyName, dataType); + var obj = node.AsObject(); + var branch = new SemanticBranchNode(name, DataType.Array); - switch (dataType) + if (!obj.TryGetPropertyValue("items", out var itemsNode) || + itemsNode is not JsonObject itemObj) { - case DataType.Object: - { - var propertiesKeyword = schema.GetKeyword(); - if (propertiesKeyword != null) - { - foreach (var prop in propertiesKeyword.Properties) - { - branchNode.AddChild(ProcessProperty(prop.Key, prop.Value, definitions)); - } - } - - break; - } - case DataType.Array: + return branch; + } + + var itemType = GetType(itemObj); + + if (itemType == DataType.Object && + itemObj.TryGetPropertyValue("properties", out var propsNode) && + propsNode is JsonObject props) + { + foreach (var prop in props) + { + branch.AddChild(ProcessProperty(prop.Key, prop.Value!, root)); + } + + return branch; + } + + if (itemObj.TryGetPropertyValue("$ref", out var refNode)) + { + var resolved = HandleReference(name, refNode!.GetValue(), root); + + if (resolved is SemanticBranchNode refBranch) + { + foreach (var child in refBranch.Children) { - var itemsKeyword = schema.GetKeyword(); - if (itemsKeyword == null) - { - var propertiesKeyword = schema.GetKeyword(); - if (propertiesKeyword != null) - { - foreach (var prop in propertiesKeyword.Properties) - { - branchNode.AddChild(ProcessProperty(prop.Key, prop.Value, definitions)); - } - } - - break; - } - - if (itemsKeyword is { SingleSchema: not null }) - { - branchNode.AddChild(ProcessProperty("item", itemsKeyword.SingleSchema, definitions)); - } - - break; + branch.AddChild(child); } + } + else + { + branch.AddChild(resolved); + } + + return branch; } - return branchNode; + branch.AddChild(new SemanticLeafNode(name, itemType, "")); + return branch; } - private static DataType GetSchemaType(TypeKeyword typeKeyword) + private static DataType GetType(JsonObject obj) { - var t = typeKeyword.Type; + if (!obj.TryGetPropertyValue("type", out var typeNode)) + { + return DataType.String; + } - return t switch + return typeNode!.ToString() switch { - _ when t.HasFlag(SchemaValueType.Object) => DataType.Object, - _ when t.HasFlag(SchemaValueType.Array) => DataType.Array, - _ when t.HasFlag(SchemaValueType.String) => DataType.String, - _ when t.HasFlag(SchemaValueType.Integer) => DataType.Integer, - _ when t.HasFlag(SchemaValueType.Number) => DataType.Number, - _ when t.HasFlag(SchemaValueType.Boolean) => DataType.Boolean, + "object" => DataType.Object, + "array" => DataType.Array, + "string" => DataType.String, + "integer" => DataType.Integer, + "number" => DataType.Number, + "boolean" => DataType.Boolean, _ => DataType.String }; } - } diff --git a/source/AAS.TwinEngine.Plugin.TestPlugin/Api/Submodel/Services/JsonSchemaValidator.cs b/source/AAS.TwinEngine.Plugin.TestPlugin/Api/Submodel/Services/JsonSchemaValidator.cs index 6081beff..7b29ac6c 100644 --- a/source/AAS.TwinEngine.Plugin.TestPlugin/Api/Submodel/Services/JsonSchemaValidator.cs +++ b/source/AAS.TwinEngine.Plugin.TestPlugin/Api/Submodel/Services/JsonSchemaValidator.cs @@ -15,7 +15,12 @@ namespace AAS.TwinEngine.Plugin.TestPlugin.Api.Submodel.Services; public class JsonSchemaValidator(IOptions semantics, ILogger logger) : IJsonSchemaValidator { private readonly string _contextPrefix = semantics.Value.IndexContextPrefix; - private const string DefinitionsPrefix = "#/definitions/"; + private const string DefsPrefix = "#/$defs/"; + + private readonly EvaluationOptions _evaluationOptions = new() + { + OutputFormat = OutputFormat.List + }; private static readonly JsonSerializerOptions Serialization = new() { @@ -41,15 +46,12 @@ public void ValidateResponseContent(string responseJson, JsonSchema requestSchem LogAndThrowException($"Failed to normalize request schema: {normalizeError}"); } - if (!TryRegisterJsonSchema(normalizedSchema, out var registerError)) - { - LogAndThrowException($"Failed to register schema: {registerError}"); - } - try { var schema = JsonSchema.FromText(normalizedSchema.ToJsonString()); - var result = schema.Evaluate(responseDoc!.RootElement, new EvaluationOptions { OutputFormat = OutputFormat.List }); + + var result = schema.Evaluate(responseDoc!.RootElement, _evaluationOptions); + if (!result.IsValid) { LogAndThrowException("Response did not validate against schema."); @@ -102,10 +104,11 @@ private bool TryNormalizeSchema(JsonSchema schema, out JsonObject normalized, ou var json = JsonSerializer.Serialize(schema, Serialization); normalized = JsonNode.Parse(json)?.AsObject() - ?? throw new ArgumentException("Failed to parse schema JSON."); + ?? throw new ArgumentException("Failed to parse schema JSON."); EscapeJsonReferencePointers(normalized); - normalized["$id"] = normalized["$id"]?.GetValue() ?? $"urn:uuid:{Guid.NewGuid():D}"; + + normalized["$schema"] ??= "https://json-schema.org/draft/2020-12/schema"; return true; } @@ -116,38 +119,19 @@ private bool TryNormalizeSchema(JsonSchema schema, out JsonObject normalized, ou } } - private static bool TryRegisterJsonSchema(JsonObject schemaJsonObject, out string? registrationErrorMessage) - { - registrationErrorMessage = null; - - try - { - var jsonSchema = JsonSchema.FromText(schemaJsonObject.ToJsonString()); - var schemaIdentifierUri = new Uri(schemaJsonObject["$id"]!.GetValue()!); - SchemaRegistry.Global.Register(schemaIdentifierUri, jsonSchema); - return true; - } - catch (Exception exception) - { - registrationErrorMessage = $"Schema registration failed: {exception.Message}"; - return false; - } - } - private void EscapeJsonReferencePointers(JsonNode? currentNode) { switch (currentNode) { - case JsonObject jsonObjectNode: - ProcessJsonObjectForEscaping(jsonObjectNode); + case JsonObject obj: + ProcessJsonObjectForEscaping(obj); break; - case JsonArray jsonArrayNode: - foreach (var arrayElement in jsonArrayNode) + case JsonArray array: + foreach (var item in array) { - EscapeJsonReferencePointers(arrayElement); + EscapeJsonReferencePointers(item); } - break; } } @@ -155,37 +139,36 @@ private void EscapeJsonReferencePointers(JsonNode? currentNode) private void ProcessJsonObjectForEscaping(JsonObject jsonObject) { var propertiesToRename = jsonObject - .Select(property => property.Key) - .Select(propertyName => (originalName: propertyName, strippedName: RemoveContextSuffix(propertyName))) - .Where(namePair => namePair.strippedName != namePair.originalName) + .Select(p => p.Key) + .Select(name => (original: name, stripped: RemoveContextSuffix(name))) + .Where(x => x.original != x.stripped) .ToList(); - foreach (var (originalName, strippedName) in propertiesToRename) + foreach (var (original, stripped) in propertiesToRename) { - RenameJsonProperty(jsonObject, originalName, strippedName); + RenameJsonProperty(jsonObject, original, stripped); } - if (jsonObject.TryGetPropertyValue("required", out var requiredPropertiesNode) && - requiredPropertiesNode is JsonArray requiredPropertiesArray) + if (jsonObject.TryGetPropertyValue("required", out var requiredNode) && + requiredNode is JsonArray requiredArray) { - RemoveContextSuffixFromRequiredProperties(requiredPropertiesArray); + RemoveContextSuffixFromRequiredProperties(requiredArray); } foreach (var property in jsonObject.ToList()) { - var propertyName = property.Key; - var propertyValue = property.Value; - - if (propertyName == "$ref" && - propertyValue is JsonValue referenceValue && - referenceValue.TryGetValue(out var referenceString) && - referenceString.StartsWith(DefinitionsPrefix, StringComparison.OrdinalIgnoreCase)) + if (property.Key == "$ref" && + property.Value is JsonValue value && + value.TryGetValue(out var reference)) { - jsonObject["$ref"] = BuildEscapedReferencePath(referenceString); + if (reference.StartsWith(DefsPrefix, StringComparison.OrdinalIgnoreCase)) + { + jsonObject["$ref"] = BuildEscapedReferencePath(reference); + } } else { - EscapeJsonReferencePointers(propertyValue); + EscapeJsonReferencePointers(property.Value); } } } @@ -201,15 +184,22 @@ private void RemoveContextSuffixFromRequiredProperties(JsonArray requiredPropert } } - private string BuildEscapedReferencePath(string originalReferencePath) + private string BuildEscapedReferencePath(string reference) { - var referenceWithoutPrefix = originalReferencePath[DefinitionsPrefix.Length..]; + if (!reference.StartsWith(DefsPrefix, StringComparison.OrdinalIgnoreCase)) + { + return reference; + } + + var body = reference[DefsPrefix.Length..]; - var strippedReference = RemoveContextSuffix(referenceWithoutPrefix); + var stripped = RemoveContextSuffix(body); - var escapedReference = strippedReference.Replace("~", "~0", StringComparison.OrdinalIgnoreCase).Replace("/", "~1", StringComparison.OrdinalIgnoreCase); + var escaped = stripped + .Replace("~", "~0", StringComparison.OrdinalIgnoreCase) + .Replace("/", "~1", StringComparison.OrdinalIgnoreCase); - return DefinitionsPrefix + escapedReference; + return DefsPrefix + escaped; } private string RemoveContextSuffix(string propertyName) @@ -226,7 +216,7 @@ private static void RenameJsonProperty(JsonObject jsonObject, string oldProperty } var propertyValue = jsonObject[oldPropertyName]; - jsonObject.Remove(oldPropertyName); + _ = jsonObject.Remove(oldPropertyName); jsonObject[newPropertyName] = propertyValue!; } } From 6b484189f099ff7cf95218a434324fbcc4e16f56 Mon Sep 17 00:00:00 2001 From: Hardi Shah Date: Fri, 24 Apr 2026 13:00:27 +0530 Subject: [PATCH 59/71] Refactor contact info structure and update mock data - Reordered and restructured "Phone", "Email", and "IPCommunication" elements in expected submodel JSON files - Swapped and updated properties, semantic IDs, and descriptions for phone and email entries - Updated mock-submodel-data.json to match new structure and removed outdated entries - Minor XML formatting change in ContactInformationAAS.aas.xml - Added JSON serialization of dataQuery in SubmodelController for debugging purposes - Ensured consistency between expected data, mock data, and controller logic --- ...ntactInfo_ContactInformation_Expected.json | 236 +++------ .../GetSubmodel_ContactInfo_Expected.json | 472 +++++------------- .../Api/Submodel/SubmodelController.cs | 7 +- .../Data/mock-submodel-data.json | 27 - .../Example/aas/ContactInformationAAS.aas.xml | 4 +- .../Example/plugin/mock-submodel-data.json | 27 - 6 files changed, 194 insertions(+), 579 deletions(-) diff --git a/source/AAS.TwinEngine.Plugin.TestPlugin.PlaywrightTests/SubmodelRepository/TestData/GetSubmodelElement_ContactInfo_ContactInformation_Expected.json b/source/AAS.TwinEngine.Plugin.TestPlugin.PlaywrightTests/SubmodelRepository/TestData/GetSubmodelElement_ContactInfo_ContactInformation_Expected.json index 2e628d84..b6135f12 100644 --- a/source/AAS.TwinEngine.Plugin.TestPlugin.PlaywrightTests/SubmodelRepository/TestData/GetSubmodelElement_ContactInfo_ContactInformation_Expected.json +++ b/source/AAS.TwinEngine.Plugin.TestPlugin.PlaywrightTests/SubmodelRepository/TestData/GetSubmodelElement_ContactInfo_ContactInformation_Expected.json @@ -245,13 +245,13 @@ "modelType": "MultiLanguageProperty" }, { - "idShort": "Email", + "idShort": "Phone", "semanticId": { "type": "ExternalReference", "keys": [ { "type": "GlobalReference", - "value": "0173-1#02-AAQ836#005" + "value": "https://admin-shell.io/zvei/nameplate/1/0/ContactInformations/ContactInformation/Phone" } ] }, @@ -265,13 +265,19 @@ ], "value": [ { - "idShort": "EmailAddress", + "idShort": "TelephoneNumber", + "description": [ + { + "language": "en", + "text": "Recommendation: property declaration as MLP is required by its semantic definition. As the property value is language independent, users are recommended to provide maximal 1 string in any language of the user’s choice." + } + ], "semanticId": { "type": "ExternalReference", "keys": [ { "type": "GlobalReference", - "value": "0173-1#02-AAO198#002" + "value": "0173-1#02-AAO136#002" } ] }, @@ -283,24 +289,22 @@ "value": "One" } ], - "valueType": "xs:string", - "value": "john.doe@example.com", - "modelType": "Property" - }, - { - "idShort": "TypeOfEmailAddress", - "description": [ + "value": [ { "language": "en", - "text": "enumeration: 0173-1#07-AAS754#001 (office), 0173-1#07-AAS756#001 (secretary), 0173-1#07-AAS757#001 (substitute), 0173-1#07-AAS758#001 (home)" + "text": "345-678-9012" } ], + "modelType": "MultiLanguageProperty" + }, + { + "idShort": "AvailableTime", "semanticId": { "type": "ExternalReference", "keys": [ { "type": "GlobalReference", - "value": "0173-1#02-AAO199#003" + "value": "https://admin-shell.io/zvei/nameplate/1/0/ContactInformations/ContactInformation/AvailableTime/" } ] }, @@ -312,87 +316,28 @@ "value": "ZeroToOne" } ], - "valueType": "xs:string", - "value": "Work", - "modelType": "Property" - } - ], - "modelType": "SubmodelElementCollection" - }, - { - "idShort": "IPCommunication__00__", - "semanticId": { - "type": "ExternalReference", - "keys": [ - { - "type": "GlobalReference", - "value": "https://admin-shell.io/zvei/nameplate/1/0/ContactInformations/" - } - ] - }, - "qualifiers": [ - { - "kind": "ConceptQualifier", - "type": "Multiplicity", - "valueType": "xs:string", - "value": "ZeroToMany" - } - ], - "value": [ - { - "idShort": "AddressOfAdditionalLink", - "semanticId": { - "type": "ExternalReference", - "keys": [ - { - "type": "GlobalReference", - "value": "0173-1#02-AAQ326#002" - } - ] - }, - "qualifiers": [ + "value": [ { - "kind": "ConceptQualifier", - "type": "Multiplicity", - "valueType": "xs:string", - "value": "One" + "language": "de", + "text": "Montags, 10-16 Uhr" } ], - "valueType": "xs:string", - "value": "https://www.mm-software.com/more-the-newsroom/", - "modelType": "Property" + "modelType": "MultiLanguageProperty" }, { - "idShort": "TypeOfCommunication", - "semanticId": { - "type": "ExternalReference", - "keys": [ - { - "type": "GlobalReference", - "value": "https://admin-shell.io/zvei/nameplate/1/0/ContactInformations/ContactInformation/IPCommunication/TypeOfCommunication" - } - ] - }, - "qualifiers": [ + "idShort": "TypeOfTelephone", + "description": [ { - "kind": "ConceptQualifier", - "type": "Multiplicity", - "valueType": "xs:string", - "value": "ZeroToOne" + "language": "en", + "text": " enumeration: 0173-1#07-AAS754#001 (office), 0173-1#07-AAS755#001 (office mobile), 0173-1#07-AAS756#001 (secretary), 0173-1#07-AAS757#001 (substitute), 0173-1#07-AAS758#001 (home), 0173-1#07-AAS759#001 (private mobile)" } ], - "valueType": "xs:string", - "value": "Chat", - "modelType": "Property" - }, - { - "idShort": "AvailableTime", "semanticId": { "type": "ExternalReference", "keys": [ { "type": "GlobalReference", - "value": "https://admin-shell.io/zvei/nameplate/1/0/ContactInformations/ContactInformation/AvailableTime/" + "value": "0173-1#02-AAO137#003" } ] }, @@ -404,25 +349,21 @@ "value": "ZeroToOne" } ], - "value": [ - { - "language": "de", - "text": "8:00 AM to 6:00 PM" - } - ], - "modelType": "MultiLanguageProperty" + "valueType": "xs:string", + "value": "Mobile", + "modelType": "Property" } ], "modelType": "SubmodelElementCollection" }, { - "idShort": "Phone0", + "idShort": "Email", "semanticId": { "type": "ExternalReference", "keys": [ { "type": "GlobalReference", - "value": "https://admin-shell.io/zvei/nameplate/1/0/ContactInformations/ContactInformation/Phone" + "value": "0173-1#02-AAQ836#005" } ] }, @@ -436,19 +377,13 @@ ], "value": [ { - "idShort": "TelephoneNumber", - "description": [ - { - "language": "en", - "text": "Recommendation: property declaration as MLP is required by its semantic definition. As the property value is language independent, users are recommended to provide maximal 1 string in any language of the user’s choice." - } - ], + "idShort": "EmailAddress", "semanticId": { "type": "ExternalReference", "keys": [ { "type": "GlobalReference", - "value": "0173-1#02-AAO136#002" + "value": "0173-1#02-AAO198#002" } ] }, @@ -460,47 +395,16 @@ "value": "One" } ], - "value": [ - { - "language": "en", - "text": "234-567-8901" - } - ], - "modelType": "MultiLanguageProperty" - }, - { - "idShort": "AvailableTime", - "semanticId": { - "type": "ExternalReference", - "keys": [ - { - "type": "GlobalReference", - "value": "https://admin-shell.io/zvei/nameplate/1/0/ContactInformations/ContactInformation/AvailableTime/" - } - ] - }, - "qualifiers": [ - { - "kind": "ConceptQualifier", - "type": "Multiplicity", - "valueType": "xs:string", - "value": "ZeroToOne" - } - ], - "value": [ - { - "language": "de", - "text": "Dienstags bis Donnerstags, 9-17 Uhr" - } - ], - "modelType": "MultiLanguageProperty" + "valueType": "xs:string", + "value": "john.doe@example.com", + "modelType": "Property" }, { - "idShort": "TypeOfTelephone", + "idShort": "TypeOfEmailAddress", "description": [ { "language": "en", - "text": " enumeration: 0173-1#07-AAS754#001 (office), 0173-1#07-AAS755#001 (office mobile), 0173-1#07-AAS756#001 (secretary), 0173-1#07-AAS757#001 (substitute), 0173-1#07-AAS758#001 (home), 0173-1#07-AAS759#001 (private mobile)" + "text": "enumeration: 0173-1#07-AAS754#001 (office), 0173-1#07-AAS756#001 (secretary), 0173-1#07-AAS757#001 (substitute), 0173-1#07-AAS758#001 (home)" } ], "semanticId": { @@ -508,7 +412,7 @@ "keys": [ { "type": "GlobalReference", - "value": "0173-1#02-AAO137#003" + "value": "0173-1#02-AAO199#003" } ] }, @@ -521,20 +425,20 @@ } ], "valueType": "xs:string", - "value": "Office", + "value": "Work", "modelType": "Property" } ], "modelType": "SubmodelElementCollection" }, { - "idShort": "Phone1", + "idShort": "IPCommunication__00__", "semanticId": { "type": "ExternalReference", "keys": [ { "type": "GlobalReference", - "value": "https://admin-shell.io/zvei/nameplate/1/0/ContactInformations/ContactInformation/Phone" + "value": "https://admin-shell.io/zvei/nameplate/1/0/ContactInformations/" } ] }, @@ -543,24 +447,18 @@ "kind": "ConceptQualifier", "type": "Multiplicity", "valueType": "xs:string", - "value": "ZeroToOne" + "value": "ZeroToMany" } ], "value": [ { - "idShort": "TelephoneNumber", - "description": [ - { - "language": "en", - "text": "Recommendation: property declaration as MLP is required by its semantic definition. As the property value is language independent, users are recommended to provide maximal 1 string in any language of the user’s choice." - } - ], + "idShort": "AddressOfAdditionalLink", "semanticId": { "type": "ExternalReference", "keys": [ { "type": "GlobalReference", - "value": "0173-1#02-AAO136#002" + "value": "0173-1#02-AAQ326#002" } ] }, @@ -572,22 +470,18 @@ "value": "One" } ], - "value": [ - { - "language": "en", - "text": "345-678-9012" - } - ], - "modelType": "MultiLanguageProperty" + "valueType": "xs:string", + "value": "https://www.mm-software.com/more-the-newsroom/", + "modelType": "Property" }, { - "idShort": "AvailableTime", + "idShort": "TypeOfCommunication", "semanticId": { "type": "ExternalReference", "keys": [ { "type": "GlobalReference", - "value": "https://admin-shell.io/zvei/nameplate/1/0/ContactInformations/ContactInformation/AvailableTime/" + "value": "https://admin-shell.io/zvei/nameplate/1/0/ContactInformations/ContactInformation/IPCommunication/TypeOfCommunication" } ] }, @@ -599,28 +493,18 @@ "value": "ZeroToOne" } ], - "value": [ - { - "language": "de", - "text": "Montags, 10-16 Uhr" - } - ], - "modelType": "MultiLanguageProperty" + "valueType": "xs:string", + "value": "Chat", + "modelType": "Property" }, { - "idShort": "TypeOfTelephone", - "description": [ - { - "language": "en", - "text": " enumeration: 0173-1#07-AAS754#001 (office), 0173-1#07-AAS755#001 (office mobile), 0173-1#07-AAS756#001 (secretary), 0173-1#07-AAS757#001 (substitute), 0173-1#07-AAS758#001 (home), 0173-1#07-AAS759#001 (private mobile)" - } - ], + "idShort": "AvailableTime", "semanticId": { "type": "ExternalReference", "keys": [ { "type": "GlobalReference", - "value": "0173-1#02-AAO137#003" + "value": "https://admin-shell.io/zvei/nameplate/1/0/ContactInformations/ContactInformation/AvailableTime/" } ] }, @@ -632,9 +516,13 @@ "value": "ZeroToOne" } ], - "valueType": "xs:string", - "value": "Mobile", - "modelType": "Property" + "value": [ + { + "language": "de", + "text": "8:00 AM to 6:00 PM" + } + ], + "modelType": "MultiLanguageProperty" } ], "modelType": "SubmodelElementCollection" diff --git a/source/AAS.TwinEngine.Plugin.TestPlugin.PlaywrightTests/SubmodelRepository/TestData/GetSubmodel_ContactInfo_Expected.json b/source/AAS.TwinEngine.Plugin.TestPlugin.PlaywrightTests/SubmodelRepository/TestData/GetSubmodel_ContactInfo_Expected.json index 249204e2..28e1f623 100644 --- a/source/AAS.TwinEngine.Plugin.TestPlugin.PlaywrightTests/SubmodelRepository/TestData/GetSubmodel_ContactInfo_Expected.json +++ b/source/AAS.TwinEngine.Plugin.TestPlugin.PlaywrightTests/SubmodelRepository/TestData/GetSubmodel_ContactInfo_Expected.json @@ -259,13 +259,13 @@ "modelType": "MultiLanguageProperty" }, { - "idShort": "Email", + "idShort": "Phone", "semanticId": { "type": "ExternalReference", "keys": [ { "type": "GlobalReference", - "value": "0173-1#02-AAQ836#005" + "value": "https://admin-shell.io/zvei/nameplate/1/0/ContactInformations/ContactInformation/Phone" } ] }, @@ -279,13 +279,19 @@ ], "value": [ { - "idShort": "EmailAddress", + "idShort": "TelephoneNumber", + "description": [ + { + "language": "en", + "text": "Recommendation: property declaration as MLP is required by its semantic definition. As the property value is language independent, users are recommended to provide maximal 1 string in any language of the user’s choice." + } + ], "semanticId": { "type": "ExternalReference", "keys": [ { "type": "GlobalReference", - "value": "0173-1#02-AAO198#002" + "value": "0173-1#02-AAO136#002" } ] }, @@ -297,24 +303,22 @@ "value": "One" } ], - "valueType": "xs:string", - "value": "john.doe@example.com", - "modelType": "Property" - }, - { - "idShort": "TypeOfEmailAddress", - "description": [ + "value": [ { "language": "en", - "text": "enumeration: 0173-1#07-AAS754#001 (office), 0173-1#07-AAS756#001 (secretary), 0173-1#07-AAS757#001 (substitute), 0173-1#07-AAS758#001 (home)" + "text": "234-567-8901" } ], + "modelType": "MultiLanguageProperty" + }, + { + "idShort": "AvailableTime", "semanticId": { "type": "ExternalReference", "keys": [ { "type": "GlobalReference", - "value": "0173-1#02-AAO199#003" + "value": "https://admin-shell.io/zvei/nameplate/1/0/ContactInformations/ContactInformation/AvailableTime/" } ] }, @@ -326,87 +330,28 @@ "value": "ZeroToOne" } ], - "valueType": "xs:string", - "value": "Work", - "modelType": "Property" - } - ], - "modelType": "SubmodelElementCollection" - }, - { - "idShort": "IPCommunication__00__", - "semanticId": { - "type": "ExternalReference", - "keys": [ - { - "type": "GlobalReference", - "value": "https://admin-shell.io/zvei/nameplate/1/0/ContactInformations/" - } - ] - }, - "qualifiers": [ - { - "kind": "ConceptQualifier", - "type": "Multiplicity", - "valueType": "xs:string", - "value": "ZeroToMany" - } - ], - "value": [ - { - "idShort": "AddressOfAdditionalLink", - "semanticId": { - "type": "ExternalReference", - "keys": [ - { - "type": "GlobalReference", - "value": "0173-1#02-AAQ326#002" - } - ] - }, - "qualifiers": [ + "value": [ { - "kind": "ConceptQualifier", - "type": "Multiplicity", - "valueType": "xs:string", - "value": "One" + "language": "de", + "text": "Dienstags bis Donnerstags, 9-17 Uhr" } ], - "valueType": "xs:string", - "value": "https://www.mm-software.com/more-the-newsroom/", - "modelType": "Property" + "modelType": "MultiLanguageProperty" }, { - "idShort": "TypeOfCommunication", - "semanticId": { - "type": "ExternalReference", - "keys": [ - { - "type": "GlobalReference", - "value": "https://admin-shell.io/zvei/nameplate/1/0/ContactInformations/ContactInformation/IPCommunication/TypeOfCommunication" - } - ] - }, - "qualifiers": [ + "idShort": "TypeOfTelephone", + "description": [ { - "kind": "ConceptQualifier", - "type": "Multiplicity", - "valueType": "xs:string", - "value": "ZeroToOne" + "language": "en", + "text": " enumeration: 0173-1#07-AAS754#001 (office), 0173-1#07-AAS755#001 (office mobile), 0173-1#07-AAS756#001 (secretary), 0173-1#07-AAS757#001 (substitute), 0173-1#07-AAS758#001 (home), 0173-1#07-AAS759#001 (private mobile)" } ], - "valueType": "xs:string", - "value": "Chat", - "modelType": "Property" - }, - { - "idShort": "AvailableTime", "semanticId": { "type": "ExternalReference", "keys": [ { "type": "GlobalReference", - "value": "https://admin-shell.io/zvei/nameplate/1/0/ContactInformations/ContactInformation/AvailableTime/" + "value": "0173-1#02-AAO137#003" } ] }, @@ -418,25 +363,21 @@ "value": "ZeroToOne" } ], - "value": [ - { - "language": "de", - "text": "8:00 AM to 6:00 PM" - } - ], - "modelType": "MultiLanguageProperty" + "valueType": "xs:string", + "value": "Office", + "modelType": "Property" } ], "modelType": "SubmodelElementCollection" }, { - "idShort": "Phone0", + "idShort": "Email", "semanticId": { "type": "ExternalReference", "keys": [ { "type": "GlobalReference", - "value": "https://admin-shell.io/zvei/nameplate/1/0/ContactInformations/ContactInformation/Phone" + "value": "0173-1#02-AAQ836#005" } ] }, @@ -450,19 +391,13 @@ ], "value": [ { - "idShort": "TelephoneNumber", - "description": [ - { - "language": "en", - "text": "Recommendation: property declaration as MLP is required by its semantic definition. As the property value is language independent, users are recommended to provide maximal 1 string in any language of the user’s choice." - } - ], + "idShort": "EmailAddress", "semanticId": { "type": "ExternalReference", "keys": [ { "type": "GlobalReference", - "value": "0173-1#02-AAO136#002" + "value": "0173-1#02-AAO198#002" } ] }, @@ -474,47 +409,16 @@ "value": "One" } ], - "value": [ - { - "language": "en", - "text": "234-567-8901" - } - ], - "modelType": "MultiLanguageProperty" - }, - { - "idShort": "AvailableTime", - "semanticId": { - "type": "ExternalReference", - "keys": [ - { - "type": "GlobalReference", - "value": "https://admin-shell.io/zvei/nameplate/1/0/ContactInformations/ContactInformation/AvailableTime/" - } - ] - }, - "qualifiers": [ - { - "kind": "ConceptQualifier", - "type": "Multiplicity", - "valueType": "xs:string", - "value": "ZeroToOne" - } - ], - "value": [ - { - "language": "de", - "text": "Dienstags bis Donnerstags, 9-17 Uhr" - } - ], - "modelType": "MultiLanguageProperty" + "valueType": "xs:string", + "value": "john.doe@example.com", + "modelType": "Property" }, { - "idShort": "TypeOfTelephone", + "idShort": "TypeOfEmailAddress", "description": [ { "language": "en", - "text": " enumeration: 0173-1#07-AAS754#001 (office), 0173-1#07-AAS755#001 (office mobile), 0173-1#07-AAS756#001 (secretary), 0173-1#07-AAS757#001 (substitute), 0173-1#07-AAS758#001 (home), 0173-1#07-AAS759#001 (private mobile)" + "text": "enumeration: 0173-1#07-AAS754#001 (office), 0173-1#07-AAS756#001 (secretary), 0173-1#07-AAS757#001 (substitute), 0173-1#07-AAS758#001 (home)" } ], "semanticId": { @@ -522,7 +426,7 @@ "keys": [ { "type": "GlobalReference", - "value": "0173-1#02-AAO137#003" + "value": "0173-1#02-AAO199#003" } ] }, @@ -535,20 +439,20 @@ } ], "valueType": "xs:string", - "value": "Office", + "value": "Work", "modelType": "Property" } ], "modelType": "SubmodelElementCollection" }, { - "idShort": "Phone1", + "idShort": "IPCommunication__00__", "semanticId": { "type": "ExternalReference", "keys": [ { "type": "GlobalReference", - "value": "https://admin-shell.io/zvei/nameplate/1/0/ContactInformations/ContactInformation/Phone" + "value": "https://admin-shell.io/zvei/nameplate/1/0/ContactInformations/" } ] }, @@ -557,24 +461,18 @@ "kind": "ConceptQualifier", "type": "Multiplicity", "valueType": "xs:string", - "value": "ZeroToOne" + "value": "ZeroToMany" } ], "value": [ { - "idShort": "TelephoneNumber", - "description": [ - { - "language": "en", - "text": "Recommendation: property declaration as MLP is required by its semantic definition. As the property value is language independent, users are recommended to provide maximal 1 string in any language of the user’s choice." - } - ], + "idShort": "AddressOfAdditionalLink", "semanticId": { "type": "ExternalReference", "keys": [ { "type": "GlobalReference", - "value": "0173-1#02-AAO136#002" + "value": "0173-1#02-AAQ326#002" } ] }, @@ -586,22 +484,18 @@ "value": "One" } ], - "value": [ - { - "language": "en", - "text": "345-678-9012" - } - ], - "modelType": "MultiLanguageProperty" + "valueType": "xs:string", + "value": "https://www.mm-software.com/more-the-newsroom/", + "modelType": "Property" }, { - "idShort": "AvailableTime", + "idShort": "TypeOfCommunication", "semanticId": { "type": "ExternalReference", "keys": [ { "type": "GlobalReference", - "value": "https://admin-shell.io/zvei/nameplate/1/0/ContactInformations/ContactInformation/AvailableTime/" + "value": "https://admin-shell.io/zvei/nameplate/1/0/ContactInformations/ContactInformation/IPCommunication/TypeOfCommunication" } ] }, @@ -613,28 +507,18 @@ "value": "ZeroToOne" } ], - "value": [ - { - "language": "de", - "text": "Montags, 10-16 Uhr" - } - ], - "modelType": "MultiLanguageProperty" + "valueType": "xs:string", + "value": "Chat", + "modelType": "Property" }, { - "idShort": "TypeOfTelephone", - "description": [ - { - "language": "en", - "text": " enumeration: 0173-1#07-AAS754#001 (office), 0173-1#07-AAS755#001 (office mobile), 0173-1#07-AAS756#001 (secretary), 0173-1#07-AAS757#001 (substitute), 0173-1#07-AAS758#001 (home), 0173-1#07-AAS759#001 (private mobile)" - } - ], + "idShort": "AvailableTime", "semanticId": { "type": "ExternalReference", "keys": [ { "type": "GlobalReference", - "value": "0173-1#02-AAO137#003" + "value": "https://admin-shell.io/zvei/nameplate/1/0/ContactInformations/ContactInformation/AvailableTime/" } ] }, @@ -646,9 +530,13 @@ "value": "ZeroToOne" } ], - "valueType": "xs:string", - "value": "Mobile", - "modelType": "Property" + "value": [ + { + "language": "de", + "text": "8:00 AM to 6:00 PM" + } + ], + "modelType": "MultiLanguageProperty" } ], "modelType": "SubmodelElementCollection" @@ -903,13 +791,13 @@ "modelType": "MultiLanguageProperty" }, { - "idShort": "Email", + "idShort": "Phone", "semanticId": { "type": "ExternalReference", "keys": [ { "type": "GlobalReference", - "value": "0173-1#02-AAQ836#005" + "value": "https://admin-shell.io/zvei/nameplate/1/0/ContactInformations/ContactInformation/Phone" } ] }, @@ -923,13 +811,19 @@ ], "value": [ { - "idShort": "EmailAddress", + "idShort": "TelephoneNumber", + "description": [ + { + "language": "en", + "text": "Recommendation: property declaration as MLP is required by its semantic definition. As the property value is language independent, users are recommended to provide maximal 1 string in any language of the user’s choice." + } + ], "semanticId": { "type": "ExternalReference", "keys": [ { "type": "GlobalReference", - "value": "0173-1#02-AAO198#002" + "value": "0173-1#02-AAO136#002" } ] }, @@ -941,24 +835,22 @@ "value": "One" } ], - "valueType": "xs:string", - "value": "john.doe@example.com", - "modelType": "Property" - }, - { - "idShort": "TypeOfEmailAddress", - "description": [ + "value": [ { "language": "en", - "text": "enumeration: 0173-1#07-AAS754#001 (office), 0173-1#07-AAS756#001 (secretary), 0173-1#07-AAS757#001 (substitute), 0173-1#07-AAS758#001 (home)" + "text": "345-678-9012" } ], + "modelType": "MultiLanguageProperty" + }, + { + "idShort": "AvailableTime", "semanticId": { "type": "ExternalReference", "keys": [ { "type": "GlobalReference", - "value": "0173-1#02-AAO199#003" + "value": "https://admin-shell.io/zvei/nameplate/1/0/ContactInformations/ContactInformation/AvailableTime/" } ] }, @@ -970,87 +862,28 @@ "value": "ZeroToOne" } ], - "valueType": "xs:string", - "value": "Work", - "modelType": "Property" - } - ], - "modelType": "SubmodelElementCollection" - }, - { - "idShort": "IPCommunication__00__", - "semanticId": { - "type": "ExternalReference", - "keys": [ - { - "type": "GlobalReference", - "value": "https://admin-shell.io/zvei/nameplate/1/0/ContactInformations/" - } - ] - }, - "qualifiers": [ - { - "kind": "ConceptQualifier", - "type": "Multiplicity", - "valueType": "xs:string", - "value": "ZeroToMany" - } - ], - "value": [ - { - "idShort": "AddressOfAdditionalLink", - "semanticId": { - "type": "ExternalReference", - "keys": [ - { - "type": "GlobalReference", - "value": "0173-1#02-AAQ326#002" - } - ] - }, - "qualifiers": [ + "value": [ { - "kind": "ConceptQualifier", - "type": "Multiplicity", - "valueType": "xs:string", - "value": "One" + "language": "de", + "text": "Montags, 10-16 Uhr" } ], - "valueType": "xs:string", - "value": "https://www.mm-software.com/more-the-newsroom/", - "modelType": "Property" + "modelType": "MultiLanguageProperty" }, { - "idShort": "TypeOfCommunication", - "semanticId": { - "type": "ExternalReference", - "keys": [ - { - "type": "GlobalReference", - "value": "https://admin-shell.io/zvei/nameplate/1/0/ContactInformations/ContactInformation/IPCommunication/TypeOfCommunication" - } - ] - }, - "qualifiers": [ + "idShort": "TypeOfTelephone", + "description": [ { - "kind": "ConceptQualifier", - "type": "Multiplicity", - "valueType": "xs:string", - "value": "ZeroToOne" + "language": "en", + "text": " enumeration: 0173-1#07-AAS754#001 (office), 0173-1#07-AAS755#001 (office mobile), 0173-1#07-AAS756#001 (secretary), 0173-1#07-AAS757#001 (substitute), 0173-1#07-AAS758#001 (home), 0173-1#07-AAS759#001 (private mobile)" } ], - "valueType": "xs:string", - "value": "Chat", - "modelType": "Property" - }, - { - "idShort": "AvailableTime", "semanticId": { "type": "ExternalReference", "keys": [ { "type": "GlobalReference", - "value": "https://admin-shell.io/zvei/nameplate/1/0/ContactInformations/ContactInformation/AvailableTime/" + "value": "0173-1#02-AAO137#003" } ] }, @@ -1062,25 +895,21 @@ "value": "ZeroToOne" } ], - "value": [ - { - "language": "de", - "text": "8:00 AM to 6:00 PM" - } - ], - "modelType": "MultiLanguageProperty" + "valueType": "xs:string", + "value": "Mobile", + "modelType": "Property" } ], "modelType": "SubmodelElementCollection" }, { - "idShort": "Phone0", + "idShort": "Email", "semanticId": { "type": "ExternalReference", "keys": [ { "type": "GlobalReference", - "value": "https://admin-shell.io/zvei/nameplate/1/0/ContactInformations/ContactInformation/Phone" + "value": "0173-1#02-AAQ836#005" } ] }, @@ -1094,19 +923,13 @@ ], "value": [ { - "idShort": "TelephoneNumber", - "description": [ - { - "language": "en", - "text": "Recommendation: property declaration as MLP is required by its semantic definition. As the property value is language independent, users are recommended to provide maximal 1 string in any language of the user’s choice." - } - ], + "idShort": "EmailAddress", "semanticId": { "type": "ExternalReference", "keys": [ { "type": "GlobalReference", - "value": "0173-1#02-AAO136#002" + "value": "0173-1#02-AAO198#002" } ] }, @@ -1118,47 +941,16 @@ "value": "One" } ], - "value": [ - { - "language": "en", - "text": "234-567-8901" - } - ], - "modelType": "MultiLanguageProperty" - }, - { - "idShort": "AvailableTime", - "semanticId": { - "type": "ExternalReference", - "keys": [ - { - "type": "GlobalReference", - "value": "https://admin-shell.io/zvei/nameplate/1/0/ContactInformations/ContactInformation/AvailableTime/" - } - ] - }, - "qualifiers": [ - { - "kind": "ConceptQualifier", - "type": "Multiplicity", - "valueType": "xs:string", - "value": "ZeroToOne" - } - ], - "value": [ - { - "language": "de", - "text": "Dienstags bis Donnerstags, 9-17 Uhr" - } - ], - "modelType": "MultiLanguageProperty" + "valueType": "xs:string", + "value": "john.doe@example.com", + "modelType": "Property" }, { - "idShort": "TypeOfTelephone", + "idShort": "TypeOfEmailAddress", "description": [ { "language": "en", - "text": " enumeration: 0173-1#07-AAS754#001 (office), 0173-1#07-AAS755#001 (office mobile), 0173-1#07-AAS756#001 (secretary), 0173-1#07-AAS757#001 (substitute), 0173-1#07-AAS758#001 (home), 0173-1#07-AAS759#001 (private mobile)" + "text": "enumeration: 0173-1#07-AAS754#001 (office), 0173-1#07-AAS756#001 (secretary), 0173-1#07-AAS757#001 (substitute), 0173-1#07-AAS758#001 (home)" } ], "semanticId": { @@ -1166,7 +958,7 @@ "keys": [ { "type": "GlobalReference", - "value": "0173-1#02-AAO137#003" + "value": "0173-1#02-AAO199#003" } ] }, @@ -1179,20 +971,20 @@ } ], "valueType": "xs:string", - "value": "Office", + "value": "Work", "modelType": "Property" } ], "modelType": "SubmodelElementCollection" }, { - "idShort": "Phone1", + "idShort": "IPCommunication__00__", "semanticId": { "type": "ExternalReference", "keys": [ { "type": "GlobalReference", - "value": "https://admin-shell.io/zvei/nameplate/1/0/ContactInformations/ContactInformation/Phone" + "value": "https://admin-shell.io/zvei/nameplate/1/0/ContactInformations/" } ] }, @@ -1201,24 +993,18 @@ "kind": "ConceptQualifier", "type": "Multiplicity", "valueType": "xs:string", - "value": "ZeroToOne" + "value": "ZeroToMany" } ], "value": [ { - "idShort": "TelephoneNumber", - "description": [ - { - "language": "en", - "text": "Recommendation: property declaration as MLP is required by its semantic definition. As the property value is language independent, users are recommended to provide maximal 1 string in any language of the user’s choice." - } - ], + "idShort": "AddressOfAdditionalLink", "semanticId": { "type": "ExternalReference", "keys": [ { "type": "GlobalReference", - "value": "0173-1#02-AAO136#002" + "value": "0173-1#02-AAQ326#002" } ] }, @@ -1230,22 +1016,18 @@ "value": "One" } ], - "value": [ - { - "language": "en", - "text": "345-678-9012" - } - ], - "modelType": "MultiLanguageProperty" + "valueType": "xs:string", + "value": "https://www.mm-software.com/more-the-newsroom/", + "modelType": "Property" }, { - "idShort": "AvailableTime", + "idShort": "TypeOfCommunication", "semanticId": { "type": "ExternalReference", "keys": [ { "type": "GlobalReference", - "value": "https://admin-shell.io/zvei/nameplate/1/0/ContactInformations/ContactInformation/AvailableTime/" + "value": "https://admin-shell.io/zvei/nameplate/1/0/ContactInformations/ContactInformation/IPCommunication/TypeOfCommunication" } ] }, @@ -1257,28 +1039,18 @@ "value": "ZeroToOne" } ], - "value": [ - { - "language": "de", - "text": "Montags, 10-16 Uhr" - } - ], - "modelType": "MultiLanguageProperty" + "valueType": "xs:string", + "value": "Chat", + "modelType": "Property" }, { - "idShort": "TypeOfTelephone", - "description": [ - { - "language": "en", - "text": " enumeration: 0173-1#07-AAS754#001 (office), 0173-1#07-AAS755#001 (office mobile), 0173-1#07-AAS756#001 (secretary), 0173-1#07-AAS757#001 (substitute), 0173-1#07-AAS758#001 (home), 0173-1#07-AAS759#001 (private mobile)" - } - ], + "idShort": "AvailableTime", "semanticId": { "type": "ExternalReference", "keys": [ { "type": "GlobalReference", - "value": "0173-1#02-AAO137#003" + "value": "https://admin-shell.io/zvei/nameplate/1/0/ContactInformations/ContactInformation/AvailableTime/" } ] }, @@ -1290,9 +1062,13 @@ "value": "ZeroToOne" } ], - "valueType": "xs:string", - "value": "Mobile", - "modelType": "Property" + "value": [ + { + "language": "de", + "text": "8:00 AM to 6:00 PM" + } + ], + "modelType": "MultiLanguageProperty" } ], "modelType": "SubmodelElementCollection" diff --git a/source/AAS.TwinEngine.Plugin.TestPlugin/Api/Submodel/SubmodelController.cs b/source/AAS.TwinEngine.Plugin.TestPlugin/Api/Submodel/SubmodelController.cs index 57bc4186..20656416 100644 --- a/source/AAS.TwinEngine.Plugin.TestPlugin/Api/Submodel/SubmodelController.cs +++ b/source/AAS.TwinEngine.Plugin.TestPlugin/Api/Submodel/SubmodelController.cs @@ -1,4 +1,5 @@ -using System.Text.Json.Nodes; +using System.Text.Json; +using System.Text.Json.Nodes; using AAS.TwinEngine.Plugin.TestPlugin.Api.Submodel.Handler; using AAS.TwinEngine.Plugin.TestPlugin.Api.Submodel.Requests; @@ -25,6 +26,10 @@ public class SubmodelController(ISubmodelHandler submodelHandler) : ControllerBa [ProducesResponseType(typeof(ActionResult), StatusCodes.Status500InternalServerError)] public async Task> RetrieveDataAsync([FromBody] JsonSchema? dataQuery, [FromRoute] string submodelId, CancellationToken cancellationToken) { + var schemaJsonString = JsonSerializer.Serialize(dataQuery, new JsonSerializerOptions + { + WriteIndented = true // Optional: for human-readable output + }); var decodedSubmodelId = submodelId.DecodeBase64(); if (dataQuery is null) { diff --git a/source/AAS.TwinEngine.Plugin.TestPlugin/Data/mock-submodel-data.json b/source/AAS.TwinEngine.Plugin.TestPlugin/Data/mock-submodel-data.json index fbcadb6b..88e91ac7 100644 --- a/source/AAS.TwinEngine.Plugin.TestPlugin/Data/mock-submodel-data.json +++ b/source/AAS.TwinEngine.Plugin.TestPlugin/Data/mock-submodel-data.json @@ -19,15 +19,6 @@ "https://mm-software.com/Phone/AvailableTime": { "https://mm-software.com/Phone/AvailableTime_de": "Dienstags bis Donnerstags, 9-17 Uhr" } - }, - { - "0173-1#02-AAO136#002": { - "0173-1#02-AAO136#002_en": "345-678-9012" - }, - "https://mm-software.com/Phone/AvailableTime": { - "https://mm-software.com/Phone/AvailableTime_de": "Montags, 10-16 Uhr" - }, - "0173-1#02-AAO137#003": "Mobile" } ], "0173-1#02-AAQ836#005": [ @@ -68,15 +59,6 @@ "0173-1#02-AAO127#003_de": "Marketing" }, "https://admin-shell.io/zvei/nameplate/1/0/ContactInformations/ContactInformation/Phone": [ - { - "0173-1#02-AAO136#002": { - "0173-1#02-AAO136#002_en": "234-567-8901" - }, - "0173-1#02-AAO137#003": "Office", - "https://mm-software.com/Phone/AvailableTime": { - "https://mm-software.com/Phone/AvailableTime_de": "Dienstags bis Donnerstags, 9-17 Uhr" - } - }, { "0173-1#02-AAO136#002": { "0173-1#02-AAO136#002_en": "345-678-9012" @@ -503,15 +485,6 @@ "0173-1#02-AAO127#003_de": "Marketing" }, "https://admin-shell.io/zvei/nameplate/1/0/ContactInformations/ContactInformation/Phone": [ - { - "0173-1#02-AAO136#002": { - "0173-1#02-AAO136#002_en": "234-567-8901" - }, - "0173-1#02-AAO137#003": "Office", - "https://mm-software.com/Phone/AvailableTime": { - "https://mm-software.com/Phone/AvailableTime_de": "Dienstags bis Donnerstags, 9-17 Uhr" - } - }, { "0173-1#02-AAO136#002": { "0173-1#02-AAO136#002_en": "345-678-9012" diff --git a/source/AAS.TwinEngine.Plugin.TestPlugin/Example/aas/ContactInformationAAS.aas.xml b/source/AAS.TwinEngine.Plugin.TestPlugin/Example/aas/ContactInformationAAS.aas.xml index 5f1c575f..4c36d654 100644 --- a/source/AAS.TwinEngine.Plugin.TestPlugin/Example/aas/ContactInformationAAS.aas.xml +++ b/source/AAS.TwinEngine.Plugin.TestPlugin/Example/aas/ContactInformationAAS.aas.xml @@ -1,4 +1,4 @@ - + CustomContactInformationAAS @@ -1788,4 +1788,4 @@ - \ No newline at end of file + diff --git a/source/AAS.TwinEngine.Plugin.TestPlugin/Example/plugin/mock-submodel-data.json b/source/AAS.TwinEngine.Plugin.TestPlugin/Example/plugin/mock-submodel-data.json index fbcadb6b..88e91ac7 100644 --- a/source/AAS.TwinEngine.Plugin.TestPlugin/Example/plugin/mock-submodel-data.json +++ b/source/AAS.TwinEngine.Plugin.TestPlugin/Example/plugin/mock-submodel-data.json @@ -19,15 +19,6 @@ "https://mm-software.com/Phone/AvailableTime": { "https://mm-software.com/Phone/AvailableTime_de": "Dienstags bis Donnerstags, 9-17 Uhr" } - }, - { - "0173-1#02-AAO136#002": { - "0173-1#02-AAO136#002_en": "345-678-9012" - }, - "https://mm-software.com/Phone/AvailableTime": { - "https://mm-software.com/Phone/AvailableTime_de": "Montags, 10-16 Uhr" - }, - "0173-1#02-AAO137#003": "Mobile" } ], "0173-1#02-AAQ836#005": [ @@ -68,15 +59,6 @@ "0173-1#02-AAO127#003_de": "Marketing" }, "https://admin-shell.io/zvei/nameplate/1/0/ContactInformations/ContactInformation/Phone": [ - { - "0173-1#02-AAO136#002": { - "0173-1#02-AAO136#002_en": "234-567-8901" - }, - "0173-1#02-AAO137#003": "Office", - "https://mm-software.com/Phone/AvailableTime": { - "https://mm-software.com/Phone/AvailableTime_de": "Dienstags bis Donnerstags, 9-17 Uhr" - } - }, { "0173-1#02-AAO136#002": { "0173-1#02-AAO136#002_en": "345-678-9012" @@ -503,15 +485,6 @@ "0173-1#02-AAO127#003_de": "Marketing" }, "https://admin-shell.io/zvei/nameplate/1/0/ContactInformations/ContactInformation/Phone": [ - { - "0173-1#02-AAO136#002": { - "0173-1#02-AAO136#002_en": "234-567-8901" - }, - "0173-1#02-AAO137#003": "Office", - "https://mm-software.com/Phone/AvailableTime": { - "https://mm-software.com/Phone/AvailableTime_de": "Dienstags bis Donnerstags, 9-17 Uhr" - } - }, { "0173-1#02-AAO136#002": { "0173-1#02-AAO136#002_en": "345-678-9012" From c8d418ad291d028414cb18add305fdd45c18da14 Mon Sep 17 00:00:00 2001 From: Hardi Shah Date: Fri, 24 Apr 2026 13:16:33 +0530 Subject: [PATCH 60/71] Improve null-safety, error logging, and minor optimizations - Add null-forgiving operator for schemaNode in JsonSchemaValidator - Enhance error logging with exception details in JsonSchemaParser - Optimize property checks and retrieval in JsonSchemaParser - Remove unused dataQuery serialization in SubmodelController --- .../PluginDataProvider/Helper/JsonSchemaValidator.cs | 2 +- .../Api/Submodel/Services/JsonSchemaParser.cs | 8 ++++---- .../Api/Submodel/SubmodelController.cs | 4 ---- 3 files changed, 5 insertions(+), 9 deletions(-) diff --git a/source/AAS.TwinEngine.DataEngine/Infrastructure/Providers/PluginDataProvider/Helper/JsonSchemaValidator.cs b/source/AAS.TwinEngine.DataEngine/Infrastructure/Providers/PluginDataProvider/Helper/JsonSchemaValidator.cs index 326a237d..1a5b1f76 100644 --- a/source/AAS.TwinEngine.DataEngine/Infrastructure/Providers/PluginDataProvider/Helper/JsonSchemaValidator.cs +++ b/source/AAS.TwinEngine.DataEngine/Infrastructure/Providers/PluginDataProvider/Helper/JsonSchemaValidator.cs @@ -45,7 +45,7 @@ public void ValidateRequestSchema(JsonSchema schema) try { - var jsonElement = JsonDocument.Parse(schemaNode.ToJsonString()).RootElement; + var jsonElement = JsonDocument.Parse(schemaNode!.ToJsonString()).RootElement; var result = MetaSchemas.Draft202012.Evaluate(jsonElement, _evaluationOptions); diff --git a/source/AAS.TwinEngine.Plugin.TestPlugin/Api/Submodel/Services/JsonSchemaParser.cs b/source/AAS.TwinEngine.Plugin.TestPlugin/Api/Submodel/Services/JsonSchemaParser.cs index 923cdf79..c9b84753 100644 --- a/source/AAS.TwinEngine.Plugin.TestPlugin/Api/Submodel/Services/JsonSchemaParser.cs +++ b/source/AAS.TwinEngine.Plugin.TestPlugin/Api/Submodel/Services/JsonSchemaParser.cs @@ -35,9 +35,9 @@ private void ValidateRequest(JsonSchema jsonSchema) throw new BadRequestException(ExceptionMessages.RequestBodyInvalid); } } - catch (JsonException) + catch (JsonException ex) { - logger.LogError("Requested schema is not valid"); + logger.LogError(ex, "Requested schema is not valid"); throw new BadRequestException(ExceptionMessages.FailedParsingJsonSchema); } } @@ -48,12 +48,12 @@ private SemanticTreeNode CreateSemanticTree(JsonSchema jsonSchema) if (!json.TryGetPropertyValue("properties", out var propsNode) || propsNode is not JsonObject props || - !props.Any()) + props.Count == 0) { throw new BadRequestException(ExceptionMessages.InvalidJsonSchemaRootElement); } - var rootProperty = props.First(); + var rootProperty = ((IList>)props)[0]; return ProcessProperty(rootProperty.Key, rootProperty.Value!, json); } diff --git a/source/AAS.TwinEngine.Plugin.TestPlugin/Api/Submodel/SubmodelController.cs b/source/AAS.TwinEngine.Plugin.TestPlugin/Api/Submodel/SubmodelController.cs index 20656416..d6236a5a 100644 --- a/source/AAS.TwinEngine.Plugin.TestPlugin/Api/Submodel/SubmodelController.cs +++ b/source/AAS.TwinEngine.Plugin.TestPlugin/Api/Submodel/SubmodelController.cs @@ -26,10 +26,6 @@ public class SubmodelController(ISubmodelHandler submodelHandler) : ControllerBa [ProducesResponseType(typeof(ActionResult), StatusCodes.Status500InternalServerError)] public async Task> RetrieveDataAsync([FromBody] JsonSchema? dataQuery, [FromRoute] string submodelId, CancellationToken cancellationToken) { - var schemaJsonString = JsonSerializer.Serialize(dataQuery, new JsonSerializerOptions - { - WriteIndented = true // Optional: for human-readable output - }); var decodedSubmodelId = submodelId.DecodeBase64(); if (dataQuery is null) { From cffcda9932cafd0853dc9d71f313a9b65a9160d8 Mon Sep 17 00:00:00 2001 From: Hardi Shah Date: Fri, 24 Apr 2026 14:16:12 +0530 Subject: [PATCH 61/71] Update namespace regex patterns in architecture tests Replaced ($|\..*) with ($|\.*) in .ResideInNamespaceMatching() for Repository, Service, and Controller checks. This change modifies which namespaces are considered valid for these types in the architecture tests. --- .../CleanArchitectureTests.cs | 8 ++++---- .../CleanArchitectureTests.cs | 8 ++++---- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/source/AAS.TwinEngine.DataEngine.UnitTests/CleanArchitectureTests.cs b/source/AAS.TwinEngine.DataEngine.UnitTests/CleanArchitectureTests.cs index 8a2a6587..abe4c304 100644 --- a/source/AAS.TwinEngine.DataEngine.UnitTests/CleanArchitectureTests.cs +++ b/source/AAS.TwinEngine.DataEngine.UnitTests/CleanArchitectureTests.cs @@ -93,7 +93,7 @@ public void RepositoryInterfacesShallBeInCorrectNamespace() .And() .DoNotHaveFullName($"{BaseNamespace}.Infrastructure.DataAccess.GenericRepository.IMongoDbRepository") .Should() - .ResideInNamespaceMatching($"{BaseNamespace}\\.ApplicationLogic($|\\..*).") + .ResideInNamespaceMatching($"{BaseNamespace}\\.ApplicationLogic($|\\.*)") .WithoutRequiringPositiveResults() .Check(_architecture); } @@ -102,7 +102,7 @@ public void RepositoryInterfacesShallBeInCorrectNamespace() public void ServicesShallBeInCorrectNamespace() { Classes().That().HaveNameEndingWith("Service").Should() - .ResideInNamespaceMatching($"{BaseNamespace}\\.ApplicationLogic\\.Services($|\\..*).") + .ResideInNamespaceMatching($"{BaseNamespace}\\.ApplicationLogic\\.Services($|\\.*)") .Check(_architecture); } @@ -111,7 +111,7 @@ public void ServiceInterfacesShallBeInCorrectNamespace() { Interfaces().That().HaveNameEndingWith("Service") .Should() - .ResideInNamespaceMatching($"{BaseNamespace}\\.ApplicationLogic\\.Services($|\\..*).") + .ResideInNamespaceMatching($"{BaseNamespace}\\.ApplicationLogic\\.Services($|\\.*)") .Check(_architecture); } @@ -119,7 +119,7 @@ public void ServiceInterfacesShallBeInCorrectNamespace() public void ControllerShallBeInCorrectNamespace() { Classes().That().HaveNameEndingWith("Controller").Should() - .ResideInNamespaceMatching($"{BaseNamespace}\\.Api($|\\..*).") + .ResideInNamespaceMatching($"{BaseNamespace}\\.Api($|\\.*)") .Check(_architecture); } } diff --git a/source/AAS.TwinEngine.Plugin.TestPlugin.UnitTests/CleanArchitectureTests.cs b/source/AAS.TwinEngine.Plugin.TestPlugin.UnitTests/CleanArchitectureTests.cs index 525d5146..1556000c 100644 --- a/source/AAS.TwinEngine.Plugin.TestPlugin.UnitTests/CleanArchitectureTests.cs +++ b/source/AAS.TwinEngine.Plugin.TestPlugin.UnitTests/CleanArchitectureTests.cs @@ -93,7 +93,7 @@ public void RepositoryInterfacesShallBeInCorrectNamespace() .And() .DoNotHaveFullName($"{BaseNamespace}.Infrastructure.DataAccess.GenericRepository.IMongoDbRepository") .Should() - .ResideInNamespaceMatching($"{BaseNamespace}\\.ApplicationLogic($|\\..*).") + .ResideInNamespaceMatching($"{BaseNamespace}\\.ApplicationLogic($|\\.*)") .WithoutRequiringPositiveResults() .Check(_architecture); } @@ -102,7 +102,7 @@ public void RepositoryInterfacesShallBeInCorrectNamespace() public void ServicesShallBeInCorrectNamespace() { Classes().That().HaveNameEndingWith("Service").Should() - .ResideInNamespaceMatching($"{BaseNamespace}\\.ApplicationLogic\\.Services($|\\..*).") + .ResideInNamespaceMatching($"{BaseNamespace}\\.ApplicationLogic\\.Services($|\\.*)") .Check(_architecture); } @@ -111,7 +111,7 @@ public void ServiceInterfacesShallBeInCorrectNamespace() { Interfaces().That().HaveNameEndingWith("Service") .Should() - .ResideInNamespaceMatching($"{BaseNamespace}\\.ApplicationLogic\\.Services($|\\..*).") + .ResideInNamespaceMatching($"{BaseNamespace}\\.ApplicationLogic\\.Services($|\\.*)") .Check(_architecture); } @@ -119,7 +119,7 @@ public void ServiceInterfacesShallBeInCorrectNamespace() public void ControllerShallBeInCorrectNamespace() { Classes().That().HaveNameEndingWith("Controller").Should() - .ResideInNamespaceMatching($"{BaseNamespace}\\.Api($|\\..*).") + .ResideInNamespaceMatching($"{BaseNamespace}\\.Api($|\\.*)") .Check(_architecture); } } From 8b45ac2004f8180b7741a3e99916828dfcee8a6f Mon Sep 17 00:00:00 2001 From: Hardi Shah Date: Fri, 24 Apr 2026 14:42:08 +0530 Subject: [PATCH 62/71] Simplify namespace matching in CleanArchitectureTests Updated namespace matching rules to use simpler wildcard patterns instead of complex regular expressions. This affects repository, service, and controller class/interface checks, improving readability and maintainability of the architecture tests. --- .../CleanArchitectureTests.cs | 10 +++++----- .../CleanArchitectureTests.cs | 10 +++++----- 2 files changed, 10 insertions(+), 10 deletions(-) diff --git a/source/AAS.TwinEngine.DataEngine.UnitTests/CleanArchitectureTests.cs b/source/AAS.TwinEngine.DataEngine.UnitTests/CleanArchitectureTests.cs index abe4c304..90475140 100644 --- a/source/AAS.TwinEngine.DataEngine.UnitTests/CleanArchitectureTests.cs +++ b/source/AAS.TwinEngine.DataEngine.UnitTests/CleanArchitectureTests.cs @@ -80,7 +80,7 @@ public void ApiShallNotHaveDependenciesToInfrastructure() public void RepositoryClassesShallBeInCorrectNamespace() { Classes().That().HaveNameEndingWith("Repository").Should() - .ResideInNamespaceMatching($"{BaseNamespace}\\.Infrastructure\\.DataAccess.*") + .ResideInNamespaceMatching($"{BaseNamespace}.Infrastructure.Providers*") .WithoutRequiringPositiveResults() .Check(_architecture); } @@ -93,7 +93,7 @@ public void RepositoryInterfacesShallBeInCorrectNamespace() .And() .DoNotHaveFullName($"{BaseNamespace}.Infrastructure.DataAccess.GenericRepository.IMongoDbRepository") .Should() - .ResideInNamespaceMatching($"{BaseNamespace}\\.ApplicationLogic($|\\.*)") + .ResideInNamespaceMatching($"{BaseNamespace}.ApplicationLogic.*") .WithoutRequiringPositiveResults() .Check(_architecture); } @@ -102,7 +102,7 @@ public void RepositoryInterfacesShallBeInCorrectNamespace() public void ServicesShallBeInCorrectNamespace() { Classes().That().HaveNameEndingWith("Service").Should() - .ResideInNamespaceMatching($"{BaseNamespace}\\.ApplicationLogic\\.Services($|\\.*)") + .ResideInNamespaceMatching($"{BaseNamespace}.ApplicationLogic.Service.*") .Check(_architecture); } @@ -111,7 +111,7 @@ public void ServiceInterfacesShallBeInCorrectNamespace() { Interfaces().That().HaveNameEndingWith("Service") .Should() - .ResideInNamespaceMatching($"{BaseNamespace}\\.ApplicationLogic\\.Services($|\\.*)") + .ResideInNamespaceMatching($"{BaseNamespace}.ApplicationLogic.Service.*") .Check(_architecture); } @@ -119,7 +119,7 @@ public void ServiceInterfacesShallBeInCorrectNamespace() public void ControllerShallBeInCorrectNamespace() { Classes().That().HaveNameEndingWith("Controller").Should() - .ResideInNamespaceMatching($"{BaseNamespace}\\.Api($|\\.*)") + .ResideInNamespaceMatching($"{BaseNamespace}.Api.*") .Check(_architecture); } } diff --git a/source/AAS.TwinEngine.Plugin.TestPlugin.UnitTests/CleanArchitectureTests.cs b/source/AAS.TwinEngine.Plugin.TestPlugin.UnitTests/CleanArchitectureTests.cs index 1556000c..942204cc 100644 --- a/source/AAS.TwinEngine.Plugin.TestPlugin.UnitTests/CleanArchitectureTests.cs +++ b/source/AAS.TwinEngine.Plugin.TestPlugin.UnitTests/CleanArchitectureTests.cs @@ -80,7 +80,7 @@ public void ApiShallNotHaveDependenciesToInfrastructure() public void RepositoryClassesShallBeInCorrectNamespace() { Classes().That().HaveNameEndingWith("Repository").Should() - .ResideInNamespaceMatching($"{BaseNamespace}\\.Infrastructure\\.DataAccess.*") + .ResideInNamespaceMatching($"{BaseNamespace}.Infrastructure.Providers*") .WithoutRequiringPositiveResults() .Check(_architecture); } @@ -93,7 +93,7 @@ public void RepositoryInterfacesShallBeInCorrectNamespace() .And() .DoNotHaveFullName($"{BaseNamespace}.Infrastructure.DataAccess.GenericRepository.IMongoDbRepository") .Should() - .ResideInNamespaceMatching($"{BaseNamespace}\\.ApplicationLogic($|\\.*)") + .ResideInNamespaceMatching($"{BaseNamespace}.ApplicationLogic.*") .WithoutRequiringPositiveResults() .Check(_architecture); } @@ -102,7 +102,7 @@ public void RepositoryInterfacesShallBeInCorrectNamespace() public void ServicesShallBeInCorrectNamespace() { Classes().That().HaveNameEndingWith("Service").Should() - .ResideInNamespaceMatching($"{BaseNamespace}\\.ApplicationLogic\\.Services($|\\.*)") + .ResideInNamespaceMatching($"{BaseNamespace}.ApplicationLogic.Service.*") .Check(_architecture); } @@ -111,7 +111,7 @@ public void ServiceInterfacesShallBeInCorrectNamespace() { Interfaces().That().HaveNameEndingWith("Service") .Should() - .ResideInNamespaceMatching($"{BaseNamespace}\\.ApplicationLogic\\.Services($|\\.*)") + .ResideInNamespaceMatching($"{BaseNamespace}.ApplicationLogic.Service.*") .Check(_architecture); } @@ -119,7 +119,7 @@ public void ServiceInterfacesShallBeInCorrectNamespace() public void ControllerShallBeInCorrectNamespace() { Classes().That().HaveNameEndingWith("Controller").Should() - .ResideInNamespaceMatching($"{BaseNamespace}\\.Api($|\\.*)") + .ResideInNamespaceMatching($"{BaseNamespace}.Api.*") .Check(_architecture); } } From ca947ff608ab2354b84898da063e928413f6dc95 Mon Sep 17 00:00:00 2001 From: Hardi Shah Date: Mon, 27 Apr 2026 11:45:36 +0530 Subject: [PATCH 63/71] Support multiple JSON Schema drafts and improve refs Add robust support for Draft-07, 2019-09, and 2020-12 in schema generation, parsing, and validation. Inline branch definitions unless referenced multiple times. Handle both `$defs` and `definitions` for references, with proper pointer decoding and escaping. Dynamically detect schema draft from `$schema` and use the correct metaschema. Refactor parser and validator for shared logic. Add extensive tests for all supported drafts and reference styles. Output arrays for leaf nodes with one-to-many cardinality. --- .../Helper/JsonSchemaGeneratorTests.cs | 74 +++++++--- .../Helper/JsonSchemaValidatorTests.cs | 84 +++++++++++ .../Helper/JsonSchemaGenerator.cs | 51 ++++++- .../Helper/JsonSchemaValidator.cs | 68 ++++++++- .../Services/JsonSchemaParserTests.cs | 106 +++++++++++++ .../Services/JsonSchemaValidatorTests.cs | 105 +++++++++++++ .../Api/Submodel/Services/JsonSchemaParser.cs | 139 +++++++++++++++--- .../Submodel/Services/JsonSchemaValidator.cs | 50 ++++++- 8 files changed, 616 insertions(+), 61 deletions(-) diff --git a/source/AAS.TwinEngine.DataEngine.UnitTests/Infrastructure/Providers/PluginDataProvider/Helper/JsonSchemaGeneratorTests.cs b/source/AAS.TwinEngine.DataEngine.UnitTests/Infrastructure/Providers/PluginDataProvider/Helper/JsonSchemaGeneratorTests.cs index 57e1db6e..4a4c1d2c 100644 --- a/source/AAS.TwinEngine.DataEngine.UnitTests/Infrastructure/Providers/PluginDataProvider/Helper/JsonSchemaGeneratorTests.cs +++ b/source/AAS.TwinEngine.DataEngine.UnitTests/Infrastructure/Providers/PluginDataProvider/Helper/JsonSchemaGeneratorTests.cs @@ -93,7 +93,7 @@ public void ConvertToJsonSchema_BranchNodeWithZeroToManyCardinality_ReturnsArray } [Fact] - public void ConvertToJsonSchema_NestedBranchNodes_UsesRefAndDefsCorrectly() + public void ConvertToJsonSchema_NestedBranchNodes_UsesInlineSchemaWithoutDefs() { const string RootSemanticId = "http://example.com/idta/digital-nameplate"; const string BranchSemanticId = "http://example.com/idta/digital-nameplate/contact-list"; @@ -108,15 +108,14 @@ public void ConvertToJsonSchema_NestedBranchNodes_UsesRefAndDefsCorrectly() Assert.Equal("object", json["type"]!.ToString()); var rootProps = json["properties"]![RootSemanticId]!["properties"]!; - var branchRef = rootProps[BranchSemanticId]!["$ref"]!.ToString(); - Assert.Equal($"#/$defs/{BranchSemanticId}", branchRef); - var defs = json["$defs"]!; - Assert.NotNull(defs[BranchSemanticId]); - var branchDef = defs[BranchSemanticId]; - Assert.Equal("object", branchDef!["type"]!.ToString()); - var branchProps = branchDef["properties"]!; + var branchSchema = rootProps[BranchSemanticId]!; + Assert.Null(branchSchema["$ref"]); + Assert.Equal("object", branchSchema["type"]!.ToString()); + var defs = json["$defs"]; + Assert.True(defs == null || defs.AsObject().Count == 0); + var branchProps = branchSchema["properties"]!; Assert.NotNull(branchProps[NameId]); - var required = branchDef["required"]!.AsArray(); + var required = branchSchema["required"]!.AsArray(); Assert.Contains(NameId, required.Select(x => x!.ToString())); } @@ -158,6 +157,20 @@ public void ConvertToJsonSchema_ArraySchema_UsesItemsKeyword() Assert.NotNull(rootProps["items"]); } + [Fact] + public void ConvertToJsonSchema_LeafWithOneToManyCardinality_UsesArrayItems() + { + var branch = new SemanticBranchNode("root", Cardinality.One); + branch.AddChild(new SemanticLeafNode("tags", "", DataType.String, Cardinality.OneToMany)); + + var schema = JsonSchemaGenerator.ConvertToJsonSchema(branch); + var json = ToJson(schema); + + var leaf = json["properties"]!["root"]!["properties"]!["tags"]!; + Assert.Equal("array", leaf["type"]!.ToString()); + Assert.Equal("string", leaf["items"]!["type"]!.ToString()); + } + [Fact] public void ConvertToJsonSchema_UnsupportedNode_ThrowsException() { @@ -225,9 +238,12 @@ public void ConvertToJsonSchema_DeepNestedStructure_WorksCorrectly() var schema = JsonSchemaGenerator.ConvertToJsonSchema(level1); var json = ToJson(schema); - var defs = json["$defs"]!; - Assert.NotNull(defs["level2"]); - Assert.NotNull(defs["level3"]); + var level2Schema = json["properties"]!["level1"]!["properties"]!["level2"]!; + Assert.Equal("object", level2Schema["type"]!.ToString()); + var level3Schema = level2Schema["properties"]!["level3"]!; + Assert.Equal("object", level3Schema["type"]!.ToString()); + var leafSchema = level3Schema["properties"]!["leaf"]!; + Assert.Equal("string", leafSchema["type"]!.ToString()); } [Fact] @@ -245,16 +261,30 @@ public void ConvertToJsonSchema_MixedCardinality_WorksCorrectly() var json = ToJson(schema); var props = json["properties"]!["root"]!["properties"]!; - var objectRef = props["objectChild"]!["$ref"]!.ToString(); - Assert.Equal("#/$defs/objectChild", objectRef); - var arrayRef = props["arrayChild"]!["$ref"]!.ToString(); - Assert.Equal("#/$defs/arrayChild", arrayRef); - var defs = json["$defs"]!; - var objectDef = defs["objectChild"]!; - Assert.Equal("object", objectDef["type"]!.ToString()); - var arrayDef = defs["arrayChild"]!; - Assert.Equal("array", arrayDef["type"]!.ToString()); - Assert.NotNull(arrayDef["items"]); + var objectSchema = props["objectChild"]!; + Assert.Equal("object", objectSchema["type"]!.ToString()); + var arraySchema = props["arrayChild"]!; + Assert.Equal("array", arraySchema["type"]!.ToString()); + Assert.NotNull(arraySchema["items"]); + var defs = json["$defs"]; + Assert.True(defs == null || defs.AsObject().Count == 0); + } + + [Fact] + public void ConvertToJsonSchema_SingleNestedBranch_OmitsRefAndDefs() + { + var root = new SemanticBranchNode("root", Cardinality.One); + var child = new SemanticBranchNode("child", Cardinality.One); + child.AddChild(new SemanticLeafNode("name", "", DataType.String, Cardinality.One)); + root.AddChild(child); + + var schema = JsonSchemaGenerator.ConvertToJsonSchema(root); + var json = ToJson(schema); + + var childSchema = json["properties"]!["root"]!["properties"]!["child"]!; + Assert.Null(childSchema["$ref"]); + Assert.Equal("object", childSchema["type"]!.ToString()); + Assert.True(json["$defs"] == null || json["$defs"]!.AsObject().Count == 0); } [Fact] diff --git a/source/AAS.TwinEngine.DataEngine.UnitTests/Infrastructure/Providers/PluginDataProvider/Helper/JsonSchemaValidatorTests.cs b/source/AAS.TwinEngine.DataEngine.UnitTests/Infrastructure/Providers/PluginDataProvider/Helper/JsonSchemaValidatorTests.cs index 0c2f2c46..4936f6b1 100644 --- a/source/AAS.TwinEngine.DataEngine.UnitTests/Infrastructure/Providers/PluginDataProvider/Helper/JsonSchemaValidatorTests.cs +++ b/source/AAS.TwinEngine.DataEngine.UnitTests/Infrastructure/Providers/PluginDataProvider/Helper/JsonSchemaValidatorTests.cs @@ -58,6 +58,24 @@ public void ValidateRequestSchema_ValidSchema_DoesNotThrow() _sut.ValidateRequestSchema(schema); } + [Theory] + [InlineData("http://json-schema.org/draft-07/schema#")] + [InlineData("https://json-schema.org/draft/2019-09/schema")] + [InlineData("https://json-schema.org/draft/2020-12/schema")] + public void ValidateRequestSchema_SupportedDrafts_DoesNotThrow(string draft) + { + var schema = new JsonSchemaBuilder() + .Schema(draft) + .Type(SchemaValueType.Object) + .Properties(new Dictionary + { + ["name"] = new JsonSchemaBuilder().Type(SchemaValueType.String) + }) + .Build(); + + _sut.ValidateRequestSchema(schema); + } + [Fact] public void ValidateResponseContent_EmptyResponse_ThrowsBadRequest() { @@ -230,6 +248,72 @@ public void ValidateResponseContent_WithDefsReference_DoesNotThrow() _sut.ValidateResponseContent(json, schema); } + [Fact] + public void ValidateResponseContent_Draft7Schema_DoesNotThrow() + { + const string schemaJson = @"{ + ""$schema"": ""http://json-schema.org/draft-07/schema#"", + ""type"": ""object"", + ""properties"": { + ""value"": { ""type"": ""string"" } + }, + ""required"": [""value""] + }"; + + var schema = JsonSchema.FromText(schemaJson); + + const string json = @"{ ""value"": ""ok"" }"; + + _sut.ValidateResponseContent(json, schema); + } + + [Fact] + public void ValidateResponseContent_Draft201909Schema_DoesNotThrow() + { + const string schemaJson = @"{ + ""$schema"": ""https://json-schema.org/draft/2019-09/schema"", + ""type"": ""object"", + ""properties"": { + ""value"": { ""type"": ""integer"" } + }, + ""required"": [""value""] + }"; + + var schema = JsonSchema.FromText(schemaJson); + + const string json = @"{ ""value"": 12 }"; + + _sut.ValidateResponseContent(json, schema); + } + + [Fact] + public void ValidateResponseContent_Draft7DefinitionsReferenceWithSuffix_DoesNotThrow() + { + const string schemaJson = @"{ + ""$schema"": ""http://json-schema.org/draft-07/schema#"", + ""type"": ""object"", + ""properties"": { + ""item_aastwinengine_00"": { ""$ref"": ""#/definitions/Type_aastwinengine_00"" } + }, + ""required"": [""item_aastwinengine_00""], + ""definitions"": { + ""Type_aastwinengine_00"": { + ""type"": ""object"", + ""properties"": { + ""name_aastwinengine_00"": { ""type"": ""string"" } + }, + ""required"": [""name_aastwinengine_00""] + } + } + }"; + + var schema = JsonSchema.FromText(schemaJson); + + const string json = @"{ ""item"": { ""name"": ""ok"" } }"; + + _sut.ValidateResponseContent(json, schema); + } + [Fact] public void ValidateResponseContent_BrokenRef_Throws() { diff --git a/source/AAS.TwinEngine.DataEngine/Infrastructure/Providers/PluginDataProvider/Helper/JsonSchemaGenerator.cs b/source/AAS.TwinEngine.DataEngine/Infrastructure/Providers/PluginDataProvider/Helper/JsonSchemaGenerator.cs index ab45b52d..ab8b29d8 100644 --- a/source/AAS.TwinEngine.DataEngine/Infrastructure/Providers/PluginDataProvider/Helper/JsonSchemaGenerator.cs +++ b/source/AAS.TwinEngine.DataEngine/Infrastructure/Providers/PluginDataProvider/Helper/JsonSchemaGenerator.cs @@ -13,8 +13,9 @@ public static class JsonSchemaGenerator public static JsonSchema ConvertToJsonSchema(SemanticTreeNode rootNode) { + var branchUsage = CountBranchUsages(rootNode); var definitions = new Dictionary(); - var rootSchema = BuildNode(rootNode, definitions, isRoot: true); + var rootSchema = BuildNode(rootNode, definitions, branchUsage, isRoot: true); return new JsonSchemaBuilder() .Schema(MetaSchemas.Draft202012Id) @@ -30,11 +31,12 @@ public static JsonSchema ConvertToJsonSchema(SemanticTreeNode rootNode) private static JsonSchemaBuilder BuildNode( SemanticTreeNode node, Dictionary definitions, + Dictionary branchUsage, bool isRoot = false) { return node switch { - SemanticBranchNode branch => BuildBranch(branch, definitions, isRoot), + SemanticBranchNode branch => BuildBranch(branch, definitions, branchUsage, isRoot), SemanticLeafNode leaf => BuildLeaf(leaf), _ => throw new InternalDataProcessingException() }; @@ -43,15 +45,16 @@ private static JsonSchemaBuilder BuildNode( private static JsonSchemaBuilder BuildBranch( SemanticBranchNode branch, Dictionary definitions, + Dictionary branchUsage, bool isRoot = false) { - if (!isRoot && definitions.ContainsKey(branch.SemanticId)) + if (!isRoot && ShouldUseReference(branch, branchUsage) && definitions.ContainsKey(branch.SemanticId)) { return CreateRefSchema(branch.SemanticId); } var requiredProperties = new List(); - var children = BuildChildren(branch.Children, definitions, requiredProperties); + var children = BuildChildren(branch.Children, definitions, branchUsage, requiredProperties); var schemaBuilder = IsArrayCardinality(branch.Cardinality) ? BuildArraySchema(children, requiredProperties) @@ -62,6 +65,11 @@ private static JsonSchemaBuilder BuildBranch( return schemaBuilder; } + if (!ShouldUseReference(branch, branchUsage)) + { + return schemaBuilder; + } + definitions[branch.SemanticId] = schemaBuilder; return CreateRefSchema(branch.SemanticId); @@ -104,6 +112,7 @@ private static JsonSchemaBuilder BuildArraySchema( private static Dictionary BuildChildren( ReadOnlyCollection children, Dictionary definitions, + Dictionary branchUsage, List required) { var properties = new Dictionary(); @@ -112,7 +121,7 @@ private static Dictionary BuildChildren( { var childSchema = child switch { - SemanticBranchNode branch => BuildNode(branch, definitions), + SemanticBranchNode branch => BuildNode(branch, definitions, branchUsage), SemanticLeafNode leaf => BuildLeaf(leaf), _ => throw new InternalDataProcessingException() }; @@ -140,9 +149,39 @@ private static JsonSchemaBuilder BuildLeaf(SemanticLeafNode leaf) _ => SchemaValueType.String }; - return new JsonSchemaBuilder().Type(type); + var primitiveSchema = new JsonSchemaBuilder().Type(type); + + return IsArrayCardinality(leaf.Cardinality) + ? new JsonSchemaBuilder().Type(SchemaValueType.Array).Items(primitiveSchema) + : primitiveSchema; + } + + private static Dictionary CountBranchUsages(SemanticTreeNode rootNode) + { + var usage = new Dictionary(StringComparer.Ordinal); + CountBranchUsages(rootNode, usage); + return usage; + } + + private static void CountBranchUsages(SemanticTreeNode node, Dictionary usage) + { + if (node is not SemanticBranchNode branch) + { + return; + } + + usage.TryGetValue(branch.SemanticId, out var count); + usage[branch.SemanticId] = count + 1; + + foreach (var child in branch.Children) + { + CountBranchUsages(child, usage); + } } + private static bool ShouldUseReference(SemanticBranchNode branch, Dictionary branchUsage) + => branchUsage.TryGetValue(branch.SemanticId, out var usageCount) && usageCount > 1; + private static bool IsArrayCardinality(Cardinality cardinality) => cardinality is Cardinality.ZeroToMany or Cardinality.OneToMany; diff --git a/source/AAS.TwinEngine.DataEngine/Infrastructure/Providers/PluginDataProvider/Helper/JsonSchemaValidator.cs b/source/AAS.TwinEngine.DataEngine/Infrastructure/Providers/PluginDataProvider/Helper/JsonSchemaValidator.cs index 1a5b1f76..33328b37 100644 --- a/source/AAS.TwinEngine.DataEngine/Infrastructure/Providers/PluginDataProvider/Helper/JsonSchemaValidator.cs +++ b/source/AAS.TwinEngine.DataEngine/Infrastructure/Providers/PluginDataProvider/Helper/JsonSchemaValidator.cs @@ -7,6 +7,7 @@ using AAS.TwinEngine.DataEngine.ServiceConfiguration.Config; using Json.Schema; +using Json.Schema.Keywords; using Microsoft.Extensions.Options; @@ -15,6 +16,11 @@ public class JsonSchemaValidator(IOptions pluginsConfig, ILogger< private readonly string _contextPrefix = pluginsConfig.Value.SubmodelElementIndexContextPrefix; private const string DefsPrefix = "#/$defs/"; + private const string DefinitionsPrefix = "#/definitions/"; + private const string Draft7Schema = "http://json-schema.org/draft-07/schema#"; + private const string Draft7SchemaHttps = "https://json-schema.org/draft-07/schema#"; + private const string Draft201909Schema = "https://json-schema.org/draft/2019-09/schema"; + private const string Draft202012Schema = "https://json-schema.org/draft/2020-12/schema"; private readonly EvaluationOptions _evaluationOptions = new() { @@ -45,18 +51,20 @@ public void ValidateRequestSchema(JsonSchema schema) try { - var jsonElement = JsonDocument.Parse(schemaNode!.ToJsonString()).RootElement; + var normalizedSchema = schemaNode!.AsObject(); + var draftUri = ExtractDraftUri(normalizedSchema); + var jsonElement = JsonDocument.Parse(normalizedSchema.ToJsonString()).RootElement; - var result = MetaSchemas.Draft202012.Evaluate(jsonElement, _evaluationOptions); + var result = ResolveMetaSchema(draftUri).Evaluate(jsonElement, _evaluationOptions); if (!result.IsValid) { - LogAndThrowException("Schema is not valid against Draft 2020-12."); + LogAndThrowException($"Schema is not valid against '{draftUri}'."); } } catch (Exception ex) { - LogAndThrowException("Draft 2020-12 evaluation failed.", ex); + LogAndThrowException("Schema dialect evaluation failed.", ex); } } @@ -173,7 +181,7 @@ private bool TryNormalizeSchema(JsonSchema schema, out JsonObject normalized, ou EscapeJsonReferencePointers(normalized); - normalized["$schema"] ??= "https://json-schema.org/draft/2020-12/schema"; + normalized["$schema"] = ExtractDraftUri(normalized); return true; } @@ -230,6 +238,10 @@ property.Value is JsonValue value && { jsonObject["$ref"] = BuildEscapedReferencePath(reference); } + else if (reference.StartsWith(DefinitionsPrefix, StringComparison.OrdinalIgnoreCase)) + { + jsonObject["$ref"] = BuildEscapedReferencePath(reference); + } } else { @@ -251,12 +263,27 @@ private void RemoveContextSuffixFromRequiredProperties(JsonArray requiredPropert private string BuildEscapedReferencePath(string reference) { - if (!reference.StartsWith(DefsPrefix, StringComparison.OrdinalIgnoreCase)) + if (reference.StartsWith(DefsPrefix, StringComparison.OrdinalIgnoreCase)) + { + return BuildEscapedReferencePath(reference, DefsPrefix); + } + + if (reference.StartsWith(DefinitionsPrefix, StringComparison.OrdinalIgnoreCase)) + { + return BuildEscapedReferencePath(reference, DefinitionsPrefix); + } + + return reference; + } + + private string BuildEscapedReferencePath(string reference, string prefix) + { + if (!reference.StartsWith(prefix, StringComparison.OrdinalIgnoreCase)) { return reference; } - var body = reference[DefsPrefix.Length..]; + var body = reference[prefix.Length..]; var stripped = RemoveContextSuffix(body); @@ -264,7 +291,7 @@ private string BuildEscapedReferencePath(string reference) .Replace("~", "~0", StringComparison.OrdinalIgnoreCase) .Replace("/", "~1", StringComparison.OrdinalIgnoreCase); - return DefsPrefix + escaped; + return prefix + escaped; } private string RemoveContextSuffix(string propertyName) @@ -284,4 +311,29 @@ private static void RenameJsonProperty(JsonObject jsonObject, string oldName, st _ = jsonObject.Remove(oldName); jsonObject[newName] = value!; } + + private static string ExtractDraftUri(JsonObject schema) + { + var raw = schema.TryGetPropertyValue("$schema", out var node) && node != null + ? node.GetValue().Trim() + : null; + + return raw switch + { + Draft7Schema or Draft7SchemaHttps => Draft7Schema, + Draft201909Schema => Draft201909Schema, + Draft202012Schema => Draft202012Schema, + _ => Draft202012Schema + }; + } + + private static JsonSchema ResolveMetaSchema(string draftUri) + { + return draftUri switch + { + Draft7Schema => MetaSchemas.Draft7, + Draft201909Schema => MetaSchemas.Draft201909, + _ => MetaSchemas.Draft202012 + }; + } } diff --git a/source/AAS.TwinEngine.Plugin.TestPlugin.UnitTests/Api/Submodel/Services/JsonSchemaParserTests.cs b/source/AAS.TwinEngine.Plugin.TestPlugin.UnitTests/Api/Submodel/Services/JsonSchemaParserTests.cs index bfb6453a..46165116 100644 --- a/source/AAS.TwinEngine.Plugin.TestPlugin.UnitTests/Api/Submodel/Services/JsonSchemaParserTests.cs +++ b/source/AAS.TwinEngine.Plugin.TestPlugin.UnitTests/Api/Submodel/Services/JsonSchemaParserTests.cs @@ -150,6 +150,27 @@ public void ParseJsonSchema_SimpleSchema_ReturnsLeafNode() Assert.Equal("foo", leaf.SemanticId); } + [Theory] + [InlineData("http://json-schema.org/draft-07/schema#")] + [InlineData("https://json-schema.org/draft/2019-09/schema")] + [InlineData("https://json-schema.org/draft/2020-12/schema")] + public void ParseJsonSchema_SimpleSchemaAcrossDrafts_ReturnsLeafNode(string draft) + { + var schema = JsonSchema.FromText($@"{{ + ""$schema"": ""{draft}"", + ""type"": ""object"", + ""properties"": {{ + ""foo"": {{ ""type"": ""string"" }} + }} + }}"); + + var node = _sut.ParseJsonSchema(schema); + + var leaf = Assert.IsType(node); + Assert.Equal("foo", leaf.SemanticId); + Assert.Equal(DataType.String, leaf.DataType); + } + [Fact] public void ParseJsonSchema_NestedObject_ReturnsBranchNodeWithChild() { @@ -192,6 +213,91 @@ public void ParseJsonSchema_ArrayWithRef_ReturnsBranchNodeWithLeafChild() Assert.Equal("val", child.SemanticId); } + [Fact] + public void ParseJsonSchema_Draft7ArrayWithDefinitionsRef_ReturnsBranchNodeWithLeafChild() + { + var schema = JsonSchema.FromText(@"{ + ""$schema"": ""http://json-schema.org/draft-07/schema#"", + ""type"": ""object"", + ""properties"": { + ""items"": { + ""type"": ""array"", + ""items"": { ""$ref"": ""#/definitions/ItemDef"" } + } + }, + ""definitions"": { + ""ItemDef"": { + ""type"": ""object"", + ""properties"": { + ""value"": { ""type"": ""integer"" } + } + } + } + }"); + + var node = _sut.ParseJsonSchema(schema); + + var branch = Assert.IsType(node); + Assert.Equal("items", branch.SemanticId); + var child = Assert.IsType(branch.Children[0]); + Assert.Equal("value", child.SemanticId); + Assert.Equal(DataType.Integer, child.DataType); + } + + [Fact] + public void ParseJsonSchema_Draft7SchemaWithDefsKeyword_ReturnsLeafNode() + { + var schema = JsonSchema.FromText(@"{ + ""$schema"": ""http://json-schema.org/draft-07/schema#"", + ""type"": ""object"", + ""properties"": { + ""item"": { ""$ref"": ""#/$defs/ItemDef"" } + }, + ""$defs"": { + ""ItemDef"": { + ""type"": ""object"", + ""properties"": { + ""value"": { ""type"": ""string"" } + } + } + } + }"); + + var node = _sut.ParseJsonSchema(schema); + + var branch = Assert.IsType(node); + var child = Assert.IsType(branch.Children[0]); + Assert.Equal("value", child.SemanticId); + Assert.Equal(DataType.String, child.DataType); + } + + [Fact] + public void ParseJsonSchema_Draft202012SchemaWithDefinitionsKeyword_ReturnsLeafNode() + { + var schema = JsonSchema.FromText(@"{ + ""$schema"": ""https://json-schema.org/draft/2020-12/schema"", + ""type"": ""object"", + ""properties"": { + ""item"": { ""$ref"": ""#/definitions/ItemDef"" } + }, + ""definitions"": { + ""ItemDef"": { + ""type"": ""object"", + ""properties"": { + ""value"": { ""type"": ""integer"" } + } + } + } + }"); + + var node = _sut.ParseJsonSchema(schema); + + var branch = Assert.IsType(node); + var child = Assert.IsType(branch.Children[0]); + Assert.Equal("value", child.SemanticId); + Assert.Equal(DataType.Integer, child.DataType); + } + [Fact] public void ParseJsonSchema_AllDataTypeSchemaWithRef_ReturnsBranchNode() { diff --git a/source/AAS.TwinEngine.Plugin.TestPlugin.UnitTests/Api/Submodel/Services/JsonSchemaValidatorTests.cs b/source/AAS.TwinEngine.Plugin.TestPlugin.UnitTests/Api/Submodel/Services/JsonSchemaValidatorTests.cs index 123ee722..61fc1a56 100644 --- a/source/AAS.TwinEngine.Plugin.TestPlugin.UnitTests/Api/Submodel/Services/JsonSchemaValidatorTests.cs +++ b/source/AAS.TwinEngine.Plugin.TestPlugin.UnitTests/Api/Submodel/Services/JsonSchemaValidatorTests.cs @@ -79,6 +79,27 @@ public void ValidateResponseContent_ValidJsonAndSchema_DoesNotThrow() _sut.ValidateResponseContent(Json, schema); } + [Theory] + [InlineData("http://json-schema.org/draft-07/schema#")] + [InlineData("https://json-schema.org/draft/2019-09/schema")] + [InlineData("https://json-schema.org/draft/2020-12/schema")] + public void ValidateResponseContent_SupportedDrafts_DoesNotThrow(string draft) + { + var schema = new JsonSchemaBuilder() + .Schema(draft) + .Type(SchemaValueType.Object) + .Properties(new Dictionary + { + ["name"] = new JsonSchemaBuilder().Type(SchemaValueType.String) + }) + .Required("name") + .Build(); + + const string json = "{\"name\": \"Test\"}"; + + _sut.ValidateResponseContent(json, schema); + } + [Theory] [MemberData(nameof(InvalidPrimitives))] public void ValidateResponseContent_InvalidValueType_ThrowsBadRequest( @@ -454,4 +475,88 @@ public void ValidateResponseContent_WithDefsReference_DoesNotThrow() _sut.ValidateResponseContent(json, schema); } + + [Fact] + public void ValidateResponseContent_Draft7DefinitionsReferenceWithSuffix_DoesNotThrow() + { + const string schemaJson = @"{ + ""$schema"": ""http://json-schema.org/draft-07/schema#"", + ""type"": ""object"", + ""properties"": { + ""item_aastwinengine_00"": { ""$ref"": ""#/definitions/Type_aastwinengine_00"" } + }, + ""required"": [""item_aastwinengine_00""], + ""definitions"": { + ""Type_aastwinengine_00"": { + ""type"": ""object"", + ""properties"": { + ""name_aastwinengine_00"": { ""type"": ""string"" } + }, + ""required"": [""name_aastwinengine_00""] + } + } + }"; + + var schema = JsonSchema.FromText(schemaJson); + + const string json = @"{ ""item"": { ""name"": ""ok"" } }"; + + _sut.ValidateResponseContent(json, schema); + } + + [Fact] + public void ValidateResponseContent_Draft202012DefinitionsReferenceWithSuffix_DoesNotThrow() + { + const string schemaJson = @"{ + ""$schema"": ""https://json-schema.org/draft/2020-12/schema"", + ""type"": ""object"", + ""properties"": { + ""item_aastwinengine_00"": { ""$ref"": ""#/definitions/Type_aastwinengine_00"" } + }, + ""required"": [""item_aastwinengine_00""], + ""definitions"": { + ""Type_aastwinengine_00"": { + ""type"": ""object"", + ""properties"": { + ""name_aastwinengine_00"": { ""type"": ""string"" } + }, + ""required"": [""name_aastwinengine_00""] + } + } + }"; + + var schema = JsonSchema.FromText(schemaJson); + + const string json = @"{ ""item"": { ""name"": ""ok"" } }"; + + _sut.ValidateResponseContent(json, schema); + } + + [Fact] + public void ValidateResponseContent_Draft7DefsReferenceWithSuffix_DoesNotThrow() + { + const string schemaJson = @"{ + ""$schema"": ""http://json-schema.org/draft-07/schema#"", + ""type"": ""object"", + ""properties"": { + ""item_aastwinengine_00"": { ""$ref"": ""#/$defs/Type_aastwinengine_00"" } + }, + ""required"": [""item_aastwinengine_00""], + ""$defs"": { + ""Type_aastwinengine_00"": { + ""type"": ""object"", + ""properties"": { + ""name_aastwinengine_00"": { ""type"": ""string"" } + }, + ""required"": [""name_aastwinengine_00""] + } + } + }"; + + var schema = JsonSchema.FromText(schemaJson); + + const string json = @"{ ""item"": { ""name"": ""ok"" } }"; + + _sut.ValidateResponseContent(json, schema); + } } diff --git a/source/AAS.TwinEngine.Plugin.TestPlugin/Api/Submodel/Services/JsonSchemaParser.cs b/source/AAS.TwinEngine.Plugin.TestPlugin/Api/Submodel/Services/JsonSchemaParser.cs index c9b84753..c1967d39 100644 --- a/source/AAS.TwinEngine.Plugin.TestPlugin/Api/Submodel/Services/JsonSchemaParser.cs +++ b/source/AAS.TwinEngine.Plugin.TestPlugin/Api/Submodel/Services/JsonSchemaParser.cs @@ -11,6 +11,15 @@ namespace AAS.TwinEngine.Plugin.TestPlugin.Api.Submodel.Services; public class JsonSchemaParser(ILogger logger) : IJsonSchemaParser { + private const string Draft7Schema = "http://json-schema.org/draft-07/schema#"; + private const string Draft7SchemaHttps = "https://json-schema.org/draft-07/schema#"; + private const string Draft201909Schema = "https://json-schema.org/draft/2019-09/schema"; + private const string Draft202012Schema = "https://json-schema.org/draft/2020-12/schema"; + private const string DefsRefPrefix = "#/$defs/"; + private const string DefinitionsRefPrefix = "#/definitions/"; + + private enum SchemaDraft { Draft7, Draft201909, Draft202012 } + public SemanticTreeNode ParseJsonSchema(JsonSchema jsonSchema) { ValidateRequest(jsonSchema); @@ -22,9 +31,11 @@ private void ValidateRequest(JsonSchema jsonSchema) try { var json = JsonSerializer.SerializeToNode(jsonSchema); - var element = JsonDocument.Parse(json!.ToJsonString()).RootElement; + var schema = json?.AsObject() ?? throw new JsonException(); + var draftUri = GetSchemaDraftUri(schema); + var element = JsonDocument.Parse(schema.ToJsonString()).RootElement; - var result = MetaSchemas.Draft202012.Evaluate(element, new EvaluationOptions + var result = ResolveMetaSchema(draftUri).Evaluate(element, new EvaluationOptions { OutputFormat = OutputFormat.List }); @@ -45,6 +56,7 @@ private void ValidateRequest(JsonSchema jsonSchema) private SemanticTreeNode CreateSemanticTree(JsonSchema jsonSchema) { var json = JsonSerializer.SerializeToNode(jsonSchema)!.AsObject(); + var draft = GetSchemaDraft(json); if (!json.TryGetPropertyValue("properties", out var propsNode) || propsNode is not JsonObject props || @@ -54,48 +66,83 @@ propsNode is not JsonObject props || } var rootProperty = ((IList>)props)[0]; - return ProcessProperty(rootProperty.Key, rootProperty.Value!, json); + return ProcessProperty(rootProperty.Key, rootProperty.Value!, json, draft); } - private SemanticTreeNode ProcessProperty(string name, JsonNode propertyNode, JsonObject root) + private SemanticTreeNode ProcessProperty(string name, JsonNode propertyNode, JsonObject root, SchemaDraft draft) { var property = propertyNode.AsObject(); if (property.TryGetPropertyValue("$ref", out var refNode)) { - return HandleReference(name, refNode!.GetValue(), root); + return HandleReference(name, refNode!.GetValue(), root, draft); } var type = GetType(property); return type switch { - DataType.Object => BuildObjectBranch(name, propertyNode, root), - DataType.Array => BuildArrayBranch(name, propertyNode, root), + DataType.Object => BuildObjectBranch(name, propertyNode, root, draft), + DataType.Array => BuildArrayBranch(name, propertyNode, root, draft), _ => new SemanticLeafNode(name, type, "") }; } - private SemanticTreeNode HandleReference(string name, string reference, JsonObject root) + private SemanticTreeNode HandleReference(string name, string reference, JsonObject root, SchemaDraft draft) { - if (!reference.StartsWith("#/$defs/", StringComparison.OrdinalIgnoreCase)) + if (TryResolveReference(reference, root, draft, out var referenceNode)) { - return new SemanticLeafNode(name, DataType.Unknown, ""); + return ProcessProperty(name, referenceNode!, root, draft); } - var key = reference.Replace("#/$defs/", "", StringComparison.OrdinalIgnoreCase); + return new SemanticLeafNode(name, DataType.Unknown, ""); + } + + private static bool TryResolveReference(string reference, JsonObject root, SchemaDraft draft, out JsonNode? referenceNode) + { + referenceNode = null; - if (!root.TryGetPropertyValue("$defs", out var defsNode) || - defsNode is not JsonObject defs || - !defs.TryGetPropertyValue(key, out var defNode)) + if (!TryGetReferenceKey(reference, out var key)) { - return new SemanticLeafNode(name, DataType.Unknown, ""); + return false; } - return ProcessProperty(name, defNode!, root); + var preferred = GetPreferredDefinitionsProperty(draft); + var fallback = preferred == "$defs" ? "definitions" : "$defs"; + + if (TryGetDefinition(root, preferred, key, out referenceNode)) + { + return true; + } + + return TryGetDefinition(root, fallback, key, out referenceNode); + } + + private static bool TryGetReferenceKey(string reference, out string key) + { + key = string.Empty; + var prefix = GetReferencePrefix(reference); + if (prefix == null) return false; + key = DecodeJsonPointerToken(reference[prefix.Length..]); + return !string.IsNullOrWhiteSpace(key); + } + + private static string? GetReferencePrefix(string reference) + { + if (reference.StartsWith(DefsRefPrefix, StringComparison.OrdinalIgnoreCase)) return DefsRefPrefix; + if (reference.StartsWith(DefinitionsRefPrefix, StringComparison.OrdinalIgnoreCase)) return DefinitionsRefPrefix; + return null; } - private SemanticBranchNode BuildObjectBranch(string name, JsonNode node, JsonObject root) + private static bool TryGetDefinition(JsonObject root, string definitionsProperty, string key, out JsonNode? defNode) + { + defNode = null; + if (!root.TryGetPropertyValue(definitionsProperty, out var defsNode) || defsNode is not JsonObject defs) + return false; + return defs.TryGetPropertyValue(key, out defNode); + } + + private SemanticBranchNode BuildObjectBranch(string name, JsonNode node, JsonObject root, SchemaDraft draft) { var obj = node.AsObject(); var branch = new SemanticBranchNode(name, DataType.Object); @@ -105,14 +152,14 @@ private SemanticBranchNode BuildObjectBranch(string name, JsonNode node, JsonObj { foreach (var prop in props) { - branch.AddChild(ProcessProperty(prop.Key, prop.Value!, root)); + branch.AddChild(ProcessProperty(prop.Key, prop.Value!, root, draft)); } } return branch; } - private SemanticBranchNode BuildArrayBranch(string name, JsonNode node, JsonObject root) + private SemanticBranchNode BuildArrayBranch(string name, JsonNode node, JsonObject root, SchemaDraft draft) { var obj = node.AsObject(); var branch = new SemanticBranchNode(name, DataType.Array); @@ -131,7 +178,7 @@ private SemanticBranchNode BuildArrayBranch(string name, JsonNode node, JsonObje { foreach (var prop in props) { - branch.AddChild(ProcessProperty(prop.Key, prop.Value!, root)); + branch.AddChild(ProcessProperty(prop.Key, prop.Value!, root, draft)); } return branch; @@ -139,7 +186,7 @@ private SemanticBranchNode BuildArrayBranch(string name, JsonNode node, JsonObje if (itemObj.TryGetPropertyValue("$ref", out var refNode)) { - var resolved = HandleReference(name, refNode!.GetValue(), root); + var resolved = HandleReference(name, refNode!.GetValue(), root, draft); if (resolved is SemanticBranchNode refBranch) { @@ -178,4 +225,54 @@ private static DataType GetType(JsonObject obj) _ => DataType.String }; } + + private static SchemaDraft GetSchemaDraft(JsonObject schema) + { + return GetSchemaDraftUri(schema) switch + { + Draft7Schema or Draft7SchemaHttps => SchemaDraft.Draft7, + Draft201909Schema => SchemaDraft.Draft201909, + _ => SchemaDraft.Draft202012 + }; + } + + private static string GetPreferredDefinitionsProperty(SchemaDraft draft) + { + return draft == SchemaDraft.Draft7 ? "definitions" : "$defs"; + } + + private static string GetSchemaDraftUri(JsonObject schema) + { + if (!schema.TryGetPropertyValue("$schema", out var schemaNode) || schemaNode == null) + { + return Draft202012Schema; + } + + var raw = schemaNode.GetValue().Trim(); + + return raw switch + { + Draft7Schema or Draft7SchemaHttps => Draft7Schema, + Draft201909Schema => Draft201909Schema, + Draft202012Schema => Draft202012Schema, + _ => Draft202012Schema + }; + } + + private static JsonSchema ResolveMetaSchema(string draftUri) + { + return draftUri switch + { + Draft7Schema => MetaSchemas.Draft7, + Draft201909Schema => MetaSchemas.Draft201909, + _ => MetaSchemas.Draft202012 + }; + } + + private static string DecodeJsonPointerToken(string token) + { + return token + .Replace("~1", "/", StringComparison.OrdinalIgnoreCase) + .Replace("~0", "~", StringComparison.OrdinalIgnoreCase); + } } diff --git a/source/AAS.TwinEngine.Plugin.TestPlugin/Api/Submodel/Services/JsonSchemaValidator.cs b/source/AAS.TwinEngine.Plugin.TestPlugin/Api/Submodel/Services/JsonSchemaValidator.cs index 7b29ac6c..4171ccfa 100644 --- a/source/AAS.TwinEngine.Plugin.TestPlugin/Api/Submodel/Services/JsonSchemaValidator.cs +++ b/source/AAS.TwinEngine.Plugin.TestPlugin/Api/Submodel/Services/JsonSchemaValidator.cs @@ -16,6 +16,11 @@ public class JsonSchemaValidator(IOptions semantics, ILogger().Trim(); + + return raw switch + { + Draft7Schema or Draft7SchemaHttps => Draft7Schema, + Draft201909Schema => Draft201909Schema, + Draft202012Schema => Draft202012Schema, + _ => Draft202012Schema + }; + } } From b10ebe27b954383e4468eecf67b55d24e60f4bdc Mon Sep 17 00:00:00 2001 From: Hardi Shah Date: Mon, 27 Apr 2026 17:12:31 +0530 Subject: [PATCH 64/71] Refactor JSON schema generation and remove unused template mapping tests Co-authored-by: Copilot --- ...acyTemplateManagementConfigAdapterTests.cs | 1 - .../Helper/JsonSchemaGeneratorTests.cs | 56 ++-- .../TemplateMappingRulesValidatorTests.cs | 265 ------------------ .../ShellTemplateMappingProviderTests.cs | 11 - .../Helper/JsonSchemaGenerator.cs | 51 +--- .../Program.cs | 4 + 6 files changed, 39 insertions(+), 349 deletions(-) delete mode 100644 source/AAS.TwinEngine.DataEngine.UnitTests/Infrastructure/Providers/TemplateProvider/Config/TemplateMappingRulesValidatorTests.cs diff --git a/source/AAS.TwinEngine.DataEngine.UnitTests/Infrastructure/Configuration/LegacyV1/LegacyTemplateManagementConfigAdapterTests.cs b/source/AAS.TwinEngine.DataEngine.UnitTests/Infrastructure/Configuration/LegacyV1/LegacyTemplateManagementConfigAdapterTests.cs index 075f1085..a25d75e9 100644 --- a/source/AAS.TwinEngine.DataEngine.UnitTests/Infrastructure/Configuration/LegacyV1/LegacyTemplateManagementConfigAdapterTests.cs +++ b/source/AAS.TwinEngine.DataEngine.UnitTests/Infrastructure/Configuration/LegacyV1/LegacyTemplateManagementConfigAdapterTests.cs @@ -1,5 +1,4 @@ using AAS.TwinEngine.DataEngine.Infrastructure.Configuration.LegacyV1; -using AAS.TwinEngine.DataEngine.Infrastructure.Providers.TemplateProvider.Config; using AAS.TwinEngine.DataEngine.ServiceConfiguration.Config; using Microsoft.Extensions.Configuration; diff --git a/source/AAS.TwinEngine.DataEngine.UnitTests/Infrastructure/Providers/PluginDataProvider/Helper/JsonSchemaGeneratorTests.cs b/source/AAS.TwinEngine.DataEngine.UnitTests/Infrastructure/Providers/PluginDataProvider/Helper/JsonSchemaGeneratorTests.cs index 4a4c1d2c..8c878319 100644 --- a/source/AAS.TwinEngine.DataEngine.UnitTests/Infrastructure/Providers/PluginDataProvider/Helper/JsonSchemaGeneratorTests.cs +++ b/source/AAS.TwinEngine.DataEngine.UnitTests/Infrastructure/Providers/PluginDataProvider/Helper/JsonSchemaGeneratorTests.cs @@ -93,7 +93,7 @@ public void ConvertToJsonSchema_BranchNodeWithZeroToManyCardinality_ReturnsArray } [Fact] - public void ConvertToJsonSchema_NestedBranchNodes_UsesInlineSchemaWithoutDefs() + public void ConvertToJsonSchema_NestedBranchNode_UsesDefs() { const string RootSemanticId = "http://example.com/idta/digital-nameplate"; const string BranchSemanticId = "http://example.com/idta/digital-nameplate/contact-list"; @@ -108,14 +108,13 @@ public void ConvertToJsonSchema_NestedBranchNodes_UsesInlineSchemaWithoutDefs() Assert.Equal("object", json["type"]!.ToString()); var rootProps = json["properties"]![RootSemanticId]!["properties"]!; - var branchSchema = rootProps[BranchSemanticId]!; - Assert.Null(branchSchema["$ref"]); - Assert.Equal("object", branchSchema["type"]!.ToString()); - var defs = json["$defs"]; - Assert.True(defs == null || defs.AsObject().Count == 0); - var branchProps = branchSchema["properties"]!; + Assert.Equal($"#/$defs/{BranchSemanticId}", rootProps[BranchSemanticId]!["$ref"]!.ToString()); + var defs = json["$defs"]!; + var branchDef = defs[BranchSemanticId]!; + Assert.Equal("object", branchDef["type"]!.ToString()); + var branchProps = branchDef["properties"]!; Assert.NotNull(branchProps[NameId]); - var required = branchSchema["required"]!.AsArray(); + var required = branchDef["required"]!.AsArray(); Assert.Contains(NameId, required.Select(x => x!.ToString())); } @@ -158,7 +157,7 @@ public void ConvertToJsonSchema_ArraySchema_UsesItemsKeyword() } [Fact] - public void ConvertToJsonSchema_LeafWithOneToManyCardinality_UsesArrayItems() + public void ConvertToJsonSchema_LeafWithOneToManyCardinality_UsesPrimitiveType() { var branch = new SemanticBranchNode("root", Cardinality.One); branch.AddChild(new SemanticLeafNode("tags", "", DataType.String, Cardinality.OneToMany)); @@ -167,8 +166,8 @@ public void ConvertToJsonSchema_LeafWithOneToManyCardinality_UsesArrayItems() var json = ToJson(schema); var leaf = json["properties"]!["root"]!["properties"]!["tags"]!; - Assert.Equal("array", leaf["type"]!.ToString()); - Assert.Equal("string", leaf["items"]!["type"]!.ToString()); + Assert.Equal("string", leaf["type"]!.ToString()); + Assert.Null(leaf["items"]); } [Fact] @@ -238,12 +237,14 @@ public void ConvertToJsonSchema_DeepNestedStructure_WorksCorrectly() var schema = JsonSchemaGenerator.ConvertToJsonSchema(level1); var json = ToJson(schema); - var level2Schema = json["properties"]!["level1"]!["properties"]!["level2"]!; + var defs = json["$defs"]!; + Assert.Equal("#/$defs/level2", json["properties"]!["level1"]!["properties"]!["level2"]!["$ref"]!.ToString()); + var level2Schema = defs["level2"]!; Assert.Equal("object", level2Schema["type"]!.ToString()); - var level3Schema = level2Schema["properties"]!["level3"]!; + Assert.Equal("#/$defs/level3", level2Schema["properties"]!["level3"]!["$ref"]!.ToString()); + var level3Schema = defs["level3"]!; Assert.Equal("object", level3Schema["type"]!.ToString()); - var leafSchema = level3Schema["properties"]!["leaf"]!; - Assert.Equal("string", leafSchema["type"]!.ToString()); + Assert.Equal("string", level3Schema["properties"]!["leaf"]!["type"]!.ToString()); } [Fact] @@ -261,17 +262,16 @@ public void ConvertToJsonSchema_MixedCardinality_WorksCorrectly() var json = ToJson(schema); var props = json["properties"]!["root"]!["properties"]!; - var objectSchema = props["objectChild"]!; - Assert.Equal("object", objectSchema["type"]!.ToString()); - var arraySchema = props["arrayChild"]!; - Assert.Equal("array", arraySchema["type"]!.ToString()); - Assert.NotNull(arraySchema["items"]); - var defs = json["$defs"]; - Assert.True(defs == null || defs.AsObject().Count == 0); + Assert.Equal("#/$defs/objectChild", props["objectChild"]!["$ref"]!.ToString()); + Assert.Equal("#/$defs/arrayChild", props["arrayChild"]!["$ref"]!.ToString()); + var defs = json["$defs"]!; + Assert.Equal("object", defs["objectChild"]!["type"]!.ToString()); + Assert.Equal("array", defs["arrayChild"]!["type"]!.ToString()); + Assert.NotNull(defs["arrayChild"]!["items"]); } [Fact] - public void ConvertToJsonSchema_SingleNestedBranch_OmitsRefAndDefs() + public void ConvertToJsonSchema_SingleNestedBranch_UsesDefs() { var root = new SemanticBranchNode("root", Cardinality.One); var child = new SemanticBranchNode("child", Cardinality.One); @@ -281,10 +281,12 @@ public void ConvertToJsonSchema_SingleNestedBranch_OmitsRefAndDefs() var schema = JsonSchemaGenerator.ConvertToJsonSchema(root); var json = ToJson(schema); - var childSchema = json["properties"]!["root"]!["properties"]!["child"]!; - Assert.Null(childSchema["$ref"]); - Assert.Equal("object", childSchema["type"]!.ToString()); - Assert.True(json["$defs"] == null || json["$defs"]!.AsObject().Count == 0); + Assert.Equal("#/$defs/child", json["properties"]!["root"]!["properties"]!["child"]!["$ref"]!.ToString()); + var childDef = json["$defs"]!["child"]!; + Assert.Equal("object", childDef["type"]!.ToString()); + Assert.NotNull(childDef["properties"]!["name"]); + var required = childDef["required"]!.AsArray(); + Assert.Contains("name", required.Select(x => x!.ToString())); } [Fact] diff --git a/source/AAS.TwinEngine.DataEngine.UnitTests/Infrastructure/Providers/TemplateProvider/Config/TemplateMappingRulesValidatorTests.cs b/source/AAS.TwinEngine.DataEngine.UnitTests/Infrastructure/Providers/TemplateProvider/Config/TemplateMappingRulesValidatorTests.cs deleted file mode 100644 index 83907170..00000000 --- a/source/AAS.TwinEngine.DataEngine.UnitTests/Infrastructure/Providers/TemplateProvider/Config/TemplateMappingRulesValidatorTests.cs +++ /dev/null @@ -1,265 +0,0 @@ -using AAS.TwinEngine.DataEngine.ApplicationLogic.Exceptions.Application; -using AAS.TwinEngine.DataEngine.Infrastructure.Providers.TemplateProvider.Config; -using AAS.TwinEngine.DataEngine.ServiceConfiguration.Config; - -namespace AAS.TwinEngine.DataEngine.UnitTests.Infrastructure.Providers.TemplateProvider.Config; - -public class TemplateMappingRulesValidatorTests -{ - private readonly TemplateMappingRulesValidator _sut = new(); - - private static TemplateManagementConfig CreateConfig(params AasIdExtractionRule[] rules) - { - return new TemplateManagementConfig - { - TemplateMappingRules = new TemplateMappingRules - { - AasIdExtractionRules = rules - } - }; - } - - // ── Rule 1: At least one rule is required ── - - [Fact] - public void Validate_ZeroRules_Fails() - { - var config = CreateConfig(); - - var result = _sut.Validate(null, config); - - Assert.True(result.Failed); - Assert.Contains("At least one AasIdExtractionRule is required", result.FailureMessage); - } - - // ── Rule 2: Single rule, no ValidationPattern → succeeds ── - - [Fact] - public void Validate_SingleRule_NoValidationPattern_Succeeds() - { - var config = CreateConfig( - new AasIdExtractionRule - { - Strategy = ExtractionStrategy.Split, - Pattern = "/", - Index = 6 - }); - - var result = _sut.Validate(null, config); - - Assert.True(result.Succeeded); - } - - // ── Rule 3: Multiple rules, all have ValidationPattern → succeeds ── - - [Fact] - public void Validate_MultipleRules_AllHaveValidationPattern_Succeeds() - { - var config = CreateConfig( - new AasIdExtractionRule - { - Strategy = ExtractionStrategy.Regex, - Pattern = @"^https?://[^/]+/ids/submodel/([^/]+/[^/]+)(?:/|$)", - Index = 1, - ValidationPattern = @"^[0-9\-/]+$" - }, - new AasIdExtractionRule - { - Strategy = ExtractionStrategy.Regex, - Pattern = @"^https?://[^/]+/ids/submodel/([^/]+)(?:/|$)", - Index = 1, - ValidationPattern = @"^[0-9\-]+$" - }); - - var result = _sut.Validate(null, config); - - Assert.True(result.Succeeded); - } - - // ── Rule 3: Multiple rules, one missing ValidationPattern → fails ── - - [Fact] - public void Validate_MultipleRules_OneMissingValidationPattern_Fails() - { - var config = CreateConfig( - new AasIdExtractionRule - { - Strategy = ExtractionStrategy.Regex, - Pattern = @"^https?://[^/]+/ids/submodel/([^/]+)(?:/|$)", - Index = 1, - ValidationPattern = @"^[0-9\-]+$" - }, - new AasIdExtractionRule - { - Strategy = ExtractionStrategy.Split, - Pattern = "/", - Index = 6 - // Missing ValidationPattern - }); - - var result = _sut.Validate(null, config); - - Assert.True(result.Failed); - Assert.Contains("ValidationPattern is required when multiple extraction rules are configured", result.FailureMessage); - } - - // ── Regex with invalid pattern → fails ── - - [Fact] - public void Validate_Regex_InvalidPattern_Fails() - { - var config = CreateConfig( - new AasIdExtractionRule - { - Strategy = ExtractionStrategy.Regex, - Pattern = "[invalid", - Index = 1 - }); - - var result = _sut.Validate(null, config); - - Assert.True(result.Failed); - Assert.Contains("invalid regex Pattern", result.FailureMessage); - } - - // ── Split with empty separator → fails ── - - [Fact] - public void Validate_Split_EmptyPattern_Fails() - { - var config = CreateConfig( - new AasIdExtractionRule - { - Strategy = ExtractionStrategy.Split, - Pattern = "", - Index = 6 - }); - - var result = _sut.Validate(null, config); - - Assert.True(result.Failed); - Assert.Contains("empty Pattern", result.FailureMessage); - } - - // ── Index < 1 → fails ── - - [Fact] - public void Validate_IndexLessThanOne_Fails() - { - var config = CreateConfig( - new AasIdExtractionRule - { - Strategy = ExtractionStrategy.Split, - Pattern = "/", - Index = 0 - }); - - var result = _sut.Validate(null, config); - - Assert.True(result.Failed); - Assert.Contains("Index must be >= 1", result.FailureMessage); - } - - // ── EndIndex < Index → fails ── - - [Fact] - public void Validate_Split_EndIndexLessThanIndex_Fails() - { - var config = CreateConfig( - new AasIdExtractionRule - { - Strategy = ExtractionStrategy.Split, - Pattern = "/", - Index = 5, - EndIndex = 3 - }); - - var result = _sut.Validate(null, config); - - Assert.True(result.Failed); - Assert.Contains("EndIndex (3) must be >= Index (5)", result.FailureMessage); - } - - // ── Invalid ValidationPattern regex → fails ── - - [Fact] - public void Validate_InvalidValidationPattern_Fails() - { - var config = CreateConfig( - new AasIdExtractionRule - { - Strategy = ExtractionStrategy.Split, - Pattern = "/", - Index = 6, - ValidationPattern = "[broken" - }); - - var result = _sut.Validate(null, config); - - Assert.True(result.Failed); - Assert.Contains("invalid ValidationPattern", result.FailureMessage); - } - - // ── Null options → throws ── - - [Fact] - public void Validate_NullOptions_ThrowsInvalidDependencyException() => Assert.Throws(() => _sut.Validate(null, null!)); - - // ── Uses Description in error message when available ── - - [Fact] - public void Validate_UsesDescriptionInErrorMessage() - { - var config = CreateConfig( - new AasIdExtractionRule - { - Strategy = ExtractionStrategy.Split, - Pattern = "/", - Index = 0, - Description = "My broken rule" - }); - - var result = _sut.Validate(null, config); - - Assert.True(result.Failed); - Assert.Contains("My broken rule", result.FailureMessage); - } - - // ── Valid Regex strategy rule → succeeds ── - - [Fact] - public void Validate_ValidRegexRule_Succeeds() - { - var config = CreateConfig( - new AasIdExtractionRule - { - Strategy = ExtractionStrategy.Regex, - Pattern = @"^https?://[^/]+/ids/submodel/([^/]+)(?:/|$)", - Index = 1, - Description = "Single-segment" - }); - - var result = _sut.Validate(null, config); - - Assert.True(result.Succeeded); - } - - // ── Valid Split with EndIndex → succeeds ── - - [Fact] - public void Validate_ValidSplitWithEndIndex_Succeeds() - { - var config = CreateConfig( - new AasIdExtractionRule - { - Strategy = ExtractionStrategy.Split, - Pattern = "/", - Index = 5, - EndIndex = 6 - }); - - var result = _sut.Validate(null, config); - - Assert.True(result.Succeeded); - } -} diff --git a/source/AAS.TwinEngine.DataEngine.UnitTests/Infrastructure/Providers/TemplateProvider/Services/ShellTemplateMappingProviderTests.cs b/source/AAS.TwinEngine.DataEngine.UnitTests/Infrastructure/Providers/TemplateProvider/Services/ShellTemplateMappingProviderTests.cs index 28ef9ee0..d0463198 100644 --- a/source/AAS.TwinEngine.DataEngine.UnitTests/Infrastructure/Providers/TemplateProvider/Services/ShellTemplateMappingProviderTests.cs +++ b/source/AAS.TwinEngine.DataEngine.UnitTests/Infrastructure/Providers/TemplateProvider/Services/ShellTemplateMappingProviderTests.cs @@ -263,8 +263,6 @@ public void GetProductIdFromRule_Split_ValidationFails_SkipsRule() sut.GetProductIdFromRule("https://test.com/ids/submodel/2000-2201/CI")); } - // ── First rule matches — stops (does not try second) ── - [Fact] public void GetProductIdFromRule_FirstRuleMatches_StopsImmediately() { @@ -290,7 +288,6 @@ public void GetProductIdFromRule_FirstRuleMatches_StopsImmediately() Assert.Equal("2000-2201/353-000", result); } - // ── Mixed strategies: Regex then Split fallback ── [Fact] public void GetProductIdFromRule_ThreeSagmentMatches_SuccessResult() @@ -341,8 +338,6 @@ public void GetProductIdFromRule_RegexFails_FallsToSplit() Assert.Equal("2000-2201", result); } - // ── Split with colon separator ── - [Fact] public void GetProductIdFromRule_SplitRule_NoValidationPattern_Works() { @@ -400,8 +395,6 @@ public void GetTemplateId_RegexExtraction_MatchesTemplate() Assert.Equal("test-template", result); } - // ── GetTemplateId: case-insensitive template matching ── - [Fact] public void GetTemplateId_CaseInsensitive_MatchesTemplate() { @@ -424,8 +417,6 @@ public void GetTemplateId_CaseInsensitive_MatchesTemplate() Assert.Equal("template1", result); } - // ── GetTemplateId: no matching template ── - [Fact] public void GetTemplateId_NoMatchingTemplate_ThrowsResourceNotFoundException() { @@ -472,8 +463,6 @@ public void Constructor_NullShellTemplateMappings_ThrowsInvalidDependencyExcepti var ex = Assert.Throws(() => new ShellTemplateMappingProvider(_logger, options)); } - // ── Constructor: null AasIdExtractionRules ── - [Fact] public void Constructor_NullAasIdExtractionRules_ThrowsInvalidDependencyException() { diff --git a/source/AAS.TwinEngine.DataEngine/Infrastructure/Providers/PluginDataProvider/Helper/JsonSchemaGenerator.cs b/source/AAS.TwinEngine.DataEngine/Infrastructure/Providers/PluginDataProvider/Helper/JsonSchemaGenerator.cs index ab8b29d8..ab45b52d 100644 --- a/source/AAS.TwinEngine.DataEngine/Infrastructure/Providers/PluginDataProvider/Helper/JsonSchemaGenerator.cs +++ b/source/AAS.TwinEngine.DataEngine/Infrastructure/Providers/PluginDataProvider/Helper/JsonSchemaGenerator.cs @@ -13,9 +13,8 @@ public static class JsonSchemaGenerator public static JsonSchema ConvertToJsonSchema(SemanticTreeNode rootNode) { - var branchUsage = CountBranchUsages(rootNode); var definitions = new Dictionary(); - var rootSchema = BuildNode(rootNode, definitions, branchUsage, isRoot: true); + var rootSchema = BuildNode(rootNode, definitions, isRoot: true); return new JsonSchemaBuilder() .Schema(MetaSchemas.Draft202012Id) @@ -31,12 +30,11 @@ public static JsonSchema ConvertToJsonSchema(SemanticTreeNode rootNode) private static JsonSchemaBuilder BuildNode( SemanticTreeNode node, Dictionary definitions, - Dictionary branchUsage, bool isRoot = false) { return node switch { - SemanticBranchNode branch => BuildBranch(branch, definitions, branchUsage, isRoot), + SemanticBranchNode branch => BuildBranch(branch, definitions, isRoot), SemanticLeafNode leaf => BuildLeaf(leaf), _ => throw new InternalDataProcessingException() }; @@ -45,16 +43,15 @@ private static JsonSchemaBuilder BuildNode( private static JsonSchemaBuilder BuildBranch( SemanticBranchNode branch, Dictionary definitions, - Dictionary branchUsage, bool isRoot = false) { - if (!isRoot && ShouldUseReference(branch, branchUsage) && definitions.ContainsKey(branch.SemanticId)) + if (!isRoot && definitions.ContainsKey(branch.SemanticId)) { return CreateRefSchema(branch.SemanticId); } var requiredProperties = new List(); - var children = BuildChildren(branch.Children, definitions, branchUsage, requiredProperties); + var children = BuildChildren(branch.Children, definitions, requiredProperties); var schemaBuilder = IsArrayCardinality(branch.Cardinality) ? BuildArraySchema(children, requiredProperties) @@ -65,11 +62,6 @@ private static JsonSchemaBuilder BuildBranch( return schemaBuilder; } - if (!ShouldUseReference(branch, branchUsage)) - { - return schemaBuilder; - } - definitions[branch.SemanticId] = schemaBuilder; return CreateRefSchema(branch.SemanticId); @@ -112,7 +104,6 @@ private static JsonSchemaBuilder BuildArraySchema( private static Dictionary BuildChildren( ReadOnlyCollection children, Dictionary definitions, - Dictionary branchUsage, List required) { var properties = new Dictionary(); @@ -121,7 +112,7 @@ private static Dictionary BuildChildren( { var childSchema = child switch { - SemanticBranchNode branch => BuildNode(branch, definitions, branchUsage), + SemanticBranchNode branch => BuildNode(branch, definitions), SemanticLeafNode leaf => BuildLeaf(leaf), _ => throw new InternalDataProcessingException() }; @@ -149,39 +140,9 @@ private static JsonSchemaBuilder BuildLeaf(SemanticLeafNode leaf) _ => SchemaValueType.String }; - var primitiveSchema = new JsonSchemaBuilder().Type(type); - - return IsArrayCardinality(leaf.Cardinality) - ? new JsonSchemaBuilder().Type(SchemaValueType.Array).Items(primitiveSchema) - : primitiveSchema; - } - - private static Dictionary CountBranchUsages(SemanticTreeNode rootNode) - { - var usage = new Dictionary(StringComparer.Ordinal); - CountBranchUsages(rootNode, usage); - return usage; - } - - private static void CountBranchUsages(SemanticTreeNode node, Dictionary usage) - { - if (node is not SemanticBranchNode branch) - { - return; - } - - usage.TryGetValue(branch.SemanticId, out var count); - usage[branch.SemanticId] = count + 1; - - foreach (var child in branch.Children) - { - CountBranchUsages(child, usage); - } + return new JsonSchemaBuilder().Type(type); } - private static bool ShouldUseReference(SemanticBranchNode branch, Dictionary branchUsage) - => branchUsage.TryGetValue(branch.SemanticId, out var usageCount) && usageCount > 1; - private static bool IsArrayCardinality(Cardinality cardinality) => cardinality is Cardinality.ZeroToMany or Cardinality.OneToMany; diff --git a/source/AAS.TwinEngine.Plugin.TestPlugin/Program.cs b/source/AAS.TwinEngine.Plugin.TestPlugin/Program.cs index 32168dff..5db632e4 100644 --- a/source/AAS.TwinEngine.Plugin.TestPlugin/Program.cs +++ b/source/AAS.TwinEngine.Plugin.TestPlugin/Program.cs @@ -26,6 +26,10 @@ public static async Task Main(string[] args) builder.Services.AddHealthChecks().AddCheck("mock_data"); builder.Services.AddControllers(); + builder.Services.Configure(options => + { + options.MaxValidationDepth = null; + }); builder.Services.AddEndpointsApiExplorer(); builder.Services.AddOpenApiDocument(settings => From 359d4d831f3012f2c2be9d8e41ff15837decf762 Mon Sep 17 00:00:00 2001 From: Hardi Shah Date: Wed, 29 Apr 2026 14:28:19 +0530 Subject: [PATCH 65/71] Add plugin schema fallback & improve JSON schema handling Introduced a fallback to Draft-07 JSON Schema when plugins reject Draft 2020-12 schemas, via ILegacySchemaRetryHandler. Refactored schema generation and validation for better draft handling, normalization, and error reporting. Updated tests and expected data for new schema logic. Enhanced mock-submodel-data.json with more realistic phone entries. Removed unused MaxValidationDepth config from Program.cs. Also improved code style and test project references. --- ...S.TwinEngine.DataEngine.ModuleTests.csproj | 10 +- ...AAS.TwinEngine.DataEngine.UnitTests.csproj | 14 +- .../CleanArchitectureTests.cs | 8 +- .../Helper/JsonSchemaGeneratorTests.cs | 464 ++++++++-------- .../Helper/JsonSchemaValidatorTests.cs | 520 ++++++++++-------- .../LegacyV1/LegacySchemaRetryHandlerTests.cs | 146 +++++ .../Services/PluginDataHandlerTests.cs | 208 ++++++- .../Services/PluginDataProviderTests.cs | 12 +- .../PluginSchemaRejectionException.cs | 7 + .../Plugin/Helper/IJsonSchemaGenerator.cs | 10 + .../Helper/ILegacySchemaRetryHandler.cs | 19 + .../Helper/JsonSchemaDraft202012Generator.cs | 34 ++ .../Helper/JsonSchemaGenerator.cs | 154 ------ .../Helper/JsonSchemaGeneratorBase.cs | 112 ++++ .../Helper/JsonSchemaValidator.cs | 251 +++++---- .../LegacyDraft7JsonSchemaGenerator.cs | 32 ++ .../LegacyV1/LegacySchemaRetryHandler.cs | 37 ++ .../Services/PluginDataHandler.cs | 21 +- .../Services/PluginDataProvider.cs | 15 +- source/AAS.TwinEngine.DataEngine/Program.cs | 5 +- ...astructureDependencyInjectionExtensions.cs | 5 + ...ntactInfo_ContactInformation_Expected.json | 236 +++++--- .../GetSubmodel_ContactInfo_Expected.json | 472 +++++++++++----- ...nEngine.Plugin.TestPlugin.UnitTests.csproj | 10 +- .../Services/JsonSchemaParserTests.cs | 483 ++++++++-------- .../Services/JsonSchemaValidatorTests.cs | 484 ++-------------- .../CleanArchitectureTests.cs | 8 +- .../AAS.TwinEngine.Plugin.TestPlugin.csproj | 2 +- .../Api/Submodel/Services/JsonSchemaParser.cs | 282 +++------- .../Submodel/Services/JsonSchemaValidator.cs | 142 ++--- .../Api/Submodel/SubmodelController.cs | 3 +- .../Data/mock-submodel-data.json | 27 + .../Example/plugin/mock-submodel-data.json | 27 + .../Program.cs | 4 - 34 files changed, 2301 insertions(+), 1963 deletions(-) create mode 100644 source/AAS.TwinEngine.DataEngine.UnitTests/Infrastructure/Providers/PluginDataProvider/Helper/LegacyV1/LegacySchemaRetryHandlerTests.cs create mode 100644 source/AAS.TwinEngine.DataEngine/ApplicationLogic/Exceptions/Infrastructure/PluginSchemaRejectionException.cs create mode 100644 source/AAS.TwinEngine.DataEngine/ApplicationLogic/Services/Plugin/Helper/IJsonSchemaGenerator.cs create mode 100644 source/AAS.TwinEngine.DataEngine/ApplicationLogic/Services/Plugin/Helper/ILegacySchemaRetryHandler.cs create mode 100644 source/AAS.TwinEngine.DataEngine/Infrastructure/Providers/PluginDataProvider/Helper/JsonSchemaDraft202012Generator.cs delete mode 100644 source/AAS.TwinEngine.DataEngine/Infrastructure/Providers/PluginDataProvider/Helper/JsonSchemaGenerator.cs create mode 100644 source/AAS.TwinEngine.DataEngine/Infrastructure/Providers/PluginDataProvider/Helper/JsonSchemaGeneratorBase.cs create mode 100644 source/AAS.TwinEngine.DataEngine/Infrastructure/Providers/PluginDataProvider/Helper/LegacyV1/LegacyDraft7JsonSchemaGenerator.cs create mode 100644 source/AAS.TwinEngine.DataEngine/Infrastructure/Providers/PluginDataProvider/Helper/LegacyV1/LegacySchemaRetryHandler.cs diff --git a/source/AAS.TwinEngine.DataEngine.ModuleTests/AAS.TwinEngine.DataEngine.ModuleTests.csproj b/source/AAS.TwinEngine.DataEngine.ModuleTests/AAS.TwinEngine.DataEngine.ModuleTests.csproj index 5e6f690b..74482347 100644 --- a/source/AAS.TwinEngine.DataEngine.ModuleTests/AAS.TwinEngine.DataEngine.ModuleTests.csproj +++ b/source/AAS.TwinEngine.DataEngine.ModuleTests/AAS.TwinEngine.DataEngine.ModuleTests.csproj @@ -10,18 +10,12 @@ - - all - runtime; build; native; contentfiles; analyzers; buildtransitive - + - - all - runtime; build; native; contentfiles; analyzers; buildtransitive - + diff --git a/source/AAS.TwinEngine.DataEngine.UnitTests/AAS.TwinEngine.DataEngine.UnitTests.csproj b/source/AAS.TwinEngine.DataEngine.UnitTests/AAS.TwinEngine.DataEngine.UnitTests.csproj index e06a0d83..fc375e94 100644 --- a/source/AAS.TwinEngine.DataEngine.UnitTests/AAS.TwinEngine.DataEngine.UnitTests.csproj +++ b/source/AAS.TwinEngine.DataEngine.UnitTests/AAS.TwinEngine.DataEngine.UnitTests.csproj @@ -15,18 +15,12 @@ - - all - runtime; build; native; contentfiles; analyzers; buildtransitive - + - - all - runtime; build; native; contentfiles; analyzers; buildtransitive - + @@ -40,5 +34,9 @@ + + + + diff --git a/source/AAS.TwinEngine.DataEngine.UnitTests/CleanArchitectureTests.cs b/source/AAS.TwinEngine.DataEngine.UnitTests/CleanArchitectureTests.cs index 90475140..1a13586e 100644 --- a/source/AAS.TwinEngine.DataEngine.UnitTests/CleanArchitectureTests.cs +++ b/source/AAS.TwinEngine.DataEngine.UnitTests/CleanArchitectureTests.cs @@ -15,10 +15,10 @@ public class CleanArchitectureTests private const string BaseNamespace = "AAS.TwinEngine.DataEngine"; private readonly Architecture _architecture = new ArchLoader().LoadAssemblies(System.Reflection.Assembly.Load(BaseNamespace)).Build(); - private readonly IObjectProvider _apiLayer = Types().That().ResideInNamespaceMatching($"{BaseNamespace}\\.Api($|\\..*)").As("Api"); - private readonly IObjectProvider _applicationLogicLayer = Types().That().ResideInNamespaceMatching($"{BaseNamespace}\\.ApplicationLogic($|\\..*)").As("ApplicationLogic"); - private readonly IObjectProvider _domainModelLayer = Types().That().ResideInNamespaceMatching($"{BaseNamespace}\\.DomainModel.*").As("DomainModel"); - private readonly IObjectProvider _infrastructureLayer = Types().That().ResideInNamespaceMatching($"{BaseNamespace}\\.Infrastructure($|\\..*)").As("Infrastructure"); + private readonly IObjectProvider _apiLayer = Types().That().ResideInNamespaceMatching($"{BaseNamespace}.Api.*").As("Api"); + private readonly IObjectProvider _applicationLogicLayer = Types().That().ResideInNamespaceMatching($"{BaseNamespace}.ApplicationLogic.*").As("ApplicationLogic"); + private readonly IObjectProvider _domainModelLayer = Types().That().ResideInNamespaceMatching($"{BaseNamespace}.DomainModel*").As("DomainModel"); + private readonly IObjectProvider _infrastructureLayer = Types().That().ResideInNamespaceMatching($"{BaseNamespace}.Infrastructure.*").As("Infrastructure"); [Fact] public void DomainModelShallNotHaveExternalDependencies() diff --git a/source/AAS.TwinEngine.DataEngine.UnitTests/Infrastructure/Providers/PluginDataProvider/Helper/JsonSchemaGeneratorTests.cs b/source/AAS.TwinEngine.DataEngine.UnitTests/Infrastructure/Providers/PluginDataProvider/Helper/JsonSchemaGeneratorTests.cs index 8c878319..60d2863d 100644 --- a/source/AAS.TwinEngine.DataEngine.UnitTests/Infrastructure/Providers/PluginDataProvider/Helper/JsonSchemaGeneratorTests.cs +++ b/source/AAS.TwinEngine.DataEngine.UnitTests/Infrastructure/Providers/PluginDataProvider/Helper/JsonSchemaGeneratorTests.cs @@ -1,330 +1,314 @@ using System.Text.Json; -using System.Text.Json.Nodes; using AAS.TwinEngine.DataEngine.ApplicationLogic.Exceptions.Application; +using AAS.TwinEngine.DataEngine.ApplicationLogic.Services.Plugin.Helper; using AAS.TwinEngine.DataEngine.DomainModel.SubmodelRepository; using AAS.TwinEngine.DataEngine.Infrastructure.Providers.PluginDataProvider.Helper; +using AAS.TwinEngine.DataEngine.Infrastructure.Providers.PluginDataProvider.Helper.LegacyV1; using Json.Schema; namespace AAS.TwinEngine.DataEngine.UnitTests.Infrastructure.Providers.PluginDataProvider.Helper; -public class JsonSchemaGeneratorTests +public class JsonSchemaDraft202012GeneratorTests { + private readonly IJsonSchemaGenerator _sut = new JsonSchemaDraft202012Generator(); + [Fact] - public void ConvertToJsonSchema_LeafNode_ReturnSchema() + public void Generate_WhenLeafNode_ReturnsDraft202012Schema() { const string SemanticId = "http://example.com/idta/digital-nameplate/contact-list/Name"; var leaf = new SemanticLeafNode(SemanticId, "", DataType.String, Cardinality.One); - var result = JsonSchemaGenerator.ConvertToJsonSchema(leaf); + var schema = _sut.Generate(leaf); - var json = ToJson(result); - Assert.Equal("object", json["type"]!.ToString()); - var props = json["properties"]!; - Assert.NotNull(props[SemanticId]); - var leafSchema = props[SemanticId]; - Assert.Equal("string", leafSchema!["type"]!.ToString()); + var el = ToElement(schema); + Assert.Equal("https://json-schema.org/draft/2020-12/schema", el.GetProperty("$schema").GetString()); + Assert.Equal("object", el.GetProperty("type").GetString()); + Assert.True(el.GetProperty("properties").TryGetProperty(SemanticId, out var leafEl)); + Assert.Equal("string", leafEl.GetProperty("type").GetString()); } [Fact] - public void ConvertToJsonSchema_OptionalLeafNode_IsNotRequired() + public void Generate_WhenNestedBranches_UsesDollarDefsAndDraft202012Refs() { - const string SemanticId = "http://example.com/optional"; - - var leaf = new SemanticLeafNode(SemanticId, "", DataType.String, Cardinality.ZeroToOne); - - var schema = JsonSchemaGenerator.ConvertToJsonSchema(leaf); - var json = ToJson(schema); - - var props = json["properties"]!; - Assert.NotNull(props[SemanticId]); - Assert.Null(json["required"]); - } - - [Fact] - public void ConvertToJsonSchema_BranchNodeWithOneCardinality_ReturnsObjectSchema() - { - const string SemanticId = "http://example.com/idta/digital-nameplate/contact-list"; - const string NameId = "http://example.com/idta/digital-nameplate/contact-list/Name"; - const string WeightId = "http://example.com/idta/digital-nameplate/contact-list/Weight"; - var branch = new SemanticBranchNode(SemanticId, Cardinality.One); + const string RootId = "http://example.com/idta/root"; + const string BranchId = "http://example.com/idta/branch"; + const string NameId = "http://example.com/idta/name"; + var root = new SemanticBranchNode(RootId, Cardinality.One); + var branch = new SemanticBranchNode(BranchId, Cardinality.One); branch.AddChild(new SemanticLeafNode(NameId, "", DataType.String, Cardinality.One)); - branch.AddChild(new SemanticLeafNode(WeightId, null!, DataType.Integer, Cardinality.ZeroToOne)); - - var result = JsonSchemaGenerator.ConvertToJsonSchema(branch); - - var json = ToJson(result); - Assert.Equal("object", json["type"]!.ToString()); - var rootProps = json["properties"]!; - var branchSchema = rootProps[SemanticId]!; - Assert.Equal("object", branchSchema["type"]!.ToString()); - var branchProps = branchSchema["properties"]!; - Assert.NotNull(branchProps[NameId]); - Assert.NotNull(branchProps[WeightId]); - Assert.Equal("string", branchProps[NameId]!["type"]!.ToString()); - Assert.Equal("integer", branchProps[WeightId]!["type"]!.ToString()); - var required = branchSchema["required"]!.AsArray(); - Assert.Contains(NameId, required.Select(x => x!.ToString())); - Assert.DoesNotContain(WeightId, required.Select(x => x!.ToString())); - } - - [Fact] - public void ConvertToJsonSchema_BranchNodeWithZeroToManyCardinality_ReturnsArraySchema() - { - const string SemanticId = "http://example.com/idta/digital-nameplate/contact-list"; - const string NameId = "http://example.com/idta/digital-nameplate/contact-list/Name"; - var branchNode = new SemanticBranchNode(SemanticId, Cardinality.ZeroToMany); - branchNode.AddChild(new SemanticLeafNode(NameId, "", DataType.String, Cardinality.One)); - - var result = JsonSchemaGenerator.ConvertToJsonSchema(branchNode); - - var json = ToJson(result); - Assert.Equal("object", json["type"]!.ToString()); - var rootProps = json["properties"]!; - var arraySchema = rootProps[SemanticId]!; - Assert.Equal("array", arraySchema["type"]!.ToString()); - var items = arraySchema["items"]!; - var props = items["properties"]!; - Assert.True(props[NameId] != null); - Assert.Equal("string", props[NameId]!["type"]!.ToString()); - var required = items["required"]!.AsArray(); - Assert.Contains(NameId, required.Select(x => x!.ToString())); + root.AddChild(branch); + + var schema = _sut.Generate(root); + + var el = ToElement(schema); + var branchRefEl = el.GetProperty("properties").GetProperty(RootId) + .GetProperty("properties").GetProperty(BranchId); + Assert.Equal($"#/$defs/{BranchId}", branchRefEl.GetProperty("$ref").GetString()); + Assert.True(el.TryGetProperty("$defs", out var defsEl)); + Assert.False(el.TryGetProperty("definitions", out _)); + Assert.True(defsEl.TryGetProperty(BranchId, out var branchDef)); + Assert.Equal("object", branchDef.GetProperty("type").GetString()); + Assert.Equal("string", branchDef.GetProperty("properties").GetProperty(NameId).GetProperty("type").GetString()); } [Fact] - public void ConvertToJsonSchema_NestedBranchNode_UsesDefs() + public void Generate_WhenBranchWithZeroToManyCardinality_WrapsChildrenInItems() { - const string RootSemanticId = "http://example.com/idta/digital-nameplate"; - const string BranchSemanticId = "http://example.com/idta/digital-nameplate/contact-list"; - const string NameId = "http://example.com/idta/digital-nameplate/contact-list/Name"; - var root = new SemanticBranchNode(RootSemanticId, Cardinality.Unknown); - var childBranch = new SemanticBranchNode(BranchSemanticId, Cardinality.One); - childBranch.AddChild(new SemanticLeafNode(NameId, "", DataType.String, Cardinality.One)); - root.AddChild(childBranch); - - var schema = JsonSchemaGenerator.ConvertToJsonSchema(root); - var json = ToJson(schema); - - Assert.Equal("object", json["type"]!.ToString()); - var rootProps = json["properties"]![RootSemanticId]!["properties"]!; - Assert.Equal($"#/$defs/{BranchSemanticId}", rootProps[BranchSemanticId]!["$ref"]!.ToString()); - var defs = json["$defs"]!; - var branchDef = defs[BranchSemanticId]!; - Assert.Equal("object", branchDef["type"]!.ToString()); - var branchProps = branchDef["properties"]!; - Assert.NotNull(branchProps[NameId]); - var required = branchDef["required"]!.AsArray(); - Assert.Contains(NameId, required.Select(x => x!.ToString())); + const string BranchId = "http://example.com/list"; + const string ChildId = "http://example.com/list/item"; + var branch = new SemanticBranchNode(BranchId, Cardinality.ZeroToMany); + branch.AddChild(new SemanticLeafNode(ChildId, "", DataType.String, Cardinality.One)); + + var schema = _sut.Generate(branch); + + var el = ToElement(schema); + var branchEl = el.GetProperty("properties").GetProperty(BranchId); + Assert.Equal("array", branchEl.GetProperty("type").GetString()); + Assert.True(branchEl.TryGetProperty("items", out var itemsEl)); + Assert.Equal("object", itemsEl.GetProperty("type").GetString()); + Assert.True(itemsEl.TryGetProperty("properties", out var itemPropsEl)); + Assert.True(itemPropsEl.TryGetProperty(ChildId, out _)); + Assert.False(branchEl.TryGetProperty("properties", out _)); } [Fact] - public void ConvertToJsonSchema_DataTypeMapping_ConvertsCorrectly() + public void Generate_WhenLeafNodeIsOptional_DoesNotMarkPropertyAsRequired() { - var branch = new SemanticBranchNode("http://example.com/schema/data-types", Cardinality.One); - - branch.AddChild(new SemanticLeafNode("string", "", DataType.String, Cardinality.One)); - branch.AddChild(new SemanticLeafNode("integer", null!, DataType.Integer, Cardinality.One)); - branch.AddChild(new SemanticLeafNode("number", null!, DataType.Number, Cardinality.One)); - branch.AddChild(new SemanticLeafNode("boolean", null!, DataType.Boolean, Cardinality.One)); - branch.AddChild(new SemanticLeafNode("unknown", null!, DataType.Unknown, Cardinality.One)); - - var schema = JsonSchemaGenerator.ConvertToJsonSchema(branch); - var json = ToJson(schema); + const string SemanticId = "http://example.com/optional"; + var leaf = new SemanticLeafNode(SemanticId, "", DataType.String, Cardinality.ZeroToOne); - var rootProps = json["properties"]!["http://example.com/schema/data-types"]!["properties"]!; + var schema = _sut.Generate(leaf); + var el = ToElement(schema); - Assert.Equal("string", rootProps["string"]!["type"]!.ToString()); - Assert.Equal("integer", rootProps["integer"]!["type"]!.ToString()); - Assert.Equal("number", rootProps["number"]!["type"]!.ToString()); - Assert.Equal("boolean", rootProps["boolean"]!["type"]!.ToString()); - Assert.Equal("string", rootProps["unknown"]!["type"]!.ToString()); + Assert.True(el.GetProperty("properties").TryGetProperty(SemanticId, out _)); + Assert.False(el.TryGetProperty("required", out _)); } [Fact] - public void ConvertToJsonSchema_ArraySchema_UsesItemsKeyword() + public void Generate_WhenBranchHasMixedCardinality_SetsRequiredOnlyForMandatoryChildren() { - var branch = new SemanticBranchNode("root", Cardinality.ZeroToMany); - branch.AddChild(new SemanticLeafNode("child", "", DataType.String, Cardinality.One)); - - var schema = JsonSchemaGenerator.ConvertToJsonSchema(branch); - var json = ToJson(schema); - - var rootProps = json["properties"]!["root"]!; - - Assert.Equal("array", rootProps["type"]!.ToString()); - Assert.NotNull(rootProps["items"]); + const string BranchId = "http://example.com/branch"; + const string RequiredChildId = "http://example.com/branch/required"; + const string OptionalChildId = "http://example.com/branch/optional"; + var branch = new SemanticBranchNode(BranchId, Cardinality.One); + branch.AddChild(new SemanticLeafNode(RequiredChildId, "", DataType.String, Cardinality.One)); + branch.AddChild(new SemanticLeafNode(OptionalChildId, "", DataType.String, Cardinality.ZeroToOne)); + + var schema = _sut.Generate(branch); + + var branchEl = ToElement(schema).GetProperty("properties").GetProperty(BranchId); + var required = branchEl.GetProperty("required").EnumerateArray().Select(e => e.GetString()).ToList(); + Assert.Contains(RequiredChildId, required); + Assert.DoesNotContain(OptionalChildId, required); } - [Fact] - public void ConvertToJsonSchema_LeafWithOneToManyCardinality_UsesPrimitiveType() + [Theory] + [InlineData(DataType.String, "string")] + [InlineData(DataType.Integer, "integer")] + [InlineData(DataType.Number, "number")] + [InlineData(DataType.Boolean, "boolean")] + [InlineData(DataType.Unknown, "string")] + public void Generate_WhenLeafWithDataType_MapsToCorrectJsonType(DataType dataType, string expectedJsonType) { - var branch = new SemanticBranchNode("root", Cardinality.One); - branch.AddChild(new SemanticLeafNode("tags", "", DataType.String, Cardinality.OneToMany)); + const string SemanticId = "http://example.com/leaf"; + var leaf = new SemanticLeafNode(SemanticId, null!, dataType, Cardinality.One); - var schema = JsonSchemaGenerator.ConvertToJsonSchema(branch); - var json = ToJson(schema); + var schema = _sut.Generate(leaf); - var leaf = json["properties"]!["root"]!["properties"]!["tags"]!; - Assert.Equal("string", leaf["type"]!.ToString()); - Assert.Null(leaf["items"]); + var leafEl = ToElement(schema).GetProperty("properties").GetProperty(SemanticId); + Assert.Equal(expectedJsonType, leafEl.GetProperty("type").GetString()); } [Fact] - public void ConvertToJsonSchema_UnsupportedNode_ThrowsException() + public void Generate_WhenUnsupportedNode_ThrowsInternalDataProcessingException() { var unsupportedNode = new UnsupportedSemanticNode("unsupported", Cardinality.One); - Assert.Throws(() => - JsonSchemaGenerator.ConvertToJsonSchema(unsupportedNode)); + Assert.Throws(() => _sut.Generate(unsupportedNode)); } - [Fact] - public void ConvertToJsonSchema_EmptyBranchNode_ReturnsEmptyObjectSchema() + private static JsonElement ToElement(JsonSchema schema) { - var branch = new SemanticBranchNode("root", Cardinality.One); + var json = JsonSerializer.Serialize(schema); + using var doc = JsonDocument.Parse(json); + return doc.RootElement.Clone(); + } - var schema = JsonSchemaGenerator.ConvertToJsonSchema(branch); - var json = ToJson(schema); + private sealed class UnsupportedSemanticNode(string semanticId, Cardinality cardinality) : SemanticTreeNode(semanticId, cardinality); +} - var rootProps = json["properties"]!["root"]!; - Assert.Equal("object", rootProps["type"]!.ToString()); - Assert.NotNull(rootProps["properties"]); - Assert.Empty(rootProps["properties"]!.AsObject()); - } +#pragma warning disable CS0618 +public class LegacyDraft7JsonSchemaGeneratorTests +{ + private readonly IJsonSchemaGenerator _sut = new LegacyDraft7JsonSchemaGenerator(); [Fact] - public void ConvertToJsonSchema_ArrayWithOptionalChildren_DoesNotContainRequired() + public void Generate_WhenLeafNode_ReturnsDraft7Schema() { - var branch = new SemanticBranchNode("root", Cardinality.ZeroToMany); - branch.AddChild(new SemanticLeafNode("child", "", DataType.String, Cardinality.ZeroToOne)); + const string SemanticId = "http://example.com/idta/digital-nameplate/contact-list/Name"; + var leaf = new SemanticLeafNode(SemanticId, "", DataType.String, Cardinality.One); - var schema = JsonSchemaGenerator.ConvertToJsonSchema(branch); - var json = ToJson(schema); + var schema = _sut.Generate(leaf); - var items = json["properties"]!["root"]!["items"]!; - Assert.Null(items["required"]); + var el = ToElement(schema); + Assert.Equal("http://json-schema.org/draft-07/schema#", el.GetProperty("$schema").GetString()); + Assert.Equal("object", el.GetProperty("type").GetString()); + Assert.True(el.GetProperty("properties").TryGetProperty(SemanticId, out var leafEl)); + Assert.Equal("string", leafEl.GetProperty("type").GetString()); } [Fact] - public void ConvertToJsonSchema_ReusedBranch_UsesSingleDefinition() + public void Generate_WhenNestedBranches_UsesDefinitionsAndDraft7Refs() { - var shared = new SemanticBranchNode("shared", Cardinality.One); - shared.AddChild(new SemanticLeafNode("name", "", DataType.String, Cardinality.One)); - var root = new SemanticBranchNode("root", Cardinality.One); - root.AddChild(shared); - root.AddChild(shared); - - var schema = JsonSchemaGenerator.ConvertToJsonSchema(root); - - var json = ToJson(schema); - var defs = json["$defs"]!; - Assert.Single(defs.AsObject()); - var rootProps = json["properties"]!["root"]!["properties"]!; - Assert.Equal("#/$defs/shared", rootProps["shared"]!["$ref"]!.ToString()); + const string RootId = "http://example.com/idta/root"; + const string BranchId = "http://example.com/idta/branch"; + const string NameId = "http://example.com/idta/name"; + var root = new SemanticBranchNode(RootId, Cardinality.One); + var branch = new SemanticBranchNode(BranchId, Cardinality.One); + branch.AddChild(new SemanticLeafNode(NameId, "", DataType.String, Cardinality.One)); + root.AddChild(branch); + + var schema = _sut.Generate(root); + + var el = ToElement(schema); + var branchRefEl = el.GetProperty("properties").GetProperty(RootId) + .GetProperty("properties").GetProperty(BranchId); + Assert.Equal($"#/definitions/{BranchId}", branchRefEl.GetProperty("$ref").GetString()); + Assert.True(el.TryGetProperty("definitions", out var defsEl)); + Assert.False(el.TryGetProperty("$defs", out _)); + Assert.True(defsEl.TryGetProperty(BranchId, out var branchDef)); + Assert.Equal("object", branchDef.GetProperty("type").GetString()); + Assert.Equal("string", branchDef.GetProperty("properties").GetProperty(NameId).GetProperty("type").GetString()); } [Fact] - public void ConvertToJsonSchema_DeepNestedStructure_WorksCorrectly() + public void Generate_WhenBranchWithZeroToManyCardinality_ReturnsArrayWithDirectProperties() { - var level3 = new SemanticBranchNode("level3", Cardinality.One); - level3.AddChild(new SemanticLeafNode("leaf", "", DataType.String, Cardinality.One)); - var level2 = new SemanticBranchNode("level2", Cardinality.One); - level2.AddChild(level3); - var level1 = new SemanticBranchNode("level1", Cardinality.One); - level1.AddChild(level2); - - var schema = JsonSchemaGenerator.ConvertToJsonSchema(level1); - - var json = ToJson(schema); - var defs = json["$defs"]!; - Assert.Equal("#/$defs/level2", json["properties"]!["level1"]!["properties"]!["level2"]!["$ref"]!.ToString()); - var level2Schema = defs["level2"]!; - Assert.Equal("object", level2Schema["type"]!.ToString()); - Assert.Equal("#/$defs/level3", level2Schema["properties"]!["level3"]!["$ref"]!.ToString()); - var level3Schema = defs["level3"]!; - Assert.Equal("object", level3Schema["type"]!.ToString()); - Assert.Equal("string", level3Schema["properties"]!["leaf"]!["type"]!.ToString()); + const string BranchId = "http://example.com/list"; + const string ChildId = "http://example.com/list/item"; + var branch = new SemanticBranchNode(BranchId, Cardinality.ZeroToMany); + branch.AddChild(new SemanticLeafNode(ChildId, "", DataType.String, Cardinality.One)); + + var schema = _sut.Generate(branch); + + var el = ToElement(schema); + var branchEl = el.GetProperty("properties").GetProperty(BranchId); + Assert.Equal("array", branchEl.GetProperty("type").GetString()); + Assert.False(branchEl.TryGetProperty("items", out _)); + Assert.True(branchEl.TryGetProperty("properties", out var directPropsEl)); + Assert.True(directPropsEl.TryGetProperty(ChildId, out _)); } [Fact] - public void ConvertToJsonSchema_MixedCardinality_WorksCorrectly() + public void Generate_WhenLeafNodeIsOptional_DoesNotMarkPropertyAsRequired() { - var root = new SemanticBranchNode("root", Cardinality.One); - var objectChild = new SemanticBranchNode("objectChild", Cardinality.One); - objectChild.AddChild(new SemanticLeafNode("name", "", DataType.String, Cardinality.One)); - var arrayChild = new SemanticBranchNode("arrayChild", Cardinality.ZeroToMany); - arrayChild.AddChild(new SemanticLeafNode("value", "", DataType.Number, Cardinality.One)); - root.AddChild(objectChild); - root.AddChild(arrayChild); - - var schema = JsonSchemaGenerator.ConvertToJsonSchema(root); - - var json = ToJson(schema); - var props = json["properties"]!["root"]!["properties"]!; - Assert.Equal("#/$defs/objectChild", props["objectChild"]!["$ref"]!.ToString()); - Assert.Equal("#/$defs/arrayChild", props["arrayChild"]!["$ref"]!.ToString()); - var defs = json["$defs"]!; - Assert.Equal("object", defs["objectChild"]!["type"]!.ToString()); - Assert.Equal("array", defs["arrayChild"]!["type"]!.ToString()); - Assert.NotNull(defs["arrayChild"]!["items"]); + const string SemanticId = "http://example.com/optional"; + var leaf = new SemanticLeafNode(SemanticId, "", DataType.String, Cardinality.ZeroToOne); + + var schema = _sut.Generate(leaf); + var el = ToElement(schema); + + Assert.True(el.GetProperty("properties").TryGetProperty(SemanticId, out _)); + Assert.False(el.TryGetProperty("required", out _)); } [Fact] - public void ConvertToJsonSchema_SingleNestedBranch_UsesDefs() + public void Generate_WhenBranchHasMixedCardinality_SetsRequiredOnlyForMandatoryChildren() { - var root = new SemanticBranchNode("root", Cardinality.One); - var child = new SemanticBranchNode("child", Cardinality.One); - child.AddChild(new SemanticLeafNode("name", "", DataType.String, Cardinality.One)); - root.AddChild(child); - - var schema = JsonSchemaGenerator.ConvertToJsonSchema(root); - var json = ToJson(schema); - - Assert.Equal("#/$defs/child", json["properties"]!["root"]!["properties"]!["child"]!["$ref"]!.ToString()); - var childDef = json["$defs"]!["child"]!; - Assert.Equal("object", childDef["type"]!.ToString()); - Assert.NotNull(childDef["properties"]!["name"]); - var required = childDef["required"]!.AsArray(); - Assert.Contains("name", required.Select(x => x!.ToString())); + const string BranchId = "http://example.com/branch"; + const string RequiredChildId = "http://example.com/branch/required"; + const string OptionalChildId = "http://example.com/branch/optional"; + var branch = new SemanticBranchNode(BranchId, Cardinality.One); + branch.AddChild(new SemanticLeafNode(RequiredChildId, "", DataType.String, Cardinality.One)); + branch.AddChild(new SemanticLeafNode(OptionalChildId, "", DataType.String, Cardinality.ZeroToOne)); + + var schema = _sut.Generate(branch); + + var branchEl = ToElement(schema).GetProperty("properties").GetProperty(BranchId); + var required = branchEl.GetProperty("required").EnumerateArray().Select(e => e.GetString()).ToList(); + Assert.Contains(RequiredChildId, required); + Assert.DoesNotContain(OptionalChildId, required); } - [Fact] - public void ConvertToJsonSchema_UnknownCardinality_TreatedAsObject() + [Theory] + [InlineData(DataType.String, "string")] + [InlineData(DataType.Integer, "integer")] + [InlineData(DataType.Number, "number")] + [InlineData(DataType.Boolean, "boolean")] + [InlineData(DataType.Unknown, "string")] + public void Generate_WhenLeafWithDataType_MapsToCorrectJsonType(DataType dataType, string expectedJsonType) { - var branch = new SemanticBranchNode("root", Cardinality.Unknown); - branch.AddChild(new SemanticLeafNode("name", "", DataType.String, Cardinality.One)); + const string SemanticId = "http://example.com/leaf"; + var leaf = new SemanticLeafNode(SemanticId, null!, dataType, Cardinality.One); - var schema = JsonSchemaGenerator.ConvertToJsonSchema(branch); + var schema = _sut.Generate(leaf); - var json = ToJson(schema); - var root = json["properties"]!["root"]!; - Assert.Equal("object", root["type"]!.ToString()); + var leafEl = ToElement(schema).GetProperty("properties").GetProperty(SemanticId); + Assert.Equal(expectedJsonType, leafEl.GetProperty("type").GetString()); } [Fact] - public void ConvertToJsonSchema_MultipleRequiredFields_AllIncluded() + public void Generate_WhenUnsupportedNode_ThrowsInternalDataProcessingException() { - var branch = new SemanticBranchNode("root", Cardinality.One); - branch.AddChild(new SemanticLeafNode("a", "", DataType.String, Cardinality.One)); - branch.AddChild(new SemanticLeafNode("b", "", DataType.String, Cardinality.One)); + var unsupportedNode = new UnsupportedSemanticNode("unsupported", Cardinality.One); - var schema = JsonSchemaGenerator.ConvertToJsonSchema(branch); + Assert.Throws(() => _sut.Generate(unsupportedNode)); + } - var json = ToJson(schema); - var required = json["properties"]!["root"]!["required"]!.AsArray(); - Assert.Contains("a", required.Select(x => x!.ToString())); - Assert.Contains("b", required.Select(x => x!.ToString())); + [Fact] + public void Generate_WhenBranchContainsUnsupportedChild_ThrowsInternalDataProcessingException() + { + var root = new SemanticBranchNode("http://example.com/root", Cardinality.One); + root.AddChild(new UnsupportedSemanticNode("http://example.com/unsupported", Cardinality.One)); + + Assert.Throws(() => _sut.Generate(root)); } - private sealed class UnsupportedSemanticNode(string semanticId, Cardinality cardinality) - : SemanticTreeNode(semanticId, cardinality); + [Fact] + public void Generate_WhenNestedBranchesReuseSemanticId_CreatesSingleDefinitionAndSharedRefs() + { + const string RootId = "http://example.com/root"; + const string ParentOneId = "http://example.com/parent/one"; + const string ParentTwoId = "http://example.com/parent/two"; + const string SharedBranchId = "http://example.com/shared"; + const string LeafId = "http://example.com/shared/leaf"; + var root = new SemanticBranchNode(RootId, Cardinality.One); + var parentOne = new SemanticBranchNode(ParentOneId, Cardinality.One); + var parentTwo = new SemanticBranchNode(ParentTwoId, Cardinality.One); + var sharedBranchTemplate = new SemanticBranchNode(SharedBranchId, Cardinality.One); + sharedBranchTemplate.AddChild(new SemanticLeafNode(LeafId, string.Empty, DataType.String, Cardinality.One)); + + parentOne.AddChild(sharedBranchTemplate); + + var sharedBranchReuse = new SemanticBranchNode(SharedBranchId, Cardinality.One); + sharedBranchReuse.AddChild(new SemanticLeafNode(LeafId, string.Empty, DataType.String, Cardinality.One)); + parentTwo.AddChild(sharedBranchReuse); + + root.AddChild(parentOne); + root.AddChild(parentTwo); + + var schema = _sut.Generate(root); + + var rootElement = ToElement(schema); + var definitions = rootElement.GetProperty("definitions"); + var parentOneShared = definitions.GetProperty(ParentOneId).GetProperty("properties").GetProperty(SharedBranchId); + var parentTwoShared = definitions.GetProperty(ParentTwoId).GetProperty("properties").GetProperty(SharedBranchId); + + Assert.Equal($"#/definitions/{SharedBranchId}", parentOneShared.GetProperty("$ref").GetString()); + Assert.Equal($"#/definitions/{SharedBranchId}", parentTwoShared.GetProperty("$ref").GetString()); + Assert.True(definitions.TryGetProperty(SharedBranchId, out var sharedDefinition)); + Assert.Equal("string", sharedDefinition.GetProperty("properties").GetProperty(LeafId).GetProperty("type").GetString()); + } - private static readonly JsonSerializerOptions SerializerOption = new() + private static JsonElement ToElement(JsonSchema schema) { - WriteIndented = false - }; + var json = JsonSerializer.Serialize(schema); + using var doc = JsonDocument.Parse(json); + return doc.RootElement.Clone(); + } - private static JsonNode ToJson(JsonSchema schema) - => JsonSerializer.SerializeToNode(schema, SerializerOption)!; + private sealed class UnsupportedSemanticNode(string semanticId, Cardinality cardinality) : SemanticTreeNode(semanticId, cardinality); } +#pragma warning restore CS0618 diff --git a/source/AAS.TwinEngine.DataEngine.UnitTests/Infrastructure/Providers/PluginDataProvider/Helper/JsonSchemaValidatorTests.cs b/source/AAS.TwinEngine.DataEngine.UnitTests/Infrastructure/Providers/PluginDataProvider/Helper/JsonSchemaValidatorTests.cs index 4936f6b1..6eefcf1a 100644 --- a/source/AAS.TwinEngine.DataEngine.UnitTests/Infrastructure/Providers/PluginDataProvider/Helper/JsonSchemaValidatorTests.cs +++ b/source/AAS.TwinEngine.DataEngine.UnitTests/Infrastructure/Providers/PluginDataProvider/Helper/JsonSchemaValidatorTests.cs @@ -1,5 +1,4 @@ using AAS.TwinEngine.DataEngine.ApplicationLogic.Exceptions.Application; -using AAS.TwinEngine.DataEngine.ApplicationLogic.Exceptions.Base; using AAS.TwinEngine.DataEngine.Infrastructure.Providers.PluginDataProvider.Helper; using AAS.TwinEngine.DataEngine.ServiceConfiguration.Config; @@ -34,44 +33,57 @@ public JsonSchemaValidatorTests() { SubmodelElementIndexContextPrefix = "_aastwinengine_" }); - var logger = Substitute.For>(); _sut = new JsonSchemaValidator(pluginsConfig, logger); } [Fact] - public void ValidateRequestSchema_NullSchema_ThrowsBadRequest() - => Assert.Throws(() => _sut.ValidateRequestSchema(null!)); + public void ValidateRequestSchema_NullSchema_ThrowsBadRequest() => Assert.Throws(() => _sut.ValidateRequestSchema(null!)); + + [Fact] + public void ValidateRequestSchema_WithInvalidJson_ThrowsParseError() + { + Assert.ThrowsAny(() => + { + var schema = JsonSchema.FromText("{\"type\": 123}"); + _sut.ValidateRequestSchema(schema); + }); + } [Fact] public void ValidateRequestSchema_ValidSchema_DoesNotThrow() { var schema = new JsonSchemaBuilder() - .Schema("https://json-schema.org/draft/2020-12/schema") - .Type(SchemaValueType.Object) - .Properties(new Dictionary - { - ["name"] = new JsonSchemaBuilder().Type(SchemaValueType.String) - }) - .Build(); + .Schema(MetaSchemas.Draft7Id) + .Type(SchemaValueType.Object) + .Properties(new Dictionary + { + ["name"] = new JsonSchemaBuilder().Type(SchemaValueType.String) + }) + .Build(); _sut.ValidateRequestSchema(schema); } - [Theory] - [InlineData("http://json-schema.org/draft-07/schema#")] - [InlineData("https://json-schema.org/draft/2019-09/schema")] - [InlineData("https://json-schema.org/draft/2020-12/schema")] - public void ValidateRequestSchema_SupportedDrafts_DoesNotThrow(string draft) + [Fact] + public void ValidateRequestSchema_ValidDraft202012Schema_DoesNotThrow() { var schema = new JsonSchemaBuilder() - .Schema(draft) - .Type(SchemaValueType.Object) - .Properties(new Dictionary - { - ["name"] = new JsonSchemaBuilder().Type(SchemaValueType.String) - }) - .Build(); + .Schema(MetaSchemas.Draft202012Id) + .Type(SchemaValueType.Object) + .Properties(new Dictionary + { + ["name"] = new JsonSchemaBuilder().Type(SchemaValueType.String) + }) + .Build(); + + _sut.ValidateRequestSchema(schema); + } + + [Fact] + public void ValidateRequestSchema_WhenSchemaHasNoSchemaKeyword_DefaultsToDraft202012() + { + var schema = BuildDraft202012Schema(); _sut.ValidateRequestSchema(schema); } @@ -79,9 +91,7 @@ public void ValidateRequestSchema_SupportedDrafts_DoesNotThrow(string draft) [Fact] public void ValidateResponseContent_EmptyResponse_ThrowsBadRequest() { - var schema = new JsonSchemaBuilder() - .Type(SchemaValueType.Object) - .Build(); + var schema = new JsonSchemaBuilder().Type(SchemaValueType.Object).Build(); Assert.Throws(() => _sut.ValidateResponseContent("", schema)); } @@ -90,13 +100,13 @@ public void ValidateResponseContent_EmptyResponse_ThrowsBadRequest() public void ValidateResponseContent_ValidateJsonSchemaRemovePrefix_DoesNotThrow() { var schema = new JsonSchemaBuilder() - .Type(SchemaValueType.Object) - .Properties(new Dictionary - { - ["ContactInformation_aastwinengine_00"] = new JsonSchemaBuilder().Type(SchemaValueType.Object) - }) - .Required("ContactInformation_aastwinengine_00") - .Build(); + .Type(SchemaValueType.Object) + .Properties(new Dictionary + { + ["ContactInformation_aastwinengine_00"] = new JsonSchemaBuilder().Type(SchemaValueType.Object) + }) + .Required("ContactInformation_aastwinengine_00") + .Build(); const string Json = "{\"ContactInformation\": {}}"; @@ -107,308 +117,332 @@ public void ValidateResponseContent_ValidateJsonSchemaRemovePrefix_DoesNotThrow( public void ValidateResponseContent_ValidJsonAndSchema_DoesNotThrow() { var schema = new JsonSchemaBuilder() - .Type(SchemaValueType.Object) - .Properties(new Dictionary - { - ["name"] = new JsonSchemaBuilder().Type(SchemaValueType.String) - }) - .Required("name") - .Build(); + .Type(SchemaValueType.Object) + .Properties(new Dictionary + { + ["name"] = new JsonSchemaBuilder().Type(SchemaValueType.String) + }) + .Required("name") + .Build(); const string Json = "{\"name\": \"Test\"}"; _sut.ValidateResponseContent(Json, schema); } - [Theory] - [MemberData(nameof(InvalidPrimitives))] - public void ValidateResponseContent_InvalidValueType_ThrowsBadRequest( - SchemaValueType expectedType, - string property, - string rawValue) + [Fact] + public void ValidateResponseContent_WhenSchemaDeclaresDraft202012_ValidatesSuccessfully() { - var schema = new JsonSchemaBuilder() - .Type(SchemaValueType.Object) - .Properties(new Dictionary - { - [property] = new JsonSchemaBuilder().Type(expectedType) - }) - .Required(property) - .Build(); + var schema = BuildDraft202012Schema(); + const string Json = "{\"asset\": {\"details\": {\"name\": \"ok\"}}}"; - var json = $"{{\"{property}\": {rawValue} }}"; - - Assert.Throws(() => _sut.ValidateResponseContent(json, schema)); + _sut.ValidateResponseContent(Json, schema); } [Fact] - public void ValidateResponseContent_SchemaMismatch_ThrowsBadRequest() + public void ValidateResponseContent_WhenSchemaHasNoSchemaKeyword_DefaultsToDraft202012() { var schema = new JsonSchemaBuilder() .Type(SchemaValueType.Object) .Properties(new Dictionary { - ["name"] = new JsonSchemaBuilder().Type(SchemaValueType.String) + ["asset"] = new JsonSchemaBuilder() + .Type(SchemaValueType.Object) + .Properties(new Dictionary + { + ["details"] = new JsonSchemaBuilder() + .Type(SchemaValueType.Object) + .Properties(new Dictionary + { + ["name"] = new JsonSchemaBuilder().Type(SchemaValueType.String) + }) + .Required("name") + }) + .Required("details") }) - .Required("name") + .Required("asset") .Build(); + const string Json = "{\"asset\": {\"details\": {\"name\": \"ok\"}}}"; - const string Json = "{}"; - - Assert.Throws(() => _sut.ValidateResponseContent(Json, schema)); + _sut.ValidateResponseContent(Json, schema); } [Fact] - public void ValidateResponseContent_InvalidJson_ThrowsBadRequest() + public void ValidateResponseContent_WhenSchemaHasNoSchemaKeyword_AndUsesDraft202012Keyword_DefaultsToDraft202012() { - var schema = new JsonSchemaBuilder() - .Type(SchemaValueType.Object) - .Build(); - - const string BadJson = "{ not valid json }"; + var schema = JsonSchema.FromText( + """ + { + "type": "object", + "properties": { + "asset": { + "type": "object", + "properties": { + "name": { "type": "string" } + }, + "required": ["name"] + } + }, + "required": ["asset"], + "unevaluatedProperties": false + } + """); + + const string Json = "{\"asset\":{\"name\":\"ok\"}}"; - Assert.Throws(() => _sut.ValidateResponseContent(BadJson, schema)); + _sut.ValidateResponseContent(Json, schema); } [Fact] - public void ValidateResponseContent_NullResponse_ThrowsException() + public void ValidateResponseContent_WhenSchemaDeclaresDraft7_ValidatesDeepHierarchy() { - var schema = new JsonSchemaBuilder() - .Type(SchemaValueType.Object) - .Build(); + var schema = BuildDraft7Schema(); + const string Json = "{\"asset\": {\"details\": {\"name\": \"Motor\", \"tags\": [\"a\",\"b\"]}}}"; - Assert.Throws(() => _sut.ValidateResponseContent(null!, schema)); + _sut.ValidateResponseContent(Json, schema); } [Fact] - public void ValidateResponseContent_WhitespaceOnlyResponse_ThrowsException() + public void ValidateResponseContent_WhenRequiredNestedPropertyMissing_ThrowsBadRequest() { - var schema = new JsonSchemaBuilder() - .Type(SchemaValueType.Object) - .Build(); + var schema = BuildDraft202012Schema(); + const string Json = "{\"asset\": {\"details\": {}}}"; - Assert.Throws(() => _sut.ValidateResponseContent(" ", schema)); + Assert.Throws(() => + _sut.ValidateResponseContent(Json, schema)); } [Fact] - public void ValidateResponseContent_MalformedJson_ThrowsException() + public void ValidateResponseContent_WhenAdditionalPropertiesNotAllowed_ThrowsBadRequest() { - var schema = new JsonSchemaBuilder() - .Type(SchemaValueType.Object) - .Build(); + var schema = BuildDraft7Schema(); + const string Json = "{\"asset\": {\"details\": {\"name\": \"Motor\", \"unexpected\": 1}}}"; - Assert.Throws(() => _sut.ValidateResponseContent("{\"key\": }", schema)); + Assert.Throws(() => + _sut.ValidateResponseContent(Json, schema)); } + [Fact] - public void ValidateResponseContent_ArrayOfObjects_ValidatesCorrectly() + public void ValidateResponseContent_WhenUnevaluatedPropertiesNotAllowed_ThrowsBadRequest() { - var schema = new JsonSchemaBuilder() - .Type(SchemaValueType.Object) - .Properties(new Dictionary - { - ["users"] = new JsonSchemaBuilder() - .Type(SchemaValueType.Array) - .Items(new JsonSchemaBuilder() - .Type(SchemaValueType.Object) - .Properties(new Dictionary - { - ["name_aastwinengine_01"] = new JsonSchemaBuilder().Type(SchemaValueType.String) - })) - }) - .Build(); - - const string Json = "{\"users\": [{\"name\": \"Alice\"}, {\"name\": \"Bob\"}]}"; + var schema = BuildDraft202012Schema(); + const string Json = "{\"asset\": {\"details\": {\"name\": \"Motor\"}}, \"unexpected\": 1}"; - _sut.ValidateResponseContent(Json, schema); + Assert.Throws(() => + _sut.ValidateResponseContent(Json, schema)); } [Fact] - public void ValidateResponseContent_WithDefsReference_DoesNotThrow() + public void ValidateResponseContent_WhenNullAtDeepLevel_ThrowsBadRequest() { - const string schemaJson = @"{ - ""$schema"": ""https://json-schema.org/draft/2020-12/schema"", - ""type"": ""object"", - ""properties"": { - ""item"": { ""$ref"": ""#/$defs/MyType"" } - }, - ""$defs"": { - ""MyType"": { - ""type"": ""object"", - ""properties"": { - ""name"": { ""type"": ""string"" } - } - } - } - }"; - - var schema = JsonSchema.FromText(schemaJson); - - const string json = @"{ ""item"": { ""name"": ""test"" } }"; + var schema = BuildDraft202012Schema(); + const string Json = "{\"asset\": {\"details\": {\"name\": null}}}"; - _sut.ValidateResponseContent(json, schema); + Assert.Throws(() => + _sut.ValidateResponseContent(Json, schema)); } [Fact] - public void ValidateResponseContent_Draft7Schema_DoesNotThrow() + public void ValidateResponseContent_WhenPartialPayloadMissingRequired_ThrowsBadRequest() { - const string schemaJson = @"{ - ""$schema"": ""http://json-schema.org/draft-07/schema#"", - ""type"": ""object"", - ""properties"": { - ""value"": { ""type"": ""string"" } - }, - ""required"": [""value""] - }"; - - var schema = JsonSchema.FromText(schemaJson); + var schema = BuildDraft7Schema(); + const string Json = "{\"asset\": {}}"; - const string json = @"{ ""value"": ""ok"" }"; - - _sut.ValidateResponseContent(json, schema); + Assert.Throws(() => + _sut.ValidateResponseContent(Json, schema)); } [Fact] - public void ValidateResponseContent_Draft201909Schema_DoesNotThrow() + public void ValidateResponseContent_WhenDraft7SchemaContainsDraft202012Construct_ThrowsBadRequest() { - const string schemaJson = @"{ - ""$schema"": ""https://json-schema.org/draft/2019-09/schema"", - ""type"": ""object"", - ""properties"": { - ""value"": { ""type"": ""integer"" } - }, - ""required"": [""value""] - }"; + var schema = JsonSchema.FromText( + """ + { + "$schema": "http://json-schema.org/draft-07/schema#", + "type": "object", + "properties": { + "name": { "type": "string" } + }, + "required": ["name"], + "unevaluatedProperties": false + } + """); + const string Json = "{\"asset\": {\"details\": {\"name\": \"ok\"}}}"; - var schema = JsonSchema.FromText(schemaJson); + Assert.Throws(() => + _sut.ValidateResponseContent(Json, schema)); + } - const string json = @"{ ""value"": 12 }"; + [Theory] + [MemberData(nameof(InvalidPrimitives))] + public void ValidateResponseContent_InvalidValueType_ThrowsBadRequest( + SchemaValueType expectedType, + string property, + string rawValue) + { + var schema = new JsonSchemaBuilder() + .Type(SchemaValueType.Object) + .Properties(new Dictionary + { + [property] = new JsonSchemaBuilder().Type(expectedType) + }) + .Required(property) + .Build(); + var json = $"{{\"{property}\": {rawValue} }}"; - _sut.ValidateResponseContent(json, schema); + Assert.Throws(() => _sut.ValidateResponseContent(json, schema)); } [Fact] - public void ValidateResponseContent_Draft7DefinitionsReferenceWithSuffix_DoesNotThrow() + public void ValidateResponseContent_SchemaMismatch_ThrowsBadRequest() { - const string schemaJson = @"{ - ""$schema"": ""http://json-schema.org/draft-07/schema#"", - ""type"": ""object"", - ""properties"": { - ""item_aastwinengine_00"": { ""$ref"": ""#/definitions/Type_aastwinengine_00"" } - }, - ""required"": [""item_aastwinengine_00""], - ""definitions"": { - ""Type_aastwinengine_00"": { - ""type"": ""object"", - ""properties"": { - ""name_aastwinengine_00"": { ""type"": ""string"" } - }, - ""required"": [""name_aastwinengine_00""] - } - } - }"; - - var schema = JsonSchema.FromText(schemaJson); + var schema = new JsonSchemaBuilder() + .Type(SchemaValueType.Object) + .Properties(new Dictionary + { + ["name"] = new JsonSchemaBuilder().Type(SchemaValueType.String) + }) + .Required("name") + .Build(); - const string json = @"{ ""item"": { ""name"": ""ok"" } }"; + const string Json = "{}"; - _sut.ValidateResponseContent(json, schema); + Assert.Throws(() => _sut.ValidateResponseContent(Json, schema)); } [Fact] - public void ValidateResponseContent_BrokenRef_Throws() + public void ValidateResponseContent_WhenSchemaExpectsObjectAndResponseIsArray_ThrowsBadRequest() { - const string schemaJson = @"{ - ""$schema"": ""https://json-schema.org/draft/2020-12/schema"", - ""type"": ""object"", - ""properties"": { - ""item"": { ""$ref"": ""#/$defs/UnknownType"" } - }, - ""$defs"": {} - }"; - - var schema = JsonSchema.FromText(schemaJson); + var schema = new JsonSchemaBuilder() + .Type(SchemaValueType.Object) + .Build(); - const string json = @"{ ""item"": {} }"; + const string Json = "[]"; - Assert.Throws(() => - _sut.ValidateResponseContent(json, schema)); + Assert.Throws(() => _sut.ValidateResponseContent(Json, schema)); } [Fact] - public void ValidateResponseContent_ArrayItemInvalid_Throws() + public void ValidateResponseContent_InvalidJson_ThrowsBadRequest() { var schema = new JsonSchemaBuilder() .Type(SchemaValueType.Object) - .Properties(new Dictionary - { - ["users"] = new JsonSchemaBuilder() - .Type(SchemaValueType.Array) - .Items(new JsonSchemaBuilder() - .Type(SchemaValueType.Object) - .Properties(new Dictionary - { - ["name"] = new JsonSchemaBuilder().Type(SchemaValueType.String) - }) - .Required("name")) - }) .Build(); + const string BadJson = "{ not valid json }"; - const string json = @"{ ""users"": [{}] }"; - - Assert.Throws(() => - _sut.ValidateResponseContent(json, schema)); + Assert.Throws(() => _sut.ValidateResponseContent(BadJson, schema)); } [Fact] - public void ValidateResponseContent_NestedDefsReference_Works() + public void ValidateResponseContent_LegacyArraySchema_WithoutItems_DoesNotThrow() { - const string schemaJson = @"{ - ""$schema"": ""https://json-schema.org/draft/2020-12/schema"", - ""type"": ""object"", - ""properties"": { - ""item"": { ""$ref"": ""#/$defs/Level1"" } - }, - ""$defs"": { - ""Level1"": { - ""type"": ""object"", - ""properties"": { - ""child"": { ""$ref"": ""#/$defs/Level2"" } + var malformedSchema = JsonSchema.FromText( + """ + { + "$schema": "https://json-schema.org/draft/2020-12/schema", + "type": "object", + "properties": { + "contactInformation": { + "type": "array", + "properties": { + "name": { "type": "string" } + }, + "required": ["name"] + } } - }, - ""Level2"": { - ""type"": ""string"" - } } - }"; - var schema = JsonSchema.FromText(schemaJson); - - const string json = @"{ ""item"": { ""child"": ""ok"" } }"; + """); + const string Json = @"{ ""contactInformation"": [{ ""name"": ""test"" }] }"; - _sut.ValidateResponseContent(json, schema); + _sut.ValidateResponseContent(Json, malformedSchema); } [Fact] - public void ValidateResponseContent_ArrayMissingRequiredField_Throws() + public void ValidateResponseContent_CorrectSchema_ArrayWithItems_Succeeds() { - var schema = new JsonSchemaBuilder() - .Type(SchemaValueType.Object) - .Properties(new Dictionary + var correctSchema = JsonSchema.FromText( + """ { - ["items"] = new JsonSchemaBuilder() - .Type(SchemaValueType.Array) - .Items(new JsonSchemaBuilder() - .Type(SchemaValueType.Object) - .Properties(new Dictionary - { - ["name"] = new JsonSchemaBuilder().Type(SchemaValueType.String) - }) - .Required("name")) - }) - .Build(); - - const string json = @"{ ""items"": [{}] }"; + "$schema": "https://json-schema.org/draft/2020-12/schema", + "type": "object", + "properties": { + "contactInformation": { + "type": "array", + "items": { + "type": "object", + "properties": { + "name": { "type": "string" } + }, + "required": ["name"] + } + } + } + } + """); + const string ValidJson = @"{ ""contactInformation"": [{ ""name"": ""test"" }] }"; - Assert.Throws(() => - _sut.ValidateResponseContent(json, schema)); + _sut.ValidateResponseContent(ValidJson, correctSchema); } + + private static JsonSchema BuildDraft7Schema() => JsonSchema.FromText( + """ + { + "$schema": "http://json-schema.org/draft-07/schema#", + "type": "object", + "properties": { + "asset": { + "type": "object", + "properties": { + "details": { + "type": "object", + "properties": { + "name": { "type": "string" }, + "tags": { + "type": "array", + "items": { "type": "string" } + } + }, + "required": ["name"], + "additionalProperties": false + } + }, + "required": ["details"], + "additionalProperties": false + } + }, + "required": ["asset"], + "additionalProperties": false + } + """); + + private static JsonSchema BuildDraft202012Schema() => JsonSchema.FromText( + """ + { + "$schema": "https://json-schema.org/draft/2020-12/schema", + "type": "object", + "properties": { + "asset": { + "type": "object", + "properties": { + "details": { + "type": "object", + "properties": { + "name": { "type": "string" }, + "tags": { + "type": "array", + "items": { "type": "string" } + } + }, + "required": ["name"], + "unevaluatedProperties": false + } + }, + "required": ["details"], + "unevaluatedProperties": false + } + }, + "required": ["asset"], + "unevaluatedProperties": false + } + """); } diff --git a/source/AAS.TwinEngine.DataEngine.UnitTests/Infrastructure/Providers/PluginDataProvider/Helper/LegacyV1/LegacySchemaRetryHandlerTests.cs b/source/AAS.TwinEngine.DataEngine.UnitTests/Infrastructure/Providers/PluginDataProvider/Helper/LegacyV1/LegacySchemaRetryHandlerTests.cs new file mode 100644 index 00000000..74dce375 --- /dev/null +++ b/source/AAS.TwinEngine.DataEngine.UnitTests/Infrastructure/Providers/PluginDataProvider/Helper/LegacyV1/LegacySchemaRetryHandlerTests.cs @@ -0,0 +1,146 @@ +using System.Net.Http.Json; +using System.Text.Json; + +using AAS.TwinEngine.DataEngine.ApplicationLogic.Services.Plugin; +using AAS.TwinEngine.DataEngine.ApplicationLogic.Services.Plugin.Providers; +using AAS.TwinEngine.DataEngine.DomainModel.Plugin; +using AAS.TwinEngine.DataEngine.DomainModel.SubmodelRepository; +using AAS.TwinEngine.DataEngine.Infrastructure.Providers.PluginDataProvider.Helper.LegacyV1; + +using Json.Schema; + +using Microsoft.Extensions.Logging; + +using NSubstitute; +using NSubstitute.ExceptionExtensions; + +namespace AAS.TwinEngine.DataEngine.UnitTests.Infrastructure.Providers.PluginDataProvider.Helper.LegacyV1; + +#pragma warning disable CS0618 +public class LegacySchemaRetryHandlerTests +{ + private readonly IPluginRequestBuilder _pluginRequestBuilder; + private readonly IPluginDataProvider _pluginDataProvider; + private readonly ILogger _logger; + private readonly LegacySchemaRetryHandler _sut; + + public LegacySchemaRetryHandlerTests() + { + _pluginRequestBuilder = Substitute.For(); + _pluginDataProvider = Substitute.For(); + _logger = Substitute.For>(); + _sut = new LegacySchemaRetryHandler(_pluginRequestBuilder, _pluginDataProvider, _logger); + } + + [Fact] + public async Task RetryWithDraft7Async_WhenSemanticNodesProvided_LogsWarningBuildsDraft7SchemasAndReturnsProviderResponse() + { + const string rootSemanticId = "https://example.com/contactInformation"; + const string submodelId = "submodel-id"; + using var responseContent = new StringContent("{\"ok\":true}"); + using var cancellationTokenSource = new CancellationTokenSource(); + var cancellationToken = cancellationTokenSource.Token; + using var requestContent = JsonContent.Create(new { }); + var rootNode = new SemanticBranchNode(rootSemanticId, Cardinality.One); + rootNode.AddChild(new SemanticLeafNode("https://example.com/name", string.Empty, DataType.String, Cardinality.One)); + var semanticNodes = new Dictionary + { + ["plugin-a"] = rootNode + }; + IDictionary? capturedSchemas = null; + var expectedRequests = new List + { + new("plugin-a", requestContent) + }; + _pluginRequestBuilder + .Build(Arg.Do>(schemas => capturedSchemas = schemas)) + .Returns(expectedRequests); + _pluginDataProvider + .GetDataForSemanticIdsAsync(expectedRequests, submodelId, cancellationToken) + .Returns([responseContent]); + + var result = await _sut.RetryWithDraft7Async(semanticNodes, submodelId, cancellationToken); + + var singleResult = Assert.Single(result); + Assert.Same(responseContent, singleResult); + Assert.NotNull(capturedSchemas); + Assert.True(capturedSchemas!.TryGetValue("plugin-a", out var generatedSchema)); + var schemaElement = ToElement(generatedSchema); + Assert.Equal("http://json-schema.org/draft-07/schema#", schemaElement.GetProperty("$schema").GetString()); + Assert.True(schemaElement.GetProperty("properties").TryGetProperty(rootSemanticId, out _)); + _logger.Received(1).Log( + LogLevel.Warning, + Arg.Any(), + Arg.Is(o => o.ToString()!.Contains("Retrying with Draft-07 fallback", StringComparison.Ordinal)), + Arg.Any(), + Arg.Any>()); + + _pluginRequestBuilder.Received(1).Build(Arg.Any>()); + await _pluginDataProvider.Received(1).GetDataForSemanticIdsAsync(expectedRequests, submodelId, cancellationToken); + } + + [Fact] + public async Task RetryWithDraft7Async_WhenSemanticNodesAreEmpty_BuildsEmptySchemaSetAndReturnsProviderResponse() + { + const string submodelId = "submodel-id"; + var semanticNodes = new Dictionary(); + var expectedRequests = new List(); + var expectedResponse = new List(); + _pluginRequestBuilder.Build(Arg.Any>()).Returns(expectedRequests); + _pluginDataProvider + .GetDataForSemanticIdsAsync(expectedRequests, submodelId, CancellationToken.None) + .Returns(expectedResponse); + + var result = await _sut.RetryWithDraft7Async(semanticNodes, submodelId, CancellationToken.None); + + Assert.Empty(result); + _pluginRequestBuilder.Received(1).Build(Arg.Is>(schemas => schemas.Count == 0)); + await _pluginDataProvider.Received(1).GetDataForSemanticIdsAsync(expectedRequests, submodelId, CancellationToken.None); + } + + [Fact] + public async Task RetryWithDraft7Async_WhenPluginRequestBuilderThrows_PropagatesExceptionAndDoesNotCallProvider() + { + var semanticNodes = new Dictionary + { + ["plugin-a"] = new SemanticLeafNode("https://example.com/name", string.Empty, DataType.String, Cardinality.One) + }; + + _pluginRequestBuilder + .Build(Arg.Any>()) + .Throws(new InvalidOperationException("build failed")); + + var exception = await Assert.ThrowsAsync(() => _sut.RetryWithDraft7Async(semanticNodes, "submodel-id", CancellationToken.None)); + + Assert.Equal("build failed", exception.Message); + await _pluginDataProvider.DidNotReceive().GetDataForSemanticIdsAsync(Arg.Any>(), Arg.Any(), Arg.Any()); + } + + [Fact] + public async Task RetryWithDraft7Async_WhenProviderThrows_PropagatesException() + { + using var requestContent = JsonContent.Create(new { }); + var semanticNodes = new Dictionary + { + ["plugin-a"] = new SemanticLeafNode("https://example.com/name", string.Empty, DataType.String, Cardinality.One) + }; + _pluginRequestBuilder + .Build(Arg.Any>()) + .Returns([new("plugin-a", requestContent)]); + _pluginDataProvider + .GetDataForSemanticIdsAsync(Arg.Any>(), Arg.Any(), Arg.Any()) + .Returns(_ => Task.FromException>(new HttpRequestException("provider failed"))); + + var exception = await Assert.ThrowsAsync(() => _sut.RetryWithDraft7Async(semanticNodes, "submodel-id", CancellationToken.None)); + + Assert.Equal("provider failed", exception.Message); + } + + private static JsonElement ToElement(JsonSchema schema) + { + var json = JsonSerializer.Serialize(schema); + using var document = JsonDocument.Parse(json); + return document.RootElement.Clone(); + } +} +#pragma warning restore CS0618 diff --git a/source/AAS.TwinEngine.DataEngine.UnitTests/Infrastructure/Providers/PluginDataProvider/Services/PluginDataHandlerTests.cs b/source/AAS.TwinEngine.DataEngine.UnitTests/Infrastructure/Providers/PluginDataProvider/Services/PluginDataHandlerTests.cs index 103dcd00..f03954d9 100644 --- a/source/AAS.TwinEngine.DataEngine.UnitTests/Infrastructure/Providers/PluginDataProvider/Services/PluginDataHandlerTests.cs +++ b/source/AAS.TwinEngine.DataEngine.UnitTests/Infrastructure/Providers/PluginDataProvider/Services/PluginDataHandlerTests.cs @@ -20,6 +20,7 @@ using Microsoft.Extensions.Logging; using NSubstitute; +using NSubstitute.ExceptionExtensions; using AAS.TwinEngine.DataEngine.ServiceConfiguration.Config; using Microsoft.Extensions.Options; @@ -31,6 +32,8 @@ public class PluginDataHandlerTests private readonly IPluginRequestBuilder _pluginRequestBuilder; private readonly IPluginDataProvider _pluginDataProvider; private readonly IJsonSchemaValidator _jsonSchemaValidator; + private readonly IJsonSchemaGenerator _jsonSchemaGenerator; + private readonly ILegacySchemaRetryHandler _legacySchemaRetryHandler; private readonly IMultiPluginDataHandler _multiPluginDataHandler; private readonly ILogger _logger; private readonly IOptions _options; @@ -41,6 +44,8 @@ public PluginDataHandlerTests() _pluginRequestBuilder = Substitute.For(); _pluginDataProvider = Substitute.For(); _jsonSchemaValidator = Substitute.For(); + _jsonSchemaGenerator = Substitute.For(); + _legacySchemaRetryHandler = Substitute.For(); _multiPluginDataHandler = Substitute.For(); _logger = Substitute.For>(); _options = Options.Create(new GeneralConfig @@ -48,7 +53,7 @@ public PluginDataHandlerTests() DataEngineRepositoryBaseUrl = new Uri("https://www.mm-software.com"), }); - _sut = new PluginDataHandler(_pluginRequestBuilder, _pluginDataProvider, _jsonSchemaValidator, _multiPluginDataHandler, _logger, _options); + _sut = new PluginDataHandler(_pluginRequestBuilder, _pluginDataProvider, _jsonSchemaValidator, _jsonSchemaGenerator, _legacySchemaRetryHandler, _multiPluginDataHandler, _logger, _options); } private readonly JsonSerializerOptions _jsonoptions = new() @@ -66,9 +71,7 @@ public PluginDataHandlerTests() [Fact] public async Task TryGetValuesAsync_WithValidManifestAndResponse_ReturnsMergedSemanticTreeNode() { - // Arrange var inputSemanticTreeNode = new SemanticLeafNode("Contact", "", DataType.String, Cardinality.One); - const string ExpectedJsonResponse = """ { "Contact": "value" @@ -133,10 +136,8 @@ public async Task TryGetValuesAsync_WithValidManifestAndResponse_ReturnsMergedSe .Merge(Arg.Any(), Arg.Any>()) .Returns(ci => ci.ArgAt>(1).First()); - // Act var result = await _sut.TryGetValuesAsync(manifests, inputSemanticTreeNode, "submodelId", CancellationToken.None); - // Assert Assert.NotNull(result); var leaf = Assert.IsType(result); Assert.Equal("Contact", leaf.SemanticId); @@ -146,6 +147,57 @@ public async Task TryGetValuesAsync_WithValidManifestAndResponse_ReturnsMergedSe _jsonSchemaValidator.Received().ValidateResponseContent(ExpectedJsonResponse, Arg.Any()); } + [Fact] + public async Task TryGetValuesAsync_WhenSchemaKeyHasPrefix_ValidatesRequestAndResponse() + { + var inputSemanticTreeNode = new SemanticLeafNode("Contact", "", DataType.String, Cardinality.One); + + const string ExpectedJsonResponse = "{\"Contact\": \"value\"}"; + const string PrefixedSchemaKey = "d07:TestPlugin"; + + var requestList = new List + { + new($"{HttpClientNames.PluginDataProviderPrefix}{PrefixedSchemaKey}", ConvertToJsonContent("{}")) + }; + + var manifests = new List + { + new() + { + PluginName = "TestPlugin", + PluginUrl = new Uri("http://localhost"), + SupportedSemanticIds = new List { "Contact" }, + Capabilities = new Capabilities { HasShellDescriptor = true } + } + }; + + _multiPluginDataHandler + .SplitByPluginManifests(Arg.Any(), Arg.Any>()) + .Returns(new Dictionary { { PrefixedSchemaKey, inputSemanticTreeNode } }); + + _pluginRequestBuilder + .Build(Arg.Any>()) + .Returns(requestList); + + var httpResponse = new HttpResponseMessage(HttpStatusCode.OK) + { + Content = new StringContent(ExpectedJsonResponse, Encoding.UTF8, "application/json") + }; + + _pluginDataProvider + .GetDataForSemanticIdsAsync(Arg.Any>(), Arg.Any(), Arg.Any()) + .Returns(_ => Task.FromResult>(new List { httpResponse.Content })); + + _multiPluginDataHandler + .Merge(Arg.Any(), Arg.Any>()) + .Returns(ci => ci.ArgAt>(1).First()); + + _ = await _sut.TryGetValuesAsync(manifests, inputSemanticTreeNode, "submodelId", CancellationToken.None); + + _jsonSchemaValidator.Received().ValidateRequestSchema(Arg.Any()); + _jsonSchemaValidator.Received().ValidateResponseContent(ExpectedJsonResponse, Arg.Any()); + } + [Fact] public async Task GetDataForAllShellDescriptorsAsync_ReturnsListWithHrefSet() { @@ -392,6 +444,152 @@ public static JsonContent ConvertToJsonContent(string json) }); } + [Fact] + public async Task TryGetValuesAsync_WhenPluginRejectsSchema_FallsBackToLegacyRetryHandler() + { + var inputNode = new SemanticLeafNode("Contact", "", DataType.String, Cardinality.One); + const string ExpectedJsonResponse = """{ "Contact": "value" }"""; + _multiPluginDataHandler + .SplitByPluginManifests(Arg.Any(), Arg.Any>()) + .Returns(new Dictionary { { "TestPlugin", inputNode } }); + _pluginRequestBuilder + .Build(Arg.Any>()) + .Returns(new List { new("TestPlugin", ConvertToJsonContent("{}")) }); + _pluginDataProvider + .GetDataForSemanticIdsAsync(Arg.Any>(), Arg.Any(), Arg.Any()) + .ThrowsAsync(new PluginSchemaRejectionException()); + using var retryContent = new StringContent(ExpectedJsonResponse, Encoding.UTF8, "application/json"); + _legacySchemaRetryHandler + .RetryWithDraft7Async(Arg.Any>(), Arg.Any(), Arg.Any()) + .Returns(Task.FromResult>(new List { retryContent })); + _multiPluginDataHandler + .Merge(Arg.Any(), Arg.Any>()) + .Returns(ci => ci.ArgAt>(1).First()); + var manifests = new List + { + new() + { + PluginName = "TestPlugin", + PluginUrl = new Uri("http://localhost"), + SupportedSemanticIds = new List { "Contact" }, + Capabilities = new Capabilities { HasShellDescriptor = false } + } + }; + + var result = await _sut.TryGetValuesAsync(manifests, inputNode, "submodelId", CancellationToken.None); + + await _legacySchemaRetryHandler.Received(1) + .RetryWithDraft7Async( + Arg.Is>(d => d.ContainsKey("TestPlugin")), + "submodelId", + Arg.Any()); + + Assert.NotNull(result); + } + + [Fact] + public async Task TryGetValuesAsync_WhenPluginRejectsSchema_DoesNotCallProviderAgainDirectly() + { + var inputNode = new SemanticLeafNode("Contact", "", DataType.String, Cardinality.One); + _multiPluginDataHandler + .SplitByPluginManifests(Arg.Any(), Arg.Any>()) + .Returns(new Dictionary { { "TestPlugin", inputNode } }); + _pluginRequestBuilder + .Build(Arg.Any>()) + .Returns([new("TestPlugin", ConvertToJsonContent("{}"))]); + _pluginDataProvider + .GetDataForSemanticIdsAsync(Arg.Any>(), Arg.Any(), Arg.Any()) + .ThrowsAsync(new PluginSchemaRejectionException()); + using var retryContent = new StringContent("{\"Contact\":\"value\"}", Encoding.UTF8, "application/json"); + _legacySchemaRetryHandler + .RetryWithDraft7Async(Arg.Any>(), Arg.Any(), Arg.Any()) + .Returns(Task.FromResult>([retryContent])); + _multiPluginDataHandler + .Merge(Arg.Any(), Arg.Any>()) + .Returns(ci => ci.ArgAt>(1).First()); + var manifests = new List + { + new() + { + PluginName = "TestPlugin", + PluginUrl = new Uri("http://localhost"), + SupportedSemanticIds = new List { "Contact" }, + Capabilities = new Capabilities { HasShellDescriptor = false } + } + }; + + _ = await _sut.TryGetValuesAsync(manifests, inputNode, "submodelId", CancellationToken.None); + + await _pluginDataProvider.Received(1) + .GetDataForSemanticIdsAsync(Arg.Any>(), Arg.Any(), Arg.Any()); + } + + [Fact] + public async Task TryGetValuesAsync_WhenRetryAlsoFails_PropagatesException() + { + var inputNode = new SemanticLeafNode("Contact", "", DataType.String, Cardinality.One); + _multiPluginDataHandler + .SplitByPluginManifests(Arg.Any(), Arg.Any>()) + .Returns(new Dictionary { { "TestPlugin", inputNode } }); + _pluginRequestBuilder + .Build(Arg.Any>()) + .Returns([new("TestPlugin", ConvertToJsonContent("{}"))]); + _pluginDataProvider + .GetDataForSemanticIdsAsync(Arg.Any>(), Arg.Any(), Arg.Any()) + .ThrowsAsync(new PluginSchemaRejectionException()); + _legacySchemaRetryHandler + .RetryWithDraft7Async(Arg.Any>(), Arg.Any(), Arg.Any()) + .ThrowsAsync(new ResponseParsingException()); + var manifests = new List + { + new() + { + PluginName = "TestPlugin", + PluginUrl = new Uri("http://localhost"), + SupportedSemanticIds = [ "Contact" ], + Capabilities = new Capabilities { HasShellDescriptor = false } + } + }; + + await Assert.ThrowsAsync(() => + _sut.TryGetValuesAsync(manifests, inputNode, "submodelId", CancellationToken.None)); + } + + [Fact] + public async Task TryGetValuesAsync_WhenProviderSucceeds_DoesNotInvokeLegacyRetryHandler() + { + var inputNode = new SemanticLeafNode("Contact", "", DataType.String, Cardinality.One); + const string ExpectedJsonResponse = """{ "Contact": "value" }"""; + _multiPluginDataHandler + .SplitByPluginManifests(Arg.Any(), Arg.Any>()) + .Returns(new Dictionary { { "TestPlugin", inputNode } }); + _pluginRequestBuilder + .Build(Arg.Any>()) + .Returns([new("TestPlugin", ConvertToJsonContent("{}"))]); + using var content = new StringContent(ExpectedJsonResponse, Encoding.UTF8, "application/json"); + _pluginDataProvider + .GetDataForSemanticIdsAsync(Arg.Any>(), Arg.Any(), Arg.Any()) + .Returns(Task.FromResult>(new List { content })); + _multiPluginDataHandler + .Merge(Arg.Any(), Arg.Any>()) + .Returns(ci => ci.ArgAt>(1).First()); + var manifests = new List + { + new() + { + PluginName = "TestPlugin", + PluginUrl = new Uri("http://localhost"), + SupportedSemanticIds = new List { "Contact" }, + Capabilities = new Capabilities { HasShellDescriptor = false } + } + }; + + _ = await _sut.TryGetValuesAsync(manifests, inputNode, "submodelId", CancellationToken.None); + + await _legacySchemaRetryHandler.DidNotReceive() + .RetryWithDraft7Async(Arg.Any>(), Arg.Any(), Arg.Any()); + } + private const string AssetData = """ { "globalAssetId": "ContactInformation", diff --git a/source/AAS.TwinEngine.DataEngine.UnitTests/Infrastructure/Providers/PluginDataProvider/Services/PluginDataProviderTests.cs b/source/AAS.TwinEngine.DataEngine.UnitTests/Infrastructure/Providers/PluginDataProvider/Services/PluginDataProviderTests.cs index 788302ea..4160f957 100644 --- a/source/AAS.TwinEngine.DataEngine.UnitTests/Infrastructure/Providers/PluginDataProvider/Services/PluginDataProviderTests.cs +++ b/source/AAS.TwinEngine.DataEngine.UnitTests/Infrastructure/Providers/PluginDataProvider/Services/PluginDataProviderTests.cs @@ -1,4 +1,4 @@ -using System.Net; +using System.Net; using System.Net.Http.Json; using System.Text; using System.Text.Json; @@ -344,8 +344,10 @@ await Assert.ThrowsAsync(() => _sut.GetDataForSemanticIdsAsync(pluginRequests, "asdf", CancellationToken.None)); } - [Fact] - public async Task GetDataForSemanticIdsAsync_WhenUnexpectedStatusCode_ThrowsResponseParsingException() + [Theory] + [InlineData(HttpStatusCode.BadRequest)] + [InlineData(HttpStatusCode.InternalServerError)] + public async Task GetDataForSemanticIdsAsync_WhenSchemaRejected_ThrowsPluginSchemaRejectionException(HttpStatusCode statusCode) { using var simpleRequestSchema = ConvertToJsonContent(SimpleRequestSchema); var pluginRequest = new PluginRequestSubmodel($"{HttpClientNames.PluginDataProviderPrefix}PluginName", simpleRequestSchema); @@ -354,14 +356,14 @@ public async Task GetDataForSemanticIdsAsync_WhenUnexpectedStatusCode_ThrowsResp using var messageHandler = new FakeHttpMessageHandler((_, _) => Task.FromResult(new HttpResponseMessage { - StatusCode = HttpStatusCode.InternalServerError, + StatusCode = statusCode, Content = new StringContent("Server error") })); using var httpClient = new HttpClient(messageHandler) { BaseAddress = new Uri("https://example.com") }; _httpClientFactory.CreateClient(pluginRequest.HttpClientName).Returns(httpClient); - await Assert.ThrowsAsync(() => + await Assert.ThrowsAsync(() => _sut.GetDataForSemanticIdsAsync(pluginRequests, "asdf", CancellationToken.None)); } diff --git a/source/AAS.TwinEngine.DataEngine/ApplicationLogic/Exceptions/Infrastructure/PluginSchemaRejectionException.cs b/source/AAS.TwinEngine.DataEngine/ApplicationLogic/Exceptions/Infrastructure/PluginSchemaRejectionException.cs new file mode 100644 index 00000000..f5ac7df8 --- /dev/null +++ b/source/AAS.TwinEngine.DataEngine/ApplicationLogic/Exceptions/Infrastructure/PluginSchemaRejectionException.cs @@ -0,0 +1,7 @@ +namespace AAS.TwinEngine.DataEngine.ApplicationLogic.Exceptions.Infrastructure; + +/// +/// Thrown when a plugin rejects the request schema with HTTP 400. +/// This triggers a legacy Draft-07 compatibility retry. +/// +public class PluginSchemaRejectionException : Exception; diff --git a/source/AAS.TwinEngine.DataEngine/ApplicationLogic/Services/Plugin/Helper/IJsonSchemaGenerator.cs b/source/AAS.TwinEngine.DataEngine/ApplicationLogic/Services/Plugin/Helper/IJsonSchemaGenerator.cs new file mode 100644 index 00000000..1c3d7ae2 --- /dev/null +++ b/source/AAS.TwinEngine.DataEngine/ApplicationLogic/Services/Plugin/Helper/IJsonSchemaGenerator.cs @@ -0,0 +1,10 @@ +using AAS.TwinEngine.DataEngine.DomainModel.SubmodelRepository; + +using Json.Schema; + +namespace AAS.TwinEngine.DataEngine.ApplicationLogic.Services.Plugin.Helper; + +public interface IJsonSchemaGenerator +{ + JsonSchema Generate(SemanticTreeNode rootNode); +} diff --git a/source/AAS.TwinEngine.DataEngine/ApplicationLogic/Services/Plugin/Helper/ILegacySchemaRetryHandler.cs b/source/AAS.TwinEngine.DataEngine/ApplicationLogic/Services/Plugin/Helper/ILegacySchemaRetryHandler.cs new file mode 100644 index 00000000..3a93d7cc --- /dev/null +++ b/source/AAS.TwinEngine.DataEngine/ApplicationLogic/Services/Plugin/Helper/ILegacySchemaRetryHandler.cs @@ -0,0 +1,19 @@ +using AAS.TwinEngine.DataEngine.DomainModel.SubmodelRepository; + +namespace AAS.TwinEngine.DataEngine.ApplicationLogic.Services.Plugin.Helper; + +/// +/// Retries a plugin data request using a Draft-07 JSON Schema when the plugin +/// rejects the default Draft 2020-12 schema with HTTP 400, 404, or 500. +/// +/// +/// This is a legacy compatibility path. It will be removed in the next major release +/// once all plugins support Draft 2020-12. +/// +public interface ILegacySchemaRetryHandler +{ + Task> RetryWithDraft7Async( + IDictionary semanticNodes, + string submodelId, + CancellationToken cancellationToken); +} diff --git a/source/AAS.TwinEngine.DataEngine/Infrastructure/Providers/PluginDataProvider/Helper/JsonSchemaDraft202012Generator.cs b/source/AAS.TwinEngine.DataEngine/Infrastructure/Providers/PluginDataProvider/Helper/JsonSchemaDraft202012Generator.cs new file mode 100644 index 00000000..95f893d3 --- /dev/null +++ b/source/AAS.TwinEngine.DataEngine/Infrastructure/Providers/PluginDataProvider/Helper/JsonSchemaDraft202012Generator.cs @@ -0,0 +1,34 @@ +using AAS.TwinEngine.DataEngine.DomainModel.SubmodelRepository; + +using Json.Schema; + +namespace AAS.TwinEngine.DataEngine.Infrastructure.Providers.PluginDataProvider.Helper; + +public sealed class JsonSchemaDraft202012Generator : JsonSchemaGeneratorBase +{ + private const string Draft202012RefPrefix = "#/$defs/"; + + protected override string RefPrefix => Draft202012RefPrefix; + + protected override JsonSchema BuildRootSchema(SemanticTreeNode rootNode, JsonSchemaBuilder rootBuilder, Dictionary defs) + { + return new JsonSchemaBuilder() + .Schema(MetaSchemas.Draft202012Id) + .Type(SchemaValueType.Object) + .Properties(new Dictionary { [rootNode.SemanticId] = rootBuilder }) + .Defs(defs) + .Build(); + } + + protected override JsonSchemaBuilder BuildArraySchema(Dictionary children, List requiredProperties) + { + var itemsBuilder = new JsonSchemaBuilder() + .Type(SchemaValueType.Object) + .Properties(children) + .Required(requiredProperties); + + return new JsonSchemaBuilder() + .Type(SchemaValueType.Array) + .Items(itemsBuilder); + } +} diff --git a/source/AAS.TwinEngine.DataEngine/Infrastructure/Providers/PluginDataProvider/Helper/JsonSchemaGenerator.cs b/source/AAS.TwinEngine.DataEngine/Infrastructure/Providers/PluginDataProvider/Helper/JsonSchemaGenerator.cs deleted file mode 100644 index ab45b52d..00000000 --- a/source/AAS.TwinEngine.DataEngine/Infrastructure/Providers/PluginDataProvider/Helper/JsonSchemaGenerator.cs +++ /dev/null @@ -1,154 +0,0 @@ -using System.Collections.ObjectModel; - -using AAS.TwinEngine.DataEngine.ApplicationLogic.Exceptions.Application; -using AAS.TwinEngine.DataEngine.DomainModel.SubmodelRepository; - -using Json.Schema; - -namespace AAS.TwinEngine.DataEngine.Infrastructure.Providers.PluginDataProvider.Helper; - -public static class JsonSchemaGenerator -{ - private const string DefinitionsRefPrefix = "#/$defs/"; - - public static JsonSchema ConvertToJsonSchema(SemanticTreeNode rootNode) - { - var definitions = new Dictionary(); - var rootSchema = BuildNode(rootNode, definitions, isRoot: true); - - return new JsonSchemaBuilder() - .Schema(MetaSchemas.Draft202012Id) - .Type(SchemaValueType.Object) - .Properties(new Dictionary - { - [rootNode!.SemanticId] = rootSchema - }) - .Defs(definitions) - .Build(); - } - - private static JsonSchemaBuilder BuildNode( - SemanticTreeNode node, - Dictionary definitions, - bool isRoot = false) - { - return node switch - { - SemanticBranchNode branch => BuildBranch(branch, definitions, isRoot), - SemanticLeafNode leaf => BuildLeaf(leaf), - _ => throw new InternalDataProcessingException() - }; - } - - private static JsonSchemaBuilder BuildBranch( - SemanticBranchNode branch, - Dictionary definitions, - bool isRoot = false) - { - if (!isRoot && definitions.ContainsKey(branch.SemanticId)) - { - return CreateRefSchema(branch.SemanticId); - } - - var requiredProperties = new List(); - var children = BuildChildren(branch.Children, definitions, requiredProperties); - - var schemaBuilder = IsArrayCardinality(branch.Cardinality) - ? BuildArraySchema(children, requiredProperties) - : BuildObjectSchema(children, requiredProperties); - - if (isRoot) - { - return schemaBuilder; - } - - definitions[branch.SemanticId] = schemaBuilder; - - return CreateRefSchema(branch.SemanticId); - } - - private static JsonSchemaBuilder BuildObjectSchema( - Dictionary properties, - List requiredProperties) - { - var schemaBuilder = new JsonSchemaBuilder() - .Type(SchemaValueType.Object) - .Properties(properties); - - if (requiredProperties.Count > 0) - { - schemaBuilder = schemaBuilder.Required(requiredProperties); - } - - return schemaBuilder; - } - - private static JsonSchemaBuilder BuildArraySchema( - Dictionary properties, - List requiredProperties) - { - var itemBuilder = new JsonSchemaBuilder() - .Type(SchemaValueType.Object) - .Properties(properties); - - if (requiredProperties.Count > 0) - { - itemBuilder = itemBuilder.Required(requiredProperties); - } - - return new JsonSchemaBuilder() - .Type(SchemaValueType.Array) - .Items(itemBuilder); - } - - private static Dictionary BuildChildren( - ReadOnlyCollection children, - Dictionary definitions, - List required) - { - var properties = new Dictionary(); - - foreach (var child in children) - { - var childSchema = child switch - { - SemanticBranchNode branch => BuildNode(branch, definitions), - SemanticLeafNode leaf => BuildLeaf(leaf), - _ => throw new InternalDataProcessingException() - }; - - properties[child.SemanticId] = childSchema; - - if (IsRequiredCardinality(child.Cardinality)) - { - required.Add(child.SemanticId); - } - } - - return properties; - } - - private static JsonSchemaBuilder BuildLeaf(SemanticLeafNode leaf) - { - var type = leaf.DataType switch - { - DataType.String => SchemaValueType.String, - DataType.Boolean => SchemaValueType.Boolean, - DataType.Integer => SchemaValueType.Integer, - DataType.Number => SchemaValueType.Number, - DataType.Unknown => SchemaValueType.String, - _ => SchemaValueType.String - }; - - return new JsonSchemaBuilder().Type(type); - } - - private static bool IsArrayCardinality(Cardinality cardinality) - => cardinality is Cardinality.ZeroToMany or Cardinality.OneToMany; - - private static bool IsRequiredCardinality(Cardinality cardinality) - => cardinality is Cardinality.One or Cardinality.OneToMany; - - private static JsonSchemaBuilder CreateRefSchema(string semanticId) - => new JsonSchemaBuilder().Ref($"{DefinitionsRefPrefix}{semanticId}"); -} diff --git a/source/AAS.TwinEngine.DataEngine/Infrastructure/Providers/PluginDataProvider/Helper/JsonSchemaGeneratorBase.cs b/source/AAS.TwinEngine.DataEngine/Infrastructure/Providers/PluginDataProvider/Helper/JsonSchemaGeneratorBase.cs new file mode 100644 index 00000000..5df064b5 --- /dev/null +++ b/source/AAS.TwinEngine.DataEngine/Infrastructure/Providers/PluginDataProvider/Helper/JsonSchemaGeneratorBase.cs @@ -0,0 +1,112 @@ +using System.Collections.ObjectModel; + +using AAS.TwinEngine.DataEngine.ApplicationLogic.Exceptions.Application; +using AAS.TwinEngine.DataEngine.ApplicationLogic.Services.Plugin.Helper; +using AAS.TwinEngine.DataEngine.DomainModel.SubmodelRepository; + +using Json.Schema; + +namespace AAS.TwinEngine.DataEngine.Infrastructure.Providers.PluginDataProvider.Helper; + +public abstract class JsonSchemaGeneratorBase : IJsonSchemaGenerator +{ + protected abstract string RefPrefix { get; } + + public JsonSchema Generate(SemanticTreeNode rootNode) + { + var defs = new Dictionary(); + var rootBuilder = BuildNode(rootNode, defs, isRoot: true); + return BuildRootSchema(rootNode, rootBuilder, defs); + } + + protected abstract JsonSchema BuildRootSchema(SemanticTreeNode rootNode, JsonSchemaBuilder rootBuilder, Dictionary defs); + + protected abstract JsonSchemaBuilder BuildArraySchema(Dictionary children, List requiredProperties); + + private JsonSchemaBuilder BuildNode(SemanticTreeNode node, Dictionary defs, bool isRoot = false) + { + return node switch + { + SemanticBranchNode branch => BuildBranch(branch, defs, isRoot), + SemanticLeafNode leaf => BuildLeaf(leaf), + _ => throw new InternalDataProcessingException() + }; + } + + private JsonSchemaBuilder BuildBranch(SemanticBranchNode branch, Dictionary defs, bool isRoot = false) + { + if (!isRoot && defs.ContainsKey(branch.SemanticId)) + { + return CreateRefBuilder(branch.SemanticId); + } + + var requiredProperties = new List(); + var children = BuildChildren(branch.Children, defs, requiredProperties); + + var schemaBuilder = IsArrayCardinality(branch.Cardinality) + ? BuildArraySchema(children, requiredProperties) + : new JsonSchemaBuilder() + .Type(SchemaValueType.Object) + .Properties(children) + .Required(requiredProperties); + + if (isRoot) + { + return schemaBuilder; + } + + defs[branch.SemanticId] = schemaBuilder; + return CreateRefBuilder(branch.SemanticId); + } + + private Dictionary BuildChildren( + ReadOnlyCollection children, + Dictionary defs, + List required) + { + var properties = new Dictionary(); + + foreach (var child in children) + { + var childBuilder = child switch + { + SemanticBranchNode branch => BuildNode(branch, defs), + SemanticLeafNode leaf => BuildLeaf(leaf), + _ => throw new InternalDataProcessingException() + }; + + properties[child.SemanticId] = childBuilder; + + if (IsRequiredCardinality(child.Cardinality)) + { + required.Add(child.SemanticId); + } + } + + return properties; + } + + private static JsonSchemaBuilder BuildLeaf(SemanticLeafNode leaf) + { + var type = leaf.DataType switch + { + DataType.String => SchemaValueType.String, + DataType.Boolean => SchemaValueType.Boolean, + DataType.Integer => SchemaValueType.Integer, + DataType.Number => SchemaValueType.Number, + DataType.Unknown => SchemaValueType.String, + _ => SchemaValueType.String + }; + + return new JsonSchemaBuilder().Type(type); + } + + private JsonSchemaBuilder CreateRefBuilder(string semanticId) + => new JsonSchemaBuilder().Ref($"{RefPrefix}{semanticId}"); + + private static bool IsArrayCardinality(Cardinality cardinality) + => cardinality is Cardinality.ZeroToMany or Cardinality.OneToMany; + + private static bool IsRequiredCardinality(Cardinality cardinality) + => cardinality is Cardinality.One or Cardinality.OneToMany; +} diff --git a/source/AAS.TwinEngine.DataEngine/Infrastructure/Providers/PluginDataProvider/Helper/JsonSchemaValidator.cs b/source/AAS.TwinEngine.DataEngine/Infrastructure/Providers/PluginDataProvider/Helper/JsonSchemaValidator.cs index 33328b37..6b2c0730 100644 --- a/source/AAS.TwinEngine.DataEngine/Infrastructure/Providers/PluginDataProvider/Helper/JsonSchemaValidator.cs +++ b/source/AAS.TwinEngine.DataEngine/Infrastructure/Providers/PluginDataProvider/Helper/JsonSchemaValidator.cs @@ -7,25 +7,17 @@ using AAS.TwinEngine.DataEngine.ServiceConfiguration.Config; using Json.Schema; -using Json.Schema.Keywords; using Microsoft.Extensions.Options; +namespace AAS.TwinEngine.DataEngine.Infrastructure.Providers.PluginDataProvider.Helper; + public class JsonSchemaValidator(IOptions pluginsConfig, ILogger logger) : IJsonSchemaValidator { private readonly string _contextPrefix = pluginsConfig.Value.SubmodelElementIndexContextPrefix; - - private const string DefsPrefix = "#/$defs/"; - private const string DefinitionsPrefix = "#/definitions/"; - private const string Draft7Schema = "http://json-schema.org/draft-07/schema#"; - private const string Draft7SchemaHttps = "https://json-schema.org/draft-07/schema#"; - private const string Draft201909Schema = "https://json-schema.org/draft/2019-09/schema"; - private const string Draft202012Schema = "https://json-schema.org/draft/2020-12/schema"; - - private readonly EvaluationOptions _evaluationOptions = new() - { - OutputFormat = OutputFormat.List - }; + private static readonly IReadOnlyList KnownRefPrefixes = ["#/definitions/", "#/$defs/"]; + private static readonly JsonSchemaDraft Draft7 = new("Draft-07", MetaSchemas.Draft7Id.OriginalString, MetaSchemas.Draft7); + private static readonly JsonSchemaDraft Draft202012 = new("Draft 2020-12", MetaSchemas.Draft202012Id.OriginalString, MetaSchemas.Draft202012); public void ValidateRequestSchema(JsonSchema schema) { @@ -51,20 +43,18 @@ public void ValidateRequestSchema(JsonSchema schema) try { - var normalizedSchema = schemaNode!.AsObject(); - var draftUri = ExtractDraftUri(normalizedSchema); - var jsonElement = JsonDocument.Parse(normalizedSchema.ToJsonString()).RootElement; - - var result = ResolveMetaSchema(draftUri).Evaluate(jsonElement, _evaluationOptions); - + var metaSchema = ResolveMetaSchema(schemaNode); + using var schemaDoc = JsonDocument.Parse(schemaNode.ToJsonString()); + var result = metaSchema.Evaluate(schemaDoc.RootElement, new EvaluationOptions { OutputFormat = OutputFormat.List }); if (!result.IsValid) { - LogAndThrowException($"Schema is not valid against '{draftUri}'."); + var details = TrySerializeEvaluationResult(result); + LogAndThrowException($"Schema is not valid against the selected JSON Schema draft. Details: {details}"); } } catch (Exception ex) { - LogAndThrowException("Schema dialect evaluation failed.", ex); + LogAndThrowException($"Meta-schema evaluation failed: {ex.Message}", ex); } } @@ -80,26 +70,63 @@ public void ValidateResponseContent(string responseJson, JsonSchema requestSchem LogAndThrowException($"Failed to parse response JSON: {parseError}"); } - if (!TryNormalizeSchema(requestSchema, out var normalizedSchema, out var normalizeError)) + if (!TryNormalizeSchema(requestSchema, out var preparedSchema, out var normalizeError)) { LogAndThrowException($"Failed to normalize request schema: {normalizeError}"); } try { - var schema = JsonSchema.FromText(normalizedSchema.ToJsonString()); - - var result = schema.Evaluate(responseDoc!.RootElement, _evaluationOptions); - + var result = preparedSchema.Evaluate(responseDoc!.RootElement, new EvaluationOptions { OutputFormat = OutputFormat.List }); if (!result.IsValid) { - LogAndThrowException("Response did not validate against schema."); + var errorDetails = TrySerializeEvaluationResult(result); + LogAndThrowException($"Response did not validate against schema. Details: {errorDetails}"); } } catch (Exception ex) { - LogAndThrowException("Exception occurred during response validation.", ex); + LogAndThrowException($"Exception occurred during response validation: {ex.Message}", ex); + } + } + + private static JsonSchemaDraft ResolveDraftFromSchemaNode(JsonNode? schemaNode) + { + var declaredSchema = schemaNode?["$schema"]?.GetValue(); + if (string.Equals(declaredSchema, Draft7.MetaSchemaId, StringComparison.OrdinalIgnoreCase)) + { + return Draft7; } + + return Draft202012; + } + + private static string TrySerializeEvaluationResult(EvaluationResults result) + { + try + { + return JsonSerializer.Serialize(result); + } + catch + { + return "Unable to serialize evaluation details."; + } + } + + private static JsonSchema ResolveMetaSchema(JsonNode? schemaNode) + { + var declaredSchema = schemaNode?["$schema"]?.GetValue(); + if (string.Equals(declaredSchema, MetaSchemas.Draft7Id.OriginalString, StringComparison.OrdinalIgnoreCase)) + { + return MetaSchemas.Draft7; + } + + if (string.Equals(declaredSchema, MetaSchemas.Draft202012Id.OriginalString, StringComparison.OrdinalIgnoreCase)) + { + return MetaSchemas.Draft202012; + } + + return MetaSchemas.Draft202012; } private void LogAndThrowException(string logMessage, Exception? ex = null) @@ -137,7 +164,6 @@ private static bool TryParseSchemaNode(string schemaText, out JsonNode? node, ou { error = null; node = null; - try { node = JsonNode.Parse(schemaText); @@ -167,21 +193,28 @@ private static bool TryParseJson(string json, out JsonDocument? document, out st } } - private bool TryNormalizeSchema(JsonSchema schema, out JsonObject normalized, out string? error) + private bool TryNormalizeSchema(JsonSchema schema, out JsonSchema normalizedSchema, out string? error) { error = null; - normalized = []; + normalizedSchema = null!; try { var json = JsonSerializer.Serialize(schema, JsonSerializationOptions.SerializationWithEnum); - normalized = JsonNode.Parse(json)?.AsObject() - ?? throw new InvalidDependencyException(nameof(normalized), logger); + var normalized = JsonNode.Parse(json)?.AsObject(); EscapeJsonReferencePointers(normalized); + if (normalized == null) + { + throw new InvalidDependencyException(nameof(normalized), logger); + } + + var draft = ResolveDraftFromSchemaNode(normalized); + RemoveSchemaIds(normalized); + normalized["$schema"] = draft.MetaSchemaId; - normalized["$schema"] = ExtractDraftUri(normalized); + normalizedSchema = JsonSchema.FromText(normalized.ToJsonString()); return true; } @@ -192,19 +225,44 @@ private bool TryNormalizeSchema(JsonSchema schema, out JsonObject normalized, ou } } + private static void RemoveSchemaIds(JsonNode? currentNode) + { + switch (currentNode) + { + case JsonObject jsonObjectNode: + _ = jsonObjectNode.Remove("$id"); + + foreach (var property in jsonObjectNode.ToList()) + { + RemoveSchemaIds(property.Value); + } + + break; + + case JsonArray jsonArrayNode: + foreach (var arrayElement in jsonArrayNode) + { + RemoveSchemaIds(arrayElement); + } + + break; + } + } + private void EscapeJsonReferencePointers(JsonNode? currentNode) { switch (currentNode) { - case JsonObject obj: - ProcessJsonObjectForEscaping(obj); + case JsonObject jsonObjectNode: + ProcessJsonObjectForEscaping(jsonObjectNode); break; - case JsonArray array: - foreach (var item in array) + case JsonArray jsonArrayNode: + foreach (var arrayElement in jsonArrayNode) { - EscapeJsonReferencePointers(item); + EscapeJsonReferencePointers(arrayElement); } + break; } } @@ -212,128 +270,81 @@ private void EscapeJsonReferencePointers(JsonNode? currentNode) private void ProcessJsonObjectForEscaping(JsonObject jsonObject) { var propertiesToRename = jsonObject - .Select(p => p.Key) - .Select(name => (original: name, stripped: RemoveContextSuffix(name))) - .Where(x => x.original != x.stripped) + .Select(property => property.Key) + .Select(propertyName => (originalName: propertyName, strippedName: RemoveContextSuffix(propertyName))) + .Where(namePair => namePair.strippedName != namePair.originalName) .ToList(); - foreach (var (original, stripped) in propertiesToRename) + foreach (var (originalName, strippedName) in propertiesToRename) { - RenameJsonProperty(jsonObject, original, stripped); + RenameJsonProperty(jsonObject, originalName, strippedName); } - if (jsonObject.TryGetPropertyValue("required", out var requiredNode) && - requiredNode is JsonArray requiredArray) + if (jsonObject.TryGetPropertyValue("required", out var requiredPropertiesNode) && + requiredPropertiesNode is JsonArray requiredPropertiesArray) { - RemoveContextSuffixFromRequiredProperties(requiredArray); + RemoveContextSuffixFromRequiredProperties(requiredPropertiesArray); } foreach (var property in jsonObject.ToList()) { - if (property.Key == "$ref" && - property.Value is JsonValue value && - value.TryGetValue(out var reference)) + var propertyName = property.Key; + var propertyValue = property.Value; + + if (propertyName == "$ref" && + propertyValue is JsonValue referenceValue && + referenceValue.TryGetValue(out var referenceString) && + KnownRefPrefixes.Any(p => referenceString.StartsWith(p, StringComparison.OrdinalIgnoreCase))) { - if (reference.StartsWith(DefsPrefix, StringComparison.OrdinalIgnoreCase)) - { - jsonObject["$ref"] = BuildEscapedReferencePath(reference); - } - else if (reference.StartsWith(DefinitionsPrefix, StringComparison.OrdinalIgnoreCase)) - { - jsonObject["$ref"] = BuildEscapedReferencePath(reference); - } + jsonObject["$ref"] = BuildEscapedReferencePath(referenceString); } else { - EscapeJsonReferencePointers(property.Value); + EscapeJsonReferencePointers(propertyValue); } } } private void RemoveContextSuffixFromRequiredProperties(JsonArray requiredProperties) { - for (var i = 0; i < requiredProperties.Count; i++) + for (var index = 0; index < requiredProperties.Count; index++) { - if (requiredProperties[i]?.GetValue() is { } name) + if (requiredProperties[index]?.GetValue() is { } propertyName) { - requiredProperties[i] = RemoveContextSuffix(name); + requiredProperties[index] = RemoveContextSuffix(propertyName); } } } - private string BuildEscapedReferencePath(string reference) - { - if (reference.StartsWith(DefsPrefix, StringComparison.OrdinalIgnoreCase)) - { - return BuildEscapedReferencePath(reference, DefsPrefix); - } - - if (reference.StartsWith(DefinitionsPrefix, StringComparison.OrdinalIgnoreCase)) - { - return BuildEscapedReferencePath(reference, DefinitionsPrefix); - } - - return reference; - } - - private string BuildEscapedReferencePath(string reference, string prefix) + private string BuildEscapedReferencePath(string originalReferencePath) { - if (!reference.StartsWith(prefix, StringComparison.OrdinalIgnoreCase)) - { - return reference; - } + var prefix = KnownRefPrefixes.First(p => originalReferencePath.StartsWith(p, StringComparison.OrdinalIgnoreCase)); + var referenceWithoutPrefix = originalReferencePath[prefix.Length..]; - var body = reference[prefix.Length..]; + var strippedReference = RemoveContextSuffix(referenceWithoutPrefix); - var stripped = RemoveContextSuffix(body); + var escapedReference = strippedReference.Replace("~", "~0", StringComparison.OrdinalIgnoreCase).Replace("/", "~1", StringComparison.OrdinalIgnoreCase); - var escaped = stripped - .Replace("~", "~0", StringComparison.OrdinalIgnoreCase) - .Replace("/", "~1", StringComparison.OrdinalIgnoreCase); - - return prefix + escaped; + return prefix + escapedReference; } private string RemoveContextSuffix(string propertyName) { - var index = propertyName.IndexOf(_contextPrefix, StringComparison.Ordinal); - return index >= 0 ? propertyName[..index] : propertyName; + var suffixIndex = propertyName.IndexOf(_contextPrefix, StringComparison.Ordinal); + return suffixIndex >= 0 ? propertyName[..suffixIndex] : propertyName; } - private static void RenameJsonProperty(JsonObject jsonObject, string oldName, string newName) + private static void RenameJsonProperty(JsonObject jsonObject, string oldPropertyName, string newPropertyName) { - if (oldName == newName) + if (oldPropertyName == newPropertyName) { return; } - var value = jsonObject[oldName]; - _ = jsonObject.Remove(oldName); - jsonObject[newName] = value!; + var propertyValue = jsonObject[oldPropertyName]; + _ = jsonObject.Remove(oldPropertyName); + jsonObject[newPropertyName] = propertyValue!; } - private static string ExtractDraftUri(JsonObject schema) - { - var raw = schema.TryGetPropertyValue("$schema", out var node) && node != null - ? node.GetValue().Trim() - : null; - - return raw switch - { - Draft7Schema or Draft7SchemaHttps => Draft7Schema, - Draft201909Schema => Draft201909Schema, - Draft202012Schema => Draft202012Schema, - _ => Draft202012Schema - }; - } - - private static JsonSchema ResolveMetaSchema(string draftUri) - { - return draftUri switch - { - Draft7Schema => MetaSchemas.Draft7, - Draft201909Schema => MetaSchemas.Draft201909, - _ => MetaSchemas.Draft202012 - }; - } + private sealed record JsonSchemaDraft(string Name, string MetaSchemaId, JsonSchema MetaSchema); } diff --git a/source/AAS.TwinEngine.DataEngine/Infrastructure/Providers/PluginDataProvider/Helper/LegacyV1/LegacyDraft7JsonSchemaGenerator.cs b/source/AAS.TwinEngine.DataEngine/Infrastructure/Providers/PluginDataProvider/Helper/LegacyV1/LegacyDraft7JsonSchemaGenerator.cs new file mode 100644 index 00000000..5b0e5b9f --- /dev/null +++ b/source/AAS.TwinEngine.DataEngine/Infrastructure/Providers/PluginDataProvider/Helper/LegacyV1/LegacyDraft7JsonSchemaGenerator.cs @@ -0,0 +1,32 @@ +using AAS.TwinEngine.DataEngine.DomainModel.SubmodelRepository; + +using Json.Schema; + +namespace AAS.TwinEngine.DataEngine.Infrastructure.Providers.PluginDataProvider.Helper.LegacyV1; + +#pragma warning disable S1133 +[Obsolete("Draft-07 schema generation is deprecated and will be removed in the next major release. Use JsonSchemaDraft202012Generator instead.")] +public sealed class LegacyDraft7JsonSchemaGenerator : JsonSchemaGeneratorBase +{ + private const string Draft7RefPrefix = "#/definitions/"; + + protected override string RefPrefix => Draft7RefPrefix; + + protected override JsonSchema BuildRootSchema(SemanticTreeNode rootNode, JsonSchemaBuilder rootBuilder, Dictionary defs) + { + return new JsonSchemaBuilder() + .Schema(MetaSchemas.Draft7Id) + .Type(SchemaValueType.Object) + .Properties(new Dictionary { [rootNode.SemanticId] = rootBuilder }) + .Definitions(defs) + .Build(); + } + + protected override JsonSchemaBuilder BuildArraySchema(Dictionary children, List requiredProperties) + { + return new JsonSchemaBuilder() + .Type(SchemaValueType.Array) + .Properties(children) + .Required(requiredProperties); + } +} diff --git a/source/AAS.TwinEngine.DataEngine/Infrastructure/Providers/PluginDataProvider/Helper/LegacyV1/LegacySchemaRetryHandler.cs b/source/AAS.TwinEngine.DataEngine/Infrastructure/Providers/PluginDataProvider/Helper/LegacyV1/LegacySchemaRetryHandler.cs new file mode 100644 index 00000000..07877c7e --- /dev/null +++ b/source/AAS.TwinEngine.DataEngine/Infrastructure/Providers/PluginDataProvider/Helper/LegacyV1/LegacySchemaRetryHandler.cs @@ -0,0 +1,37 @@ +using AAS.TwinEngine.DataEngine.ApplicationLogic.Services.Plugin; +using AAS.TwinEngine.DataEngine.ApplicationLogic.Services.Plugin.Helper; +using AAS.TwinEngine.DataEngine.ApplicationLogic.Services.Plugin.Providers; +using AAS.TwinEngine.DataEngine.DomainModel.SubmodelRepository; + +namespace AAS.TwinEngine.DataEngine.Infrastructure.Providers.PluginDataProvider.Helper.LegacyV1; + +#pragma warning disable S1133 +[Obsolete("Draft-07 schema retry is deprecated and will be removed in the next major release. All plugins should support Draft 2020-12.")] +public sealed class LegacySchemaRetryHandler( + IPluginRequestBuilder pluginRequestBuilder, + IPluginDataProvider pluginDataProvider, + ILogger logger) : ILegacySchemaRetryHandler +{ + private readonly LegacyDraft7JsonSchemaGenerator _generator = new(); + + public async Task> RetryWithDraft7Async( + IDictionary semanticNodes, + string submodelId, + CancellationToken cancellationToken) + { + logger.LogWarning( + "Plugin rejected Draft 2020-12 schema (HTTP 400 or 500). Retrying with Draft-07 fallback. " + + "This is a legacy compatibility path scheduled for removal in the next major release."); + + var draft7Schemas = semanticNodes.ToDictionary( + kvp => kvp.Key, + kvp => _generator.Generate(kvp.Value)); + + var pluginRequests = pluginRequestBuilder.Build(draft7Schemas); + + return await pluginDataProvider + .GetDataForSemanticIdsAsync(pluginRequests, submodelId, cancellationToken) + .ConfigureAwait(false); + } +} +#pragma warning restore S1133 diff --git a/source/AAS.TwinEngine.DataEngine/Infrastructure/Providers/PluginDataProvider/Services/PluginDataHandler.cs b/source/AAS.TwinEngine.DataEngine/Infrastructure/Providers/PluginDataProvider/Services/PluginDataHandler.cs index 587b5574..1506c113 100644 --- a/source/AAS.TwinEngine.DataEngine/Infrastructure/Providers/PluginDataProvider/Services/PluginDataHandler.cs +++ b/source/AAS.TwinEngine.DataEngine/Infrastructure/Providers/PluginDataProvider/Services/PluginDataHandler.cs @@ -6,11 +6,11 @@ using AAS.TwinEngine.DataEngine.ApplicationLogic.Services.Plugin; using AAS.TwinEngine.DataEngine.ApplicationLogic.Services.Plugin.Helper; using AAS.TwinEngine.DataEngine.ApplicationLogic.Services.Plugin.Providers; +using AAS.TwinEngine.DataEngine.Infrastructure.Providers.PluginDataProvider.Helper; using AAS.TwinEngine.DataEngine.DomainModel.AasRegistry; using AAS.TwinEngine.DataEngine.DomainModel.AasRepository; using AAS.TwinEngine.DataEngine.DomainModel.Plugin; using AAS.TwinEngine.DataEngine.DomainModel.SubmodelRepository; -using AAS.TwinEngine.DataEngine.Infrastructure.Providers.PluginDataProvider.Helper; using AAS.TwinEngine.DataEngine.Infrastructure.Shared; using AAS.TwinEngine.DataEngine.ServiceConfiguration.Config; @@ -24,6 +24,8 @@ public class PluginDataHandler( IPluginRequestBuilder pluginRequestBuilder, IPluginDataProvider pluginDataProvider, IJsonSchemaValidator jsonSchemaValidator, + IJsonSchemaGenerator jsonSchemaGenerator, + ILegacySchemaRetryHandler legacySchemaRetryHandler, IMultiPluginDataHandler multiPluginDataHandler, ILogger logger, IOptions generalConfig) : IPluginDataHandler @@ -40,14 +42,23 @@ public async Task TryGetValuesAsync(IReadOnlyList response; + try + { + response = await pluginDataProvider.GetDataForSemanticIdsAsync(pluginRequests, submodelId, cancellationToken).ConfigureAwait(false); + } + catch (PluginSchemaRejectionException) + { + logger.LogWarning("Plugin rejected Draft 2020-12 schema. Falling back to Draft-07 compatibility mode."); + response = await legacySchemaRetryHandler.RetryWithDraft7Async(dicSemanticTreeNode, submodelId, cancellationToken).ConfigureAwait(false); + } var result = new List(); @@ -55,8 +66,8 @@ public async Task TryGetValuesAsync(IReadOnlyList ProcessResponseAsync(HttpResponseMessage respons switch (response.StatusCode) { - case System.Net.HttpStatusCode.NotFound: + case HttpStatusCode.NotFound: logger.LogError("Requested resource could not be found. Endpoint: {Url}", url); throw new ResourceNotFoundException(); - case System.Net.HttpStatusCode.Unauthorized: - case System.Net.HttpStatusCode.Forbidden: + case HttpStatusCode.Unauthorized: + case HttpStatusCode.Forbidden: logger.LogError("Unauthorized access. Endpoint: {Url}", url); throw new UnauthorizedAccessException(); + case HttpStatusCode.BadRequest: + logger.LogWarning("Plugin rejected request schema (HTTP 400). Endpoint: {Url}", url); + throw new PluginSchemaRejectionException(); + + case HttpStatusCode.InternalServerError: + logger.LogWarning("Plugin failed to process request schema (HTTP 500). Endpoint: {Url}", url); + throw new PluginSchemaRejectionException(); + default: logger.LogError("Invalid response format. Endpoint: {Url}", url); throw new ResponseParsingException(); diff --git a/source/AAS.TwinEngine.DataEngine/Program.cs b/source/AAS.TwinEngine.DataEngine/Program.cs index 9aed68d8..73c4ed29 100644 --- a/source/AAS.TwinEngine.DataEngine/Program.cs +++ b/source/AAS.TwinEngine.DataEngine/Program.cs @@ -45,10 +45,7 @@ public static async Task Main(string[] args) _ = builder.Services.AddAuthorization(); _ = builder.Services.AddControllers() - .AddJsonOptions(options => - { - options.JsonSerializerOptions.Converters.Add(new JsonStringEnumConverter()); - }); + .AddJsonOptions(options => options.JsonSerializerOptions.Converters.Add(new JsonStringEnumConverter())); _ = builder.Services.AddEndpointsApiExplorer(); _ = builder.Services.AddOpenApiDocument(settings => diff --git a/source/AAS.TwinEngine.DataEngine/ServiceConfiguration/InfrastructureDependencyInjectionExtensions.cs b/source/AAS.TwinEngine.DataEngine/ServiceConfiguration/InfrastructureDependencyInjectionExtensions.cs index 79d9a164..8ee34e65 100644 --- a/source/AAS.TwinEngine.DataEngine/ServiceConfiguration/InfrastructureDependencyInjectionExtensions.cs +++ b/source/AAS.TwinEngine.DataEngine/ServiceConfiguration/InfrastructureDependencyInjectionExtensions.cs @@ -11,6 +11,7 @@ using AAS.TwinEngine.DataEngine.Infrastructure.Monitoring; using AAS.TwinEngine.DataEngine.Infrastructure.Providers.AasRegistryProvider.Services; using AAS.TwinEngine.DataEngine.Infrastructure.Providers.PluginDataProvider.Helper; +using AAS.TwinEngine.DataEngine.Infrastructure.Providers.PluginDataProvider.Helper.LegacyV1; using AAS.TwinEngine.DataEngine.Infrastructure.Providers.PluginDataProvider.Services; using AAS.TwinEngine.DataEngine.Infrastructure.Providers.SubmodelRegistryProvider.Services; using AAS.TwinEngine.DataEngine.Infrastructure.Providers.TemplateProvider.Services; @@ -121,6 +122,10 @@ public static void ConfigureInfrastructure(this IServiceCollection services, ICo _ = services.AddScoped(); _ = services.AddScoped(); _ = services.AddScoped(); + _ = services.AddScoped(); +#pragma warning disable CS0618 // Obsolete — intentional legacy Draft-07 fallback registration + _ = services.AddScoped(); +#pragma warning restore CS0618 _ = services.AddScoped(); _ = services.AddScoped(); _ = services.AddScoped(); diff --git a/source/AAS.TwinEngine.Plugin.TestPlugin.PlaywrightTests/SubmodelRepository/TestData/GetSubmodelElement_ContactInfo_ContactInformation_Expected.json b/source/AAS.TwinEngine.Plugin.TestPlugin.PlaywrightTests/SubmodelRepository/TestData/GetSubmodelElement_ContactInfo_ContactInformation_Expected.json index b6135f12..2e628d84 100644 --- a/source/AAS.TwinEngine.Plugin.TestPlugin.PlaywrightTests/SubmodelRepository/TestData/GetSubmodelElement_ContactInfo_ContactInformation_Expected.json +++ b/source/AAS.TwinEngine.Plugin.TestPlugin.PlaywrightTests/SubmodelRepository/TestData/GetSubmodelElement_ContactInfo_ContactInformation_Expected.json @@ -245,13 +245,13 @@ "modelType": "MultiLanguageProperty" }, { - "idShort": "Phone", + "idShort": "Email", "semanticId": { "type": "ExternalReference", "keys": [ { "type": "GlobalReference", - "value": "https://admin-shell.io/zvei/nameplate/1/0/ContactInformations/ContactInformation/Phone" + "value": "0173-1#02-AAQ836#005" } ] }, @@ -265,19 +265,13 @@ ], "value": [ { - "idShort": "TelephoneNumber", - "description": [ - { - "language": "en", - "text": "Recommendation: property declaration as MLP is required by its semantic definition. As the property value is language independent, users are recommended to provide maximal 1 string in any language of the user’s choice." - } - ], + "idShort": "EmailAddress", "semanticId": { "type": "ExternalReference", "keys": [ { "type": "GlobalReference", - "value": "0173-1#02-AAO136#002" + "value": "0173-1#02-AAO198#002" } ] }, @@ -289,22 +283,24 @@ "value": "One" } ], - "value": [ + "valueType": "xs:string", + "value": "john.doe@example.com", + "modelType": "Property" + }, + { + "idShort": "TypeOfEmailAddress", + "description": [ { "language": "en", - "text": "345-678-9012" + "text": "enumeration: 0173-1#07-AAS754#001 (office), 0173-1#07-AAS756#001 (secretary), 0173-1#07-AAS757#001 (substitute), 0173-1#07-AAS758#001 (home)" } ], - "modelType": "MultiLanguageProperty" - }, - { - "idShort": "AvailableTime", "semanticId": { "type": "ExternalReference", "keys": [ { "type": "GlobalReference", - "value": "https://admin-shell.io/zvei/nameplate/1/0/ContactInformations/ContactInformation/AvailableTime/" + "value": "0173-1#02-AAO199#003" } ] }, @@ -316,28 +312,87 @@ "value": "ZeroToOne" } ], - "value": [ + "valueType": "xs:string", + "value": "Work", + "modelType": "Property" + } + ], + "modelType": "SubmodelElementCollection" + }, + { + "idShort": "IPCommunication__00__", + "semanticId": { + "type": "ExternalReference", + "keys": [ + { + "type": "GlobalReference", + "value": "https://admin-shell.io/zvei/nameplate/1/0/ContactInformations/" + } + ] + }, + "qualifiers": [ + { + "kind": "ConceptQualifier", + "type": "Multiplicity", + "valueType": "xs:string", + "value": "ZeroToMany" + } + ], + "value": [ + { + "idShort": "AddressOfAdditionalLink", + "semanticId": { + "type": "ExternalReference", + "keys": [ + { + "type": "GlobalReference", + "value": "0173-1#02-AAQ326#002" + } + ] + }, + "qualifiers": [ { - "language": "de", - "text": "Montags, 10-16 Uhr" + "kind": "ConceptQualifier", + "type": "Multiplicity", + "valueType": "xs:string", + "value": "One" } ], - "modelType": "MultiLanguageProperty" + "valueType": "xs:string", + "value": "https://www.mm-software.com/more-the-newsroom/", + "modelType": "Property" }, { - "idShort": "TypeOfTelephone", - "description": [ + "idShort": "TypeOfCommunication", + "semanticId": { + "type": "ExternalReference", + "keys": [ + { + "type": "GlobalReference", + "value": "https://admin-shell.io/zvei/nameplate/1/0/ContactInformations/ContactInformation/IPCommunication/TypeOfCommunication" + } + ] + }, + "qualifiers": [ { - "language": "en", - "text": " enumeration: 0173-1#07-AAS754#001 (office), 0173-1#07-AAS755#001 (office mobile), 0173-1#07-AAS756#001 (secretary), 0173-1#07-AAS757#001 (substitute), 0173-1#07-AAS758#001 (home), 0173-1#07-AAS759#001 (private mobile)" + "kind": "ConceptQualifier", + "type": "Multiplicity", + "valueType": "xs:string", + "value": "ZeroToOne" } ], + "valueType": "xs:string", + "value": "Chat", + "modelType": "Property" + }, + { + "idShort": "AvailableTime", "semanticId": { "type": "ExternalReference", "keys": [ { "type": "GlobalReference", - "value": "0173-1#02-AAO137#003" + "value": "https://admin-shell.io/zvei/nameplate/1/0/ContactInformations/ContactInformation/AvailableTime/" } ] }, @@ -349,21 +404,25 @@ "value": "ZeroToOne" } ], - "valueType": "xs:string", - "value": "Mobile", - "modelType": "Property" + "value": [ + { + "language": "de", + "text": "8:00 AM to 6:00 PM" + } + ], + "modelType": "MultiLanguageProperty" } ], "modelType": "SubmodelElementCollection" }, { - "idShort": "Email", + "idShort": "Phone0", "semanticId": { "type": "ExternalReference", "keys": [ { "type": "GlobalReference", - "value": "0173-1#02-AAQ836#005" + "value": "https://admin-shell.io/zvei/nameplate/1/0/ContactInformations/ContactInformation/Phone" } ] }, @@ -377,13 +436,19 @@ ], "value": [ { - "idShort": "EmailAddress", + "idShort": "TelephoneNumber", + "description": [ + { + "language": "en", + "text": "Recommendation: property declaration as MLP is required by its semantic definition. As the property value is language independent, users are recommended to provide maximal 1 string in any language of the user’s choice." + } + ], "semanticId": { "type": "ExternalReference", "keys": [ { "type": "GlobalReference", - "value": "0173-1#02-AAO198#002" + "value": "0173-1#02-AAO136#002" } ] }, @@ -395,16 +460,47 @@ "value": "One" } ], - "valueType": "xs:string", - "value": "john.doe@example.com", - "modelType": "Property" + "value": [ + { + "language": "en", + "text": "234-567-8901" + } + ], + "modelType": "MultiLanguageProperty" }, { - "idShort": "TypeOfEmailAddress", + "idShort": "AvailableTime", + "semanticId": { + "type": "ExternalReference", + "keys": [ + { + "type": "GlobalReference", + "value": "https://admin-shell.io/zvei/nameplate/1/0/ContactInformations/ContactInformation/AvailableTime/" + } + ] + }, + "qualifiers": [ + { + "kind": "ConceptQualifier", + "type": "Multiplicity", + "valueType": "xs:string", + "value": "ZeroToOne" + } + ], + "value": [ + { + "language": "de", + "text": "Dienstags bis Donnerstags, 9-17 Uhr" + } + ], + "modelType": "MultiLanguageProperty" + }, + { + "idShort": "TypeOfTelephone", "description": [ { "language": "en", - "text": "enumeration: 0173-1#07-AAS754#001 (office), 0173-1#07-AAS756#001 (secretary), 0173-1#07-AAS757#001 (substitute), 0173-1#07-AAS758#001 (home)" + "text": " enumeration: 0173-1#07-AAS754#001 (office), 0173-1#07-AAS755#001 (office mobile), 0173-1#07-AAS756#001 (secretary), 0173-1#07-AAS757#001 (substitute), 0173-1#07-AAS758#001 (home), 0173-1#07-AAS759#001 (private mobile)" } ], "semanticId": { @@ -412,7 +508,7 @@ "keys": [ { "type": "GlobalReference", - "value": "0173-1#02-AAO199#003" + "value": "0173-1#02-AAO137#003" } ] }, @@ -425,20 +521,20 @@ } ], "valueType": "xs:string", - "value": "Work", + "value": "Office", "modelType": "Property" } ], "modelType": "SubmodelElementCollection" }, { - "idShort": "IPCommunication__00__", + "idShort": "Phone1", "semanticId": { "type": "ExternalReference", "keys": [ { "type": "GlobalReference", - "value": "https://admin-shell.io/zvei/nameplate/1/0/ContactInformations/" + "value": "https://admin-shell.io/zvei/nameplate/1/0/ContactInformations/ContactInformation/Phone" } ] }, @@ -447,18 +543,24 @@ "kind": "ConceptQualifier", "type": "Multiplicity", "valueType": "xs:string", - "value": "ZeroToMany" + "value": "ZeroToOne" } ], "value": [ { - "idShort": "AddressOfAdditionalLink", + "idShort": "TelephoneNumber", + "description": [ + { + "language": "en", + "text": "Recommendation: property declaration as MLP is required by its semantic definition. As the property value is language independent, users are recommended to provide maximal 1 string in any language of the user’s choice." + } + ], "semanticId": { "type": "ExternalReference", "keys": [ { "type": "GlobalReference", - "value": "0173-1#02-AAQ326#002" + "value": "0173-1#02-AAO136#002" } ] }, @@ -470,18 +572,22 @@ "value": "One" } ], - "valueType": "xs:string", - "value": "https://www.mm-software.com/more-the-newsroom/", - "modelType": "Property" + "value": [ + { + "language": "en", + "text": "345-678-9012" + } + ], + "modelType": "MultiLanguageProperty" }, { - "idShort": "TypeOfCommunication", + "idShort": "AvailableTime", "semanticId": { "type": "ExternalReference", "keys": [ { "type": "GlobalReference", - "value": "https://admin-shell.io/zvei/nameplate/1/0/ContactInformations/ContactInformation/IPCommunication/TypeOfCommunication" + "value": "https://admin-shell.io/zvei/nameplate/1/0/ContactInformations/ContactInformation/AvailableTime/" } ] }, @@ -493,18 +599,28 @@ "value": "ZeroToOne" } ], - "valueType": "xs:string", - "value": "Chat", - "modelType": "Property" + "value": [ + { + "language": "de", + "text": "Montags, 10-16 Uhr" + } + ], + "modelType": "MultiLanguageProperty" }, { - "idShort": "AvailableTime", + "idShort": "TypeOfTelephone", + "description": [ + { + "language": "en", + "text": " enumeration: 0173-1#07-AAS754#001 (office), 0173-1#07-AAS755#001 (office mobile), 0173-1#07-AAS756#001 (secretary), 0173-1#07-AAS757#001 (substitute), 0173-1#07-AAS758#001 (home), 0173-1#07-AAS759#001 (private mobile)" + } + ], "semanticId": { "type": "ExternalReference", "keys": [ { "type": "GlobalReference", - "value": "https://admin-shell.io/zvei/nameplate/1/0/ContactInformations/ContactInformation/AvailableTime/" + "value": "0173-1#02-AAO137#003" } ] }, @@ -516,13 +632,9 @@ "value": "ZeroToOne" } ], - "value": [ - { - "language": "de", - "text": "8:00 AM to 6:00 PM" - } - ], - "modelType": "MultiLanguageProperty" + "valueType": "xs:string", + "value": "Mobile", + "modelType": "Property" } ], "modelType": "SubmodelElementCollection" diff --git a/source/AAS.TwinEngine.Plugin.TestPlugin.PlaywrightTests/SubmodelRepository/TestData/GetSubmodel_ContactInfo_Expected.json b/source/AAS.TwinEngine.Plugin.TestPlugin.PlaywrightTests/SubmodelRepository/TestData/GetSubmodel_ContactInfo_Expected.json index 28e1f623..249204e2 100644 --- a/source/AAS.TwinEngine.Plugin.TestPlugin.PlaywrightTests/SubmodelRepository/TestData/GetSubmodel_ContactInfo_Expected.json +++ b/source/AAS.TwinEngine.Plugin.TestPlugin.PlaywrightTests/SubmodelRepository/TestData/GetSubmodel_ContactInfo_Expected.json @@ -259,13 +259,13 @@ "modelType": "MultiLanguageProperty" }, { - "idShort": "Phone", + "idShort": "Email", "semanticId": { "type": "ExternalReference", "keys": [ { "type": "GlobalReference", - "value": "https://admin-shell.io/zvei/nameplate/1/0/ContactInformations/ContactInformation/Phone" + "value": "0173-1#02-AAQ836#005" } ] }, @@ -279,19 +279,13 @@ ], "value": [ { - "idShort": "TelephoneNumber", - "description": [ - { - "language": "en", - "text": "Recommendation: property declaration as MLP is required by its semantic definition. As the property value is language independent, users are recommended to provide maximal 1 string in any language of the user’s choice." - } - ], + "idShort": "EmailAddress", "semanticId": { "type": "ExternalReference", "keys": [ { "type": "GlobalReference", - "value": "0173-1#02-AAO136#002" + "value": "0173-1#02-AAO198#002" } ] }, @@ -303,22 +297,24 @@ "value": "One" } ], - "value": [ + "valueType": "xs:string", + "value": "john.doe@example.com", + "modelType": "Property" + }, + { + "idShort": "TypeOfEmailAddress", + "description": [ { "language": "en", - "text": "234-567-8901" + "text": "enumeration: 0173-1#07-AAS754#001 (office), 0173-1#07-AAS756#001 (secretary), 0173-1#07-AAS757#001 (substitute), 0173-1#07-AAS758#001 (home)" } ], - "modelType": "MultiLanguageProperty" - }, - { - "idShort": "AvailableTime", "semanticId": { "type": "ExternalReference", "keys": [ { "type": "GlobalReference", - "value": "https://admin-shell.io/zvei/nameplate/1/0/ContactInformations/ContactInformation/AvailableTime/" + "value": "0173-1#02-AAO199#003" } ] }, @@ -330,28 +326,87 @@ "value": "ZeroToOne" } ], - "value": [ + "valueType": "xs:string", + "value": "Work", + "modelType": "Property" + } + ], + "modelType": "SubmodelElementCollection" + }, + { + "idShort": "IPCommunication__00__", + "semanticId": { + "type": "ExternalReference", + "keys": [ + { + "type": "GlobalReference", + "value": "https://admin-shell.io/zvei/nameplate/1/0/ContactInformations/" + } + ] + }, + "qualifiers": [ + { + "kind": "ConceptQualifier", + "type": "Multiplicity", + "valueType": "xs:string", + "value": "ZeroToMany" + } + ], + "value": [ + { + "idShort": "AddressOfAdditionalLink", + "semanticId": { + "type": "ExternalReference", + "keys": [ + { + "type": "GlobalReference", + "value": "0173-1#02-AAQ326#002" + } + ] + }, + "qualifiers": [ { - "language": "de", - "text": "Dienstags bis Donnerstags, 9-17 Uhr" + "kind": "ConceptQualifier", + "type": "Multiplicity", + "valueType": "xs:string", + "value": "One" } ], - "modelType": "MultiLanguageProperty" + "valueType": "xs:string", + "value": "https://www.mm-software.com/more-the-newsroom/", + "modelType": "Property" }, { - "idShort": "TypeOfTelephone", - "description": [ + "idShort": "TypeOfCommunication", + "semanticId": { + "type": "ExternalReference", + "keys": [ + { + "type": "GlobalReference", + "value": "https://admin-shell.io/zvei/nameplate/1/0/ContactInformations/ContactInformation/IPCommunication/TypeOfCommunication" + } + ] + }, + "qualifiers": [ { - "language": "en", - "text": " enumeration: 0173-1#07-AAS754#001 (office), 0173-1#07-AAS755#001 (office mobile), 0173-1#07-AAS756#001 (secretary), 0173-1#07-AAS757#001 (substitute), 0173-1#07-AAS758#001 (home), 0173-1#07-AAS759#001 (private mobile)" + "kind": "ConceptQualifier", + "type": "Multiplicity", + "valueType": "xs:string", + "value": "ZeroToOne" } ], + "valueType": "xs:string", + "value": "Chat", + "modelType": "Property" + }, + { + "idShort": "AvailableTime", "semanticId": { "type": "ExternalReference", "keys": [ { "type": "GlobalReference", - "value": "0173-1#02-AAO137#003" + "value": "https://admin-shell.io/zvei/nameplate/1/0/ContactInformations/ContactInformation/AvailableTime/" } ] }, @@ -363,21 +418,25 @@ "value": "ZeroToOne" } ], - "valueType": "xs:string", - "value": "Office", - "modelType": "Property" + "value": [ + { + "language": "de", + "text": "8:00 AM to 6:00 PM" + } + ], + "modelType": "MultiLanguageProperty" } ], "modelType": "SubmodelElementCollection" }, { - "idShort": "Email", + "idShort": "Phone0", "semanticId": { "type": "ExternalReference", "keys": [ { "type": "GlobalReference", - "value": "0173-1#02-AAQ836#005" + "value": "https://admin-shell.io/zvei/nameplate/1/0/ContactInformations/ContactInformation/Phone" } ] }, @@ -391,13 +450,19 @@ ], "value": [ { - "idShort": "EmailAddress", + "idShort": "TelephoneNumber", + "description": [ + { + "language": "en", + "text": "Recommendation: property declaration as MLP is required by its semantic definition. As the property value is language independent, users are recommended to provide maximal 1 string in any language of the user’s choice." + } + ], "semanticId": { "type": "ExternalReference", "keys": [ { "type": "GlobalReference", - "value": "0173-1#02-AAO198#002" + "value": "0173-1#02-AAO136#002" } ] }, @@ -409,16 +474,47 @@ "value": "One" } ], - "valueType": "xs:string", - "value": "john.doe@example.com", - "modelType": "Property" + "value": [ + { + "language": "en", + "text": "234-567-8901" + } + ], + "modelType": "MultiLanguageProperty" }, { - "idShort": "TypeOfEmailAddress", + "idShort": "AvailableTime", + "semanticId": { + "type": "ExternalReference", + "keys": [ + { + "type": "GlobalReference", + "value": "https://admin-shell.io/zvei/nameplate/1/0/ContactInformations/ContactInformation/AvailableTime/" + } + ] + }, + "qualifiers": [ + { + "kind": "ConceptQualifier", + "type": "Multiplicity", + "valueType": "xs:string", + "value": "ZeroToOne" + } + ], + "value": [ + { + "language": "de", + "text": "Dienstags bis Donnerstags, 9-17 Uhr" + } + ], + "modelType": "MultiLanguageProperty" + }, + { + "idShort": "TypeOfTelephone", "description": [ { "language": "en", - "text": "enumeration: 0173-1#07-AAS754#001 (office), 0173-1#07-AAS756#001 (secretary), 0173-1#07-AAS757#001 (substitute), 0173-1#07-AAS758#001 (home)" + "text": " enumeration: 0173-1#07-AAS754#001 (office), 0173-1#07-AAS755#001 (office mobile), 0173-1#07-AAS756#001 (secretary), 0173-1#07-AAS757#001 (substitute), 0173-1#07-AAS758#001 (home), 0173-1#07-AAS759#001 (private mobile)" } ], "semanticId": { @@ -426,7 +522,7 @@ "keys": [ { "type": "GlobalReference", - "value": "0173-1#02-AAO199#003" + "value": "0173-1#02-AAO137#003" } ] }, @@ -439,20 +535,20 @@ } ], "valueType": "xs:string", - "value": "Work", + "value": "Office", "modelType": "Property" } ], "modelType": "SubmodelElementCollection" }, { - "idShort": "IPCommunication__00__", + "idShort": "Phone1", "semanticId": { "type": "ExternalReference", "keys": [ { "type": "GlobalReference", - "value": "https://admin-shell.io/zvei/nameplate/1/0/ContactInformations/" + "value": "https://admin-shell.io/zvei/nameplate/1/0/ContactInformations/ContactInformation/Phone" } ] }, @@ -461,18 +557,24 @@ "kind": "ConceptQualifier", "type": "Multiplicity", "valueType": "xs:string", - "value": "ZeroToMany" + "value": "ZeroToOne" } ], "value": [ { - "idShort": "AddressOfAdditionalLink", + "idShort": "TelephoneNumber", + "description": [ + { + "language": "en", + "text": "Recommendation: property declaration as MLP is required by its semantic definition. As the property value is language independent, users are recommended to provide maximal 1 string in any language of the user’s choice." + } + ], "semanticId": { "type": "ExternalReference", "keys": [ { "type": "GlobalReference", - "value": "0173-1#02-AAQ326#002" + "value": "0173-1#02-AAO136#002" } ] }, @@ -484,18 +586,22 @@ "value": "One" } ], - "valueType": "xs:string", - "value": "https://www.mm-software.com/more-the-newsroom/", - "modelType": "Property" + "value": [ + { + "language": "en", + "text": "345-678-9012" + } + ], + "modelType": "MultiLanguageProperty" }, { - "idShort": "TypeOfCommunication", + "idShort": "AvailableTime", "semanticId": { "type": "ExternalReference", "keys": [ { "type": "GlobalReference", - "value": "https://admin-shell.io/zvei/nameplate/1/0/ContactInformations/ContactInformation/IPCommunication/TypeOfCommunication" + "value": "https://admin-shell.io/zvei/nameplate/1/0/ContactInformations/ContactInformation/AvailableTime/" } ] }, @@ -507,18 +613,28 @@ "value": "ZeroToOne" } ], - "valueType": "xs:string", - "value": "Chat", - "modelType": "Property" + "value": [ + { + "language": "de", + "text": "Montags, 10-16 Uhr" + } + ], + "modelType": "MultiLanguageProperty" }, { - "idShort": "AvailableTime", + "idShort": "TypeOfTelephone", + "description": [ + { + "language": "en", + "text": " enumeration: 0173-1#07-AAS754#001 (office), 0173-1#07-AAS755#001 (office mobile), 0173-1#07-AAS756#001 (secretary), 0173-1#07-AAS757#001 (substitute), 0173-1#07-AAS758#001 (home), 0173-1#07-AAS759#001 (private mobile)" + } + ], "semanticId": { "type": "ExternalReference", "keys": [ { "type": "GlobalReference", - "value": "https://admin-shell.io/zvei/nameplate/1/0/ContactInformations/ContactInformation/AvailableTime/" + "value": "0173-1#02-AAO137#003" } ] }, @@ -530,13 +646,9 @@ "value": "ZeroToOne" } ], - "value": [ - { - "language": "de", - "text": "8:00 AM to 6:00 PM" - } - ], - "modelType": "MultiLanguageProperty" + "valueType": "xs:string", + "value": "Mobile", + "modelType": "Property" } ], "modelType": "SubmodelElementCollection" @@ -791,13 +903,13 @@ "modelType": "MultiLanguageProperty" }, { - "idShort": "Phone", + "idShort": "Email", "semanticId": { "type": "ExternalReference", "keys": [ { "type": "GlobalReference", - "value": "https://admin-shell.io/zvei/nameplate/1/0/ContactInformations/ContactInformation/Phone" + "value": "0173-1#02-AAQ836#005" } ] }, @@ -811,19 +923,13 @@ ], "value": [ { - "idShort": "TelephoneNumber", - "description": [ - { - "language": "en", - "text": "Recommendation: property declaration as MLP is required by its semantic definition. As the property value is language independent, users are recommended to provide maximal 1 string in any language of the user’s choice." - } - ], + "idShort": "EmailAddress", "semanticId": { "type": "ExternalReference", "keys": [ { "type": "GlobalReference", - "value": "0173-1#02-AAO136#002" + "value": "0173-1#02-AAO198#002" } ] }, @@ -835,22 +941,24 @@ "value": "One" } ], - "value": [ + "valueType": "xs:string", + "value": "john.doe@example.com", + "modelType": "Property" + }, + { + "idShort": "TypeOfEmailAddress", + "description": [ { "language": "en", - "text": "345-678-9012" + "text": "enumeration: 0173-1#07-AAS754#001 (office), 0173-1#07-AAS756#001 (secretary), 0173-1#07-AAS757#001 (substitute), 0173-1#07-AAS758#001 (home)" } ], - "modelType": "MultiLanguageProperty" - }, - { - "idShort": "AvailableTime", "semanticId": { "type": "ExternalReference", "keys": [ { "type": "GlobalReference", - "value": "https://admin-shell.io/zvei/nameplate/1/0/ContactInformations/ContactInformation/AvailableTime/" + "value": "0173-1#02-AAO199#003" } ] }, @@ -862,28 +970,87 @@ "value": "ZeroToOne" } ], - "value": [ + "valueType": "xs:string", + "value": "Work", + "modelType": "Property" + } + ], + "modelType": "SubmodelElementCollection" + }, + { + "idShort": "IPCommunication__00__", + "semanticId": { + "type": "ExternalReference", + "keys": [ + { + "type": "GlobalReference", + "value": "https://admin-shell.io/zvei/nameplate/1/0/ContactInformations/" + } + ] + }, + "qualifiers": [ + { + "kind": "ConceptQualifier", + "type": "Multiplicity", + "valueType": "xs:string", + "value": "ZeroToMany" + } + ], + "value": [ + { + "idShort": "AddressOfAdditionalLink", + "semanticId": { + "type": "ExternalReference", + "keys": [ + { + "type": "GlobalReference", + "value": "0173-1#02-AAQ326#002" + } + ] + }, + "qualifiers": [ { - "language": "de", - "text": "Montags, 10-16 Uhr" + "kind": "ConceptQualifier", + "type": "Multiplicity", + "valueType": "xs:string", + "value": "One" } ], - "modelType": "MultiLanguageProperty" + "valueType": "xs:string", + "value": "https://www.mm-software.com/more-the-newsroom/", + "modelType": "Property" }, { - "idShort": "TypeOfTelephone", - "description": [ + "idShort": "TypeOfCommunication", + "semanticId": { + "type": "ExternalReference", + "keys": [ + { + "type": "GlobalReference", + "value": "https://admin-shell.io/zvei/nameplate/1/0/ContactInformations/ContactInformation/IPCommunication/TypeOfCommunication" + } + ] + }, + "qualifiers": [ { - "language": "en", - "text": " enumeration: 0173-1#07-AAS754#001 (office), 0173-1#07-AAS755#001 (office mobile), 0173-1#07-AAS756#001 (secretary), 0173-1#07-AAS757#001 (substitute), 0173-1#07-AAS758#001 (home), 0173-1#07-AAS759#001 (private mobile)" + "kind": "ConceptQualifier", + "type": "Multiplicity", + "valueType": "xs:string", + "value": "ZeroToOne" } ], + "valueType": "xs:string", + "value": "Chat", + "modelType": "Property" + }, + { + "idShort": "AvailableTime", "semanticId": { "type": "ExternalReference", "keys": [ { "type": "GlobalReference", - "value": "0173-1#02-AAO137#003" + "value": "https://admin-shell.io/zvei/nameplate/1/0/ContactInformations/ContactInformation/AvailableTime/" } ] }, @@ -895,21 +1062,25 @@ "value": "ZeroToOne" } ], - "valueType": "xs:string", - "value": "Mobile", - "modelType": "Property" + "value": [ + { + "language": "de", + "text": "8:00 AM to 6:00 PM" + } + ], + "modelType": "MultiLanguageProperty" } ], "modelType": "SubmodelElementCollection" }, { - "idShort": "Email", + "idShort": "Phone0", "semanticId": { "type": "ExternalReference", "keys": [ { "type": "GlobalReference", - "value": "0173-1#02-AAQ836#005" + "value": "https://admin-shell.io/zvei/nameplate/1/0/ContactInformations/ContactInformation/Phone" } ] }, @@ -923,13 +1094,19 @@ ], "value": [ { - "idShort": "EmailAddress", + "idShort": "TelephoneNumber", + "description": [ + { + "language": "en", + "text": "Recommendation: property declaration as MLP is required by its semantic definition. As the property value is language independent, users are recommended to provide maximal 1 string in any language of the user’s choice." + } + ], "semanticId": { "type": "ExternalReference", "keys": [ { "type": "GlobalReference", - "value": "0173-1#02-AAO198#002" + "value": "0173-1#02-AAO136#002" } ] }, @@ -941,16 +1118,47 @@ "value": "One" } ], - "valueType": "xs:string", - "value": "john.doe@example.com", - "modelType": "Property" + "value": [ + { + "language": "en", + "text": "234-567-8901" + } + ], + "modelType": "MultiLanguageProperty" }, { - "idShort": "TypeOfEmailAddress", + "idShort": "AvailableTime", + "semanticId": { + "type": "ExternalReference", + "keys": [ + { + "type": "GlobalReference", + "value": "https://admin-shell.io/zvei/nameplate/1/0/ContactInformations/ContactInformation/AvailableTime/" + } + ] + }, + "qualifiers": [ + { + "kind": "ConceptQualifier", + "type": "Multiplicity", + "valueType": "xs:string", + "value": "ZeroToOne" + } + ], + "value": [ + { + "language": "de", + "text": "Dienstags bis Donnerstags, 9-17 Uhr" + } + ], + "modelType": "MultiLanguageProperty" + }, + { + "idShort": "TypeOfTelephone", "description": [ { "language": "en", - "text": "enumeration: 0173-1#07-AAS754#001 (office), 0173-1#07-AAS756#001 (secretary), 0173-1#07-AAS757#001 (substitute), 0173-1#07-AAS758#001 (home)" + "text": " enumeration: 0173-1#07-AAS754#001 (office), 0173-1#07-AAS755#001 (office mobile), 0173-1#07-AAS756#001 (secretary), 0173-1#07-AAS757#001 (substitute), 0173-1#07-AAS758#001 (home), 0173-1#07-AAS759#001 (private mobile)" } ], "semanticId": { @@ -958,7 +1166,7 @@ "keys": [ { "type": "GlobalReference", - "value": "0173-1#02-AAO199#003" + "value": "0173-1#02-AAO137#003" } ] }, @@ -971,20 +1179,20 @@ } ], "valueType": "xs:string", - "value": "Work", + "value": "Office", "modelType": "Property" } ], "modelType": "SubmodelElementCollection" }, { - "idShort": "IPCommunication__00__", + "idShort": "Phone1", "semanticId": { "type": "ExternalReference", "keys": [ { "type": "GlobalReference", - "value": "https://admin-shell.io/zvei/nameplate/1/0/ContactInformations/" + "value": "https://admin-shell.io/zvei/nameplate/1/0/ContactInformations/ContactInformation/Phone" } ] }, @@ -993,18 +1201,24 @@ "kind": "ConceptQualifier", "type": "Multiplicity", "valueType": "xs:string", - "value": "ZeroToMany" + "value": "ZeroToOne" } ], "value": [ { - "idShort": "AddressOfAdditionalLink", + "idShort": "TelephoneNumber", + "description": [ + { + "language": "en", + "text": "Recommendation: property declaration as MLP is required by its semantic definition. As the property value is language independent, users are recommended to provide maximal 1 string in any language of the user’s choice." + } + ], "semanticId": { "type": "ExternalReference", "keys": [ { "type": "GlobalReference", - "value": "0173-1#02-AAQ326#002" + "value": "0173-1#02-AAO136#002" } ] }, @@ -1016,18 +1230,22 @@ "value": "One" } ], - "valueType": "xs:string", - "value": "https://www.mm-software.com/more-the-newsroom/", - "modelType": "Property" + "value": [ + { + "language": "en", + "text": "345-678-9012" + } + ], + "modelType": "MultiLanguageProperty" }, { - "idShort": "TypeOfCommunication", + "idShort": "AvailableTime", "semanticId": { "type": "ExternalReference", "keys": [ { "type": "GlobalReference", - "value": "https://admin-shell.io/zvei/nameplate/1/0/ContactInformations/ContactInformation/IPCommunication/TypeOfCommunication" + "value": "https://admin-shell.io/zvei/nameplate/1/0/ContactInformations/ContactInformation/AvailableTime/" } ] }, @@ -1039,18 +1257,28 @@ "value": "ZeroToOne" } ], - "valueType": "xs:string", - "value": "Chat", - "modelType": "Property" + "value": [ + { + "language": "de", + "text": "Montags, 10-16 Uhr" + } + ], + "modelType": "MultiLanguageProperty" }, { - "idShort": "AvailableTime", + "idShort": "TypeOfTelephone", + "description": [ + { + "language": "en", + "text": " enumeration: 0173-1#07-AAS754#001 (office), 0173-1#07-AAS755#001 (office mobile), 0173-1#07-AAS756#001 (secretary), 0173-1#07-AAS757#001 (substitute), 0173-1#07-AAS758#001 (home), 0173-1#07-AAS759#001 (private mobile)" + } + ], "semanticId": { "type": "ExternalReference", "keys": [ { "type": "GlobalReference", - "value": "https://admin-shell.io/zvei/nameplate/1/0/ContactInformations/ContactInformation/AvailableTime/" + "value": "0173-1#02-AAO137#003" } ] }, @@ -1062,13 +1290,9 @@ "value": "ZeroToOne" } ], - "value": [ - { - "language": "de", - "text": "8:00 AM to 6:00 PM" - } - ], - "modelType": "MultiLanguageProperty" + "valueType": "xs:string", + "value": "Mobile", + "modelType": "Property" } ], "modelType": "SubmodelElementCollection" diff --git a/source/AAS.TwinEngine.Plugin.TestPlugin.UnitTests/AAS.TwinEngine.Plugin.TestPlugin.UnitTests.csproj b/source/AAS.TwinEngine.Plugin.TestPlugin.UnitTests/AAS.TwinEngine.Plugin.TestPlugin.UnitTests.csproj index 8e1a44c8..34751550 100644 --- a/source/AAS.TwinEngine.Plugin.TestPlugin.UnitTests/AAS.TwinEngine.Plugin.TestPlugin.UnitTests.csproj +++ b/source/AAS.TwinEngine.Plugin.TestPlugin.UnitTests/AAS.TwinEngine.Plugin.TestPlugin.UnitTests.csproj @@ -14,19 +14,13 @@ - - all - runtime; build; native; contentfiles; analyzers; buildtransitive - + - - all - runtime; build; native; contentfiles; analyzers; buildtransitive - + diff --git a/source/AAS.TwinEngine.Plugin.TestPlugin.UnitTests/Api/Submodel/Services/JsonSchemaParserTests.cs b/source/AAS.TwinEngine.Plugin.TestPlugin.UnitTests/Api/Submodel/Services/JsonSchemaParserTests.cs index 46165116..8587368c 100644 --- a/source/AAS.TwinEngine.Plugin.TestPlugin.UnitTests/Api/Submodel/Services/JsonSchemaParserTests.cs +++ b/source/AAS.TwinEngine.Plugin.TestPlugin.UnitTests/Api/Submodel/Services/JsonSchemaParserTests.cs @@ -27,366 +27,365 @@ public class JsonSchemaParserTests } }; + private readonly string _invalidJson = @"{""Invalid json"": {}}"; + private const string ValidationFailSchemaString = @"{ ""type"" : ""null"" }"; private const string NoPropertiesSchemaString = @"{ - ""$schema"": ""https://json-schema.org/draft/2020-12/schema"", - ""type"": ""object"" - }"; + ""$schema"": ""http://json-schema.org/draft-07/schema#"", + ""type"": ""object"" + }"; private const string SimpleSchemaString = @"{ - ""$schema"": ""https://json-schema.org/draft/2020-12/schema"", - ""type"": ""object"", - ""properties"": { - ""foo"": { ""type"": ""string"" } - }}"; + ""$schema"": ""http://json-schema.org/draft-07/schema#"", + ""type"": ""object"", + ""properties"": { + ""foo"": { ""type"": ""string"" } + }}"; private const string NestedSchemaString = @"{ - ""$schema"": ""https://json-schema.org/draft/2020-12/schema"", - ""type"": ""object"", - ""properties"": { - ""parent"": { - ""type"": ""object"", - ""properties"": { - ""child"": { ""type"": ""number"" } - }}}}"; + ""$schema"": ""http://json-schema.org/draft-07/schema#"", + ""type"": ""object"", + ""properties"": { + ""parent"": { + ""type"": ""object"", + ""properties"": { + ""child"": { ""type"": ""number"" } + }}}}"; private const string ArraySchemaString = @"{ - ""$schema"": ""https://json-schema.org/draft/2020-12/schema"", - ""type"": ""object"", - ""properties"": { - ""list"": { - ""type"": ""array"", - ""items"": { - ""type"": ""object"", + ""$schema"": ""http://json-schema.org/draft-07/schema#"", + ""type"": ""object"", + ""properties"": { + ""list"": { + ""type"": ""array"", ""properties"": { ""id"": { ""type"": ""integer"" } } - } - }}}"; + }}}"; private const string ArrayWithRefSchemaString = @"{ - ""$schema"": ""https://json-schema.org/draft/2020-12/schema"", - ""type"": ""object"", - ""properties"": { - ""items"": { - ""type"": ""array"", - ""items"": { ""$ref"": ""#/$defs/ItemDef"" } - } - }, - ""$defs"": { - ""ItemDef"": { - ""type"": ""object"", - ""properties"": { ""val"": { ""type"": ""integer"" } } - } - } - }"; - - private const string AllDataTypesSchemaWithRefString = @"{ - ""$schema"": ""https://json-schema.org/draft/2020-12/schema"", - ""type"": ""object"", - ""properties"": { - ""root"" :{ - ""type"" : ""array"", - ""items"" : { + ""$schema"": ""http://json-schema.org/draft-07/schema#"", + ""type"": ""object"", + ""properties"": { + ""items"": { + ""type"": ""array"", + ""$ref"": ""#/definitions/ItemDef"" + } + }, + ""definitions"": { + ""ItemDef"": { ""type"": ""object"", - ""properties"" : { - ""stringField"": { ""type"": ""string"" }, - ""numberField"": { ""type"": ""number"" }, - ""integerField"": { ""type"": ""integer"" }, - ""booleanField"": { ""type"": ""boolean"" }, - ""arrayField"": { - ""$ref"" : ""#/$defs/itemField"" - }, - ""objectField"": { - ""type"": ""object"", - ""properties"": { - ""nestedProp"": { ""type"": ""string"" } - } - } - } + ""properties"": { ""val"": { ""type"": ""integer"" } } } } + }"; + + private const string AllDataTypesSchemaWithRefString = @"{ + ""$schema"": ""http://json-schema.org/draft-07/schema#"", + ""type"": ""object"", + ""properties"": { + ""root"" :{ + ""type"" : ""array"", + ""properties"" : { + ""stringField"": { ""type"": ""string"" }, + ""numberField"": { ""type"": ""number"" }, + ""integerField"": { ""type"": ""integer"" }, + ""booleanField"": { ""type"": ""boolean"" }, + ""arrayField"": { + ""$ref"" : ""#/definitions/itemField"" }, - ""$defs"" : { - ""itemField"":{ - ""type"":""array"", - ""items"": { - ""type"": ""string"" - } + ""objectField"": { + ""type"": ""object"", + ""properties"": { + ""nestedProp"": { ""type"": ""string"" } } + }, + ""nullField"": { ""type"": ""null"" } + } } + }, + ""definitions"" : { + ""itemField"":{ + ""type"":""array"", + ""properties"": { + ""items"": { ""type"": ""string"" } + } + } + } }"; private readonly ILogger _logger; private readonly JsonSchemaParser _sut; + private readonly IJsonSchemaValidator _jsonSchemaValidator; public JsonSchemaParserTests() { _logger = Substitute.For>(); + _jsonSchemaValidator = Substitute.For(); _sut = new JsonSchemaParser(_logger); } [Fact] - public void ParseJsonSchema_SchemaValidationFails_ThrowsBadRequestException() + public void ParseJsonSchema_InvalidJson_ThrowsBadRequestException() { - var schema = JsonSerializer.Deserialize(ValidationFailSchemaString, _options); - Assert.Throws(() => _sut.ParseJsonSchema(schema)); + var InvalidJsonSchema = JsonSerializer.Deserialize(_invalidJson, _options); + + Assert.Throws(() => _sut.ParseJsonSchema(InvalidJsonSchema)); } [Fact] - public void ParseJsonSchema_NoRootProperties_ThrowsBadRequestException() + public void ParseJsonSchema_SchemaValidationFails_ThrowsBadRequestException() { - var schema = JsonSerializer.Deserialize(NoPropertiesSchemaString, _options); - Assert.Throws(() => _sut.ParseJsonSchema(schema)); + var ValidationFailSchema = JsonSerializer.Deserialize(ValidationFailSchemaString, _options); + + Assert.Throws(() => _sut.ParseJsonSchema(ValidationFailSchema)); } [Fact] - public void ParseJsonSchema_SimpleSchema_ReturnsLeafNode() + public void ParseJsonSchema_NoRootProperties_ThrowsBadRequestException() { - var schema = JsonSerializer.Deserialize(SimpleSchemaString, _options); + var NoPropertiesSchema = JsonSerializer.Deserialize(NoPropertiesSchemaString, _options); - var node = _sut.ParseJsonSchema(schema); - - var leaf = Assert.IsType(node); - Assert.Equal("foo", leaf.SemanticId); + Assert.Throws(() => _sut.ParseJsonSchema(NoPropertiesSchema)); } - [Theory] - [InlineData("http://json-schema.org/draft-07/schema#")] - [InlineData("https://json-schema.org/draft/2019-09/schema")] - [InlineData("https://json-schema.org/draft/2020-12/schema")] - public void ParseJsonSchema_SimpleSchemaAcrossDrafts_ReturnsLeafNode(string draft) + [Fact] + public void ParseJsonSchema_SimpleSchema_ReturnsLeafNode() { - var schema = JsonSchema.FromText($@"{{ - ""$schema"": ""{draft}"", - ""type"": ""object"", - ""properties"": {{ - ""foo"": {{ ""type"": ""string"" }} - }} - }}"); + var SimpleSchema = JsonSerializer.Deserialize(SimpleSchemaString, _options); - var node = _sut.ParseJsonSchema(schema); + var node = _sut.ParseJsonSchema(SimpleSchema); - var leaf = Assert.IsType(node); + Assert.NotNull(node); + Assert.IsType(node); + var leaf = (SemanticLeafNode)node; Assert.Equal("foo", leaf.SemanticId); - Assert.Equal(DataType.String, leaf.DataType); + Assert.Equal(string.Empty, leaf.Value); } [Fact] public void ParseJsonSchema_NestedObject_ReturnsBranchNodeWithChild() { - var schema = JsonSerializer.Deserialize(NestedSchemaString, _options); + var NestedSchema = JsonSerializer.Deserialize(NestedSchemaString, _options); - var node = _sut.ParseJsonSchema(schema); + var node = _sut.ParseJsonSchema(NestedSchema); - var branch = Assert.IsType(node); + Assert.NotNull(node); + Assert.IsType(node); + var branch = (SemanticBranchNode)node; Assert.Equal("parent", branch.SemanticId); - - var child = Assert.IsType(branch.Children[0]); + Assert.Single(branch.Children); + var child = branch.Children[0] as SemanticLeafNode; + Assert.NotNull(child); Assert.Equal("child", child.SemanticId); } [Fact] public void ParseJsonSchema_ArrayOfObjects_ReturnsBranchNodeWithLeafChild() { - var schema = JsonSerializer.Deserialize(ArraySchemaString, _options); + var arraySchema = JsonSerializer.Deserialize(ArraySchemaString, _options); - var node = _sut.ParseJsonSchema(schema); + var node = _sut.ParseJsonSchema(arraySchema!); - var branch = Assert.IsType(node); + Assert.NotNull(node); + Assert.IsType(node); + var branch = (SemanticBranchNode)node; Assert.Equal("list", branch.SemanticId); - - var child = Assert.IsType(branch.Children[0]); + Assert.Single(branch.Children); + var child = branch.Children[0] as SemanticLeafNode; + Assert.NotNull(child); Assert.Equal("id", child.SemanticId); } [Fact] public void ParseJsonSchema_ArrayWithRef_ReturnsBranchNodeWithLeafChild() { - var schema = JsonSerializer.Deserialize(ArrayWithRefSchemaString, _options); + var arrayWithRefSchema = JsonSerializer.Deserialize(ArrayWithRefSchemaString, _options); - var node = _sut.ParseJsonSchema(schema); + var node = _sut.ParseJsonSchema(arrayWithRefSchema!); - var branch = Assert.IsType(node); + Assert.NotNull(node); + Assert.IsType(node); + var branch = (SemanticBranchNode)node; Assert.Equal("items", branch.SemanticId); - - var child = Assert.IsType(branch.Children[0]); + Assert.Single(branch.Children); + var child = branch.Children[0] as SemanticLeafNode; + Assert.NotNull(child); Assert.Equal("val", child.SemanticId); } [Fact] - public void ParseJsonSchema_Draft7ArrayWithDefinitionsRef_ReturnsBranchNodeWithLeafChild() + public void ParseJsonSchema_AllDataTypeSchemaWithRef_ReturnsBranchNodeWithLeafChild() { - var schema = JsonSchema.FromText(@"{ - ""$schema"": ""http://json-schema.org/draft-07/schema#"", - ""type"": ""object"", - ""properties"": { - ""items"": { - ""type"": ""array"", - ""items"": { ""$ref"": ""#/definitions/ItemDef"" } - } - }, - ""definitions"": { - ""ItemDef"": { - ""type"": ""object"", - ""properties"": { - ""value"": { ""type"": ""integer"" } - } - } - } - }"); + var allDataTypesSchemaWithRef = JsonSerializer.Deserialize(AllDataTypesSchemaWithRefString, _options); - var node = _sut.ParseJsonSchema(schema); + var node = _sut.ParseJsonSchema(allDataTypesSchemaWithRef!); - var branch = Assert.IsType(node); - Assert.Equal("items", branch.SemanticId); - var child = Assert.IsType(branch.Children[0]); - Assert.Equal("value", child.SemanticId); - Assert.Equal(DataType.Integer, child.DataType); + Assert.NotNull(node); + Assert.IsType(node); + var branch = (SemanticBranchNode)node; + Assert.Equal("root", branch.SemanticId); + Assert.Equal(DataType.Array, branch.DataType); + var child1 = branch.Children[0] as SemanticLeafNode; + Assert.Equal("stringField", child1!.SemanticId); + Assert.Equal(DataType.String, child1.DataType); + var child2 = branch.Children[1] as SemanticLeafNode; + Assert.Equal("numberField", child2!.SemanticId); + Assert.Equal(DataType.Number, child2.DataType); + var child3 = branch.Children[2] as SemanticLeafNode; + Assert.Equal("integerField", child3!.SemanticId); + Assert.Equal(DataType.Integer, child3.DataType); + var child4 = branch.Children[3] as SemanticLeafNode; + Assert.Equal("booleanField", child4!.SemanticId); + Assert.Equal(DataType.Boolean, child4.DataType); + var branch1 = branch.Children[4] as SemanticBranchNode; + Assert.Equal("arrayField", branch1?.SemanticId); + Assert.Equal(DataType.Array, branch1?.DataType); + var leaf1 = branch1!.Children[0] as SemanticLeafNode; + Assert.Equal("items", leaf1!.SemanticId); + Assert.Equal(DataType.String, leaf1.DataType); } [Fact] - public void ParseJsonSchema_Draft7SchemaWithDefsKeyword_ReturnsLeafNode() + public void ParseJsonSchema_ReferenceNotFound_ReturnsLeafNode() { - var schema = JsonSchema.FromText(@"{ - ""$schema"": ""http://json-schema.org/draft-07/schema#"", - ""type"": ""object"", - ""properties"": { - ""item"": { ""$ref"": ""#/$defs/ItemDef"" } - }, - ""$defs"": { - ""ItemDef"": { - ""type"": ""object"", - ""properties"": { - ""value"": { ""type"": ""string"" } - } - } - } - }"); + const string SchemaString = @"{ + ""$schema"": ""http://json-schema.org/draft-07/schema#"", + ""type"": ""object"", + ""properties"": { + ""mystery"": { ""$ref"": ""#/definitions/DoesNotExist"" } + } + }"; + var schema = JsonSerializer.Deserialize(SchemaString, _options); - var node = _sut.ParseJsonSchema(schema); + var node = _sut.ParseJsonSchema(schema!); - var branch = Assert.IsType(node); - var child = Assert.IsType(branch.Children[0]); - Assert.Equal("value", child.SemanticId); - Assert.Equal(DataType.String, child.DataType); + Assert.NotNull(node); + Assert.IsType(node); + var leaf = (SemanticLeafNode)node; + Assert.Equal("mystery", leaf.SemanticId); + Assert.Equal(DataType.Unknown, leaf.DataType); } [Fact] - public void ParseJsonSchema_Draft202012SchemaWithDefinitionsKeyword_ReturnsLeafNode() + public void ParseJsonSchema_ReferenceToObjectDefinition_ReturnsBranchNode() { - var schema = JsonSchema.FromText(@"{ - ""$schema"": ""https://json-schema.org/draft/2020-12/schema"", - ""type"": ""object"", - ""properties"": { - ""item"": { ""$ref"": ""#/definitions/ItemDef"" } - }, - ""definitions"": { - ""ItemDef"": { - ""type"": ""object"", - ""properties"": { - ""value"": { ""type"": ""integer"" } - } + const string SchemaString = @"{ + ""$schema"": ""http://json-schema.org/draft-07/schema#"", + ""type"": ""object"", + ""properties"": { + ""person"": { ""$ref"": ""#/definitions/Person"" } + }, + ""definitions"": { + ""Person"": { + ""type"": ""object"", + ""properties"": { + ""name"": { ""type"": ""string"" }, + ""age"": { ""type"": ""integer"" } } } - }"); - - var node = _sut.ParseJsonSchema(schema); - - var branch = Assert.IsType(node); - var child = Assert.IsType(branch.Children[0]); - Assert.Equal("value", child.SemanticId); - Assert.Equal(DataType.Integer, child.DataType); - } - - [Fact] - public void ParseJsonSchema_AllDataTypeSchemaWithRef_ReturnsBranchNode() - { - var schema = JsonSerializer.Deserialize(AllDataTypesSchemaWithRefString, _options); + } + }"; + var schema = JsonSerializer.Deserialize(SchemaString, _options); var node = _sut.ParseJsonSchema(schema); - var branch = Assert.IsType(node); - Assert.Equal("root", branch.SemanticId); - Assert.Equal(DataType.Array, branch.DataType); + Assert.NotNull(node); + Assert.IsType(node); + var branch = (SemanticBranchNode)node; + Assert.Equal("person", branch.SemanticId); + Assert.Equal(DataType.Object, branch.DataType); + Assert.Collection(branch.Children, + child => + { + var leaf = Assert.IsType(child); + Assert.Equal("name", leaf.SemanticId); + Assert.Equal(DataType.String, leaf.DataType); + }, + child => + { + var leaf = Assert.IsType(child); + Assert.Equal("age", leaf.SemanticId); + Assert.Equal(DataType.Integer, leaf.DataType); + }); } [Fact] - public void ParseJsonSchema_ReferenceNotFound_ReturnsLeafNode() + public void ParseJsonSchema_ReferenceToArrayDefinition_ReturnsBranchNode() { - const string Schema = @"{ - ""type"": ""object"", - ""properties"": { - ""mystery"": { ""$ref"": ""#/$defs/DoesNotExist"" } + const string SchemaString = @"{ + ""$schema"": ""http://json-schema.org/draft-07/schema#"", + ""type"": ""object"", + ""properties"": { + ""primes"": { ""$ref"": ""#/definitions/PrimeList"" } + }, + ""definitions"": { + ""PrimeList"": { + ""type"": ""array"" + } } }"; - - var schema = JsonSerializer.Deserialize(Schema, _options); - - var node = _sut.ParseJsonSchema(schema); - - var leaf = Assert.IsType(node); - Assert.Equal("mystery", leaf.SemanticId); - Assert.Equal(DataType.Unknown, leaf.DataType); - } - - [Fact] - public void ParseJsonSchema_MissingType_DefaultsToString() - { - var schema = JsonSchema.FromText(@"{ - ""type"": ""object"", - ""properties"": { - ""unknown"": {} - } - }"); + var schema = JsonSerializer.Deserialize(SchemaString, _options); var node = _sut.ParseJsonSchema(schema); - var leaf = Assert.IsType(node); - Assert.Equal(DataType.String, leaf.DataType); + Assert.NotNull(node); + Assert.IsType(node); + var branch = (SemanticBranchNode)node; + Assert.Equal("primes", branch.SemanticId); + Assert.Equal(DataType.Array, branch.DataType); + Assert.Empty(branch.Children); } [Fact] - public void ParseJsonSchema_InvalidRef_ReturnsUnknownLeaf() + public void ParseJsonSchema_ReferenceToLeafNodeDefinition_ReturnsLeafNode() { - var schema = JsonSchema.FromText(@"{ - ""type"": ""object"", - ""properties"": { - ""bad"": { ""$ref"": ""#/$defs/Unknown"" } + const string SchemaString = @"{ + ""$schema"": ""http://json-schema.org/draft-07/schema#"", + ""type"": ""object"", + ""properties"": { + ""person"": { ""$ref"": ""#/definitions/Person"" } + }, + ""definitions"": { + ""Person"": { + ""type"": ""string"" } - }"); + } + }"; + var schema = JsonSerializer.Deserialize(SchemaString, _options); var node = _sut.ParseJsonSchema(schema); - var leaf = Assert.IsType(node); - Assert.Equal(DataType.Unknown, leaf.DataType); + Assert.NotNull(node); + Assert.IsType(node); + var branch = (SemanticLeafNode)node; + Assert.Equal("person", branch.SemanticId); + Assert.Equal(DataType.String, branch.DataType); } [Fact] - public void ParseJsonSchema_DeepNestedDefs_ResolvesCorrectly() + public void ParseJsonSchema_InlineObjectWithNoProperties_CoversProcessObjectFallback() { - var schema = JsonSchema.FromText(@"{ - ""type"": ""object"", - ""properties"": { - ""root"": { ""$ref"": ""#/$defs/A"" } - }, - ""$defs"": { - ""A"": { - ""type"": ""object"", - ""properties"": { - ""child"": { ""$ref"": ""#/$defs/B"" } - } - }, - ""B"": { - ""type"": ""string"" + const string SchemaString = @"{ + ""$schema"": ""http://json-schema.org/draft-07/schema#"", + ""type"": ""object"", + ""properties"": { + ""outer"": { + ""type"": ""object"", + ""properties"": { + ""inner"": { ""type"": ""object"" } } } - }"); + } + }"; + var schema = JsonSerializer.Deserialize(SchemaString, _options); - var node = _sut.ParseJsonSchema(schema); + var root = _sut.ParseJsonSchema(schema); - var branch = Assert.IsType(node); - var child = Assert.IsType(branch.Children[0]); - Assert.Equal("child", child.SemanticId); - Assert.Equal(DataType.String, child.DataType); + var outerBranch = Assert.IsType(root); + Assert.Equal("outer", outerBranch.SemanticId); + Assert.Single(outerBranch.Children); + var innerBranch = Assert.IsType(outerBranch.Children[0]); + Assert.Equal("inner", innerBranch.SemanticId); + Assert.Empty(innerBranch.Children); } } diff --git a/source/AAS.TwinEngine.Plugin.TestPlugin.UnitTests/Api/Submodel/Services/JsonSchemaValidatorTests.cs b/source/AAS.TwinEngine.Plugin.TestPlugin.UnitTests/Api/Submodel/Services/JsonSchemaValidatorTests.cs index 61fc1a56..808a4104 100644 --- a/source/AAS.TwinEngine.Plugin.TestPlugin.UnitTests/Api/Submodel/Services/JsonSchemaValidatorTests.cs +++ b/source/AAS.TwinEngine.Plugin.TestPlugin.UnitTests/Api/Submodel/Services/JsonSchemaValidatorTests.cs @@ -49,13 +49,13 @@ public void ValidateResponseContent_EmptyResponse_ThrowsBadRequest() public void ValidateResponseContent_ValidateJsonSchemaRemovePrefix_DoesNotThrow() { var schema = new JsonSchemaBuilder() - .Type(SchemaValueType.Object) - .Properties(new Dictionary - { - ["ContactInformation_aastwinengine_00"] = new JsonSchemaBuilder().Type(SchemaValueType.Object) - }) - .Required("ContactInformation_aastwinengine_00") - .Build(); + .Type(SchemaValueType.Object) + .Properties(new Dictionary + { + ["ContactInformation_aastwinengine_00"] = new JsonSchemaBuilder().Type(SchemaValueType.Object).Build() + }) + .Required("ContactInformation_aastwinengine_00") + .Build(); const string Json = "{\"ContactInformation\": {}}"; @@ -66,40 +66,19 @@ public void ValidateResponseContent_ValidateJsonSchemaRemovePrefix_DoesNotThrow( public void ValidateResponseContent_ValidJsonAndSchema_DoesNotThrow() { var schema = new JsonSchemaBuilder() - .Type(SchemaValueType.Object) - .Properties(new Dictionary - { - ["name"] = new JsonSchemaBuilder().Type(SchemaValueType.String) - }) - .Required("name") - .Build(); + .Type(SchemaValueType.Object) + .Properties(new Dictionary + { + ["name"] = new JsonSchemaBuilder().Type(SchemaValueType.String).Build() + }) + .Required("name") + .Build(); const string Json = "{\"name\": \"Test\"}"; _sut.ValidateResponseContent(Json, schema); } - [Theory] - [InlineData("http://json-schema.org/draft-07/schema#")] - [InlineData("https://json-schema.org/draft/2019-09/schema")] - [InlineData("https://json-schema.org/draft/2020-12/schema")] - public void ValidateResponseContent_SupportedDrafts_DoesNotThrow(string draft) - { - var schema = new JsonSchemaBuilder() - .Schema(draft) - .Type(SchemaValueType.Object) - .Properties(new Dictionary - { - ["name"] = new JsonSchemaBuilder().Type(SchemaValueType.String) - }) - .Required("name") - .Build(); - - const string json = "{\"name\": \"Test\"}"; - - _sut.ValidateResponseContent(json, schema); - } - [Theory] [MemberData(nameof(InvalidPrimitives))] public void ValidateResponseContent_InvalidValueType_ThrowsBadRequest( @@ -109,220 +88,78 @@ public void ValidateResponseContent_InvalidValueType_ThrowsBadRequest( { var schema = new JsonSchemaBuilder() .Type(SchemaValueType.Object) - .Properties(new Dictionary + .Properties(new Dictionary { - [property] = new JsonSchemaBuilder().Type(expectedType) + [property] = new JsonSchemaBuilder().Type(expectedType).Build() }) .Required(property) .Build(); - var json = $"{{\"{property}\": {rawValue} }}"; Assert.Throws(() => _sut.ValidateResponseContent(json, schema)); } [Fact] - public void ValidateResponseContent_SchemaMismatch_ThrowsBadRequest() - { - var schema = new JsonSchemaBuilder() - .Type(SchemaValueType.Object) - .Properties(new Dictionary - { - ["name"] = new JsonSchemaBuilder().Type(SchemaValueType.String) - }) - .Required("name") - .Build(); - - const string Json = "{}"; - - Assert.Throws(() => _sut.ValidateResponseContent(Json, schema)); - } - - [Fact] - public void ValidateResponseContent_InvalidJson_ThrowsBadRequest() - { - var schema = new JsonSchemaBuilder() - .Type(SchemaValueType.Object) - .Build(); - - const string BadJson = "{ not valid json }"; - - Assert.Throws(() => _sut.ValidateResponseContent(BadJson, schema)); - } - - [Fact] - public void ValidateResponseContent_NullResponse_ThrowsException() - { - var schema = new JsonSchemaBuilder() - .Type(SchemaValueType.Object) - .Build(); - - Assert.Throws(() => _sut.ValidateResponseContent(null!, schema)); - } - - [Fact] - public void ValidateResponseContent_WhitespaceOnlyResponse_ThrowsException() - { - var schema = new JsonSchemaBuilder() - .Type(SchemaValueType.Object) - .Build(); - - Assert.Throws(() => _sut.ValidateResponseContent(" ", schema)); - } - - [Fact] - public void ValidateResponseContent_MalformedJson_ThrowsException() - { - var schema = new JsonSchemaBuilder() - .Type(SchemaValueType.Object) - .Build(); - - Assert.Throws(() => _sut.ValidateResponseContent("{\"key\": }", schema)); - } - - [Fact] - public void ValidateResponseContent_PropertyWithSuffix_RemovesSuffix() - { - var schema = new JsonSchemaBuilder() - .Type(SchemaValueType.Object) - .Properties(new Dictionary - { - ["PropertyName_aastwinengine_123"] = new JsonSchemaBuilder().Type(SchemaValueType.String) - }) - .Required("PropertyName_aastwinengine_123") - .Build(); - - const string Json = "{\"PropertyName\": \"value\"}"; - - _sut.ValidateResponseContent(Json, schema); - } - - [Fact] - public void ValidateResponseContent_PropertyWithoutSuffix_DoesNotModify() - { - var schema = new JsonSchemaBuilder() - .Type(SchemaValueType.Object) - .Properties(new Dictionary - { - ["PropertyWithoutSuffix"] = new JsonSchemaBuilder().Type(SchemaValueType.String) - }) - .Required("PropertyWithoutSuffix") - .Build(); - - const string Json = "{\"PropertyWithoutSuffix\": \"value\"}"; - - _sut.ValidateResponseContent(Json, schema); - } - - [Fact] - public void ValidateResponseContent_NestedObjectWithSuffixedProperties_RemovesSuffixes() - { - var nestedSchema = new JsonSchemaBuilder() - .Type(SchemaValueType.Object) - .Properties(new Dictionary - { - ["nestedField_aastwinengine_01"] = new JsonSchemaBuilder().Type(SchemaValueType.String) - }); - - var schema = new JsonSchemaBuilder() - .Type(SchemaValueType.Object) - .Properties(new Dictionary - { - ["parent_aastwinengine_00"] = nestedSchema - }) - .Build(); - - const string Json = "{\"parent\": {\"nestedField\": \"value\"}}"; - - _sut.ValidateResponseContent(Json, schema); - } - - [Fact] - public void ValidateResponseContent_ArrayWithSuffixedItems_RemovesSuffixes() - { - var schema = new JsonSchemaBuilder() - .Type(SchemaValueType.Object) - .Properties(new Dictionary - { - ["items_aastwinengine_01"] = new JsonSchemaBuilder() - .Type(SchemaValueType.Array) - .Items(new JsonSchemaBuilder().Type(SchemaValueType.String)) - }) - .Build(); - - const string Json = "{\"items\": [\"item1\", \"item2\"]}"; - - _sut.ValidateResponseContent(Json, schema); - } - - [Fact] - public void ValidateResponseContent_ArrayOfObjects_ValidatesCorrectly() + public void ValidateResponseContent_PropertyTypeStringOrArray_WithString_DoesNotThrow() { var schema = new JsonSchemaBuilder() - .Type(SchemaValueType.Object) - .Properties(new Dictionary - { - ["users"] = new JsonSchemaBuilder() - .Type(SchemaValueType.Array) - .Items(new JsonSchemaBuilder() - .Type(SchemaValueType.Object) - .Properties(new Dictionary - { - ["name_aastwinengine_01"] = new JsonSchemaBuilder().Type(SchemaValueType.String) - })) - }) - .Build(); + .Type(SchemaValueType.Object) + .Properties(new Dictionary + { + ["value"] = new JsonSchemaBuilder().Type(SchemaValueType.String, SchemaValueType.Array).Build() + }) + .Required("value") + .Build(); - const string Json = "{\"users\": [{\"name\": \"Alice\"}, {\"name\": \"Bob\"}]}"; + const string Json = "{\"value\": \"hello\"}"; _sut.ValidateResponseContent(Json, schema); } [Fact] - public void ValidateResponseContent_EmptyArray_ValidatesCorrectly() + public void ValidateResponseContent_PropertyTypeStringOrArray_WithArray_DoesNotThrow() { var schema = new JsonSchemaBuilder() - .Type(SchemaValueType.Object) - .Properties(new Dictionary - { - ["items"] = new JsonSchemaBuilder() - .Type(SchemaValueType.Array) - .Items(new JsonSchemaBuilder().Type(SchemaValueType.String)) - }) - .Build(); + .Type(SchemaValueType.Object) + .Properties(new Dictionary + { + ["value"] = new JsonSchemaBuilder().Type(SchemaValueType.String, SchemaValueType.Array).Build() + }) + .Required("value") + .Build(); - const string Json = "{\"items\": []}"; + const string Json = "{\"value\": [\"one\", \"two\"]}"; _sut.ValidateResponseContent(Json, schema); } [Fact] - public void ValidateResponseContent_RequiredPropertyWithSuffix_ValidatesAfterRemoval() + public void ValidateResponseContent_PropertyTypeStringOrArray_WithNumber_ThrowsBadRequest() { var schema = new JsonSchemaBuilder() - .Type(SchemaValueType.Object) - .Properties(new Dictionary - { - ["requiredField_aastwinengine_01"] = new JsonSchemaBuilder().Type(SchemaValueType.String) - }) - .Required("requiredField_aastwinengine_01") - .Build(); + .Type(SchemaValueType.Object) + .Properties(new Dictionary + { + ["value"] = new JsonSchemaBuilder().Type(SchemaValueType.String, SchemaValueType.Array).Build() + }) + .Required("value") + .Build(); - const string Json = "{\"requiredField\": \"value\"}"; + const string Json = "{\"value\": 123}"; - _sut.ValidateResponseContent(Json, schema); + Assert.Throws(() => _sut.ValidateResponseContent(Json, schema)); } [Fact] - public void ValidateResponseContent_MissingRequiredProperty_ThrowsException() + public void ValidateResponseContent_SchemaMismatch_ThrowsBadRequest() { var schema = new JsonSchemaBuilder() .Type(SchemaValueType.Object) - .Properties(new Dictionary + .Properties(new Dictionary { - ["requiredField_aastwinengine_01"] = new JsonSchemaBuilder().Type(SchemaValueType.String) + ["name"] = new JsonSchemaBuilder().Type(SchemaValueType.String).Build() }) - .Required("requiredField_aastwinengine_01") + .Required("name") .Build(); const string Json = "{}"; @@ -331,232 +168,13 @@ public void ValidateResponseContent_MissingRequiredProperty_ThrowsException() } [Fact] - public void ValidateResponseContent_SchemaWithoutId_ValidatesCorrectly() - { - var schema = new JsonSchemaBuilder() - .Type(SchemaValueType.Object) - .Properties(new Dictionary - { - ["field"] = new JsonSchemaBuilder().Type(SchemaValueType.String) - }) - .Build(); - - const string Json = "{\"field\": \"value\"}"; - - _sut.ValidateResponseContent(Json, schema); - } - - [Fact] - public void ValidateResponseContent_PropertyNameWithHyphen_HandlesCorrectly() - { - var schema = new JsonSchemaBuilder() - .Type(SchemaValueType.Object) - .Properties(new Dictionary - { - ["field-name_aastwinengine_01"] = new JsonSchemaBuilder().Type(SchemaValueType.String) - }) - .Build(); - - const string Json = "{\"field-name\": \"value\"}"; - - _sut.ValidateResponseContent(Json, schema); - } - - [Fact] - public void ValidateResponseContent_PropertyNameWithDot_HandlesCorrectly() - { - var schema = new JsonSchemaBuilder() - .Type(SchemaValueType.Object) - .Properties(new Dictionary - { - ["field.name_aastwinengine_01"] = new JsonSchemaBuilder().Type(SchemaValueType.String) - }) - .Build(); - - const string Json = "{\"field.name\": \"value\"}"; - - _sut.ValidateResponseContent(Json, schema); - } - - [Fact] - public void ValidateResponseContent_PropertyNameWithUnicode_HandlesCorrectly() - { - var schema = new JsonSchemaBuilder() - .Type(SchemaValueType.Object) - .Properties(new Dictionary - { - ["field名前_aastwinengine_01"] = new JsonSchemaBuilder().Type(SchemaValueType.String) - }) - .Build(); - - const string Json = "{\"field名前\": \"value\"}"; - - _sut.ValidateResponseContent(Json, schema); - } - - [Fact] - public void ValidateResponseContent_DeeplyNestedStructure_ValidatesCorrectly() - { - var level3 = new JsonSchemaBuilder() - .Type(SchemaValueType.Object) - .Properties(new Dictionary - { - ["deepField_aastwinengine_03"] = new JsonSchemaBuilder().Type(SchemaValueType.String) - }); - - var level2 = new JsonSchemaBuilder() - .Type(SchemaValueType.Object) - .Properties(new Dictionary - { - ["midField_aastwinengine_02"] = level3 - }); - - var schema = new JsonSchemaBuilder() - .Type(SchemaValueType.Object) - .Properties(new Dictionary - { - ["topField_aastwinengine_01"] = level2 - }) - .Build(); - - const string Json = "{\"topField\": {\"midField\": {\"deepField\": \"value\"}}}"; - - _sut.ValidateResponseContent(Json, schema); - } - - [Fact] - public void ValidateResponseContent_MixedArrayAndObjectNesting_ValidatesCorrectly() + public void ValidateResponseContent_InvalidJson_ThrowsBadRequest() { - var objectSchema = new JsonSchemaBuilder() - .Type(SchemaValueType.Object) - .Properties(new Dictionary - { - ["items_aastwinengine_02"] = new JsonSchemaBuilder() - .Type(SchemaValueType.Array) - .Items(new JsonSchemaBuilder().Type(SchemaValueType.String)) - }); - var schema = new JsonSchemaBuilder() .Type(SchemaValueType.Object) - .Properties(new Dictionary - { - ["data_aastwinengine_01"] = new JsonSchemaBuilder() - .Type(SchemaValueType.Array) - .Items(objectSchema) - }) .Build(); + const string BadJson = "{ not valid json }"; - const string Json = "{\"data\": [{\"items\": [\"a\", \"b\"]}, {\"items\": [\"c\"]}]}"; - - _sut.ValidateResponseContent(Json, schema); - } - - [Fact] - public void ValidateResponseContent_WithDefsReference_DoesNotThrow() - { - const string schemaJson = @"{ - ""type"": ""object"", - ""properties"": { - ""item"": { ""$ref"": ""#/$defs/MyType"" } - }, - ""$defs"": { - ""MyType"": { - ""type"": ""object"", - ""properties"": { - ""name"": { ""type"": ""string"" } - } - } - } - }"; - - var schema = JsonSchema.FromText(schemaJson); - - const string json = @"{ ""item"": { ""name"": ""test"" } }"; - - _sut.ValidateResponseContent(json, schema); - } - - [Fact] - public void ValidateResponseContent_Draft7DefinitionsReferenceWithSuffix_DoesNotThrow() - { - const string schemaJson = @"{ - ""$schema"": ""http://json-schema.org/draft-07/schema#"", - ""type"": ""object"", - ""properties"": { - ""item_aastwinengine_00"": { ""$ref"": ""#/definitions/Type_aastwinengine_00"" } - }, - ""required"": [""item_aastwinengine_00""], - ""definitions"": { - ""Type_aastwinengine_00"": { - ""type"": ""object"", - ""properties"": { - ""name_aastwinengine_00"": { ""type"": ""string"" } - }, - ""required"": [""name_aastwinengine_00""] - } - } - }"; - - var schema = JsonSchema.FromText(schemaJson); - - const string json = @"{ ""item"": { ""name"": ""ok"" } }"; - - _sut.ValidateResponseContent(json, schema); - } - - [Fact] - public void ValidateResponseContent_Draft202012DefinitionsReferenceWithSuffix_DoesNotThrow() - { - const string schemaJson = @"{ - ""$schema"": ""https://json-schema.org/draft/2020-12/schema"", - ""type"": ""object"", - ""properties"": { - ""item_aastwinengine_00"": { ""$ref"": ""#/definitions/Type_aastwinengine_00"" } - }, - ""required"": [""item_aastwinengine_00""], - ""definitions"": { - ""Type_aastwinengine_00"": { - ""type"": ""object"", - ""properties"": { - ""name_aastwinengine_00"": { ""type"": ""string"" } - }, - ""required"": [""name_aastwinengine_00""] - } - } - }"; - - var schema = JsonSchema.FromText(schemaJson); - - const string json = @"{ ""item"": { ""name"": ""ok"" } }"; - - _sut.ValidateResponseContent(json, schema); - } - - [Fact] - public void ValidateResponseContent_Draft7DefsReferenceWithSuffix_DoesNotThrow() - { - const string schemaJson = @"{ - ""$schema"": ""http://json-schema.org/draft-07/schema#"", - ""type"": ""object"", - ""properties"": { - ""item_aastwinengine_00"": { ""$ref"": ""#/$defs/Type_aastwinengine_00"" } - }, - ""required"": [""item_aastwinengine_00""], - ""$defs"": { - ""Type_aastwinengine_00"": { - ""type"": ""object"", - ""properties"": { - ""name_aastwinengine_00"": { ""type"": ""string"" } - }, - ""required"": [""name_aastwinengine_00""] - } - } - }"; - - var schema = JsonSchema.FromText(schemaJson); - - const string json = @"{ ""item"": { ""name"": ""ok"" } }"; - - _sut.ValidateResponseContent(json, schema); + Assert.Throws(() => _sut.ValidateResponseContent(BadJson, schema)); } } diff --git a/source/AAS.TwinEngine.Plugin.TestPlugin.UnitTests/CleanArchitectureTests.cs b/source/AAS.TwinEngine.Plugin.TestPlugin.UnitTests/CleanArchitectureTests.cs index 942204cc..563ff721 100644 --- a/source/AAS.TwinEngine.Plugin.TestPlugin.UnitTests/CleanArchitectureTests.cs +++ b/source/AAS.TwinEngine.Plugin.TestPlugin.UnitTests/CleanArchitectureTests.cs @@ -15,10 +15,10 @@ public class CleanArchitectureTests private const string BaseNamespace = "AAS.TwinEngine.Plugin.TestPlugin"; private readonly Architecture _architecture = new ArchLoader().LoadAssemblies(System.Reflection.Assembly.Load(BaseNamespace)).Build(); - private readonly IObjectProvider _apiLayer = Types().That().ResideInNamespaceMatching($"{BaseNamespace}\\.Api($|\\..*)").As("Api"); - private readonly IObjectProvider _applicationLogicLayer = Types().That().ResideInNamespaceMatching($"{BaseNamespace}\\.ApplicationLogic($|\\..*)").As("ApplicationLogic"); - private readonly IObjectProvider _domainModelLayer = Types().That().ResideInNamespaceMatching($"{BaseNamespace}\\.DomainModel.*").As("DomainModel"); - private readonly IObjectProvider _infrastructureLayer = Types().That().ResideInNamespaceMatching($"{BaseNamespace}\\.Infrastructure($|\\..*)").As("Infrastructure"); + private readonly IObjectProvider _apiLayer = Types().That().ResideInNamespaceMatching($"{BaseNamespace}.Api.*").As("Api"); + private readonly IObjectProvider _applicationLogicLayer = Types().That().ResideInNamespaceMatching($"{BaseNamespace}.ApplicationLogic.*").As("ApplicationLogic"); + private readonly IObjectProvider _domainModelLayer = Types().That().ResideInNamespaceMatching($"{BaseNamespace}.DomainModel*").As("DomainModel"); + private readonly IObjectProvider _infrastructureLayer = Types().That().ResideInNamespaceMatching($"{BaseNamespace}.Infrastructure.*").As("Infrastructure"); [Fact] public void DomainModelShallNotHaveExternalDependencies() diff --git a/source/AAS.TwinEngine.Plugin.TestPlugin/AAS.TwinEngine.Plugin.TestPlugin.csproj b/source/AAS.TwinEngine.Plugin.TestPlugin/AAS.TwinEngine.Plugin.TestPlugin.csproj index 615935a6..82fc72ab 100644 --- a/source/AAS.TwinEngine.Plugin.TestPlugin/AAS.TwinEngine.Plugin.TestPlugin.csproj +++ b/source/AAS.TwinEngine.Plugin.TestPlugin/AAS.TwinEngine.Plugin.TestPlugin.csproj @@ -23,7 +23,7 @@ - + diff --git a/source/AAS.TwinEngine.Plugin.TestPlugin/Api/Submodel/Services/JsonSchemaParser.cs b/source/AAS.TwinEngine.Plugin.TestPlugin/Api/Submodel/Services/JsonSchemaParser.cs index c1967d39..f346f229 100644 --- a/source/AAS.TwinEngine.Plugin.TestPlugin/Api/Submodel/Services/JsonSchemaParser.cs +++ b/source/AAS.TwinEngine.Plugin.TestPlugin/Api/Submodel/Services/JsonSchemaParser.cs @@ -1,5 +1,4 @@ using System.Text.Json; -using System.Text.Json.Nodes; using AAS.TwinEngine.Plugin.TestPlugin.ApplicationLogic.Constants; using AAS.TwinEngine.Plugin.TestPlugin.ApplicationLogic.Exceptions; @@ -11,15 +10,6 @@ namespace AAS.TwinEngine.Plugin.TestPlugin.Api.Submodel.Services; public class JsonSchemaParser(ILogger logger) : IJsonSchemaParser { - private const string Draft7Schema = "http://json-schema.org/draft-07/schema#"; - private const string Draft7SchemaHttps = "https://json-schema.org/draft-07/schema#"; - private const string Draft201909Schema = "https://json-schema.org/draft/2019-09/schema"; - private const string Draft202012Schema = "https://json-schema.org/draft/2020-12/schema"; - private const string DefsRefPrefix = "#/$defs/"; - private const string DefinitionsRefPrefix = "#/definitions/"; - - private enum SchemaDraft { Draft7, Draft201909, Draft202012 } - public SemanticTreeNode ParseJsonSchema(JsonSchema jsonSchema) { ValidateRequest(jsonSchema); @@ -30,249 +20,147 @@ private void ValidateRequest(JsonSchema jsonSchema) { try { - var json = JsonSerializer.SerializeToNode(jsonSchema); - var schema = json?.AsObject() ?? throw new JsonException(); - var draftUri = GetSchemaDraftUri(schema); - var element = JsonDocument.Parse(schema.ToJsonString()).RootElement; - - var result = ResolveMetaSchema(draftUri).Evaluate(element, new EvaluationOptions - { - OutputFormat = OutputFormat.List - }); - + var node = JsonSerializer.SerializeToNode(jsonSchema); + var result = MetaSchemas.Draft7.Evaluate(node, new EvaluationOptions { OutputFormat = OutputFormat.List }); if (!result.IsValid) { - logger.LogError("Requested schema is not valid"); + logger.LogError("Requested schema is not validate"); throw new BadRequestException(ExceptionMessages.RequestBodyInvalid); } } - catch (JsonException ex) + catch (JsonException) { - logger.LogError(ex, "Requested schema is not valid"); + logger.LogError("Requested schema is not validate"); throw new BadRequestException(ExceptionMessages.FailedParsingJsonSchema); } } private SemanticTreeNode CreateSemanticTree(JsonSchema jsonSchema) { - var json = JsonSerializer.SerializeToNode(jsonSchema)!.AsObject(); - var draft = GetSchemaDraft(json); - - if (!json.TryGetPropertyValue("properties", out var propsNode) || - propsNode is not JsonObject props || - props.Count == 0) + var propertiesKeyword = jsonSchema.GetKeyword(); + if (propertiesKeyword == null || !propertiesKeyword.Properties.Any()) { throw new BadRequestException(ExceptionMessages.InvalidJsonSchemaRootElement); } - var rootProperty = ((IList>)props)[0]; - return ProcessProperty(rootProperty.Key, rootProperty.Value!, json, draft); + var rootProperty = propertiesKeyword.Properties.First(); + return ProcessProperty(rootProperty.Key, rootProperty.Value, jsonSchema.GetKeyword()); } - private SemanticTreeNode ProcessProperty(string name, JsonNode propertyNode, JsonObject root, SchemaDraft draft) + private SemanticTreeNode ProcessProperty(string schemaPropertyName, JsonSchema property, DefinitionsKeyword definitions) { - var property = propertyNode.AsObject(); - - if (property.TryGetPropertyValue("$ref", out var refNode)) + var refKeyword = property.GetKeyword(); + if (refKeyword != null) { - return HandleReference(name, refNode!.GetValue(), root, draft); + return HandleReference(schemaPropertyName, property, definitions); } - var type = GetType(property); - - return type switch + var typeKeyword = property.GetKeyword(); + if (typeKeyword == null) { - DataType.Object => BuildObjectBranch(name, propertyNode, root, draft), - DataType.Array => BuildArrayBranch(name, propertyNode, root, draft), - _ => new SemanticLeafNode(name, type, "") - }; - } + return new SemanticLeafNode(schemaPropertyName, DataType.String, ""); + } - private SemanticTreeNode HandleReference(string name, string reference, JsonObject root, SchemaDraft draft) - { - if (TryResolveReference(reference, root, draft, out var referenceNode)) + var schemaType = GetSchemaType(typeKeyword); + if (schemaType is DataType.Object or DataType.Array) { - return ProcessProperty(name, referenceNode!, root, draft); + return BuildObjectNode(schemaPropertyName, schemaType, property, definitions); } - return new SemanticLeafNode(name, DataType.Unknown, ""); + return new SemanticLeafNode(schemaPropertyName, schemaType, ""); } - private static bool TryResolveReference(string reference, JsonObject root, SchemaDraft draft, out JsonNode? referenceNode) + private SemanticTreeNode HandleReference(string schemaPropertyName, JsonSchema property, DefinitionsKeyword definitions) { - referenceNode = null; - - if (!TryGetReferenceKey(reference, out var key)) + var refKeyword = property.GetKeyword(); + if (refKeyword == null) { - return false; + return new SemanticLeafNode(schemaPropertyName, DataType.String, ""); } - var preferred = GetPreferredDefinitionsProperty(draft); - var fallback = preferred == "$defs" ? "definitions" : "$defs"; - - if (TryGetDefinition(root, preferred, key, out referenceNode)) + var definitionKey = refKeyword.Reference.ToString().Replace("#/definitions/", ""); + if (definitions == null || !definitions.Definitions.TryGetValue(definitionKey, out var def)) { - return true; + return new SemanticLeafNode(schemaPropertyName, DataType.Unknown, ""); } - return TryGetDefinition(root, fallback, key, out referenceNode); - } - - private static bool TryGetReferenceKey(string reference, out string key) - { - key = string.Empty; - var prefix = GetReferencePrefix(reference); - if (prefix == null) return false; - key = DecodeJsonPointerToken(reference[prefix.Length..]); - return !string.IsNullOrWhiteSpace(key); - } - - private static string? GetReferencePrefix(string reference) - { - if (reference.StartsWith(DefsRefPrefix, StringComparison.OrdinalIgnoreCase)) return DefsRefPrefix; - if (reference.StartsWith(DefinitionsRefPrefix, StringComparison.OrdinalIgnoreCase)) return DefinitionsRefPrefix; - return null; - } - - private static bool TryGetDefinition(JsonObject root, string definitionsProperty, string key, out JsonNode? defNode) - { - defNode = null; - if (!root.TryGetPropertyValue(definitionsProperty, out var defsNode) || defsNode is not JsonObject defs) - return false; - return defs.TryGetPropertyValue(key, out defNode); - } - - private SemanticBranchNode BuildObjectBranch(string name, JsonNode node, JsonObject root, SchemaDraft draft) - { - var obj = node.AsObject(); - var branch = new SemanticBranchNode(name, DataType.Object); - - if (obj.TryGetPropertyValue("properties", out var propsNode) && - propsNode is JsonObject props) + var defTypeKeyword = def.GetKeyword(); + if (defTypeKeyword == null) { - foreach (var prop in props) - { - branch.AddChild(ProcessProperty(prop.Key, prop.Value!, root, draft)); - } + return new SemanticLeafNode(schemaPropertyName, DataType.String, ""); } - return branch; - } - - private SemanticBranchNode BuildArrayBranch(string name, JsonNode node, JsonObject root, SchemaDraft draft) - { - var obj = node.AsObject(); - var branch = new SemanticBranchNode(name, DataType.Array); - - if (!obj.TryGetPropertyValue("items", out var itemsNode) || - itemsNode is not JsonObject itemObj) + var schemaType = GetSchemaType(defTypeKeyword); + if (schemaType is DataType.Object or DataType.Array) { - return branch; + return BuildObjectNode(schemaPropertyName, schemaType, def, definitions); } - var itemType = GetType(itemObj); - - if (itemType == DataType.Object && - itemObj.TryGetPropertyValue("properties", out var propsNode) && - propsNode is JsonObject props) - { - foreach (var prop in props) - { - branch.AddChild(ProcessProperty(prop.Key, prop.Value!, root, draft)); - } + return new SemanticLeafNode(schemaPropertyName, schemaType, ""); + } - return branch; - } + private SemanticBranchNode BuildObjectNode(string schemaPropertyName, DataType dataType, JsonSchema schema, DefinitionsKeyword definitions) + { + var branchNode = new SemanticBranchNode(schemaPropertyName, dataType); - if (itemObj.TryGetPropertyValue("$ref", out var refNode)) + switch (dataType) { - var resolved = HandleReference(name, refNode!.GetValue(), root, draft); - - if (resolved is SemanticBranchNode refBranch) - { - foreach (var child in refBranch.Children) + case DataType.Object: { - branch.AddChild(child); + var propertiesKeyword = schema.GetKeyword(); + if (propertiesKeyword != null) + { + foreach (var prop in propertiesKeyword.Properties) + { + branchNode.AddChild(ProcessProperty(prop.Key, prop.Value, definitions)); + } + } + + break; + } + case DataType.Array: + { + var itemsKeyword = schema.GetKeyword(); + if (itemsKeyword == null) + { + var propertiesKeyword = schema.GetKeyword(); + if (propertiesKeyword != null) + { + foreach (var prop in propertiesKeyword.Properties) + { + branchNode.AddChild(ProcessProperty(prop.Key, prop.Value, definitions)); + } + } + + break; + } + + if (itemsKeyword is { SingleSchema: not null }) + { + branchNode.AddChild(ProcessProperty("item", itemsKeyword.SingleSchema, definitions)); + } + + break; } - } - else - { - branch.AddChild(resolved); - } - - return branch; } - branch.AddChild(new SemanticLeafNode(name, itemType, "")); - return branch; + return branchNode; } - private static DataType GetType(JsonObject obj) + private static DataType GetSchemaType(TypeKeyword typeKeyword) { - if (!obj.TryGetPropertyValue("type", out var typeNode)) - { - return DataType.String; - } + var t = typeKeyword.Type; - return typeNode!.ToString() switch + return t switch { - "object" => DataType.Object, - "array" => DataType.Array, - "string" => DataType.String, - "integer" => DataType.Integer, - "number" => DataType.Number, - "boolean" => DataType.Boolean, + _ when t.HasFlag(SchemaValueType.Object) => DataType.Object, + _ when t.HasFlag(SchemaValueType.Array) => DataType.Array, + _ when t.HasFlag(SchemaValueType.String) => DataType.String, + _ when t.HasFlag(SchemaValueType.Integer) => DataType.Integer, + _ when t.HasFlag(SchemaValueType.Number) => DataType.Number, + _ when t.HasFlag(SchemaValueType.Boolean) => DataType.Boolean, _ => DataType.String }; } - private static SchemaDraft GetSchemaDraft(JsonObject schema) - { - return GetSchemaDraftUri(schema) switch - { - Draft7Schema or Draft7SchemaHttps => SchemaDraft.Draft7, - Draft201909Schema => SchemaDraft.Draft201909, - _ => SchemaDraft.Draft202012 - }; - } - - private static string GetPreferredDefinitionsProperty(SchemaDraft draft) - { - return draft == SchemaDraft.Draft7 ? "definitions" : "$defs"; - } - - private static string GetSchemaDraftUri(JsonObject schema) - { - if (!schema.TryGetPropertyValue("$schema", out var schemaNode) || schemaNode == null) - { - return Draft202012Schema; - } - - var raw = schemaNode.GetValue().Trim(); - - return raw switch - { - Draft7Schema or Draft7SchemaHttps => Draft7Schema, - Draft201909Schema => Draft201909Schema, - Draft202012Schema => Draft202012Schema, - _ => Draft202012Schema - }; - } - - private static JsonSchema ResolveMetaSchema(string draftUri) - { - return draftUri switch - { - Draft7Schema => MetaSchemas.Draft7, - Draft201909Schema => MetaSchemas.Draft201909, - _ => MetaSchemas.Draft202012 - }; - } - - private static string DecodeJsonPointerToken(string token) - { - return token - .Replace("~1", "/", StringComparison.OrdinalIgnoreCase) - .Replace("~0", "~", StringComparison.OrdinalIgnoreCase); - } } diff --git a/source/AAS.TwinEngine.Plugin.TestPlugin/Api/Submodel/Services/JsonSchemaValidator.cs b/source/AAS.TwinEngine.Plugin.TestPlugin/Api/Submodel/Services/JsonSchemaValidator.cs index 4171ccfa..6081beff 100644 --- a/source/AAS.TwinEngine.Plugin.TestPlugin/Api/Submodel/Services/JsonSchemaValidator.cs +++ b/source/AAS.TwinEngine.Plugin.TestPlugin/Api/Submodel/Services/JsonSchemaValidator.cs @@ -15,17 +15,7 @@ namespace AAS.TwinEngine.Plugin.TestPlugin.Api.Submodel.Services; public class JsonSchemaValidator(IOptions semantics, ILogger logger) : IJsonSchemaValidator { private readonly string _contextPrefix = semantics.Value.IndexContextPrefix; - private const string DefsPrefix = "#/$defs/"; private const string DefinitionsPrefix = "#/definitions/"; - private const string Draft7Schema = "http://json-schema.org/draft-07/schema#"; - private const string Draft7SchemaHttps = "https://json-schema.org/draft-07/schema#"; - private const string Draft201909Schema = "https://json-schema.org/draft/2019-09/schema"; - private const string Draft202012Schema = "https://json-schema.org/draft/2020-12/schema"; - - private readonly EvaluationOptions _evaluationOptions = new() - { - OutputFormat = OutputFormat.List - }; private static readonly JsonSerializerOptions Serialization = new() { @@ -51,12 +41,15 @@ public void ValidateResponseContent(string responseJson, JsonSchema requestSchem LogAndThrowException($"Failed to normalize request schema: {normalizeError}"); } + if (!TryRegisterJsonSchema(normalizedSchema, out var registerError)) + { + LogAndThrowException($"Failed to register schema: {registerError}"); + } + try { var schema = JsonSchema.FromText(normalizedSchema.ToJsonString()); - - var result = schema.Evaluate(responseDoc!.RootElement, _evaluationOptions); - + var result = schema.Evaluate(responseDoc!.RootElement, new EvaluationOptions { OutputFormat = OutputFormat.List }); if (!result.IsValid) { LogAndThrowException("Response did not validate against schema."); @@ -109,11 +102,10 @@ private bool TryNormalizeSchema(JsonSchema schema, out JsonObject normalized, ou var json = JsonSerializer.Serialize(schema, Serialization); normalized = JsonNode.Parse(json)?.AsObject() - ?? throw new ArgumentException("Failed to parse schema JSON."); + ?? throw new ArgumentException("Failed to parse schema JSON."); EscapeJsonReferencePointers(normalized); - - normalized["$schema"] = GetSchemaDraftUri(normalized); + normalized["$id"] = normalized["$id"]?.GetValue() ?? $"urn:uuid:{Guid.NewGuid():D}"; return true; } @@ -124,19 +116,38 @@ private bool TryNormalizeSchema(JsonSchema schema, out JsonObject normalized, ou } } + private static bool TryRegisterJsonSchema(JsonObject schemaJsonObject, out string? registrationErrorMessage) + { + registrationErrorMessage = null; + + try + { + var jsonSchema = JsonSchema.FromText(schemaJsonObject.ToJsonString()); + var schemaIdentifierUri = new Uri(schemaJsonObject["$id"]!.GetValue()!); + SchemaRegistry.Global.Register(schemaIdentifierUri, jsonSchema); + return true; + } + catch (Exception exception) + { + registrationErrorMessage = $"Schema registration failed: {exception.Message}"; + return false; + } + } + private void EscapeJsonReferencePointers(JsonNode? currentNode) { switch (currentNode) { - case JsonObject obj: - ProcessJsonObjectForEscaping(obj); + case JsonObject jsonObjectNode: + ProcessJsonObjectForEscaping(jsonObjectNode); break; - case JsonArray array: - foreach (var item in array) + case JsonArray jsonArrayNode: + foreach (var arrayElement in jsonArrayNode) { - EscapeJsonReferencePointers(item); + EscapeJsonReferencePointers(arrayElement); } + break; } } @@ -144,40 +155,37 @@ private void EscapeJsonReferencePointers(JsonNode? currentNode) private void ProcessJsonObjectForEscaping(JsonObject jsonObject) { var propertiesToRename = jsonObject - .Select(p => p.Key) - .Select(name => (original: name, stripped: RemoveContextSuffix(name))) - .Where(x => x.original != x.stripped) + .Select(property => property.Key) + .Select(propertyName => (originalName: propertyName, strippedName: RemoveContextSuffix(propertyName))) + .Where(namePair => namePair.strippedName != namePair.originalName) .ToList(); - foreach (var (original, stripped) in propertiesToRename) + foreach (var (originalName, strippedName) in propertiesToRename) { - RenameJsonProperty(jsonObject, original, stripped); + RenameJsonProperty(jsonObject, originalName, strippedName); } - if (jsonObject.TryGetPropertyValue("required", out var requiredNode) && - requiredNode is JsonArray requiredArray) + if (jsonObject.TryGetPropertyValue("required", out var requiredPropertiesNode) && + requiredPropertiesNode is JsonArray requiredPropertiesArray) { - RemoveContextSuffixFromRequiredProperties(requiredArray); + RemoveContextSuffixFromRequiredProperties(requiredPropertiesArray); } foreach (var property in jsonObject.ToList()) { - if (property.Key == "$ref" && - property.Value is JsonValue value && - value.TryGetValue(out var reference)) + var propertyName = property.Key; + var propertyValue = property.Value; + + if (propertyName == "$ref" && + propertyValue is JsonValue referenceValue && + referenceValue.TryGetValue(out var referenceString) && + referenceString.StartsWith(DefinitionsPrefix, StringComparison.OrdinalIgnoreCase)) { - if (reference.StartsWith(DefsPrefix, StringComparison.OrdinalIgnoreCase)) - { - jsonObject["$ref"] = BuildEscapedReferencePath(reference); - } - else if (reference.StartsWith(DefinitionsPrefix, StringComparison.OrdinalIgnoreCase)) - { - jsonObject["$ref"] = BuildEscapedReferencePath(reference); - } + jsonObject["$ref"] = BuildEscapedReferencePath(referenceString); } else { - EscapeJsonReferencePointers(property.Value); + EscapeJsonReferencePointers(propertyValue); } } } @@ -193,37 +201,15 @@ private void RemoveContextSuffixFromRequiredProperties(JsonArray requiredPropert } } - private string BuildEscapedReferencePath(string reference) + private string BuildEscapedReferencePath(string originalReferencePath) { - if (reference.StartsWith(DefsPrefix, StringComparison.OrdinalIgnoreCase)) - { - return BuildEscapedReferencePath(reference, DefsPrefix); - } - - if (reference.StartsWith(DefinitionsPrefix, StringComparison.OrdinalIgnoreCase)) - { - return BuildEscapedReferencePath(reference, DefinitionsPrefix); - } + var referenceWithoutPrefix = originalReferencePath[DefinitionsPrefix.Length..]; - return reference; - } + var strippedReference = RemoveContextSuffix(referenceWithoutPrefix); - private string BuildEscapedReferencePath(string reference, string prefix) - { - if (!reference.StartsWith(prefix, StringComparison.OrdinalIgnoreCase)) - { - return reference; - } + var escapedReference = strippedReference.Replace("~", "~0", StringComparison.OrdinalIgnoreCase).Replace("/", "~1", StringComparison.OrdinalIgnoreCase); - var body = reference[prefix.Length..]; - - var stripped = RemoveContextSuffix(body); - - var escaped = stripped - .Replace("~", "~0", StringComparison.OrdinalIgnoreCase) - .Replace("/", "~1", StringComparison.OrdinalIgnoreCase); - - return prefix + escaped; + return DefinitionsPrefix + escapedReference; } private string RemoveContextSuffix(string propertyName) @@ -240,25 +226,7 @@ private static void RenameJsonProperty(JsonObject jsonObject, string oldProperty } var propertyValue = jsonObject[oldPropertyName]; - _ = jsonObject.Remove(oldPropertyName); + jsonObject.Remove(oldPropertyName); jsonObject[newPropertyName] = propertyValue!; } - - private static string GetSchemaDraftUri(JsonObject schema) - { - if (!schema.TryGetPropertyValue("$schema", out var schemaNode) || schemaNode == null) - { - return Draft202012Schema; - } - - var raw = schemaNode.GetValue().Trim(); - - return raw switch - { - Draft7Schema or Draft7SchemaHttps => Draft7Schema, - Draft201909Schema => Draft201909Schema, - Draft202012Schema => Draft202012Schema, - _ => Draft202012Schema - }; - } } diff --git a/source/AAS.TwinEngine.Plugin.TestPlugin/Api/Submodel/SubmodelController.cs b/source/AAS.TwinEngine.Plugin.TestPlugin/Api/Submodel/SubmodelController.cs index d6236a5a..57bc4186 100644 --- a/source/AAS.TwinEngine.Plugin.TestPlugin/Api/Submodel/SubmodelController.cs +++ b/source/AAS.TwinEngine.Plugin.TestPlugin/Api/Submodel/SubmodelController.cs @@ -1,5 +1,4 @@ -using System.Text.Json; -using System.Text.Json.Nodes; +using System.Text.Json.Nodes; using AAS.TwinEngine.Plugin.TestPlugin.Api.Submodel.Handler; using AAS.TwinEngine.Plugin.TestPlugin.Api.Submodel.Requests; diff --git a/source/AAS.TwinEngine.Plugin.TestPlugin/Data/mock-submodel-data.json b/source/AAS.TwinEngine.Plugin.TestPlugin/Data/mock-submodel-data.json index 88e91ac7..fbcadb6b 100644 --- a/source/AAS.TwinEngine.Plugin.TestPlugin/Data/mock-submodel-data.json +++ b/source/AAS.TwinEngine.Plugin.TestPlugin/Data/mock-submodel-data.json @@ -19,6 +19,15 @@ "https://mm-software.com/Phone/AvailableTime": { "https://mm-software.com/Phone/AvailableTime_de": "Dienstags bis Donnerstags, 9-17 Uhr" } + }, + { + "0173-1#02-AAO136#002": { + "0173-1#02-AAO136#002_en": "345-678-9012" + }, + "https://mm-software.com/Phone/AvailableTime": { + "https://mm-software.com/Phone/AvailableTime_de": "Montags, 10-16 Uhr" + }, + "0173-1#02-AAO137#003": "Mobile" } ], "0173-1#02-AAQ836#005": [ @@ -59,6 +68,15 @@ "0173-1#02-AAO127#003_de": "Marketing" }, "https://admin-shell.io/zvei/nameplate/1/0/ContactInformations/ContactInformation/Phone": [ + { + "0173-1#02-AAO136#002": { + "0173-1#02-AAO136#002_en": "234-567-8901" + }, + "0173-1#02-AAO137#003": "Office", + "https://mm-software.com/Phone/AvailableTime": { + "https://mm-software.com/Phone/AvailableTime_de": "Dienstags bis Donnerstags, 9-17 Uhr" + } + }, { "0173-1#02-AAO136#002": { "0173-1#02-AAO136#002_en": "345-678-9012" @@ -485,6 +503,15 @@ "0173-1#02-AAO127#003_de": "Marketing" }, "https://admin-shell.io/zvei/nameplate/1/0/ContactInformations/ContactInformation/Phone": [ + { + "0173-1#02-AAO136#002": { + "0173-1#02-AAO136#002_en": "234-567-8901" + }, + "0173-1#02-AAO137#003": "Office", + "https://mm-software.com/Phone/AvailableTime": { + "https://mm-software.com/Phone/AvailableTime_de": "Dienstags bis Donnerstags, 9-17 Uhr" + } + }, { "0173-1#02-AAO136#002": { "0173-1#02-AAO136#002_en": "345-678-9012" diff --git a/source/AAS.TwinEngine.Plugin.TestPlugin/Example/plugin/mock-submodel-data.json b/source/AAS.TwinEngine.Plugin.TestPlugin/Example/plugin/mock-submodel-data.json index 88e91ac7..fbcadb6b 100644 --- a/source/AAS.TwinEngine.Plugin.TestPlugin/Example/plugin/mock-submodel-data.json +++ b/source/AAS.TwinEngine.Plugin.TestPlugin/Example/plugin/mock-submodel-data.json @@ -19,6 +19,15 @@ "https://mm-software.com/Phone/AvailableTime": { "https://mm-software.com/Phone/AvailableTime_de": "Dienstags bis Donnerstags, 9-17 Uhr" } + }, + { + "0173-1#02-AAO136#002": { + "0173-1#02-AAO136#002_en": "345-678-9012" + }, + "https://mm-software.com/Phone/AvailableTime": { + "https://mm-software.com/Phone/AvailableTime_de": "Montags, 10-16 Uhr" + }, + "0173-1#02-AAO137#003": "Mobile" } ], "0173-1#02-AAQ836#005": [ @@ -59,6 +68,15 @@ "0173-1#02-AAO127#003_de": "Marketing" }, "https://admin-shell.io/zvei/nameplate/1/0/ContactInformations/ContactInformation/Phone": [ + { + "0173-1#02-AAO136#002": { + "0173-1#02-AAO136#002_en": "234-567-8901" + }, + "0173-1#02-AAO137#003": "Office", + "https://mm-software.com/Phone/AvailableTime": { + "https://mm-software.com/Phone/AvailableTime_de": "Dienstags bis Donnerstags, 9-17 Uhr" + } + }, { "0173-1#02-AAO136#002": { "0173-1#02-AAO136#002_en": "345-678-9012" @@ -485,6 +503,15 @@ "0173-1#02-AAO127#003_de": "Marketing" }, "https://admin-shell.io/zvei/nameplate/1/0/ContactInformations/ContactInformation/Phone": [ + { + "0173-1#02-AAO136#002": { + "0173-1#02-AAO136#002_en": "234-567-8901" + }, + "0173-1#02-AAO137#003": "Office", + "https://mm-software.com/Phone/AvailableTime": { + "https://mm-software.com/Phone/AvailableTime_de": "Dienstags bis Donnerstags, 9-17 Uhr" + } + }, { "0173-1#02-AAO136#002": { "0173-1#02-AAO136#002_en": "345-678-9012" diff --git a/source/AAS.TwinEngine.Plugin.TestPlugin/Program.cs b/source/AAS.TwinEngine.Plugin.TestPlugin/Program.cs index 5db632e4..32168dff 100644 --- a/source/AAS.TwinEngine.Plugin.TestPlugin/Program.cs +++ b/source/AAS.TwinEngine.Plugin.TestPlugin/Program.cs @@ -26,10 +26,6 @@ public static async Task Main(string[] args) builder.Services.AddHealthChecks().AddCheck("mock_data"); builder.Services.AddControllers(); - builder.Services.Configure(options => - { - options.MaxValidationDepth = null; - }); builder.Services.AddEndpointsApiExplorer(); builder.Services.AddOpenApiDocument(settings => From 1c3f0cef5995bb3786ef776b6100f0bc4ffb4945 Mon Sep 17 00:00:00 2001 From: Hardi Shah Date: Wed, 29 Apr 2026 14:59:59 +0530 Subject: [PATCH 66/71] Refactor schema compatibility handling for plugins Introduced IPluginSchemaCompatibilityHandler to encapsulate schema generation and legacy fallback logic. Replaced direct usage of IJsonSchemaGenerator and ILegacySchemaRetryHandler in PluginDataHandler with the new handler. Updated all usages and tests accordingly. Changed PluginDataProvider to throw PluginSchemaRejectionException on 404 responses. Registered the new handler in DI and modernized test setup with C# 12 collection expressions. --- .../Services/PluginDataHandlerTests.cs | 184 +++++++++++++----- .../Services/PluginDataProviderTests.cs | 5 +- .../IPluginSchemaCompatibilityHandler.cs | 15 ++ .../PluginSchemaCompatibilityHandler.cs | 20 ++ .../Services/PluginDataHandler.cs | 14 +- .../Services/PluginDataProvider.cs | 4 +- ...astructureDependencyInjectionExtensions.cs | 1 + 7 files changed, 180 insertions(+), 63 deletions(-) create mode 100644 source/AAS.TwinEngine.DataEngine/ApplicationLogic/Services/Plugin/Helper/IPluginSchemaCompatibilityHandler.cs create mode 100644 source/AAS.TwinEngine.DataEngine/Infrastructure/Providers/PluginDataProvider/Helper/PluginSchemaCompatibilityHandler.cs diff --git a/source/AAS.TwinEngine.DataEngine.UnitTests/Infrastructure/Providers/PluginDataProvider/Services/PluginDataHandlerTests.cs b/source/AAS.TwinEngine.DataEngine.UnitTests/Infrastructure/Providers/PluginDataProvider/Services/PluginDataHandlerTests.cs index f03954d9..fe5b731f 100644 --- a/source/AAS.TwinEngine.DataEngine.UnitTests/Infrastructure/Providers/PluginDataProvider/Services/PluginDataHandlerTests.cs +++ b/source/AAS.TwinEngine.DataEngine.UnitTests/Infrastructure/Providers/PluginDataProvider/Services/PluginDataHandlerTests.cs @@ -32,8 +32,7 @@ public class PluginDataHandlerTests private readonly IPluginRequestBuilder _pluginRequestBuilder; private readonly IPluginDataProvider _pluginDataProvider; private readonly IJsonSchemaValidator _jsonSchemaValidator; - private readonly IJsonSchemaGenerator _jsonSchemaGenerator; - private readonly ILegacySchemaRetryHandler _legacySchemaRetryHandler; + private readonly IPluginSchemaCompatibilityHandler _pluginSchemaCompatibilityHandler; private readonly IMultiPluginDataHandler _multiPluginDataHandler; private readonly ILogger _logger; private readonly IOptions _options; @@ -44,8 +43,7 @@ public PluginDataHandlerTests() _pluginRequestBuilder = Substitute.For(); _pluginDataProvider = Substitute.For(); _jsonSchemaValidator = Substitute.For(); - _jsonSchemaGenerator = Substitute.For(); - _legacySchemaRetryHandler = Substitute.For(); + _pluginSchemaCompatibilityHandler = Substitute.For(); _multiPluginDataHandler = Substitute.For(); _logger = Substitute.For>(); _options = Options.Create(new GeneralConfig @@ -53,7 +51,7 @@ public PluginDataHandlerTests() DataEngineRepositoryBaseUrl = new Uri("https://www.mm-software.com"), }); - _sut = new PluginDataHandler(_pluginRequestBuilder, _pluginDataProvider, _jsonSchemaValidator, _jsonSchemaGenerator, _legacySchemaRetryHandler, _multiPluginDataHandler, _logger, _options); + _sut = new PluginDataHandler(_pluginRequestBuilder, _pluginDataProvider, _jsonSchemaValidator, _pluginSchemaCompatibilityHandler, _multiPluginDataHandler, _logger, _options); } private readonly JsonSerializerOptions _jsonoptions = new() @@ -72,6 +70,7 @@ public PluginDataHandlerTests() public async Task TryGetValuesAsync_WithValidManifestAndResponse_ReturnsMergedSemanticTreeNode() { var inputSemanticTreeNode = new SemanticLeafNode("Contact", "", DataType.String, Cardinality.One); + const string ExpectedJsonResponse = """ { "Contact": "value" @@ -101,7 +100,7 @@ public async Task TryGetValuesAsync_WithValidManifestAndResponse_ReturnsMergedSe { PluginName = "TestPlugin", PluginUrl = new Uri("http://localhost"), - SupportedSemanticIds = new List { "Contact" }, + SupportedSemanticIds = ["Contact"], Capabilities = new Capabilities { HasShellDescriptor = true } } }; @@ -110,6 +109,9 @@ public async Task TryGetValuesAsync_WithValidManifestAndResponse_ReturnsMergedSe .SplitByPluginManifests(Arg.Any(), Arg.Any>()) .Returns(new Dictionary { { "TestPlugin", inputSemanticTreeNode } }); + var generatedSchema = JsonSchema.FromText(JsonSchemaString); + _pluginSchemaCompatibilityHandler.GenerateSchema(inputSemanticTreeNode).Returns(generatedSchema); + _jsonSchemaValidator .When(x => x.ValidateRequestSchema(Arg.Any())) .Do(_ => { }); @@ -143,8 +145,9 @@ public async Task TryGetValuesAsync_WithValidManifestAndResponse_ReturnsMergedSe Assert.Equal("Contact", leaf.SemanticId); Assert.Equal("value", leaf.Value); - _jsonSchemaValidator.Received().ValidateRequestSchema(Arg.Any()); - _jsonSchemaValidator.Received().ValidateResponseContent(ExpectedJsonResponse, Arg.Any()); + _pluginSchemaCompatibilityHandler.Received(1).GenerateSchema(inputSemanticTreeNode); + _jsonSchemaValidator.Received(1).ValidateRequestSchema(generatedSchema); + _jsonSchemaValidator.Received(1).ValidateResponseContent(ExpectedJsonResponse, generatedSchema); } [Fact] @@ -166,7 +169,7 @@ public async Task TryGetValuesAsync_WhenSchemaKeyHasPrefix_ValidatesRequestAndRe { PluginName = "TestPlugin", PluginUrl = new Uri("http://localhost"), - SupportedSemanticIds = new List { "Contact" }, + SupportedSemanticIds = ["Contact"], Capabilities = new Capabilities { HasShellDescriptor = true } } }; @@ -175,6 +178,9 @@ public async Task TryGetValuesAsync_WhenSchemaKeyHasPrefix_ValidatesRequestAndRe .SplitByPluginManifests(Arg.Any(), Arg.Any>()) .Returns(new Dictionary { { PrefixedSchemaKey, inputSemanticTreeNode } }); + var generatedSchema = CreateGeneratedSchema(); + _pluginSchemaCompatibilityHandler.GenerateSchema(inputSemanticTreeNode).Returns(generatedSchema); + _pluginRequestBuilder .Build(Arg.Any>()) .Returns(requestList); @@ -186,7 +192,7 @@ public async Task TryGetValuesAsync_WhenSchemaKeyHasPrefix_ValidatesRequestAndRe _pluginDataProvider .GetDataForSemanticIdsAsync(Arg.Any>(), Arg.Any(), Arg.Any()) - .Returns(_ => Task.FromResult>(new List { httpResponse.Content })); + .Returns(_ => Task.FromResult>([httpResponse.Content])); _multiPluginDataHandler .Merge(Arg.Any(), Arg.Any>()) @@ -194,8 +200,9 @@ public async Task TryGetValuesAsync_WhenSchemaKeyHasPrefix_ValidatesRequestAndRe _ = await _sut.TryGetValuesAsync(manifests, inputSemanticTreeNode, "submodelId", CancellationToken.None); - _jsonSchemaValidator.Received().ValidateRequestSchema(Arg.Any()); - _jsonSchemaValidator.Received().ValidateResponseContent(ExpectedJsonResponse, Arg.Any()); + _pluginSchemaCompatibilityHandler.Received(1).GenerateSchema(inputSemanticTreeNode); + _jsonSchemaValidator.Received(1).ValidateRequestSchema(generatedSchema); + _jsonSchemaValidator.Received(1).ValidateResponseContent(ExpectedJsonResponse, generatedSchema); } [Fact] @@ -226,19 +233,19 @@ public async Task GetDataForAllShellDescriptorsAsync_ReturnsListWithHrefSet() }; _multiPluginDataHandler.GetAvailablePlugins(manifests, Arg.Any>()) - .Returns(new List { "PluginA" }); + .Returns(["PluginA"]); _pluginRequestBuilder.Build(Arg.Any>()) - .Returns(new List { new($"{HttpClientNames.PluginDataProviderPrefix}PluginA", "") }); + .Returns([new($"{HttpClientNames.PluginDataProviderPrefix}PluginA", "")]); _pluginDataProvider .GetDataForAllShellDescriptorsAsync(null, null, Arg.Any>(), Arg.Any()) - .Returns(new List { httpResponse.Content }); + .Returns([httpResponse.Content]); var result = await _sut.GetDataForAllShellDescriptorsAsync(null, null, manifests, CancellationToken.None); - Assert.Equal(2, result.ShellDescriptors.Count); - Assert.All(result.ShellDescriptors, dto => Assert.StartsWith("https://www.mm-software.com/shells/", dto.Href)); + Assert.Equal(2, result.ShellDescriptors!.Count); + Assert.All(result.ShellDescriptors, dto => Assert.StartsWith("https://www.mm-software.com/shells/", dto.Href, StringComparison.OrdinalIgnoreCase)); } [Fact] @@ -256,7 +263,7 @@ public async Task GetDataForAllShellDescriptorsAsync_Throws_WhenDeserializationF }; _multiPluginDataHandler.GetAvailablePlugins(manifests, Arg.Any>()) - .Returns(new List { "PluginA" }); + .Returns(["PluginA"]); var httpResponse = new HttpResponseMessage(HttpStatusCode.OK) { @@ -265,7 +272,7 @@ public async Task GetDataForAllShellDescriptorsAsync_Throws_WhenDeserializationF _pluginDataProvider .GetDataForAllShellDescriptorsAsync(null, null, Arg.Any>(), Arg.Any()) - .Returns(new List { httpResponse.Content }); + .Returns([httpResponse.Content]); await Assert.ThrowsAsync(() => _sut.GetDataForAllShellDescriptorsAsync(null, null, manifests, CancellationToken.None)); @@ -294,19 +301,18 @@ public async Task GetDataForShellDescriptorAsync_ReturnsSingleWithHrefSet() }; _multiPluginDataHandler.GetAvailablePlugins(manifests, Arg.Any>()) - .Returns(new List { "PluginA" }); + .Returns(["PluginA"]); _pluginRequestBuilder.Build(Arg.Any>(), Arg.Any()) - .Returns(new List { new($"{HttpClientNames.PluginDataProviderPrefix}PluginA", "") }); + .Returns([new($"{HttpClientNames.PluginDataProviderPrefix}PluginA", "")]); _pluginDataProvider .GetDataForShellDescriptorByIdAsync(Arg.Any>(), Arg.Any()) - .Returns(new List { httpResponse.Content }); - + .Returns([httpResponse.Content]); var result = await _sut.GetDataForShellDescriptorAsync(manifests, "ContactInformation", CancellationToken.None); Assert.Equal("ContactInformation", result.Id); - Assert.StartsWith("https://www.mm-software.com/shells/", result.Href); + Assert.StartsWith("https://www.mm-software.com/shells/", result.Href, StringComparison.OrdinalIgnoreCase); } [Fact] @@ -324,8 +330,7 @@ public async Task GetDataForShellDescriptorAsync_ShouldThrow_WhenJsonMalformed() }; _multiPluginDataHandler.GetAvailablePlugins(manifests, Arg.Any>()) - .Returns(new List { "PluginA" }); - + .Returns(["PluginA"]); var httpResponse = new HttpResponseMessage(HttpStatusCode.OK) { Content = new StringContent("{ invalid json }", Encoding.UTF8, "application/json") @@ -333,7 +338,7 @@ public async Task GetDataForShellDescriptorAsync_ShouldThrow_WhenJsonMalformed() _pluginDataProvider .GetDataForShellDescriptorByIdAsync(Arg.Any>(), Arg.Any()) - .Returns(new List { httpResponse.Content }); + .Returns([httpResponse.Content]); await Assert.ThrowsAsync(() => _sut.GetDataForShellDescriptorAsync(manifests, "id", CancellationToken.None)); @@ -359,15 +364,14 @@ public async Task GetDataForAssetInformationByIdAsync_ReturnsAssetInformation() }; _multiPluginDataHandler.GetAvailablePlugins(manifests, Arg.Any>()) - .Returns(new List { "PluginA" }); + .Returns(["PluginA"]); _pluginRequestBuilder.Build(Arg.Any>(), Arg.Any()) - .Returns(new List { new($"{HttpClientNames.PluginDataProviderPrefix}PluginA", "") }); + .Returns([new($"{HttpClientNames.PluginDataProviderPrefix}PluginA", "")]); _pluginDataProvider .GetDataForAssetInformationByIdAsync(Arg.Any>(), Arg.Any()) - .Returns(new List { httpResponse.Content }); - + .Returns([httpResponse.Content]); var result = await _sut.GetDataForAssetInformationByIdAsync(manifests, "ContactInformation", CancellationToken.None); Assert.IsType(result); @@ -389,7 +393,7 @@ public async Task GetDataForAssetInformationByIdAsync_ShouldThrow_WhenJsonInvali }; _multiPluginDataHandler.GetAvailablePlugins(manifests, Arg.Any>()) - .Returns(new List { "PluginA" }); + .Returns(["PluginA"]); var httpResponse = new HttpResponseMessage(HttpStatusCode.OK) { @@ -398,7 +402,7 @@ public async Task GetDataForAssetInformationByIdAsync_ShouldThrow_WhenJsonInvali _pluginDataProvider .GetDataForAssetInformationByIdAsync(Arg.Any>(), Arg.Any()) - .Returns(new List { httpResponse.Content }); + .Returns([httpResponse.Content]); await Assert.ThrowsAsync(() => _sut.GetDataForAssetInformationByIdAsync(manifests, "ContactInformation", CancellationToken.None)); @@ -419,7 +423,7 @@ public async Task GetDataForAssetInformationByIdAsync_ShouldThrow_WhenDeserializ }; _multiPluginDataHandler.GetAvailablePlugins(manifests, Arg.Any>()) - .Returns(new List { "PluginA" }); + .Returns(["PluginA"]); var httpResponse = new HttpResponseMessage(HttpStatusCode.OK) { @@ -428,7 +432,7 @@ public async Task GetDataForAssetInformationByIdAsync_ShouldThrow_WhenDeserializ _pluginDataProvider .GetDataForAssetInformationByIdAsync(Arg.Any>(), Arg.Any()) - .Returns(new List { httpResponse.Content }); + .Returns([httpResponse.Content]); await Assert.ThrowsAsync(() => _sut.GetDataForAssetInformationByIdAsync(manifests, "ContactInformation", CancellationToken.None)); @@ -444,76 +448,151 @@ public static JsonContent ConvertToJsonContent(string json) }); } + private static JsonSchema CreateGeneratedSchema() => JsonSchema.FromText( + """ + { + "$schema": "https://json-schema.org/draft/2020-12/schema", + "type": "object", + "properties": { + "Contact": { "type": "string" } + } + } + """); + [Fact] public async Task TryGetValuesAsync_WhenPluginRejectsSchema_FallsBackToLegacyRetryHandler() { var inputNode = new SemanticLeafNode("Contact", "", DataType.String, Cardinality.One); const string ExpectedJsonResponse = """{ "Contact": "value" }"""; + var generatedSchema = CreateGeneratedSchema(); + _multiPluginDataHandler .SplitByPluginManifests(Arg.Any(), Arg.Any>()) .Returns(new Dictionary { { "TestPlugin", inputNode } }); + + _pluginSchemaCompatibilityHandler.GenerateSchema(inputNode).Returns(generatedSchema); _pluginRequestBuilder .Build(Arg.Any>()) - .Returns(new List { new("TestPlugin", ConvertToJsonContent("{}")) }); + .Returns([new("TestPlugin", ConvertToJsonContent("{}"))]); _pluginDataProvider .GetDataForSemanticIdsAsync(Arg.Any>(), Arg.Any(), Arg.Any()) .ThrowsAsync(new PluginSchemaRejectionException()); + using var retryContent = new StringContent(ExpectedJsonResponse, Encoding.UTF8, "application/json"); - _legacySchemaRetryHandler - .RetryWithDraft7Async(Arg.Any>(), Arg.Any(), Arg.Any()) - .Returns(Task.FromResult>(new List { retryContent })); + _pluginSchemaCompatibilityHandler + .RetryWithLegacySchemaAsync(Arg.Any>(), Arg.Any(), Arg.Any()) + .Returns(Task.FromResult>([retryContent])); + _multiPluginDataHandler .Merge(Arg.Any(), Arg.Any>()) .Returns(ci => ci.ArgAt>(1).First()); + var manifests = new List { new() { PluginName = "TestPlugin", PluginUrl = new Uri("http://localhost"), - SupportedSemanticIds = new List { "Contact" }, + SupportedSemanticIds = ["Contact"], Capabilities = new Capabilities { HasShellDescriptor = false } } }; var result = await _sut.TryGetValuesAsync(manifests, inputNode, "submodelId", CancellationToken.None); - await _legacySchemaRetryHandler.Received(1) - .RetryWithDraft7Async( + await _pluginSchemaCompatibilityHandler.Received(1) + .RetryWithLegacySchemaAsync( Arg.Is>(d => d.ContainsKey("TestPlugin")), "submodelId", Arg.Any()); Assert.NotNull(result); + _pluginSchemaCompatibilityHandler.Received(1).GenerateSchema(inputNode); + } + + [Fact] + public async Task TryGetValuesAsync_WhenBuildingRequests_UsesInjectedJsonSchemaGeneratorOutput() + { + var inputNode = new SemanticLeafNode("Contact", "", DataType.String, Cardinality.One); + var generatedSchema = CreateGeneratedSchema(); + const string expectedJsonResponse = "{\"Contact\":\"value\"}"; + + _multiPluginDataHandler + .SplitByPluginManifests(Arg.Any(), Arg.Any>()) + .Returns(new Dictionary { { "TestPlugin", inputNode } }); + + _pluginSchemaCompatibilityHandler.GenerateSchema(inputNode).Returns(generatedSchema); + + _pluginRequestBuilder + .Build(Arg.Is>(schemas => + schemas.Count == 1 && + schemas["TestPlugin"] == generatedSchema)) + .Returns([new("TestPlugin", ConvertToJsonContent("{}"))]); + + using var content = new StringContent(expectedJsonResponse, Encoding.UTF8, "application/json"); + _pluginDataProvider + .GetDataForSemanticIdsAsync(Arg.Any>(), Arg.Any(), Arg.Any()) + .Returns(Task.FromResult>(new List { content })); + + _multiPluginDataHandler + .Merge(Arg.Any(), Arg.Any>()) + .Returns(callInfo => callInfo.ArgAt>(1).First()); + + var manifests = new List + { + new() + { + PluginName = "TestPlugin", + PluginUrl = new Uri("http://localhost"), + SupportedSemanticIds = ["Contact"], + Capabilities = new Capabilities { HasShellDescriptor = false } + } + }; + + _ = await _sut.TryGetValuesAsync(manifests, inputNode, "submodelId", CancellationToken.None); + + _pluginSchemaCompatibilityHandler.Received(1).GenerateSchema(inputNode); + _pluginRequestBuilder.Received(1) + .Build(Arg.Is>(schemas => + schemas.Count == 1 && + schemas["TestPlugin"] == generatedSchema)); } [Fact] public async Task TryGetValuesAsync_WhenPluginRejectsSchema_DoesNotCallProviderAgainDirectly() { var inputNode = new SemanticLeafNode("Contact", "", DataType.String, Cardinality.One); + _multiPluginDataHandler .SplitByPluginManifests(Arg.Any(), Arg.Any>()) .Returns(new Dictionary { { "TestPlugin", inputNode } }); + + _pluginSchemaCompatibilityHandler.GenerateSchema(inputNode).Returns(CreateGeneratedSchema()); + _pluginRequestBuilder .Build(Arg.Any>()) .Returns([new("TestPlugin", ConvertToJsonContent("{}"))]); + _pluginDataProvider .GetDataForSemanticIdsAsync(Arg.Any>(), Arg.Any(), Arg.Any()) .ThrowsAsync(new PluginSchemaRejectionException()); + using var retryContent = new StringContent("{\"Contact\":\"value\"}", Encoding.UTF8, "application/json"); - _legacySchemaRetryHandler - .RetryWithDraft7Async(Arg.Any>(), Arg.Any(), Arg.Any()) + _pluginSchemaCompatibilityHandler + .RetryWithLegacySchemaAsync(Arg.Any>(), Arg.Any(), Arg.Any()) .Returns(Task.FromResult>([retryContent])); + _multiPluginDataHandler .Merge(Arg.Any(), Arg.Any>()) .Returns(ci => ci.ArgAt>(1).First()); + var manifests = new List { new() { PluginName = "TestPlugin", PluginUrl = new Uri("http://localhost"), - SupportedSemanticIds = new List { "Contact" }, + SupportedSemanticIds = ["Contact"], Capabilities = new Capabilities { HasShellDescriptor = false } } }; @@ -531,14 +610,15 @@ public async Task TryGetValuesAsync_WhenRetryAlsoFails_PropagatesException() _multiPluginDataHandler .SplitByPluginManifests(Arg.Any(), Arg.Any>()) .Returns(new Dictionary { { "TestPlugin", inputNode } }); + _pluginSchemaCompatibilityHandler.GenerateSchema(inputNode).Returns(CreateGeneratedSchema()); _pluginRequestBuilder .Build(Arg.Any>()) .Returns([new("TestPlugin", ConvertToJsonContent("{}"))]); _pluginDataProvider .GetDataForSemanticIdsAsync(Arg.Any>(), Arg.Any(), Arg.Any()) .ThrowsAsync(new PluginSchemaRejectionException()); - _legacySchemaRetryHandler - .RetryWithDraft7Async(Arg.Any>(), Arg.Any(), Arg.Any()) + _pluginSchemaCompatibilityHandler + .RetryWithLegacySchemaAsync(Arg.Any>(), Arg.Any(), Arg.Any()) .ThrowsAsync(new ResponseParsingException()); var manifests = new List { @@ -546,7 +626,7 @@ public async Task TryGetValuesAsync_WhenRetryAlsoFails_PropagatesException() { PluginName = "TestPlugin", PluginUrl = new Uri("http://localhost"), - SupportedSemanticIds = [ "Contact" ], + SupportedSemanticIds = new List { "Contact" }, Capabilities = new Capabilities { HasShellDescriptor = false } } }; @@ -560,9 +640,11 @@ public async Task TryGetValuesAsync_WhenProviderSucceeds_DoesNotInvokeLegacyRetr { var inputNode = new SemanticLeafNode("Contact", "", DataType.String, Cardinality.One); const string ExpectedJsonResponse = """{ "Contact": "value" }"""; + _multiPluginDataHandler .SplitByPluginManifests(Arg.Any(), Arg.Any>()) .Returns(new Dictionary { { "TestPlugin", inputNode } }); + _pluginSchemaCompatibilityHandler.GenerateSchema(inputNode).Returns(CreateGeneratedSchema()); _pluginRequestBuilder .Build(Arg.Any>()) .Returns([new("TestPlugin", ConvertToJsonContent("{}"))]); @@ -579,15 +661,15 @@ public async Task TryGetValuesAsync_WhenProviderSucceeds_DoesNotInvokeLegacyRetr { PluginName = "TestPlugin", PluginUrl = new Uri("http://localhost"), - SupportedSemanticIds = new List { "Contact" }, + SupportedSemanticIds = ["Contact"], Capabilities = new Capabilities { HasShellDescriptor = false } } }; _ = await _sut.TryGetValuesAsync(manifests, inputNode, "submodelId", CancellationToken.None); - await _legacySchemaRetryHandler.DidNotReceive() - .RetryWithDraft7Async(Arg.Any>(), Arg.Any(), Arg.Any()); + await _pluginSchemaCompatibilityHandler.DidNotReceive() + .RetryWithLegacySchemaAsync(Arg.Any>(), Arg.Any(), Arg.Any()); } private const string AssetData = """ diff --git a/source/AAS.TwinEngine.DataEngine.UnitTests/Infrastructure/Providers/PluginDataProvider/Services/PluginDataProviderTests.cs b/source/AAS.TwinEngine.DataEngine.UnitTests/Infrastructure/Providers/PluginDataProvider/Services/PluginDataProviderTests.cs index 4160f957..638b2ad4 100644 --- a/source/AAS.TwinEngine.DataEngine.UnitTests/Infrastructure/Providers/PluginDataProvider/Services/PluginDataProviderTests.cs +++ b/source/AAS.TwinEngine.DataEngine.UnitTests/Infrastructure/Providers/PluginDataProvider/Services/PluginDataProviderTests.cs @@ -84,7 +84,7 @@ public async Task GetDataForSemanticIdsAsync_ReturnsValuesAsync(string endpoint) } [Fact] - public async Task GetDataForSemanticIdsAsync_404Response_ThrowsResourceNotFoundExceptionAsync() + public async Task GetDataForSemanticIdsAsync_WhenPluginReturnsNotFound_ThrowsPluginSchemaRejectionException() { using var simpleRequestSchema = ConvertToJsonContent(SimpleRequestSchema); var pluginRequest = new PluginRequestSubmodel($"{HttpClientNames.PluginDataProviderPrefix}PluginName", simpleRequestSchema); @@ -101,7 +101,7 @@ public async Task GetDataForSemanticIdsAsync_404Response_ThrowsResourceNotFoundE httpClient.BaseAddress = new Uri("https://example.com"); _httpClientFactory.CreateClient(pluginRequest.HttpClientName).Returns(httpClient); - await Assert.ThrowsAsync(() => + await Assert.ThrowsAsync(() => _sut.GetDataForSemanticIdsAsync(pluginRequests, "asdf", CancellationToken.None)); } @@ -345,6 +345,7 @@ await Assert.ThrowsAsync(() => } [Theory] + [InlineData(HttpStatusCode.NotFound)] [InlineData(HttpStatusCode.BadRequest)] [InlineData(HttpStatusCode.InternalServerError)] public async Task GetDataForSemanticIdsAsync_WhenSchemaRejected_ThrowsPluginSchemaRejectionException(HttpStatusCode statusCode) diff --git a/source/AAS.TwinEngine.DataEngine/ApplicationLogic/Services/Plugin/Helper/IPluginSchemaCompatibilityHandler.cs b/source/AAS.TwinEngine.DataEngine/ApplicationLogic/Services/Plugin/Helper/IPluginSchemaCompatibilityHandler.cs new file mode 100644 index 00000000..a4f9a199 --- /dev/null +++ b/source/AAS.TwinEngine.DataEngine/ApplicationLogic/Services/Plugin/Helper/IPluginSchemaCompatibilityHandler.cs @@ -0,0 +1,15 @@ +using AAS.TwinEngine.DataEngine.DomainModel.SubmodelRepository; + +using Json.Schema; + +namespace AAS.TwinEngine.DataEngine.ApplicationLogic.Services.Plugin.Helper; + +public interface IPluginSchemaCompatibilityHandler +{ + JsonSchema GenerateSchema(SemanticTreeNode semanticNode); + + Task> RetryWithLegacySchemaAsync( + IDictionary semanticNodes, + string submodelId, + CancellationToken cancellationToken); +} diff --git a/source/AAS.TwinEngine.DataEngine/Infrastructure/Providers/PluginDataProvider/Helper/PluginSchemaCompatibilityHandler.cs b/source/AAS.TwinEngine.DataEngine/Infrastructure/Providers/PluginDataProvider/Helper/PluginSchemaCompatibilityHandler.cs new file mode 100644 index 00000000..1850e2ed --- /dev/null +++ b/source/AAS.TwinEngine.DataEngine/Infrastructure/Providers/PluginDataProvider/Helper/PluginSchemaCompatibilityHandler.cs @@ -0,0 +1,20 @@ +using AAS.TwinEngine.DataEngine.ApplicationLogic.Services.Plugin.Helper; +using AAS.TwinEngine.DataEngine.DomainModel.SubmodelRepository; + +using Json.Schema; + +namespace AAS.TwinEngine.DataEngine.Infrastructure.Providers.PluginDataProvider.Helper; + +public sealed class PluginSchemaCompatibilityHandler( + IJsonSchemaGenerator jsonSchemaGenerator, + ILegacySchemaRetryHandler legacySchemaRetryHandler) : IPluginSchemaCompatibilityHandler +{ + public JsonSchema GenerateSchema(SemanticTreeNode semanticNode) + => jsonSchemaGenerator.Generate(semanticNode); + + public Task> RetryWithLegacySchemaAsync( + IDictionary semanticNodes, + string submodelId, + CancellationToken cancellationToken) + => legacySchemaRetryHandler.RetryWithDraft7Async(semanticNodes, submodelId, cancellationToken); +} diff --git a/source/AAS.TwinEngine.DataEngine/Infrastructure/Providers/PluginDataProvider/Services/PluginDataHandler.cs b/source/AAS.TwinEngine.DataEngine/Infrastructure/Providers/PluginDataProvider/Services/PluginDataHandler.cs index 1506c113..fe6147c3 100644 --- a/source/AAS.TwinEngine.DataEngine/Infrastructure/Providers/PluginDataProvider/Services/PluginDataHandler.cs +++ b/source/AAS.TwinEngine.DataEngine/Infrastructure/Providers/PluginDataProvider/Services/PluginDataHandler.cs @@ -6,11 +6,11 @@ using AAS.TwinEngine.DataEngine.ApplicationLogic.Services.Plugin; using AAS.TwinEngine.DataEngine.ApplicationLogic.Services.Plugin.Helper; using AAS.TwinEngine.DataEngine.ApplicationLogic.Services.Plugin.Providers; -using AAS.TwinEngine.DataEngine.Infrastructure.Providers.PluginDataProvider.Helper; using AAS.TwinEngine.DataEngine.DomainModel.AasRegistry; using AAS.TwinEngine.DataEngine.DomainModel.AasRepository; using AAS.TwinEngine.DataEngine.DomainModel.Plugin; using AAS.TwinEngine.DataEngine.DomainModel.SubmodelRepository; +using AAS.TwinEngine.DataEngine.Infrastructure.Providers.PluginDataProvider.Helper; using AAS.TwinEngine.DataEngine.Infrastructure.Shared; using AAS.TwinEngine.DataEngine.ServiceConfiguration.Config; @@ -24,8 +24,7 @@ public class PluginDataHandler( IPluginRequestBuilder pluginRequestBuilder, IPluginDataProvider pluginDataProvider, IJsonSchemaValidator jsonSchemaValidator, - IJsonSchemaGenerator jsonSchemaGenerator, - ILegacySchemaRetryHandler legacySchemaRetryHandler, + IPluginSchemaCompatibilityHandler pluginSchemaCompatibilityHandler, IMultiPluginDataHandler multiPluginDataHandler, ILogger logger, IOptions generalConfig) : IPluginDataHandler @@ -42,7 +41,7 @@ public async Task TryGetValuesAsync(IReadOnlyList TryGetValuesAsync(IReadOnlyList(); @@ -66,8 +64,8 @@ public async Task TryGetValuesAsync(IReadOnlyList ProcessResponseAsync(HttpResponseMessage respons switch (response.StatusCode) { case HttpStatusCode.NotFound: - logger.LogError("Requested resource could not be found. Endpoint: {Url}", url); - throw new ResourceNotFoundException(); + logger.LogWarning("Plugin could not resolve the request schema (HTTP 404). Endpoint: {Url}", url); + throw new PluginSchemaRejectionException(); case HttpStatusCode.Unauthorized: case HttpStatusCode.Forbidden: diff --git a/source/AAS.TwinEngine.DataEngine/ServiceConfiguration/InfrastructureDependencyInjectionExtensions.cs b/source/AAS.TwinEngine.DataEngine/ServiceConfiguration/InfrastructureDependencyInjectionExtensions.cs index 8ee34e65..ad9f1bf6 100644 --- a/source/AAS.TwinEngine.DataEngine/ServiceConfiguration/InfrastructureDependencyInjectionExtensions.cs +++ b/source/AAS.TwinEngine.DataEngine/ServiceConfiguration/InfrastructureDependencyInjectionExtensions.cs @@ -126,6 +126,7 @@ public static void ConfigureInfrastructure(this IServiceCollection services, ICo #pragma warning disable CS0618 // Obsolete — intentional legacy Draft-07 fallback registration _ = services.AddScoped(); #pragma warning restore CS0618 + _ = services.AddScoped(); _ = services.AddScoped(); _ = services.AddScoped(); _ = services.AddScoped(); From 6b48467b184bf4eba0318475f7c98c2d10f9ff42 Mon Sep 17 00:00:00 2001 From: Hardi Shah Date: Wed, 29 Apr 2026 15:03:46 +0530 Subject: [PATCH 67/71] Use null-forgiving operator on schemaNode in validator Added the null-forgiving operator (!) to schemaNode before calling ToJsonString() in JsonSchemaValidator. This clarifies to the compiler that schemaNode is not null and suppresses nullability warnings. No functional changes were made. --- .../Providers/PluginDataProvider/Helper/JsonSchemaValidator.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/source/AAS.TwinEngine.DataEngine/Infrastructure/Providers/PluginDataProvider/Helper/JsonSchemaValidator.cs b/source/AAS.TwinEngine.DataEngine/Infrastructure/Providers/PluginDataProvider/Helper/JsonSchemaValidator.cs index 6b2c0730..4769d7e7 100644 --- a/source/AAS.TwinEngine.DataEngine/Infrastructure/Providers/PluginDataProvider/Helper/JsonSchemaValidator.cs +++ b/source/AAS.TwinEngine.DataEngine/Infrastructure/Providers/PluginDataProvider/Helper/JsonSchemaValidator.cs @@ -44,7 +44,7 @@ public void ValidateRequestSchema(JsonSchema schema) try { var metaSchema = ResolveMetaSchema(schemaNode); - using var schemaDoc = JsonDocument.Parse(schemaNode.ToJsonString()); + using var schemaDoc = JsonDocument.Parse(schemaNode!.ToJsonString()); var result = metaSchema.Evaluate(schemaDoc.RootElement, new EvaluationOptions { OutputFormat = OutputFormat.List }); if (!result.IsValid) { From 93b52e8dd763791bce7de83fef433c811c766719 Mon Sep 17 00:00:00 2001 From: Hardi Shah Date: Wed, 29 Apr 2026 15:22:48 +0530 Subject: [PATCH 68/71] Refactor contact info JSON structure for clarity Restructured Phone, Email, and IPCommunication elements to use object-based property mapping instead of arrays. Updated semantic IDs, value types, and mock data to match the new structure. Improved consistency and accessibility of contact information properties. --- ...ntactInfo_ContactInformation_Expected.json | 236 +++------ .../GetSubmodel_ContactInfo_Expected.json | 472 +++++------------- .../Data/mock-submodel-data.json | 171 +++---- .../Example/plugin/mock-submodel-data.json | 171 +++---- 4 files changed, 298 insertions(+), 752 deletions(-) diff --git a/source/AAS.TwinEngine.Plugin.TestPlugin.PlaywrightTests/SubmodelRepository/TestData/GetSubmodelElement_ContactInfo_ContactInformation_Expected.json b/source/AAS.TwinEngine.Plugin.TestPlugin.PlaywrightTests/SubmodelRepository/TestData/GetSubmodelElement_ContactInfo_ContactInformation_Expected.json index 2e628d84..b6135f12 100644 --- a/source/AAS.TwinEngine.Plugin.TestPlugin.PlaywrightTests/SubmodelRepository/TestData/GetSubmodelElement_ContactInfo_ContactInformation_Expected.json +++ b/source/AAS.TwinEngine.Plugin.TestPlugin.PlaywrightTests/SubmodelRepository/TestData/GetSubmodelElement_ContactInfo_ContactInformation_Expected.json @@ -245,13 +245,13 @@ "modelType": "MultiLanguageProperty" }, { - "idShort": "Email", + "idShort": "Phone", "semanticId": { "type": "ExternalReference", "keys": [ { "type": "GlobalReference", - "value": "0173-1#02-AAQ836#005" + "value": "https://admin-shell.io/zvei/nameplate/1/0/ContactInformations/ContactInformation/Phone" } ] }, @@ -265,13 +265,19 @@ ], "value": [ { - "idShort": "EmailAddress", + "idShort": "TelephoneNumber", + "description": [ + { + "language": "en", + "text": "Recommendation: property declaration as MLP is required by its semantic definition. As the property value is language independent, users are recommended to provide maximal 1 string in any language of the user’s choice." + } + ], "semanticId": { "type": "ExternalReference", "keys": [ { "type": "GlobalReference", - "value": "0173-1#02-AAO198#002" + "value": "0173-1#02-AAO136#002" } ] }, @@ -283,24 +289,22 @@ "value": "One" } ], - "valueType": "xs:string", - "value": "john.doe@example.com", - "modelType": "Property" - }, - { - "idShort": "TypeOfEmailAddress", - "description": [ + "value": [ { "language": "en", - "text": "enumeration: 0173-1#07-AAS754#001 (office), 0173-1#07-AAS756#001 (secretary), 0173-1#07-AAS757#001 (substitute), 0173-1#07-AAS758#001 (home)" + "text": "345-678-9012" } ], + "modelType": "MultiLanguageProperty" + }, + { + "idShort": "AvailableTime", "semanticId": { "type": "ExternalReference", "keys": [ { "type": "GlobalReference", - "value": "0173-1#02-AAO199#003" + "value": "https://admin-shell.io/zvei/nameplate/1/0/ContactInformations/ContactInformation/AvailableTime/" } ] }, @@ -312,87 +316,28 @@ "value": "ZeroToOne" } ], - "valueType": "xs:string", - "value": "Work", - "modelType": "Property" - } - ], - "modelType": "SubmodelElementCollection" - }, - { - "idShort": "IPCommunication__00__", - "semanticId": { - "type": "ExternalReference", - "keys": [ - { - "type": "GlobalReference", - "value": "https://admin-shell.io/zvei/nameplate/1/0/ContactInformations/" - } - ] - }, - "qualifiers": [ - { - "kind": "ConceptQualifier", - "type": "Multiplicity", - "valueType": "xs:string", - "value": "ZeroToMany" - } - ], - "value": [ - { - "idShort": "AddressOfAdditionalLink", - "semanticId": { - "type": "ExternalReference", - "keys": [ - { - "type": "GlobalReference", - "value": "0173-1#02-AAQ326#002" - } - ] - }, - "qualifiers": [ + "value": [ { - "kind": "ConceptQualifier", - "type": "Multiplicity", - "valueType": "xs:string", - "value": "One" + "language": "de", + "text": "Montags, 10-16 Uhr" } ], - "valueType": "xs:string", - "value": "https://www.mm-software.com/more-the-newsroom/", - "modelType": "Property" + "modelType": "MultiLanguageProperty" }, { - "idShort": "TypeOfCommunication", - "semanticId": { - "type": "ExternalReference", - "keys": [ - { - "type": "GlobalReference", - "value": "https://admin-shell.io/zvei/nameplate/1/0/ContactInformations/ContactInformation/IPCommunication/TypeOfCommunication" - } - ] - }, - "qualifiers": [ + "idShort": "TypeOfTelephone", + "description": [ { - "kind": "ConceptQualifier", - "type": "Multiplicity", - "valueType": "xs:string", - "value": "ZeroToOne" + "language": "en", + "text": " enumeration: 0173-1#07-AAS754#001 (office), 0173-1#07-AAS755#001 (office mobile), 0173-1#07-AAS756#001 (secretary), 0173-1#07-AAS757#001 (substitute), 0173-1#07-AAS758#001 (home), 0173-1#07-AAS759#001 (private mobile)" } ], - "valueType": "xs:string", - "value": "Chat", - "modelType": "Property" - }, - { - "idShort": "AvailableTime", "semanticId": { "type": "ExternalReference", "keys": [ { "type": "GlobalReference", - "value": "https://admin-shell.io/zvei/nameplate/1/0/ContactInformations/ContactInformation/AvailableTime/" + "value": "0173-1#02-AAO137#003" } ] }, @@ -404,25 +349,21 @@ "value": "ZeroToOne" } ], - "value": [ - { - "language": "de", - "text": "8:00 AM to 6:00 PM" - } - ], - "modelType": "MultiLanguageProperty" + "valueType": "xs:string", + "value": "Mobile", + "modelType": "Property" } ], "modelType": "SubmodelElementCollection" }, { - "idShort": "Phone0", + "idShort": "Email", "semanticId": { "type": "ExternalReference", "keys": [ { "type": "GlobalReference", - "value": "https://admin-shell.io/zvei/nameplate/1/0/ContactInformations/ContactInformation/Phone" + "value": "0173-1#02-AAQ836#005" } ] }, @@ -436,19 +377,13 @@ ], "value": [ { - "idShort": "TelephoneNumber", - "description": [ - { - "language": "en", - "text": "Recommendation: property declaration as MLP is required by its semantic definition. As the property value is language independent, users are recommended to provide maximal 1 string in any language of the user’s choice." - } - ], + "idShort": "EmailAddress", "semanticId": { "type": "ExternalReference", "keys": [ { "type": "GlobalReference", - "value": "0173-1#02-AAO136#002" + "value": "0173-1#02-AAO198#002" } ] }, @@ -460,47 +395,16 @@ "value": "One" } ], - "value": [ - { - "language": "en", - "text": "234-567-8901" - } - ], - "modelType": "MultiLanguageProperty" - }, - { - "idShort": "AvailableTime", - "semanticId": { - "type": "ExternalReference", - "keys": [ - { - "type": "GlobalReference", - "value": "https://admin-shell.io/zvei/nameplate/1/0/ContactInformations/ContactInformation/AvailableTime/" - } - ] - }, - "qualifiers": [ - { - "kind": "ConceptQualifier", - "type": "Multiplicity", - "valueType": "xs:string", - "value": "ZeroToOne" - } - ], - "value": [ - { - "language": "de", - "text": "Dienstags bis Donnerstags, 9-17 Uhr" - } - ], - "modelType": "MultiLanguageProperty" + "valueType": "xs:string", + "value": "john.doe@example.com", + "modelType": "Property" }, { - "idShort": "TypeOfTelephone", + "idShort": "TypeOfEmailAddress", "description": [ { "language": "en", - "text": " enumeration: 0173-1#07-AAS754#001 (office), 0173-1#07-AAS755#001 (office mobile), 0173-1#07-AAS756#001 (secretary), 0173-1#07-AAS757#001 (substitute), 0173-1#07-AAS758#001 (home), 0173-1#07-AAS759#001 (private mobile)" + "text": "enumeration: 0173-1#07-AAS754#001 (office), 0173-1#07-AAS756#001 (secretary), 0173-1#07-AAS757#001 (substitute), 0173-1#07-AAS758#001 (home)" } ], "semanticId": { @@ -508,7 +412,7 @@ "keys": [ { "type": "GlobalReference", - "value": "0173-1#02-AAO137#003" + "value": "0173-1#02-AAO199#003" } ] }, @@ -521,20 +425,20 @@ } ], "valueType": "xs:string", - "value": "Office", + "value": "Work", "modelType": "Property" } ], "modelType": "SubmodelElementCollection" }, { - "idShort": "Phone1", + "idShort": "IPCommunication__00__", "semanticId": { "type": "ExternalReference", "keys": [ { "type": "GlobalReference", - "value": "https://admin-shell.io/zvei/nameplate/1/0/ContactInformations/ContactInformation/Phone" + "value": "https://admin-shell.io/zvei/nameplate/1/0/ContactInformations/" } ] }, @@ -543,24 +447,18 @@ "kind": "ConceptQualifier", "type": "Multiplicity", "valueType": "xs:string", - "value": "ZeroToOne" + "value": "ZeroToMany" } ], "value": [ { - "idShort": "TelephoneNumber", - "description": [ - { - "language": "en", - "text": "Recommendation: property declaration as MLP is required by its semantic definition. As the property value is language independent, users are recommended to provide maximal 1 string in any language of the user’s choice." - } - ], + "idShort": "AddressOfAdditionalLink", "semanticId": { "type": "ExternalReference", "keys": [ { "type": "GlobalReference", - "value": "0173-1#02-AAO136#002" + "value": "0173-1#02-AAQ326#002" } ] }, @@ -572,22 +470,18 @@ "value": "One" } ], - "value": [ - { - "language": "en", - "text": "345-678-9012" - } - ], - "modelType": "MultiLanguageProperty" + "valueType": "xs:string", + "value": "https://www.mm-software.com/more-the-newsroom/", + "modelType": "Property" }, { - "idShort": "AvailableTime", + "idShort": "TypeOfCommunication", "semanticId": { "type": "ExternalReference", "keys": [ { "type": "GlobalReference", - "value": "https://admin-shell.io/zvei/nameplate/1/0/ContactInformations/ContactInformation/AvailableTime/" + "value": "https://admin-shell.io/zvei/nameplate/1/0/ContactInformations/ContactInformation/IPCommunication/TypeOfCommunication" } ] }, @@ -599,28 +493,18 @@ "value": "ZeroToOne" } ], - "value": [ - { - "language": "de", - "text": "Montags, 10-16 Uhr" - } - ], - "modelType": "MultiLanguageProperty" + "valueType": "xs:string", + "value": "Chat", + "modelType": "Property" }, { - "idShort": "TypeOfTelephone", - "description": [ - { - "language": "en", - "text": " enumeration: 0173-1#07-AAS754#001 (office), 0173-1#07-AAS755#001 (office mobile), 0173-1#07-AAS756#001 (secretary), 0173-1#07-AAS757#001 (substitute), 0173-1#07-AAS758#001 (home), 0173-1#07-AAS759#001 (private mobile)" - } - ], + "idShort": "AvailableTime", "semanticId": { "type": "ExternalReference", "keys": [ { "type": "GlobalReference", - "value": "0173-1#02-AAO137#003" + "value": "https://admin-shell.io/zvei/nameplate/1/0/ContactInformations/ContactInformation/AvailableTime/" } ] }, @@ -632,9 +516,13 @@ "value": "ZeroToOne" } ], - "valueType": "xs:string", - "value": "Mobile", - "modelType": "Property" + "value": [ + { + "language": "de", + "text": "8:00 AM to 6:00 PM" + } + ], + "modelType": "MultiLanguageProperty" } ], "modelType": "SubmodelElementCollection" diff --git a/source/AAS.TwinEngine.Plugin.TestPlugin.PlaywrightTests/SubmodelRepository/TestData/GetSubmodel_ContactInfo_Expected.json b/source/AAS.TwinEngine.Plugin.TestPlugin.PlaywrightTests/SubmodelRepository/TestData/GetSubmodel_ContactInfo_Expected.json index 249204e2..28e1f623 100644 --- a/source/AAS.TwinEngine.Plugin.TestPlugin.PlaywrightTests/SubmodelRepository/TestData/GetSubmodel_ContactInfo_Expected.json +++ b/source/AAS.TwinEngine.Plugin.TestPlugin.PlaywrightTests/SubmodelRepository/TestData/GetSubmodel_ContactInfo_Expected.json @@ -259,13 +259,13 @@ "modelType": "MultiLanguageProperty" }, { - "idShort": "Email", + "idShort": "Phone", "semanticId": { "type": "ExternalReference", "keys": [ { "type": "GlobalReference", - "value": "0173-1#02-AAQ836#005" + "value": "https://admin-shell.io/zvei/nameplate/1/0/ContactInformations/ContactInformation/Phone" } ] }, @@ -279,13 +279,19 @@ ], "value": [ { - "idShort": "EmailAddress", + "idShort": "TelephoneNumber", + "description": [ + { + "language": "en", + "text": "Recommendation: property declaration as MLP is required by its semantic definition. As the property value is language independent, users are recommended to provide maximal 1 string in any language of the user’s choice." + } + ], "semanticId": { "type": "ExternalReference", "keys": [ { "type": "GlobalReference", - "value": "0173-1#02-AAO198#002" + "value": "0173-1#02-AAO136#002" } ] }, @@ -297,24 +303,22 @@ "value": "One" } ], - "valueType": "xs:string", - "value": "john.doe@example.com", - "modelType": "Property" - }, - { - "idShort": "TypeOfEmailAddress", - "description": [ + "value": [ { "language": "en", - "text": "enumeration: 0173-1#07-AAS754#001 (office), 0173-1#07-AAS756#001 (secretary), 0173-1#07-AAS757#001 (substitute), 0173-1#07-AAS758#001 (home)" + "text": "234-567-8901" } ], + "modelType": "MultiLanguageProperty" + }, + { + "idShort": "AvailableTime", "semanticId": { "type": "ExternalReference", "keys": [ { "type": "GlobalReference", - "value": "0173-1#02-AAO199#003" + "value": "https://admin-shell.io/zvei/nameplate/1/0/ContactInformations/ContactInformation/AvailableTime/" } ] }, @@ -326,87 +330,28 @@ "value": "ZeroToOne" } ], - "valueType": "xs:string", - "value": "Work", - "modelType": "Property" - } - ], - "modelType": "SubmodelElementCollection" - }, - { - "idShort": "IPCommunication__00__", - "semanticId": { - "type": "ExternalReference", - "keys": [ - { - "type": "GlobalReference", - "value": "https://admin-shell.io/zvei/nameplate/1/0/ContactInformations/" - } - ] - }, - "qualifiers": [ - { - "kind": "ConceptQualifier", - "type": "Multiplicity", - "valueType": "xs:string", - "value": "ZeroToMany" - } - ], - "value": [ - { - "idShort": "AddressOfAdditionalLink", - "semanticId": { - "type": "ExternalReference", - "keys": [ - { - "type": "GlobalReference", - "value": "0173-1#02-AAQ326#002" - } - ] - }, - "qualifiers": [ + "value": [ { - "kind": "ConceptQualifier", - "type": "Multiplicity", - "valueType": "xs:string", - "value": "One" + "language": "de", + "text": "Dienstags bis Donnerstags, 9-17 Uhr" } ], - "valueType": "xs:string", - "value": "https://www.mm-software.com/more-the-newsroom/", - "modelType": "Property" + "modelType": "MultiLanguageProperty" }, { - "idShort": "TypeOfCommunication", - "semanticId": { - "type": "ExternalReference", - "keys": [ - { - "type": "GlobalReference", - "value": "https://admin-shell.io/zvei/nameplate/1/0/ContactInformations/ContactInformation/IPCommunication/TypeOfCommunication" - } - ] - }, - "qualifiers": [ + "idShort": "TypeOfTelephone", + "description": [ { - "kind": "ConceptQualifier", - "type": "Multiplicity", - "valueType": "xs:string", - "value": "ZeroToOne" + "language": "en", + "text": " enumeration: 0173-1#07-AAS754#001 (office), 0173-1#07-AAS755#001 (office mobile), 0173-1#07-AAS756#001 (secretary), 0173-1#07-AAS757#001 (substitute), 0173-1#07-AAS758#001 (home), 0173-1#07-AAS759#001 (private mobile)" } ], - "valueType": "xs:string", - "value": "Chat", - "modelType": "Property" - }, - { - "idShort": "AvailableTime", "semanticId": { "type": "ExternalReference", "keys": [ { "type": "GlobalReference", - "value": "https://admin-shell.io/zvei/nameplate/1/0/ContactInformations/ContactInformation/AvailableTime/" + "value": "0173-1#02-AAO137#003" } ] }, @@ -418,25 +363,21 @@ "value": "ZeroToOne" } ], - "value": [ - { - "language": "de", - "text": "8:00 AM to 6:00 PM" - } - ], - "modelType": "MultiLanguageProperty" + "valueType": "xs:string", + "value": "Office", + "modelType": "Property" } ], "modelType": "SubmodelElementCollection" }, { - "idShort": "Phone0", + "idShort": "Email", "semanticId": { "type": "ExternalReference", "keys": [ { "type": "GlobalReference", - "value": "https://admin-shell.io/zvei/nameplate/1/0/ContactInformations/ContactInformation/Phone" + "value": "0173-1#02-AAQ836#005" } ] }, @@ -450,19 +391,13 @@ ], "value": [ { - "idShort": "TelephoneNumber", - "description": [ - { - "language": "en", - "text": "Recommendation: property declaration as MLP is required by its semantic definition. As the property value is language independent, users are recommended to provide maximal 1 string in any language of the user’s choice." - } - ], + "idShort": "EmailAddress", "semanticId": { "type": "ExternalReference", "keys": [ { "type": "GlobalReference", - "value": "0173-1#02-AAO136#002" + "value": "0173-1#02-AAO198#002" } ] }, @@ -474,47 +409,16 @@ "value": "One" } ], - "value": [ - { - "language": "en", - "text": "234-567-8901" - } - ], - "modelType": "MultiLanguageProperty" - }, - { - "idShort": "AvailableTime", - "semanticId": { - "type": "ExternalReference", - "keys": [ - { - "type": "GlobalReference", - "value": "https://admin-shell.io/zvei/nameplate/1/0/ContactInformations/ContactInformation/AvailableTime/" - } - ] - }, - "qualifiers": [ - { - "kind": "ConceptQualifier", - "type": "Multiplicity", - "valueType": "xs:string", - "value": "ZeroToOne" - } - ], - "value": [ - { - "language": "de", - "text": "Dienstags bis Donnerstags, 9-17 Uhr" - } - ], - "modelType": "MultiLanguageProperty" + "valueType": "xs:string", + "value": "john.doe@example.com", + "modelType": "Property" }, { - "idShort": "TypeOfTelephone", + "idShort": "TypeOfEmailAddress", "description": [ { "language": "en", - "text": " enumeration: 0173-1#07-AAS754#001 (office), 0173-1#07-AAS755#001 (office mobile), 0173-1#07-AAS756#001 (secretary), 0173-1#07-AAS757#001 (substitute), 0173-1#07-AAS758#001 (home), 0173-1#07-AAS759#001 (private mobile)" + "text": "enumeration: 0173-1#07-AAS754#001 (office), 0173-1#07-AAS756#001 (secretary), 0173-1#07-AAS757#001 (substitute), 0173-1#07-AAS758#001 (home)" } ], "semanticId": { @@ -522,7 +426,7 @@ "keys": [ { "type": "GlobalReference", - "value": "0173-1#02-AAO137#003" + "value": "0173-1#02-AAO199#003" } ] }, @@ -535,20 +439,20 @@ } ], "valueType": "xs:string", - "value": "Office", + "value": "Work", "modelType": "Property" } ], "modelType": "SubmodelElementCollection" }, { - "idShort": "Phone1", + "idShort": "IPCommunication__00__", "semanticId": { "type": "ExternalReference", "keys": [ { "type": "GlobalReference", - "value": "https://admin-shell.io/zvei/nameplate/1/0/ContactInformations/ContactInformation/Phone" + "value": "https://admin-shell.io/zvei/nameplate/1/0/ContactInformations/" } ] }, @@ -557,24 +461,18 @@ "kind": "ConceptQualifier", "type": "Multiplicity", "valueType": "xs:string", - "value": "ZeroToOne" + "value": "ZeroToMany" } ], "value": [ { - "idShort": "TelephoneNumber", - "description": [ - { - "language": "en", - "text": "Recommendation: property declaration as MLP is required by its semantic definition. As the property value is language independent, users are recommended to provide maximal 1 string in any language of the user’s choice." - } - ], + "idShort": "AddressOfAdditionalLink", "semanticId": { "type": "ExternalReference", "keys": [ { "type": "GlobalReference", - "value": "0173-1#02-AAO136#002" + "value": "0173-1#02-AAQ326#002" } ] }, @@ -586,22 +484,18 @@ "value": "One" } ], - "value": [ - { - "language": "en", - "text": "345-678-9012" - } - ], - "modelType": "MultiLanguageProperty" + "valueType": "xs:string", + "value": "https://www.mm-software.com/more-the-newsroom/", + "modelType": "Property" }, { - "idShort": "AvailableTime", + "idShort": "TypeOfCommunication", "semanticId": { "type": "ExternalReference", "keys": [ { "type": "GlobalReference", - "value": "https://admin-shell.io/zvei/nameplate/1/0/ContactInformations/ContactInformation/AvailableTime/" + "value": "https://admin-shell.io/zvei/nameplate/1/0/ContactInformations/ContactInformation/IPCommunication/TypeOfCommunication" } ] }, @@ -613,28 +507,18 @@ "value": "ZeroToOne" } ], - "value": [ - { - "language": "de", - "text": "Montags, 10-16 Uhr" - } - ], - "modelType": "MultiLanguageProperty" + "valueType": "xs:string", + "value": "Chat", + "modelType": "Property" }, { - "idShort": "TypeOfTelephone", - "description": [ - { - "language": "en", - "text": " enumeration: 0173-1#07-AAS754#001 (office), 0173-1#07-AAS755#001 (office mobile), 0173-1#07-AAS756#001 (secretary), 0173-1#07-AAS757#001 (substitute), 0173-1#07-AAS758#001 (home), 0173-1#07-AAS759#001 (private mobile)" - } - ], + "idShort": "AvailableTime", "semanticId": { "type": "ExternalReference", "keys": [ { "type": "GlobalReference", - "value": "0173-1#02-AAO137#003" + "value": "https://admin-shell.io/zvei/nameplate/1/0/ContactInformations/ContactInformation/AvailableTime/" } ] }, @@ -646,9 +530,13 @@ "value": "ZeroToOne" } ], - "valueType": "xs:string", - "value": "Mobile", - "modelType": "Property" + "value": [ + { + "language": "de", + "text": "8:00 AM to 6:00 PM" + } + ], + "modelType": "MultiLanguageProperty" } ], "modelType": "SubmodelElementCollection" @@ -903,13 +791,13 @@ "modelType": "MultiLanguageProperty" }, { - "idShort": "Email", + "idShort": "Phone", "semanticId": { "type": "ExternalReference", "keys": [ { "type": "GlobalReference", - "value": "0173-1#02-AAQ836#005" + "value": "https://admin-shell.io/zvei/nameplate/1/0/ContactInformations/ContactInformation/Phone" } ] }, @@ -923,13 +811,19 @@ ], "value": [ { - "idShort": "EmailAddress", + "idShort": "TelephoneNumber", + "description": [ + { + "language": "en", + "text": "Recommendation: property declaration as MLP is required by its semantic definition. As the property value is language independent, users are recommended to provide maximal 1 string in any language of the user’s choice." + } + ], "semanticId": { "type": "ExternalReference", "keys": [ { "type": "GlobalReference", - "value": "0173-1#02-AAO198#002" + "value": "0173-1#02-AAO136#002" } ] }, @@ -941,24 +835,22 @@ "value": "One" } ], - "valueType": "xs:string", - "value": "john.doe@example.com", - "modelType": "Property" - }, - { - "idShort": "TypeOfEmailAddress", - "description": [ + "value": [ { "language": "en", - "text": "enumeration: 0173-1#07-AAS754#001 (office), 0173-1#07-AAS756#001 (secretary), 0173-1#07-AAS757#001 (substitute), 0173-1#07-AAS758#001 (home)" + "text": "345-678-9012" } ], + "modelType": "MultiLanguageProperty" + }, + { + "idShort": "AvailableTime", "semanticId": { "type": "ExternalReference", "keys": [ { "type": "GlobalReference", - "value": "0173-1#02-AAO199#003" + "value": "https://admin-shell.io/zvei/nameplate/1/0/ContactInformations/ContactInformation/AvailableTime/" } ] }, @@ -970,87 +862,28 @@ "value": "ZeroToOne" } ], - "valueType": "xs:string", - "value": "Work", - "modelType": "Property" - } - ], - "modelType": "SubmodelElementCollection" - }, - { - "idShort": "IPCommunication__00__", - "semanticId": { - "type": "ExternalReference", - "keys": [ - { - "type": "GlobalReference", - "value": "https://admin-shell.io/zvei/nameplate/1/0/ContactInformations/" - } - ] - }, - "qualifiers": [ - { - "kind": "ConceptQualifier", - "type": "Multiplicity", - "valueType": "xs:string", - "value": "ZeroToMany" - } - ], - "value": [ - { - "idShort": "AddressOfAdditionalLink", - "semanticId": { - "type": "ExternalReference", - "keys": [ - { - "type": "GlobalReference", - "value": "0173-1#02-AAQ326#002" - } - ] - }, - "qualifiers": [ + "value": [ { - "kind": "ConceptQualifier", - "type": "Multiplicity", - "valueType": "xs:string", - "value": "One" + "language": "de", + "text": "Montags, 10-16 Uhr" } ], - "valueType": "xs:string", - "value": "https://www.mm-software.com/more-the-newsroom/", - "modelType": "Property" + "modelType": "MultiLanguageProperty" }, { - "idShort": "TypeOfCommunication", - "semanticId": { - "type": "ExternalReference", - "keys": [ - { - "type": "GlobalReference", - "value": "https://admin-shell.io/zvei/nameplate/1/0/ContactInformations/ContactInformation/IPCommunication/TypeOfCommunication" - } - ] - }, - "qualifiers": [ + "idShort": "TypeOfTelephone", + "description": [ { - "kind": "ConceptQualifier", - "type": "Multiplicity", - "valueType": "xs:string", - "value": "ZeroToOne" + "language": "en", + "text": " enumeration: 0173-1#07-AAS754#001 (office), 0173-1#07-AAS755#001 (office mobile), 0173-1#07-AAS756#001 (secretary), 0173-1#07-AAS757#001 (substitute), 0173-1#07-AAS758#001 (home), 0173-1#07-AAS759#001 (private mobile)" } ], - "valueType": "xs:string", - "value": "Chat", - "modelType": "Property" - }, - { - "idShort": "AvailableTime", "semanticId": { "type": "ExternalReference", "keys": [ { "type": "GlobalReference", - "value": "https://admin-shell.io/zvei/nameplate/1/0/ContactInformations/ContactInformation/AvailableTime/" + "value": "0173-1#02-AAO137#003" } ] }, @@ -1062,25 +895,21 @@ "value": "ZeroToOne" } ], - "value": [ - { - "language": "de", - "text": "8:00 AM to 6:00 PM" - } - ], - "modelType": "MultiLanguageProperty" + "valueType": "xs:string", + "value": "Mobile", + "modelType": "Property" } ], "modelType": "SubmodelElementCollection" }, { - "idShort": "Phone0", + "idShort": "Email", "semanticId": { "type": "ExternalReference", "keys": [ { "type": "GlobalReference", - "value": "https://admin-shell.io/zvei/nameplate/1/0/ContactInformations/ContactInformation/Phone" + "value": "0173-1#02-AAQ836#005" } ] }, @@ -1094,19 +923,13 @@ ], "value": [ { - "idShort": "TelephoneNumber", - "description": [ - { - "language": "en", - "text": "Recommendation: property declaration as MLP is required by its semantic definition. As the property value is language independent, users are recommended to provide maximal 1 string in any language of the user’s choice." - } - ], + "idShort": "EmailAddress", "semanticId": { "type": "ExternalReference", "keys": [ { "type": "GlobalReference", - "value": "0173-1#02-AAO136#002" + "value": "0173-1#02-AAO198#002" } ] }, @@ -1118,47 +941,16 @@ "value": "One" } ], - "value": [ - { - "language": "en", - "text": "234-567-8901" - } - ], - "modelType": "MultiLanguageProperty" - }, - { - "idShort": "AvailableTime", - "semanticId": { - "type": "ExternalReference", - "keys": [ - { - "type": "GlobalReference", - "value": "https://admin-shell.io/zvei/nameplate/1/0/ContactInformations/ContactInformation/AvailableTime/" - } - ] - }, - "qualifiers": [ - { - "kind": "ConceptQualifier", - "type": "Multiplicity", - "valueType": "xs:string", - "value": "ZeroToOne" - } - ], - "value": [ - { - "language": "de", - "text": "Dienstags bis Donnerstags, 9-17 Uhr" - } - ], - "modelType": "MultiLanguageProperty" + "valueType": "xs:string", + "value": "john.doe@example.com", + "modelType": "Property" }, { - "idShort": "TypeOfTelephone", + "idShort": "TypeOfEmailAddress", "description": [ { "language": "en", - "text": " enumeration: 0173-1#07-AAS754#001 (office), 0173-1#07-AAS755#001 (office mobile), 0173-1#07-AAS756#001 (secretary), 0173-1#07-AAS757#001 (substitute), 0173-1#07-AAS758#001 (home), 0173-1#07-AAS759#001 (private mobile)" + "text": "enumeration: 0173-1#07-AAS754#001 (office), 0173-1#07-AAS756#001 (secretary), 0173-1#07-AAS757#001 (substitute), 0173-1#07-AAS758#001 (home)" } ], "semanticId": { @@ -1166,7 +958,7 @@ "keys": [ { "type": "GlobalReference", - "value": "0173-1#02-AAO137#003" + "value": "0173-1#02-AAO199#003" } ] }, @@ -1179,20 +971,20 @@ } ], "valueType": "xs:string", - "value": "Office", + "value": "Work", "modelType": "Property" } ], "modelType": "SubmodelElementCollection" }, { - "idShort": "Phone1", + "idShort": "IPCommunication__00__", "semanticId": { "type": "ExternalReference", "keys": [ { "type": "GlobalReference", - "value": "https://admin-shell.io/zvei/nameplate/1/0/ContactInformations/ContactInformation/Phone" + "value": "https://admin-shell.io/zvei/nameplate/1/0/ContactInformations/" } ] }, @@ -1201,24 +993,18 @@ "kind": "ConceptQualifier", "type": "Multiplicity", "valueType": "xs:string", - "value": "ZeroToOne" + "value": "ZeroToMany" } ], "value": [ { - "idShort": "TelephoneNumber", - "description": [ - { - "language": "en", - "text": "Recommendation: property declaration as MLP is required by its semantic definition. As the property value is language independent, users are recommended to provide maximal 1 string in any language of the user’s choice." - } - ], + "idShort": "AddressOfAdditionalLink", "semanticId": { "type": "ExternalReference", "keys": [ { "type": "GlobalReference", - "value": "0173-1#02-AAO136#002" + "value": "0173-1#02-AAQ326#002" } ] }, @@ -1230,22 +1016,18 @@ "value": "One" } ], - "value": [ - { - "language": "en", - "text": "345-678-9012" - } - ], - "modelType": "MultiLanguageProperty" + "valueType": "xs:string", + "value": "https://www.mm-software.com/more-the-newsroom/", + "modelType": "Property" }, { - "idShort": "AvailableTime", + "idShort": "TypeOfCommunication", "semanticId": { "type": "ExternalReference", "keys": [ { "type": "GlobalReference", - "value": "https://admin-shell.io/zvei/nameplate/1/0/ContactInformations/ContactInformation/AvailableTime/" + "value": "https://admin-shell.io/zvei/nameplate/1/0/ContactInformations/ContactInformation/IPCommunication/TypeOfCommunication" } ] }, @@ -1257,28 +1039,18 @@ "value": "ZeroToOne" } ], - "value": [ - { - "language": "de", - "text": "Montags, 10-16 Uhr" - } - ], - "modelType": "MultiLanguageProperty" + "valueType": "xs:string", + "value": "Chat", + "modelType": "Property" }, { - "idShort": "TypeOfTelephone", - "description": [ - { - "language": "en", - "text": " enumeration: 0173-1#07-AAS754#001 (office), 0173-1#07-AAS755#001 (office mobile), 0173-1#07-AAS756#001 (secretary), 0173-1#07-AAS757#001 (substitute), 0173-1#07-AAS758#001 (home), 0173-1#07-AAS759#001 (private mobile)" - } - ], + "idShort": "AvailableTime", "semanticId": { "type": "ExternalReference", "keys": [ { "type": "GlobalReference", - "value": "0173-1#02-AAO137#003" + "value": "https://admin-shell.io/zvei/nameplate/1/0/ContactInformations/ContactInformation/AvailableTime/" } ] }, @@ -1290,9 +1062,13 @@ "value": "ZeroToOne" } ], - "valueType": "xs:string", - "value": "Mobile", - "modelType": "Property" + "value": [ + { + "language": "de", + "text": "8:00 AM to 6:00 PM" + } + ], + "modelType": "MultiLanguageProperty" } ], "modelType": "SubmodelElementCollection" diff --git a/source/AAS.TwinEngine.Plugin.TestPlugin/Data/mock-submodel-data.json b/source/AAS.TwinEngine.Plugin.TestPlugin/Data/mock-submodel-data.json index fbcadb6b..c2b4dbec 100644 --- a/source/AAS.TwinEngine.Plugin.TestPlugin/Data/mock-submodel-data.json +++ b/source/AAS.TwinEngine.Plugin.TestPlugin/Data/mock-submodel-data.json @@ -10,26 +10,15 @@ "0173-1#02-AAO127#003": { "0173-1#02-AAO127#003_de": "Marketing" }, - "https://admin-shell.io/zvei/nameplate/1/0/ContactInformations/ContactInformation/Phone": [ - { - "0173-1#02-AAO136#002": { - "0173-1#02-AAO136#002_en": "234-567-8901" - }, - "0173-1#02-AAO137#003": "Office", - "https://mm-software.com/Phone/AvailableTime": { - "https://mm-software.com/Phone/AvailableTime_de": "Dienstags bis Donnerstags, 9-17 Uhr" - } + "https://admin-shell.io/zvei/nameplate/1/0/ContactInformations/ContactInformation/Phone": { + "0173-1#02-AAO136#002": { + "0173-1#02-AAO136#002_en": "234-567-8901" }, - { - "0173-1#02-AAO136#002": { - "0173-1#02-AAO136#002_en": "345-678-9012" - }, - "https://mm-software.com/Phone/AvailableTime": { - "https://mm-software.com/Phone/AvailableTime_de": "Montags, 10-16 Uhr" - }, - "0173-1#02-AAO137#003": "Mobile" + "0173-1#02-AAO137#003": "Office", + "https://mm-software.com/Phone/AvailableTime": { + "https://mm-software.com/Phone/AvailableTime_de": "Dienstags bis Donnerstags, 9-17 Uhr" } - ], + }, "0173-1#02-AAQ836#005": [ { "0173-1#02-AAO198#002": "john.doe@example.com", @@ -67,26 +56,15 @@ "0173-1#02-AAO127#003": { "0173-1#02-AAO127#003_de": "Marketing" }, - "https://admin-shell.io/zvei/nameplate/1/0/ContactInformations/ContactInformation/Phone": [ - { - "0173-1#02-AAO136#002": { - "0173-1#02-AAO136#002_en": "234-567-8901" - }, - "0173-1#02-AAO137#003": "Office", - "https://mm-software.com/Phone/AvailableTime": { - "https://mm-software.com/Phone/AvailableTime_de": "Dienstags bis Donnerstags, 9-17 Uhr" - } + "https://admin-shell.io/zvei/nameplate/1/0/ContactInformations/ContactInformation/Phone": { + "0173-1#02-AAO136#002": { + "0173-1#02-AAO136#002_en": "345-678-9012" }, - { - "0173-1#02-AAO136#002": { - "0173-1#02-AAO136#002_en": "345-678-9012" - }, - "https://mm-software.com/Phone/AvailableTime": { - "https://mm-software.com/Phone/AvailableTime_de": "Montags, 10-16 Uhr" - }, - "0173-1#02-AAO137#003": "Mobile" - } - ], + "https://mm-software.com/Phone/AvailableTime": { + "https://mm-software.com/Phone/AvailableTime_de": "Montags, 10-16 Uhr" + }, + "0173-1#02-AAO137#003": "Mobile" + }, "0173-1#02-AAQ836#005": [ { "0173-1#02-AAO198#002": "john.doe@example.com", @@ -476,7 +454,7 @@ "https://admin-shell.io/SMT/General/Arbitrary/date_max": "2030-02-06" }, "0112/2///61987#ABA588#004": "56.5", - "http://admin-shell.io/aasx-package-explorer/functions/asciidoc/textblock/1/0": "UmVnYXJkaW5nIHByb3BlcnR5IGBNYXJraW5nTmFtZWAsIHRoZSBwcmVmZXJhYmxlIHNvbHV0aW9uIGlzIHRvIHByb3ZpZGUgYSB2YWx1ZUlkIGluIElSREkgb3JpZ2luYXRpbmcgZnJvbSBJRUMgQ0REIG9yIEVDTEFTUyBlbnVtZXJhdGlvbiB2YWx1ZSBsaXN0LCBlLmcuICJDRSIgKElSREk6IDAxMTIvMi8vLzYxOTg3I0FCTzQwOSMwMDMgb3IgMDE3My0xIzA3LURBQTYwMyMwMDQpLiBJbiBjYXNlIG5vbmUgb2YgdGhlIGV4aXN0aW5nIEVDTEFTUyBlbnVtZXJhdGlvbiB2YWx1ZXMgbWF0Y2hlcywgZmlsbGluZyBwbGFpbiBzdHJpbmcgdGV4dCBpbnRvIHRoZSDigJx2YWx1ZeKAnSBmaWVsZCBvZiB0aGUgcHJvcGVydHkgYE1hcmtpbmdOYW1lYCBjYW4gYmUgYWNjZXB0ZWQgYWx0ZXJuYXRpdmVseS4gSXQgbmVlZHMgdG8gYmUgcG9pbnRlZCBvdXQgdGhhdCBFQ0xBU1MgYWxzbyBwcm92aWRlcyBtYXJraW5nIGRlZmluaXRpb25zIGluIHRlcm1zIG9mIGJvb2xlYW4gcHJvcGVydHksIGUuZy4g4oCcQ0UtIHF1YWxpZmljYXRpb24gcHJlc2VudOKAnSAoSVJESTogMDE3My0xIzAyLUJBRjA1MyMwMDgpLiBJbiB0aGlzIGNhc2UgdXNlcnMgc2hvdWxkIGluc3RlYWQgdXNlIGEgbWF0Y2hpbmcgRUNMQVNTIGVudW1lcmF0aW9uIHZhbHVlIG9yLCBpZiBub3QgcHJvdmlkZWQgYXMgZW51bWVyYXRpb24sIGZpbGwgaW4gcGxhaW4gc3RyaW5nIHRleHQuDQoNClRoZSBmb2xsb3dpbmcgZXhhbXBsZSAoc2VlIDw8RmlndXJlXzU\u002BPikgaWxsdXN0cmF0ZXMgaG93IHRvIG1vZGVsIHByb2R1Y3QgbWFya2luZyBpbiBhbiBBQVMuIE9uIHRoZSBsZWZ0IHNpZGUgdGhlcmUgaXMgYSBzYW1wbGUgbmFtZXBsYXRlIHdoaWNoIGNvbnRhaW5zIHR3byBtYXJraW5ncyB0byBiZSBtb2RlbGxlZDogdGhlIENFIG1hcmtpbmcgYW5kIHRoZSBXRUVFIG1hcmtpbmcgd2l0aCBhIGNyb3NzZWQtb3V0IHdoZWVsZWQgYmluLiBOZXh0IHRvIHRoZSBuYW1lcGxhdGUgYSB0YWJsZSBsaXN0cyBhbGwgcHJvcGVydGllcyBhbmQgdGhlaXIgYXR0cmlidXRlcy4gDQo=", + "http://admin-shell.io/aasx-package-explorer/functions/asciidoc/textblock/1/0": "UmVnYXJkaW5nIHByb3BlcnR5IGBNYXJraW5nTmFtZWAsIHRoZSBwcmVmZXJhYmxlIHNvbHV0aW9uIGlzIHRvIHByb3ZpZGUgYSB2YWx1ZUlkIGluIElSREkgb3JpZ2luYXRpbmcgZnJvbSBJRUMgQ0REIG9yIEVDTEFTUyBlbnVtZXJhdGlvbiB2YWx1ZSBsaXN0LCBlLmcuICJDRSIgKElSREk6IDAxMTIvMi8vLzYxOTg3I0FCTzQwOSMwMDMgb3IgMDE3My0xIzA3LURBQTYwMyMwMDQpLiBJbiBjYXNlIG5vbmUgb2YgdGhlIGV4aXN0aW5nIEVDTEFTUyBlbnVtZXJhdGlvbiB2YWx1ZXMgbWF0Y2hlcywgZmlsbGluZyBwbGFpbiBzdHJpbmcgdGV4dCBpbnRvIHRoZSDigJx2YWx1ZeKAnSBmaWVsZCBvZiB0aGUgcHJvcGVydHkgYE1hcmtpbmdOYW1lYCBjYW4gYmUgYWNjZXB0ZWQgYWx0ZXJuYXRpdmVseS4gSXQgbmVlZHMgdG8gYmUgcG9pbnRlZCBvdXQgdGhhdCBFQ0xBU1MgYWxzbyBwcm92aWRlcyBtYXJraW5nIGRlZmluaXRpb25zIGluIHRlcm1zIG9mIGJvb2xlYW4gcHJvcGVydHksIGUuZy4g4oCcQ0UtIHF1YWxpZmljYXRpb24gcHJlc2VudOKAnSAoSVJESTogMDE3My0xIzAyLUJBRjA1MyMwMDgpLiBJbiB0aGlzIGNhc2UgdXNlcnMgc2hvdWxkIGluc3RlYWQgdXNlIGEgbWF0Y2hpbmcgRUNMQVNTIGVudW1lcmF0aW9uIHZhbHVlIG9yLCBpZiBub3QgcHJvdmlkZWQgYXMgZW51bWVyYXRpb24sIGZpbGwgaW4gcGxhaW4gc3RyaW5nIHRleHQuDQoNClRoZSBmb2xsb3dpbmcgZXhhbXBsZSAoc2VlIDw8RmlndXJlXzU+PikgaWxsdXN0cmF0ZXMgaG93IHRvIG1vZGVsIHByb2R1Y3QgbWFya2luZyBpbiBhbiBBQVMuIE9uIHRoZSBsZWZ0IHNpZGUgdGhlcmUgaXMgYSBzYW1wbGUgbmFtZXBsYXRlIHdoaWNoIGNvbnRhaW5zIHR3byBtYXJraW5ncyB0byBiZSBtb2RlbGxlZDogdGhlIENFIG1hcmtpbmcgYW5kIHRoZSBXRUVFIG1hcmtpbmcgd2l0aCBhIGNyb3NzZWQtb3V0IHdoZWVsZWQgYmluLiBOZXh0IHRvIHRoZSBuYW1lcGxhdGUgYSB0YWJsZSBsaXN0cyBhbGwgcHJvcGVydGllcyBhbmQgdGhlaXIgYXR0cmlidXRlcy4gDQo=", "0173-1#02-ABK273#002": { "0173-1#02-ABK273#002_en": "Name of product class in the used classification system", "0173-1#02-ABK273#002_de": "Name der Produktklasse im verwendeten Klassifizierungssystem" @@ -502,26 +480,15 @@ "0173-1#02-AAO127#003": { "0173-1#02-AAO127#003_de": "Marketing" }, - "https://admin-shell.io/zvei/nameplate/1/0/ContactInformations/ContactInformation/Phone": [ - { - "0173-1#02-AAO136#002": { - "0173-1#02-AAO136#002_en": "234-567-8901" - }, - "0173-1#02-AAO137#003": "Office", - "https://mm-software.com/Phone/AvailableTime": { - "https://mm-software.com/Phone/AvailableTime_de": "Dienstags bis Donnerstags, 9-17 Uhr" - } + "https://admin-shell.io/zvei/nameplate/1/0/ContactInformations/ContactInformation/Phone": { + "0173-1#02-AAO136#002": { + "0173-1#02-AAO136#002_en": "345-678-9012" }, - { - "0173-1#02-AAO136#002": { - "0173-1#02-AAO136#002_en": "345-678-9012" - }, - "https://mm-software.com/Phone/AvailableTime": { - "https://mm-software.com/Phone/AvailableTime_de": "Montags, 10-16 Uhr" - }, - "0173-1#02-AAO137#003": "Mobile" - } - ], + "https://mm-software.com/Phone/AvailableTime": { + "https://mm-software.com/Phone/AvailableTime_de": "Montags, 10-16 Uhr" + }, + "0173-1#02-AAO137#003": "Mobile" + }, "0173-1#02-AAQ836#005": [ { "0173-1#02-AAO198#002": "john.doe@example.com", @@ -559,17 +526,15 @@ "0173-1#02-AAO127#003": { "0173-1#02-AAO127#003_de": "Vertrieb" }, - "https://admin-shell.io/zvei/nameplate/1/0/ContactInformations/ContactInformation/Phone": [ - { - "0173-1#02-AAO136#002": { - "0173-1#02-AAO136#002_en": "123-456-7890" - }, - "0173-1#02-AAO137#003": "assistant", - "https://mm-software.com/Phone/AvailableTime": { - "https://mm-software.com/Phone/AvailableTime_de": "Dienstags bis Donnerstags, 9-17 Uhr" - } + "https://admin-shell.io/zvei/nameplate/1/0/ContactInformations/ContactInformation/Phone": { + "0173-1#02-AAO136#002": { + "0173-1#02-AAO136#002_en": "123-456-7890" + }, + "0173-1#02-AAO137#003": "assistant", + "https://mm-software.com/Phone/AvailableTime": { + "https://mm-software.com/Phone/AvailableTime_de": "Dienstags bis Donnerstags, 9-17 Uhr" } - ], + }, "0173-1#02-AAQ836#005": [ { "0173-1#02-AAO198#002": "jane.doe@betacorp.com", @@ -830,7 +795,7 @@ "https://admin-shell.io/SMT/General/Arbitrary/date_max": "2050-02-06" }, "0112/2///61987#ABA588#004": "56.54", - "http://admin-shell.io/aasx-package-explorer/functions/asciidoc/textblock/1/0": "UmVnYXJkaW5nIHByb3BlcnR5IGBNYXJraW5nTmFtZWAsIHRoZSBwcmVmZXJhYmxlIHNvbHV0aW9uIGlzIHRvIHByb3ZpZGUgYSB2YWx1ZUlkIGluIElSREkgb3JpZ2luYXRpbmcgZnJvbSBJRUMgQ0REIG9yIEVDTEFTUyBlbnVtZXJhdGlvbiB2YWx1ZSBsaXN0LCBlLmcuICJDRSIgKElSREk6IDAxMTIvMi8vLzYxOTg3I0FCTzQwOSMwMDMgb3IgMDE3My0xIzA3LURBQTYwMyMwMDQpLiBJbiBjYXNlIG5vbmUgb2YgdGhlIGV4aXN0aW5nIEVDTEFTUyBlbnVtZXJhdGlvbiB2YWx1ZXMgbWF0Y2hlcywgZmlsbGluZyBwbGFpbiBzdHJpbmcgdGV4dCBpbnRvIHRoZSDigJx2YWx1ZeKAnSBmaWVsZCBvZiB0aGUgcHJvcGVydHkgYE1hcmtpbmdOYW1lYCBjYW4gYmUgYWNjZXB0ZWQgYWx0ZXJuYXRpdmVseS4gSXQgbmVlZHMgdG8gYmUgcG9pbnRlZCBvdXQgdGhhdCBFQ0xBU1MgYWxzbyBwcm92aWRlcyBtYXJraW5nIGRlZmluaXRpb25zIGluIHRlcm1zIG9mIGJvb2xlYW4gcHJvcGVydHksIGUuZy4g4oCcQ0UtIHF1YWxpZmljYXRpb24gcHJlc2VudOKAnSAoSVJESTogMDE3My0xIzAyLUJBRjA1MyMwMDgpLiBJbiB0aGlzIGNhc2UgdXNlcnMgc2hvdWxkIGluc3RlYWQgdXNlIGEgbWF0Y2hpbmcgRUNMQVNTIGVudW1lcmF0aW9uIHZhbHVlIG9yLCBpZiBub3QgcHJvdmlkZWQgYXMgZW51bWVyYXRpb24sIGZpbGwgaW4gcGxhaW4gc3RyaW5nIHRleHQuDQoNClRoZSBmb2xsb3dpbmcgZXhhbXBsZSAoc2VlIDw8RmlndXJlXzU\u002BPikgaWxsdXN0cmF0ZXMgaG93IHRvIG1vZGVsIHByb2R1Y3QgbWFya2luZyBpbiBhbiBBQVMuIE9uIHRoZSBsZWZ0IHNpZGUgdGhlcmUgaXMgYSBzYW1wbGUgbmFtZXBsYXRlIHdoaWNoIGNvbnRhaW5zIHR3byBtYXJraW5ncyB0byBiZSBtb2RlbGxlZDogdGhlIENFIG1hcmtpbmcgYW5kIHRoZSBXRUVFIG1hcmtpbmcgd2l0aCBhIGNyb3NzZWQtb3V0IHdoZWVsZWQgYmluLiBOZXh0IHRvIHRoZSBuYW1lcGxhdGUgYSB0YWJsZSBsaXN0cyBhbGwgcHJvcGVydGllcyBhbmQgdGhlaXIgYXR0cmlidXRlcy4gDQo=", + "http://admin-shell.io/aasx-package-explorer/functions/asciidoc/textblock/1/0": "UmVnYXJkaW5nIHByb3BlcnR5IGBNYXJraW5nTmFtZWAsIHRoZSBwcmVmZXJhYmxlIHNvbHV0aW9uIGlzIHRvIHByb3ZpZGUgYSB2YWx1ZUlkIGluIElSREkgb3JpZ2luYXRpbmcgZnJvbSBJRUMgQ0REIG9yIEVDTEFTUyBlbnVtZXJhdGlvbiB2YWx1ZSBsaXN0LCBlLmcuICJDRSIgKElSREk6IDAxMTIvMi8vLzYxOTg3I0FCTzQwOSMwMDMgb3IgMDE3My0xIzA3LURBQTYwMyMwMDQpLiBJbiBjYXNlIG5vbmUgb2YgdGhlIGV4aXN0aW5nIEVDTEFTUyBlbnVtZXJhdGlvbiB2YWx1ZXMgbWF0Y2hlcywgZmlsbGluZyBwbGFpbiBzdHJpbmcgdGV4dCBpbnRvIHRoZSDigJx2YWx1ZeKAnSBmaWVsZCBvZiB0aGUgcHJvcGVydHkgYE1hcmtpbmdOYW1lYCBjYW4gYmUgYWNjZXB0ZWQgYWx0ZXJuYXRpdmVseS4gSXQgbmVlZHMgdG8gYmUgcG9pbnRlZCBvdXQgdGhhdCBFQ0xBU1MgYWxzbyBwcm92aWRlcyBtYXJraW5nIGRlZmluaXRpb25zIGluIHRlcm1zIG9mIGJvb2xlYW4gcHJvcGVydHksIGUuZy4g4oCcQ0UtIHF1YWxpZmljYXRpb24gcHJlc2VudOKAnSAoSVJESTogMDE3My0xIzAyLUJBRjA1MyMwMDgpLiBJbiB0aGlzIGNhc2UgdXNlcnMgc2hvdWxkIGluc3RlYWQgdXNlIGEgbWF0Y2hpbmcgRUNMQVNTIGVudW1lcmF0aW9uIHZhbHVlIG9yLCBpZiBub3QgcHJvdmlkZWQgYXMgZW51bWVyYXRpb24sIGZpbGwgaW4gcGxhaW4gc3RyaW5nIHRleHQuDQoNClRoZSBmb2xsb3dpbmcgZXhhbXBsZSAoc2VlIDw8RmlndXJlXzU+PikgaWxsdXN0cmF0ZXMgaG93IHRvIG1vZGVsIHByb2R1Y3QgbWFya2luZyBpbiBhbiBBQVMuIE9uIHRoZSBsZWZ0IHNpZGUgdGhlcmUgaXMgYSBzYW1wbGUgbmFtZXBsYXRlIHdoaWNoIGNvbnRhaW5zIHR3byBtYXJraW5ncyB0byBiZSBtb2RlbGxlZDogdGhlIENFIG1hcmtpbmcgYW5kIHRoZSBXRUVFIG1hcmtpbmcgd2l0aCBhIGNyb3NzZWQtb3V0IHdoZWVsZWQgYmluLiBOZXh0IHRvIHRoZSBuYW1lcGxhdGUgYSB0YWJsZSBsaXN0cyBhbGwgcHJvcGVydGllcyBhbmQgdGhlaXIgYXR0cmlidXRlcy4gDQo=", "0173-1#02-ABK273#002": { "0173-1#02-ABK273#002_en": "Name of product class in the used classification system", "0173-1#02-ABK273#002_de": "Name der Produktklasse im verwendeten Klassifizierungssystem" @@ -856,26 +821,15 @@ "0173-1#02-AAO127#003": { "0173-1#02-AAO127#003_de": "Marketing" }, - "https://admin-shell.io/zvei/nameplate/1/0/ContactInformations/ContactInformation/Phone": [ - { - "0173-1#02-AAO136#002": { - "0173-1#02-AAO136#002_en": "234-567-8901" - }, - "0173-1#02-AAO137#003": "Office", - "https://mm-software.com/Phone/AvailableTime": { - "https://mm-software.com/Phone/AvailableTime_de": "Dienstags bis Donnerstags, 9-17 Uhr" - } + "https://admin-shell.io/zvei/nameplate/1/0/ContactInformations/ContactInformation/Phone": { + "0173-1#02-AAO136#002": { + "0173-1#02-AAO136#002_en": "234-567-8901" }, - { - "0173-1#02-AAO136#002": { - "0173-1#02-AAO136#002_en": "345-678-9012" - }, - "https://mm-software.com/Phone/AvailableTime": { - "https://mm-software.com/Phone/AvailableTime_de": "Montags, 10-16 Uhr" - }, - "0173-1#02-AAO137#003": "Mobile" + "0173-1#02-AAO137#003": "Office", + "https://mm-software.com/Phone/AvailableTime": { + "https://mm-software.com/Phone/AvailableTime_de": "Dienstags bis Donnerstags, 9-17 Uhr" } - ], + }, "0173-1#02-AAQ836#005": [ { "0173-1#02-AAO198#002": "john.doe@example.com", @@ -913,26 +867,15 @@ "0173-1#02-AAO127#003": { "0173-1#02-AAO127#003_de": "Marketing" }, - "https://admin-shell.io/zvei/nameplate/1/0/ContactInformations/ContactInformation/Phone": [ - { - "0173-1#02-AAO136#002": { - "0173-1#02-AAO136#002_en": "234-567-8901" - }, - "0173-1#02-AAO137#003": "Office", - "https://mm-software.com/Phone/AvailableTime": { - "https://mm-software.com/Phone/AvailableTime_de": "Dienstags bis Donnerstags, 9-17 Uhr" - } + "https://admin-shell.io/zvei/nameplate/1/0/ContactInformations/ContactInformation/Phone": { + "0173-1#02-AAO136#002": { + "0173-1#02-AAO136#002_en": "234-567-8901" }, - { - "0173-1#02-AAO136#002": { - "0173-1#02-AAO136#002_en": "345-678-9012" - }, - "https://mm-software.com/Phone/AvailableTime": { - "https://mm-software.com/Phone/AvailableTime_de": "Montags, 10-16 Uhr" - }, - "0173-1#02-AAO137#003": "Mobile" + "0173-1#02-AAO137#003": "Office", + "https://mm-software.com/Phone/AvailableTime": { + "https://mm-software.com/Phone/AvailableTime_de": "Dienstags bis Donnerstags, 9-17 Uhr" } - ], + }, "0173-1#02-AAQ836#005": [ { "0173-1#02-AAO198#002": "john.doe@example.com", @@ -970,17 +913,15 @@ "0173-1#02-AAO127#003": { "0173-1#02-AAO127#003_de": "Vertrieb" }, - "https://admin-shell.io/zvei/nameplate/1/0/ContactInformations/ContactInformation/Phone": [ - { - "0173-1#02-AAO136#002": { - "0173-1#02-AAO136#002_en": "123-456-7890" - }, - "0173-1#02-AAO137#003": "assistant", - "https://mm-software.com/Phone/AvailableTime": { - "https://mm-software.com/Phone/AvailableTime_de": "Dienstags bis Donnerstags, 9-17 Uhr" - } + "https://admin-shell.io/zvei/nameplate/1/0/ContactInformations/ContactInformation/Phone": { + "0173-1#02-AAO136#002": { + "0173-1#02-AAO136#002_en": "123-456-7890" + }, + "0173-1#02-AAO137#003": "assistant", + "https://mm-software.com/Phone/AvailableTime": { + "https://mm-software.com/Phone/AvailableTime_de": "Dienstags bis Donnerstags, 9-17 Uhr" } - ], + }, "0173-1#02-AAQ836#005": [ { "0173-1#02-AAO198#002": "jane.doe@betacorp.com", @@ -1353,7 +1294,7 @@ "https://admin-shell.io/SMT/General/Arbitrary/date_max": "2032-02-06" }, "0112/2///61987#ABA588#004": "5.5", - "http://admin-shell.io/aasx-package-explorer/functions/asciidoc/textblock/1/0": "UmVnYXJkaW5nIHByb3BlcnR5IGBNYXJraW5nTmFtZWAsIHRoZSBwcmVmZXJhYmxlIHNvbHV0aW9uIGlzIHRvIHByb3ZpZGUgYSB2YWx1ZUlkIGluIElSREkgb3JpZ2luYXRpbmcgZnJvbSBJRUMgQ0REIG9yIEVDTEFTUyBlbnVtZXJhdGlvbiB2YWx1ZSBsaXN0LCBlLmcuICJDRSIgKElSREk6IDAxMTIvMi8vLzYxOTg3I0FCTzQwOSMwMDMgb3IgMDE3My0xIzA3LURBQTYwMyMwMDQpLiBJbiBjYXNlIG5vbmUgb2YgdGhlIGV4aXN0aW5nIEVDTEFTUyBlbnVtZXJhdGlvbiB2YWx1ZXMgbWF0Y2hlcywgZmlsbGluZyBwbGFpbiBzdHJpbmcgdGV4dCBpbnRvIHRoZSDigJx2YWx1ZeKAnSBmaWVsZCBvZiB0aGUgcHJvcGVydHkgYE1hcmtpbmdOYW1lYCBjYW4gYmUgYWNjZXB0ZWQgYWx0ZXJuYXRpdmVseS4gSXQgbmVlZHMgdG8gYmUgcG9pbnRlZCBvdXQgdGhhdCBFQ0xBU1MgYWxzbyBwcm92aWRlcyBtYXJraW5nIGRlZmluaXRpb25zIGluIHRlcm1zIG9mIGJvb2xlYW4gcHJvcGVydHksIGUuZy4g4oCcQ0UtIHF1YWxpZmljYXRpb24gcHJlc2VudOKAnSAoSVJESTogMDE3My0xIzAyLUJBRjA1MyMwMDgpLiBJbiB0aGlzIGNhc2UgdXNlcnMgc2hvdWxkIGluc3RlYWQgdXNlIGEgbWF0Y2hpbmcgRUNMQVNTIGVudW1lcmF0aW9uIHZhbHVlIG9yLCBpZiBub3QgcHJvdmlkZWQgYXMgZW51bWVyYXRpb24sIGZpbGwgaW4gcGxhaW4gc3RyaW5nIHRleHQuDQoNClRoZSBmb2xsb3dpbmcgZXhhbXBsZSAoc2VlIDw8RmlndXJlXzU\u002BPikgaWxsdXN0cmF0ZXMgaG93IHRvIG1vZGVsIHByb2R1Y3QgbWFya2luZyBpbiBhbiBBQVMuIE9uIHRoZSBsZWZ0IHNpZGUgdGhlcmUgaXMgYSBzYW1wbGUgbmFtZXBsYXRlIHdoaWNoIGNvbnRhaW5zIHR3byBtYXJraW5ncyB0byBiZSBtb2RlbGxlZDogdGhlIENFIG1hcmtpbmcgYW5kIHRoZSBXRUVFIG1hcmtpbmcgd2l0aCBhIGNyb3NzZWQtb3V0IHdoZWVsZWQgYmluLiBOZXh0IHRvIHRoZSBuYW1lcGxhdGUgYSB0YWJsZSBsaXN0cyBhbGwgcHJvcGVydGllcyBhbmQgdGhlaXIgYXR0cmlidXRlcy4gDQo=", + "http://admin-shell.io/aasx-package-explorer/functions/asciidoc/textblock/1/0": "UmVnYXJkaW5nIHByb3BlcnR5IGBNYXJraW5nTmFtZWAsIHRoZSBwcmVmZXJhYmxlIHNvbHV0aW9uIGlzIHRvIHByb3ZpZGUgYSB2YWx1ZUlkIGluIElSREkgb3JpZ2luYXRpbmcgZnJvbSBJRUMgQ0REIG9yIEVDTEFTUyBlbnVtZXJhdGlvbiB2YWx1ZSBsaXN0LCBlLmcuICJDRSIgKElSREk6IDAxMTIvMi8vLzYxOTg3I0FCTzQwOSMwMDMgb3IgMDE3My0xIzA3LURBQTYwMyMwMDQpLiBJbiBjYXNlIG5vbmUgb2YgdGhlIGV4aXN0aW5nIEVDTEFTUyBlbnVtZXJhdGlvbiB2YWx1ZXMgbWF0Y2hlcywgZmlsbGluZyBwbGFpbiBzdHJpbmcgdGV4dCBpbnRvIHRoZSDigJx2YWx1ZeKAnSBmaWVsZCBvZiB0aGUgcHJvcGVydHkgYE1hcmtpbmdOYW1lYCBjYW4gYmUgYWNjZXB0ZWQgYWx0ZXJuYXRpdmVseS4gSXQgbmVlZHMgdG8gYmUgcG9pbnRlZCBvdXQgdGhhdCBFQ0xBU1MgYWxzbyBwcm92aWRlcyBtYXJraW5nIGRlZmluaXRpb25zIGluIHRlcm1zIG9mIGJvb2xlYW4gcHJvcGVydHksIGUuZy4g4oCcQ0UtIHF1YWxpZmljYXRpb24gcHJlc2VudOKAnSAoSVJESTogMDE3My0xIzAyLUJBRjA1MyMwMDgpLiBJbiB0aGlzIGNhc2UgdXNlcnMgc2hvdWxkIGluc3RlYWQgdXNlIGEgbWF0Y2hpbmcgRUNMQVNTIGVudW1lcmF0aW9uIHZhbHVlIG9yLCBpZiBub3QgcHJvdmlkZWQgYXMgZW51bWVyYXRpb24sIGZpbGwgaW4gcGxhaW4gc3RyaW5nIHRleHQuDQoNClRoZSBmb2xsb3dpbmcgZXhhbXBsZSAoc2VlIDw8RmlndXJlXzU+PikgaWxsdXN0cmF0ZXMgaG93IHRvIG1vZGVsIHByb2R1Y3QgbWFya2luZyBpbiBhbiBBQVMuIE9uIHRoZSBsZWZ0IHNpZGUgdGhlcmUgaXMgYSBzYW1wbGUgbmFtZXBsYXRlIHdoaWNoIGNvbnRhaW5zIHR3byBtYXJraW5ncyB0byBiZSBtb2RlbGxlZDogdGhlIENFIG1hcmtpbmcgYW5kIHRoZSBXRUVFIG1hcmtpbmcgd2l0aCBhIGNyb3NzZWQtb3V0IHdoZWVsZWQgYmluLiBOZXh0IHRvIHRoZSBuYW1lcGxhdGUgYSB0YWJsZSBsaXN0cyBhbGwgcHJvcGVydGllcyBhbmQgdGhlaXIgYXR0cmlidXRlcy4gDQo=", "0173-1#02-ABK273#002": { "0173-1#02-ABK273#002_en": "Name of product class in the used classification system", "0173-1#02-ABK273#002_de": "Name der Produktklasse im verwendeten Klassifizierungssystem" diff --git a/source/AAS.TwinEngine.Plugin.TestPlugin/Example/plugin/mock-submodel-data.json b/source/AAS.TwinEngine.Plugin.TestPlugin/Example/plugin/mock-submodel-data.json index fbcadb6b..c2b4dbec 100644 --- a/source/AAS.TwinEngine.Plugin.TestPlugin/Example/plugin/mock-submodel-data.json +++ b/source/AAS.TwinEngine.Plugin.TestPlugin/Example/plugin/mock-submodel-data.json @@ -10,26 +10,15 @@ "0173-1#02-AAO127#003": { "0173-1#02-AAO127#003_de": "Marketing" }, - "https://admin-shell.io/zvei/nameplate/1/0/ContactInformations/ContactInformation/Phone": [ - { - "0173-1#02-AAO136#002": { - "0173-1#02-AAO136#002_en": "234-567-8901" - }, - "0173-1#02-AAO137#003": "Office", - "https://mm-software.com/Phone/AvailableTime": { - "https://mm-software.com/Phone/AvailableTime_de": "Dienstags bis Donnerstags, 9-17 Uhr" - } + "https://admin-shell.io/zvei/nameplate/1/0/ContactInformations/ContactInformation/Phone": { + "0173-1#02-AAO136#002": { + "0173-1#02-AAO136#002_en": "234-567-8901" }, - { - "0173-1#02-AAO136#002": { - "0173-1#02-AAO136#002_en": "345-678-9012" - }, - "https://mm-software.com/Phone/AvailableTime": { - "https://mm-software.com/Phone/AvailableTime_de": "Montags, 10-16 Uhr" - }, - "0173-1#02-AAO137#003": "Mobile" + "0173-1#02-AAO137#003": "Office", + "https://mm-software.com/Phone/AvailableTime": { + "https://mm-software.com/Phone/AvailableTime_de": "Dienstags bis Donnerstags, 9-17 Uhr" } - ], + }, "0173-1#02-AAQ836#005": [ { "0173-1#02-AAO198#002": "john.doe@example.com", @@ -67,26 +56,15 @@ "0173-1#02-AAO127#003": { "0173-1#02-AAO127#003_de": "Marketing" }, - "https://admin-shell.io/zvei/nameplate/1/0/ContactInformations/ContactInformation/Phone": [ - { - "0173-1#02-AAO136#002": { - "0173-1#02-AAO136#002_en": "234-567-8901" - }, - "0173-1#02-AAO137#003": "Office", - "https://mm-software.com/Phone/AvailableTime": { - "https://mm-software.com/Phone/AvailableTime_de": "Dienstags bis Donnerstags, 9-17 Uhr" - } + "https://admin-shell.io/zvei/nameplate/1/0/ContactInformations/ContactInformation/Phone": { + "0173-1#02-AAO136#002": { + "0173-1#02-AAO136#002_en": "345-678-9012" }, - { - "0173-1#02-AAO136#002": { - "0173-1#02-AAO136#002_en": "345-678-9012" - }, - "https://mm-software.com/Phone/AvailableTime": { - "https://mm-software.com/Phone/AvailableTime_de": "Montags, 10-16 Uhr" - }, - "0173-1#02-AAO137#003": "Mobile" - } - ], + "https://mm-software.com/Phone/AvailableTime": { + "https://mm-software.com/Phone/AvailableTime_de": "Montags, 10-16 Uhr" + }, + "0173-1#02-AAO137#003": "Mobile" + }, "0173-1#02-AAQ836#005": [ { "0173-1#02-AAO198#002": "john.doe@example.com", @@ -476,7 +454,7 @@ "https://admin-shell.io/SMT/General/Arbitrary/date_max": "2030-02-06" }, "0112/2///61987#ABA588#004": "56.5", - "http://admin-shell.io/aasx-package-explorer/functions/asciidoc/textblock/1/0": "UmVnYXJkaW5nIHByb3BlcnR5IGBNYXJraW5nTmFtZWAsIHRoZSBwcmVmZXJhYmxlIHNvbHV0aW9uIGlzIHRvIHByb3ZpZGUgYSB2YWx1ZUlkIGluIElSREkgb3JpZ2luYXRpbmcgZnJvbSBJRUMgQ0REIG9yIEVDTEFTUyBlbnVtZXJhdGlvbiB2YWx1ZSBsaXN0LCBlLmcuICJDRSIgKElSREk6IDAxMTIvMi8vLzYxOTg3I0FCTzQwOSMwMDMgb3IgMDE3My0xIzA3LURBQTYwMyMwMDQpLiBJbiBjYXNlIG5vbmUgb2YgdGhlIGV4aXN0aW5nIEVDTEFTUyBlbnVtZXJhdGlvbiB2YWx1ZXMgbWF0Y2hlcywgZmlsbGluZyBwbGFpbiBzdHJpbmcgdGV4dCBpbnRvIHRoZSDigJx2YWx1ZeKAnSBmaWVsZCBvZiB0aGUgcHJvcGVydHkgYE1hcmtpbmdOYW1lYCBjYW4gYmUgYWNjZXB0ZWQgYWx0ZXJuYXRpdmVseS4gSXQgbmVlZHMgdG8gYmUgcG9pbnRlZCBvdXQgdGhhdCBFQ0xBU1MgYWxzbyBwcm92aWRlcyBtYXJraW5nIGRlZmluaXRpb25zIGluIHRlcm1zIG9mIGJvb2xlYW4gcHJvcGVydHksIGUuZy4g4oCcQ0UtIHF1YWxpZmljYXRpb24gcHJlc2VudOKAnSAoSVJESTogMDE3My0xIzAyLUJBRjA1MyMwMDgpLiBJbiB0aGlzIGNhc2UgdXNlcnMgc2hvdWxkIGluc3RlYWQgdXNlIGEgbWF0Y2hpbmcgRUNMQVNTIGVudW1lcmF0aW9uIHZhbHVlIG9yLCBpZiBub3QgcHJvdmlkZWQgYXMgZW51bWVyYXRpb24sIGZpbGwgaW4gcGxhaW4gc3RyaW5nIHRleHQuDQoNClRoZSBmb2xsb3dpbmcgZXhhbXBsZSAoc2VlIDw8RmlndXJlXzU\u002BPikgaWxsdXN0cmF0ZXMgaG93IHRvIG1vZGVsIHByb2R1Y3QgbWFya2luZyBpbiBhbiBBQVMuIE9uIHRoZSBsZWZ0IHNpZGUgdGhlcmUgaXMgYSBzYW1wbGUgbmFtZXBsYXRlIHdoaWNoIGNvbnRhaW5zIHR3byBtYXJraW5ncyB0byBiZSBtb2RlbGxlZDogdGhlIENFIG1hcmtpbmcgYW5kIHRoZSBXRUVFIG1hcmtpbmcgd2l0aCBhIGNyb3NzZWQtb3V0IHdoZWVsZWQgYmluLiBOZXh0IHRvIHRoZSBuYW1lcGxhdGUgYSB0YWJsZSBsaXN0cyBhbGwgcHJvcGVydGllcyBhbmQgdGhlaXIgYXR0cmlidXRlcy4gDQo=", + "http://admin-shell.io/aasx-package-explorer/functions/asciidoc/textblock/1/0": "UmVnYXJkaW5nIHByb3BlcnR5IGBNYXJraW5nTmFtZWAsIHRoZSBwcmVmZXJhYmxlIHNvbHV0aW9uIGlzIHRvIHByb3ZpZGUgYSB2YWx1ZUlkIGluIElSREkgb3JpZ2luYXRpbmcgZnJvbSBJRUMgQ0REIG9yIEVDTEFTUyBlbnVtZXJhdGlvbiB2YWx1ZSBsaXN0LCBlLmcuICJDRSIgKElSREk6IDAxMTIvMi8vLzYxOTg3I0FCTzQwOSMwMDMgb3IgMDE3My0xIzA3LURBQTYwMyMwMDQpLiBJbiBjYXNlIG5vbmUgb2YgdGhlIGV4aXN0aW5nIEVDTEFTUyBlbnVtZXJhdGlvbiB2YWx1ZXMgbWF0Y2hlcywgZmlsbGluZyBwbGFpbiBzdHJpbmcgdGV4dCBpbnRvIHRoZSDigJx2YWx1ZeKAnSBmaWVsZCBvZiB0aGUgcHJvcGVydHkgYE1hcmtpbmdOYW1lYCBjYW4gYmUgYWNjZXB0ZWQgYWx0ZXJuYXRpdmVseS4gSXQgbmVlZHMgdG8gYmUgcG9pbnRlZCBvdXQgdGhhdCBFQ0xBU1MgYWxzbyBwcm92aWRlcyBtYXJraW5nIGRlZmluaXRpb25zIGluIHRlcm1zIG9mIGJvb2xlYW4gcHJvcGVydHksIGUuZy4g4oCcQ0UtIHF1YWxpZmljYXRpb24gcHJlc2VudOKAnSAoSVJESTogMDE3My0xIzAyLUJBRjA1MyMwMDgpLiBJbiB0aGlzIGNhc2UgdXNlcnMgc2hvdWxkIGluc3RlYWQgdXNlIGEgbWF0Y2hpbmcgRUNMQVNTIGVudW1lcmF0aW9uIHZhbHVlIG9yLCBpZiBub3QgcHJvdmlkZWQgYXMgZW51bWVyYXRpb24sIGZpbGwgaW4gcGxhaW4gc3RyaW5nIHRleHQuDQoNClRoZSBmb2xsb3dpbmcgZXhhbXBsZSAoc2VlIDw8RmlndXJlXzU+PikgaWxsdXN0cmF0ZXMgaG93IHRvIG1vZGVsIHByb2R1Y3QgbWFya2luZyBpbiBhbiBBQVMuIE9uIHRoZSBsZWZ0IHNpZGUgdGhlcmUgaXMgYSBzYW1wbGUgbmFtZXBsYXRlIHdoaWNoIGNvbnRhaW5zIHR3byBtYXJraW5ncyB0byBiZSBtb2RlbGxlZDogdGhlIENFIG1hcmtpbmcgYW5kIHRoZSBXRUVFIG1hcmtpbmcgd2l0aCBhIGNyb3NzZWQtb3V0IHdoZWVsZWQgYmluLiBOZXh0IHRvIHRoZSBuYW1lcGxhdGUgYSB0YWJsZSBsaXN0cyBhbGwgcHJvcGVydGllcyBhbmQgdGhlaXIgYXR0cmlidXRlcy4gDQo=", "0173-1#02-ABK273#002": { "0173-1#02-ABK273#002_en": "Name of product class in the used classification system", "0173-1#02-ABK273#002_de": "Name der Produktklasse im verwendeten Klassifizierungssystem" @@ -502,26 +480,15 @@ "0173-1#02-AAO127#003": { "0173-1#02-AAO127#003_de": "Marketing" }, - "https://admin-shell.io/zvei/nameplate/1/0/ContactInformations/ContactInformation/Phone": [ - { - "0173-1#02-AAO136#002": { - "0173-1#02-AAO136#002_en": "234-567-8901" - }, - "0173-1#02-AAO137#003": "Office", - "https://mm-software.com/Phone/AvailableTime": { - "https://mm-software.com/Phone/AvailableTime_de": "Dienstags bis Donnerstags, 9-17 Uhr" - } + "https://admin-shell.io/zvei/nameplate/1/0/ContactInformations/ContactInformation/Phone": { + "0173-1#02-AAO136#002": { + "0173-1#02-AAO136#002_en": "345-678-9012" }, - { - "0173-1#02-AAO136#002": { - "0173-1#02-AAO136#002_en": "345-678-9012" - }, - "https://mm-software.com/Phone/AvailableTime": { - "https://mm-software.com/Phone/AvailableTime_de": "Montags, 10-16 Uhr" - }, - "0173-1#02-AAO137#003": "Mobile" - } - ], + "https://mm-software.com/Phone/AvailableTime": { + "https://mm-software.com/Phone/AvailableTime_de": "Montags, 10-16 Uhr" + }, + "0173-1#02-AAO137#003": "Mobile" + }, "0173-1#02-AAQ836#005": [ { "0173-1#02-AAO198#002": "john.doe@example.com", @@ -559,17 +526,15 @@ "0173-1#02-AAO127#003": { "0173-1#02-AAO127#003_de": "Vertrieb" }, - "https://admin-shell.io/zvei/nameplate/1/0/ContactInformations/ContactInformation/Phone": [ - { - "0173-1#02-AAO136#002": { - "0173-1#02-AAO136#002_en": "123-456-7890" - }, - "0173-1#02-AAO137#003": "assistant", - "https://mm-software.com/Phone/AvailableTime": { - "https://mm-software.com/Phone/AvailableTime_de": "Dienstags bis Donnerstags, 9-17 Uhr" - } + "https://admin-shell.io/zvei/nameplate/1/0/ContactInformations/ContactInformation/Phone": { + "0173-1#02-AAO136#002": { + "0173-1#02-AAO136#002_en": "123-456-7890" + }, + "0173-1#02-AAO137#003": "assistant", + "https://mm-software.com/Phone/AvailableTime": { + "https://mm-software.com/Phone/AvailableTime_de": "Dienstags bis Donnerstags, 9-17 Uhr" } - ], + }, "0173-1#02-AAQ836#005": [ { "0173-1#02-AAO198#002": "jane.doe@betacorp.com", @@ -830,7 +795,7 @@ "https://admin-shell.io/SMT/General/Arbitrary/date_max": "2050-02-06" }, "0112/2///61987#ABA588#004": "56.54", - "http://admin-shell.io/aasx-package-explorer/functions/asciidoc/textblock/1/0": "UmVnYXJkaW5nIHByb3BlcnR5IGBNYXJraW5nTmFtZWAsIHRoZSBwcmVmZXJhYmxlIHNvbHV0aW9uIGlzIHRvIHByb3ZpZGUgYSB2YWx1ZUlkIGluIElSREkgb3JpZ2luYXRpbmcgZnJvbSBJRUMgQ0REIG9yIEVDTEFTUyBlbnVtZXJhdGlvbiB2YWx1ZSBsaXN0LCBlLmcuICJDRSIgKElSREk6IDAxMTIvMi8vLzYxOTg3I0FCTzQwOSMwMDMgb3IgMDE3My0xIzA3LURBQTYwMyMwMDQpLiBJbiBjYXNlIG5vbmUgb2YgdGhlIGV4aXN0aW5nIEVDTEFTUyBlbnVtZXJhdGlvbiB2YWx1ZXMgbWF0Y2hlcywgZmlsbGluZyBwbGFpbiBzdHJpbmcgdGV4dCBpbnRvIHRoZSDigJx2YWx1ZeKAnSBmaWVsZCBvZiB0aGUgcHJvcGVydHkgYE1hcmtpbmdOYW1lYCBjYW4gYmUgYWNjZXB0ZWQgYWx0ZXJuYXRpdmVseS4gSXQgbmVlZHMgdG8gYmUgcG9pbnRlZCBvdXQgdGhhdCBFQ0xBU1MgYWxzbyBwcm92aWRlcyBtYXJraW5nIGRlZmluaXRpb25zIGluIHRlcm1zIG9mIGJvb2xlYW4gcHJvcGVydHksIGUuZy4g4oCcQ0UtIHF1YWxpZmljYXRpb24gcHJlc2VudOKAnSAoSVJESTogMDE3My0xIzAyLUJBRjA1MyMwMDgpLiBJbiB0aGlzIGNhc2UgdXNlcnMgc2hvdWxkIGluc3RlYWQgdXNlIGEgbWF0Y2hpbmcgRUNMQVNTIGVudW1lcmF0aW9uIHZhbHVlIG9yLCBpZiBub3QgcHJvdmlkZWQgYXMgZW51bWVyYXRpb24sIGZpbGwgaW4gcGxhaW4gc3RyaW5nIHRleHQuDQoNClRoZSBmb2xsb3dpbmcgZXhhbXBsZSAoc2VlIDw8RmlndXJlXzU\u002BPikgaWxsdXN0cmF0ZXMgaG93IHRvIG1vZGVsIHByb2R1Y3QgbWFya2luZyBpbiBhbiBBQVMuIE9uIHRoZSBsZWZ0IHNpZGUgdGhlcmUgaXMgYSBzYW1wbGUgbmFtZXBsYXRlIHdoaWNoIGNvbnRhaW5zIHR3byBtYXJraW5ncyB0byBiZSBtb2RlbGxlZDogdGhlIENFIG1hcmtpbmcgYW5kIHRoZSBXRUVFIG1hcmtpbmcgd2l0aCBhIGNyb3NzZWQtb3V0IHdoZWVsZWQgYmluLiBOZXh0IHRvIHRoZSBuYW1lcGxhdGUgYSB0YWJsZSBsaXN0cyBhbGwgcHJvcGVydGllcyBhbmQgdGhlaXIgYXR0cmlidXRlcy4gDQo=", + "http://admin-shell.io/aasx-package-explorer/functions/asciidoc/textblock/1/0": "UmVnYXJkaW5nIHByb3BlcnR5IGBNYXJraW5nTmFtZWAsIHRoZSBwcmVmZXJhYmxlIHNvbHV0aW9uIGlzIHRvIHByb3ZpZGUgYSB2YWx1ZUlkIGluIElSREkgb3JpZ2luYXRpbmcgZnJvbSBJRUMgQ0REIG9yIEVDTEFTUyBlbnVtZXJhdGlvbiB2YWx1ZSBsaXN0LCBlLmcuICJDRSIgKElSREk6IDAxMTIvMi8vLzYxOTg3I0FCTzQwOSMwMDMgb3IgMDE3My0xIzA3LURBQTYwMyMwMDQpLiBJbiBjYXNlIG5vbmUgb2YgdGhlIGV4aXN0aW5nIEVDTEFTUyBlbnVtZXJhdGlvbiB2YWx1ZXMgbWF0Y2hlcywgZmlsbGluZyBwbGFpbiBzdHJpbmcgdGV4dCBpbnRvIHRoZSDigJx2YWx1ZeKAnSBmaWVsZCBvZiB0aGUgcHJvcGVydHkgYE1hcmtpbmdOYW1lYCBjYW4gYmUgYWNjZXB0ZWQgYWx0ZXJuYXRpdmVseS4gSXQgbmVlZHMgdG8gYmUgcG9pbnRlZCBvdXQgdGhhdCBFQ0xBU1MgYWxzbyBwcm92aWRlcyBtYXJraW5nIGRlZmluaXRpb25zIGluIHRlcm1zIG9mIGJvb2xlYW4gcHJvcGVydHksIGUuZy4g4oCcQ0UtIHF1YWxpZmljYXRpb24gcHJlc2VudOKAnSAoSVJESTogMDE3My0xIzAyLUJBRjA1MyMwMDgpLiBJbiB0aGlzIGNhc2UgdXNlcnMgc2hvdWxkIGluc3RlYWQgdXNlIGEgbWF0Y2hpbmcgRUNMQVNTIGVudW1lcmF0aW9uIHZhbHVlIG9yLCBpZiBub3QgcHJvdmlkZWQgYXMgZW51bWVyYXRpb24sIGZpbGwgaW4gcGxhaW4gc3RyaW5nIHRleHQuDQoNClRoZSBmb2xsb3dpbmcgZXhhbXBsZSAoc2VlIDw8RmlndXJlXzU+PikgaWxsdXN0cmF0ZXMgaG93IHRvIG1vZGVsIHByb2R1Y3QgbWFya2luZyBpbiBhbiBBQVMuIE9uIHRoZSBsZWZ0IHNpZGUgdGhlcmUgaXMgYSBzYW1wbGUgbmFtZXBsYXRlIHdoaWNoIGNvbnRhaW5zIHR3byBtYXJraW5ncyB0byBiZSBtb2RlbGxlZDogdGhlIENFIG1hcmtpbmcgYW5kIHRoZSBXRUVFIG1hcmtpbmcgd2l0aCBhIGNyb3NzZWQtb3V0IHdoZWVsZWQgYmluLiBOZXh0IHRvIHRoZSBuYW1lcGxhdGUgYSB0YWJsZSBsaXN0cyBhbGwgcHJvcGVydGllcyBhbmQgdGhlaXIgYXR0cmlidXRlcy4gDQo=", "0173-1#02-ABK273#002": { "0173-1#02-ABK273#002_en": "Name of product class in the used classification system", "0173-1#02-ABK273#002_de": "Name der Produktklasse im verwendeten Klassifizierungssystem" @@ -856,26 +821,15 @@ "0173-1#02-AAO127#003": { "0173-1#02-AAO127#003_de": "Marketing" }, - "https://admin-shell.io/zvei/nameplate/1/0/ContactInformations/ContactInformation/Phone": [ - { - "0173-1#02-AAO136#002": { - "0173-1#02-AAO136#002_en": "234-567-8901" - }, - "0173-1#02-AAO137#003": "Office", - "https://mm-software.com/Phone/AvailableTime": { - "https://mm-software.com/Phone/AvailableTime_de": "Dienstags bis Donnerstags, 9-17 Uhr" - } + "https://admin-shell.io/zvei/nameplate/1/0/ContactInformations/ContactInformation/Phone": { + "0173-1#02-AAO136#002": { + "0173-1#02-AAO136#002_en": "234-567-8901" }, - { - "0173-1#02-AAO136#002": { - "0173-1#02-AAO136#002_en": "345-678-9012" - }, - "https://mm-software.com/Phone/AvailableTime": { - "https://mm-software.com/Phone/AvailableTime_de": "Montags, 10-16 Uhr" - }, - "0173-1#02-AAO137#003": "Mobile" + "0173-1#02-AAO137#003": "Office", + "https://mm-software.com/Phone/AvailableTime": { + "https://mm-software.com/Phone/AvailableTime_de": "Dienstags bis Donnerstags, 9-17 Uhr" } - ], + }, "0173-1#02-AAQ836#005": [ { "0173-1#02-AAO198#002": "john.doe@example.com", @@ -913,26 +867,15 @@ "0173-1#02-AAO127#003": { "0173-1#02-AAO127#003_de": "Marketing" }, - "https://admin-shell.io/zvei/nameplate/1/0/ContactInformations/ContactInformation/Phone": [ - { - "0173-1#02-AAO136#002": { - "0173-1#02-AAO136#002_en": "234-567-8901" - }, - "0173-1#02-AAO137#003": "Office", - "https://mm-software.com/Phone/AvailableTime": { - "https://mm-software.com/Phone/AvailableTime_de": "Dienstags bis Donnerstags, 9-17 Uhr" - } + "https://admin-shell.io/zvei/nameplate/1/0/ContactInformations/ContactInformation/Phone": { + "0173-1#02-AAO136#002": { + "0173-1#02-AAO136#002_en": "234-567-8901" }, - { - "0173-1#02-AAO136#002": { - "0173-1#02-AAO136#002_en": "345-678-9012" - }, - "https://mm-software.com/Phone/AvailableTime": { - "https://mm-software.com/Phone/AvailableTime_de": "Montags, 10-16 Uhr" - }, - "0173-1#02-AAO137#003": "Mobile" + "0173-1#02-AAO137#003": "Office", + "https://mm-software.com/Phone/AvailableTime": { + "https://mm-software.com/Phone/AvailableTime_de": "Dienstags bis Donnerstags, 9-17 Uhr" } - ], + }, "0173-1#02-AAQ836#005": [ { "0173-1#02-AAO198#002": "john.doe@example.com", @@ -970,17 +913,15 @@ "0173-1#02-AAO127#003": { "0173-1#02-AAO127#003_de": "Vertrieb" }, - "https://admin-shell.io/zvei/nameplate/1/0/ContactInformations/ContactInformation/Phone": [ - { - "0173-1#02-AAO136#002": { - "0173-1#02-AAO136#002_en": "123-456-7890" - }, - "0173-1#02-AAO137#003": "assistant", - "https://mm-software.com/Phone/AvailableTime": { - "https://mm-software.com/Phone/AvailableTime_de": "Dienstags bis Donnerstags, 9-17 Uhr" - } + "https://admin-shell.io/zvei/nameplate/1/0/ContactInformations/ContactInformation/Phone": { + "0173-1#02-AAO136#002": { + "0173-1#02-AAO136#002_en": "123-456-7890" + }, + "0173-1#02-AAO137#003": "assistant", + "https://mm-software.com/Phone/AvailableTime": { + "https://mm-software.com/Phone/AvailableTime_de": "Dienstags bis Donnerstags, 9-17 Uhr" } - ], + }, "0173-1#02-AAQ836#005": [ { "0173-1#02-AAO198#002": "jane.doe@betacorp.com", @@ -1353,7 +1294,7 @@ "https://admin-shell.io/SMT/General/Arbitrary/date_max": "2032-02-06" }, "0112/2///61987#ABA588#004": "5.5", - "http://admin-shell.io/aasx-package-explorer/functions/asciidoc/textblock/1/0": "UmVnYXJkaW5nIHByb3BlcnR5IGBNYXJraW5nTmFtZWAsIHRoZSBwcmVmZXJhYmxlIHNvbHV0aW9uIGlzIHRvIHByb3ZpZGUgYSB2YWx1ZUlkIGluIElSREkgb3JpZ2luYXRpbmcgZnJvbSBJRUMgQ0REIG9yIEVDTEFTUyBlbnVtZXJhdGlvbiB2YWx1ZSBsaXN0LCBlLmcuICJDRSIgKElSREk6IDAxMTIvMi8vLzYxOTg3I0FCTzQwOSMwMDMgb3IgMDE3My0xIzA3LURBQTYwMyMwMDQpLiBJbiBjYXNlIG5vbmUgb2YgdGhlIGV4aXN0aW5nIEVDTEFTUyBlbnVtZXJhdGlvbiB2YWx1ZXMgbWF0Y2hlcywgZmlsbGluZyBwbGFpbiBzdHJpbmcgdGV4dCBpbnRvIHRoZSDigJx2YWx1ZeKAnSBmaWVsZCBvZiB0aGUgcHJvcGVydHkgYE1hcmtpbmdOYW1lYCBjYW4gYmUgYWNjZXB0ZWQgYWx0ZXJuYXRpdmVseS4gSXQgbmVlZHMgdG8gYmUgcG9pbnRlZCBvdXQgdGhhdCBFQ0xBU1MgYWxzbyBwcm92aWRlcyBtYXJraW5nIGRlZmluaXRpb25zIGluIHRlcm1zIG9mIGJvb2xlYW4gcHJvcGVydHksIGUuZy4g4oCcQ0UtIHF1YWxpZmljYXRpb24gcHJlc2VudOKAnSAoSVJESTogMDE3My0xIzAyLUJBRjA1MyMwMDgpLiBJbiB0aGlzIGNhc2UgdXNlcnMgc2hvdWxkIGluc3RlYWQgdXNlIGEgbWF0Y2hpbmcgRUNMQVNTIGVudW1lcmF0aW9uIHZhbHVlIG9yLCBpZiBub3QgcHJvdmlkZWQgYXMgZW51bWVyYXRpb24sIGZpbGwgaW4gcGxhaW4gc3RyaW5nIHRleHQuDQoNClRoZSBmb2xsb3dpbmcgZXhhbXBsZSAoc2VlIDw8RmlndXJlXzU\u002BPikgaWxsdXN0cmF0ZXMgaG93IHRvIG1vZGVsIHByb2R1Y3QgbWFya2luZyBpbiBhbiBBQVMuIE9uIHRoZSBsZWZ0IHNpZGUgdGhlcmUgaXMgYSBzYW1wbGUgbmFtZXBsYXRlIHdoaWNoIGNvbnRhaW5zIHR3byBtYXJraW5ncyB0byBiZSBtb2RlbGxlZDogdGhlIENFIG1hcmtpbmcgYW5kIHRoZSBXRUVFIG1hcmtpbmcgd2l0aCBhIGNyb3NzZWQtb3V0IHdoZWVsZWQgYmluLiBOZXh0IHRvIHRoZSBuYW1lcGxhdGUgYSB0YWJsZSBsaXN0cyBhbGwgcHJvcGVydGllcyBhbmQgdGhlaXIgYXR0cmlidXRlcy4gDQo=", + "http://admin-shell.io/aasx-package-explorer/functions/asciidoc/textblock/1/0": "UmVnYXJkaW5nIHByb3BlcnR5IGBNYXJraW5nTmFtZWAsIHRoZSBwcmVmZXJhYmxlIHNvbHV0aW9uIGlzIHRvIHByb3ZpZGUgYSB2YWx1ZUlkIGluIElSREkgb3JpZ2luYXRpbmcgZnJvbSBJRUMgQ0REIG9yIEVDTEFTUyBlbnVtZXJhdGlvbiB2YWx1ZSBsaXN0LCBlLmcuICJDRSIgKElSREk6IDAxMTIvMi8vLzYxOTg3I0FCTzQwOSMwMDMgb3IgMDE3My0xIzA3LURBQTYwMyMwMDQpLiBJbiBjYXNlIG5vbmUgb2YgdGhlIGV4aXN0aW5nIEVDTEFTUyBlbnVtZXJhdGlvbiB2YWx1ZXMgbWF0Y2hlcywgZmlsbGluZyBwbGFpbiBzdHJpbmcgdGV4dCBpbnRvIHRoZSDigJx2YWx1ZeKAnSBmaWVsZCBvZiB0aGUgcHJvcGVydHkgYE1hcmtpbmdOYW1lYCBjYW4gYmUgYWNjZXB0ZWQgYWx0ZXJuYXRpdmVseS4gSXQgbmVlZHMgdG8gYmUgcG9pbnRlZCBvdXQgdGhhdCBFQ0xBU1MgYWxzbyBwcm92aWRlcyBtYXJraW5nIGRlZmluaXRpb25zIGluIHRlcm1zIG9mIGJvb2xlYW4gcHJvcGVydHksIGUuZy4g4oCcQ0UtIHF1YWxpZmljYXRpb24gcHJlc2VudOKAnSAoSVJESTogMDE3My0xIzAyLUJBRjA1MyMwMDgpLiBJbiB0aGlzIGNhc2UgdXNlcnMgc2hvdWxkIGluc3RlYWQgdXNlIGEgbWF0Y2hpbmcgRUNMQVNTIGVudW1lcmF0aW9uIHZhbHVlIG9yLCBpZiBub3QgcHJvdmlkZWQgYXMgZW51bWVyYXRpb24sIGZpbGwgaW4gcGxhaW4gc3RyaW5nIHRleHQuDQoNClRoZSBmb2xsb3dpbmcgZXhhbXBsZSAoc2VlIDw8RmlndXJlXzU+PikgaWxsdXN0cmF0ZXMgaG93IHRvIG1vZGVsIHByb2R1Y3QgbWFya2luZyBpbiBhbiBBQVMuIE9uIHRoZSBsZWZ0IHNpZGUgdGhlcmUgaXMgYSBzYW1wbGUgbmFtZXBsYXRlIHdoaWNoIGNvbnRhaW5zIHR3byBtYXJraW5ncyB0byBiZSBtb2RlbGxlZDogdGhlIENFIG1hcmtpbmcgYW5kIHRoZSBXRUVFIG1hcmtpbmcgd2l0aCBhIGNyb3NzZWQtb3V0IHdoZWVsZWQgYmluLiBOZXh0IHRvIHRoZSBuYW1lcGxhdGUgYSB0YWJsZSBsaXN0cyBhbGwgcHJvcGVydGllcyBhbmQgdGhlaXIgYXR0cmlidXRlcy4gDQo=", "0173-1#02-ABK273#002": { "0173-1#02-ABK273#002_en": "Name of product class in the used classification system", "0173-1#02-ABK273#002_de": "Name der Produktklasse im verwendeten Klassifizierungssystem" From 2ae18070a5b8ac6093637e28e4e84e3c83641b2a Mon Sep 17 00:00:00 2001 From: Hardi Shah Date: Wed, 29 Apr 2026 15:35:30 +0530 Subject: [PATCH 69/71] Clean up tests and docs; update exception summary Removed obsolete test and unused mocks, clarified exception documentation, and cleaned up csproj folder entries. --- .../AAS.TwinEngine.DataEngine.UnitTests.csproj | 4 ---- .../Helper/JsonSchemaValidatorTests.cs | 10 ---------- .../Infrastructure/PluginSchemaRejectionException.cs | 4 ++-- .../Api/Submodel/Services/JsonSchemaParserTests.cs | 2 -- 4 files changed, 2 insertions(+), 18 deletions(-) diff --git a/source/AAS.TwinEngine.DataEngine.UnitTests/AAS.TwinEngine.DataEngine.UnitTests.csproj b/source/AAS.TwinEngine.DataEngine.UnitTests/AAS.TwinEngine.DataEngine.UnitTests.csproj index fc375e94..ed4a3b57 100644 --- a/source/AAS.TwinEngine.DataEngine.UnitTests/AAS.TwinEngine.DataEngine.UnitTests.csproj +++ b/source/AAS.TwinEngine.DataEngine.UnitTests/AAS.TwinEngine.DataEngine.UnitTests.csproj @@ -34,9 +34,5 @@ - - - - diff --git a/source/AAS.TwinEngine.DataEngine.UnitTests/Infrastructure/Providers/PluginDataProvider/Helper/JsonSchemaValidatorTests.cs b/source/AAS.TwinEngine.DataEngine.UnitTests/Infrastructure/Providers/PluginDataProvider/Helper/JsonSchemaValidatorTests.cs index 6eefcf1a..7a213ef3 100644 --- a/source/AAS.TwinEngine.DataEngine.UnitTests/Infrastructure/Providers/PluginDataProvider/Helper/JsonSchemaValidatorTests.cs +++ b/source/AAS.TwinEngine.DataEngine.UnitTests/Infrastructure/Providers/PluginDataProvider/Helper/JsonSchemaValidatorTests.cs @@ -40,16 +40,6 @@ public JsonSchemaValidatorTests() [Fact] public void ValidateRequestSchema_NullSchema_ThrowsBadRequest() => Assert.Throws(() => _sut.ValidateRequestSchema(null!)); - [Fact] - public void ValidateRequestSchema_WithInvalidJson_ThrowsParseError() - { - Assert.ThrowsAny(() => - { - var schema = JsonSchema.FromText("{\"type\": 123}"); - _sut.ValidateRequestSchema(schema); - }); - } - [Fact] public void ValidateRequestSchema_ValidSchema_DoesNotThrow() { diff --git a/source/AAS.TwinEngine.DataEngine/ApplicationLogic/Exceptions/Infrastructure/PluginSchemaRejectionException.cs b/source/AAS.TwinEngine.DataEngine/ApplicationLogic/Exceptions/Infrastructure/PluginSchemaRejectionException.cs index f5ac7df8..85c69733 100644 --- a/source/AAS.TwinEngine.DataEngine/ApplicationLogic/Exceptions/Infrastructure/PluginSchemaRejectionException.cs +++ b/source/AAS.TwinEngine.DataEngine/ApplicationLogic/Exceptions/Infrastructure/PluginSchemaRejectionException.cs @@ -1,7 +1,7 @@ namespace AAS.TwinEngine.DataEngine.ApplicationLogic.Exceptions.Infrastructure; /// -/// Thrown when a plugin rejects the request schema with HTTP 400. -/// This triggers a legacy Draft-07 compatibility retry. +/// Thrown when a plugin indicates the request schema is rejected or incompatible, +/// currently via HTTP 400, 404, or 500. /// public class PluginSchemaRejectionException : Exception; diff --git a/source/AAS.TwinEngine.Plugin.TestPlugin.UnitTests/Api/Submodel/Services/JsonSchemaParserTests.cs b/source/AAS.TwinEngine.Plugin.TestPlugin.UnitTests/Api/Submodel/Services/JsonSchemaParserTests.cs index 8587368c..a0513cdd 100644 --- a/source/AAS.TwinEngine.Plugin.TestPlugin.UnitTests/Api/Submodel/Services/JsonSchemaParserTests.cs +++ b/source/AAS.TwinEngine.Plugin.TestPlugin.UnitTests/Api/Submodel/Services/JsonSchemaParserTests.cs @@ -115,12 +115,10 @@ public class JsonSchemaParserTests private readonly ILogger _logger; private readonly JsonSchemaParser _sut; - private readonly IJsonSchemaValidator _jsonSchemaValidator; public JsonSchemaParserTests() { _logger = Substitute.For>(); - _jsonSchemaValidator = Substitute.For(); _sut = new JsonSchemaParser(_logger); } From df7bd8b99e475dd9c7fd38c54303657c9c5ace10 Mon Sep 17 00:00:00 2001 From: Hardi Shah Date: Wed, 6 May 2026 11:51:15 +0530 Subject: [PATCH 70/71] Support multiple JSON Schema drafts with extensible handlers Refactor JSON Schema validation to use a new draft handler abstraction (`IJsonSchemaDraftHandler`) and selector, supporting both Draft 2020-12 and legacy Draft-07. Introduce a schema normalizer and update dependency injection to register all components. Move legacy and validation code to clearer namespaces. Add comprehensive unit tests for draft selection and validation. This modular approach improves extensibility, testability, and prepares for future draft support while maintaining backward compatibility. --- .../JsonSchemaDraftHandlersTests.cs | 54 +++ .../JsonSchemaDraftSelectorTests.cs | 61 +++ .../JsonSchemaValidatorTests.cs | 15 +- .../Services/PluginDataHandlerTests.cs | 7 +- .../Services/PluginDataProviderTests.cs | 1 + .../PluginSchemaRejectionException.cs | 2 +- .../ILegacySchemaRetryHandler.cs | 2 +- .../IPluginSchemaCompatibilityHandler.cs | 2 +- .../Validation/IJsonSchemaDraftHandler.cs | 13 + .../Validation/IJsonSchemaNormalizer.cs | 12 + .../Helper/JsonSchemaValidator.cs | 350 ------------------ .../LegacyDraft7JsonSchemaValidatorHandler.cs | 28 ++ .../LegacyV1/LegacySchemaRetryHandler.cs | 2 +- .../PluginSchemaCompatibilityHandler.cs | 2 + .../JsonSchemaDraft202012Handler.cs | 26 ++ .../Validation/JsonSchemaDraftSelector.cs | 28 ++ .../Helper/Validation/JsonSchemaNormalizer.cs | 176 +++++++++ .../Helper/Validation/JsonSchemaValidator.cs | 182 +++++++++ .../Services/PluginDataHandler.cs | 2 + .../Services/PluginDataProvider.cs | 1 + ...astructureDependencyInjectionExtensions.cs | 10 + 21 files changed, 616 insertions(+), 360 deletions(-) create mode 100644 source/AAS.TwinEngine.DataEngine.UnitTests/Infrastructure/Providers/PluginDataProvider/Helper/Validation/JsonSchemaDraftHandlersTests.cs create mode 100644 source/AAS.TwinEngine.DataEngine.UnitTests/Infrastructure/Providers/PluginDataProvider/Helper/Validation/JsonSchemaDraftSelectorTests.cs rename source/AAS.TwinEngine.DataEngine.UnitTests/Infrastructure/Providers/PluginDataProvider/Helper/{ => Validation}/JsonSchemaValidatorTests.cs (95%) rename source/AAS.TwinEngine.DataEngine/ApplicationLogic/Exceptions/Infrastructure/{ => LegacyV1}/PluginSchemaRejectionException.cs (90%) rename source/AAS.TwinEngine.DataEngine/ApplicationLogic/Services/Plugin/Helper/{ => LegacyV1}/ILegacySchemaRetryHandler.cs (97%) rename source/AAS.TwinEngine.DataEngine/ApplicationLogic/Services/Plugin/Helper/{ => LegacyV1}/IPluginSchemaCompatibilityHandler.cs (95%) create mode 100644 source/AAS.TwinEngine.DataEngine/ApplicationLogic/Services/Plugin/Helper/Validation/IJsonSchemaDraftHandler.cs create mode 100644 source/AAS.TwinEngine.DataEngine/ApplicationLogic/Services/Plugin/Helper/Validation/IJsonSchemaNormalizer.cs delete mode 100644 source/AAS.TwinEngine.DataEngine/Infrastructure/Providers/PluginDataProvider/Helper/JsonSchemaValidator.cs create mode 100644 source/AAS.TwinEngine.DataEngine/Infrastructure/Providers/PluginDataProvider/Helper/LegacyV1/LegacyDraft7JsonSchemaValidatorHandler.cs create mode 100644 source/AAS.TwinEngine.DataEngine/Infrastructure/Providers/PluginDataProvider/Helper/Validation/JsonSchemaDraft202012Handler.cs create mode 100644 source/AAS.TwinEngine.DataEngine/Infrastructure/Providers/PluginDataProvider/Helper/Validation/JsonSchemaDraftSelector.cs create mode 100644 source/AAS.TwinEngine.DataEngine/Infrastructure/Providers/PluginDataProvider/Helper/Validation/JsonSchemaNormalizer.cs create mode 100644 source/AAS.TwinEngine.DataEngine/Infrastructure/Providers/PluginDataProvider/Helper/Validation/JsonSchemaValidator.cs diff --git a/source/AAS.TwinEngine.DataEngine.UnitTests/Infrastructure/Providers/PluginDataProvider/Helper/Validation/JsonSchemaDraftHandlersTests.cs b/source/AAS.TwinEngine.DataEngine.UnitTests/Infrastructure/Providers/PluginDataProvider/Helper/Validation/JsonSchemaDraftHandlersTests.cs new file mode 100644 index 00000000..cda63162 --- /dev/null +++ b/source/AAS.TwinEngine.DataEngine.UnitTests/Infrastructure/Providers/PluginDataProvider/Helper/Validation/JsonSchemaDraftHandlersTests.cs @@ -0,0 +1,54 @@ +using AAS.TwinEngine.DataEngine.Infrastructure.Providers.PluginDataProvider.Helper.LegacyV1; +using AAS.TwinEngine.DataEngine.Infrastructure.Providers.PluginDataProvider.Helper.Validation; + +using Json.Schema; + +namespace AAS.TwinEngine.DataEngine.UnitTests.Infrastructure.Providers.PluginDataProvider.Helper.Validation; + +public class JsonSchemaDraft202012HandlerTests +{ + private readonly JsonSchemaDraft202012Handler _sut = new(); + + [Fact] + public void CanHandle_WhenDeclaredSchemaIsMissing_ReturnsTrue() + { + Assert.True(_sut.CanHandle(null)); + } + + [Fact] + public void CanHandle_WhenDeclaredSchemaIsDraft202012_ReturnsTrue() + { + Assert.True(_sut.CanHandle(MetaSchemas.Draft202012Id.OriginalString)); + } + + [Fact] + public void CanHandle_WhenDeclaredSchemaIsDraft7_ReturnsFalse() + { + Assert.False(_sut.CanHandle(MetaSchemas.Draft7Id.OriginalString)); + } +} + +#pragma warning disable CS0618 +public class LegacyDraft7JsonSchemaValidatorHandlerTests +{ + private readonly LegacyDraft7JsonSchemaValidatorHandler _sut = new(); + + [Fact] + public void CanHandle_WhenDeclaredSchemaIsDraft7_ReturnsTrue() + { + Assert.True(_sut.CanHandle(MetaSchemas.Draft7Id.OriginalString)); + } + + [Fact] + public void CanHandle_WhenDeclaredSchemaIsMissing_ReturnsFalse() + { + Assert.False(_sut.CanHandle(null)); + } + + [Fact] + public void CanHandle_WhenDeclaredSchemaIsDraft202012_ReturnsFalse() + { + Assert.False(_sut.CanHandle(MetaSchemas.Draft202012Id.OriginalString)); + } +} +#pragma warning restore CS0618 diff --git a/source/AAS.TwinEngine.DataEngine.UnitTests/Infrastructure/Providers/PluginDataProvider/Helper/Validation/JsonSchemaDraftSelectorTests.cs b/source/AAS.TwinEngine.DataEngine.UnitTests/Infrastructure/Providers/PluginDataProvider/Helper/Validation/JsonSchemaDraftSelectorTests.cs new file mode 100644 index 00000000..c8469411 --- /dev/null +++ b/source/AAS.TwinEngine.DataEngine.UnitTests/Infrastructure/Providers/PluginDataProvider/Helper/Validation/JsonSchemaDraftSelectorTests.cs @@ -0,0 +1,61 @@ +using AAS.TwinEngine.DataEngine.Infrastructure.Providers.PluginDataProvider.Helper.LegacyV1; +using AAS.TwinEngine.DataEngine.Infrastructure.Providers.PluginDataProvider.Helper.Validation; + +namespace AAS.TwinEngine.DataEngine.UnitTests.Infrastructure.Providers.PluginDataProvider.Helper.Validation; + +public class JsonSchemaDraftSelectorTests +{ + [Fact] + public void Constructor_WhenNoHandlersRegistered_ThrowsInvalidOperationException() + { + Assert.Throws(() => new JsonSchemaDraftSelector([])); + } + + [Fact] + public void Resolve_WhenSchemaIsMissing_DefaultsToDraft202012() + { +#pragma warning disable CS0618 + var sut = new JsonSchemaDraftSelector([ + new JsonSchemaDraft202012Handler(), + new LegacyDraft7JsonSchemaValidatorHandler() + ]); +#pragma warning restore CS0618 + + var resolved = sut.Resolve(null); + + Assert.IsType(resolved); + } + + [Fact] + public void Resolve_WhenSchemaIsDraft7_ResolvesLegacyDraft7Handler() + { +#pragma warning disable CS0618 + var sut = new JsonSchemaDraftSelector([ + new JsonSchemaDraft202012Handler(), + new LegacyDraft7JsonSchemaValidatorHandler() + ]); +#pragma warning restore CS0618 + + var resolved = sut.Resolve("http://json-schema.org/draft-07/schema#"); + +#pragma warning disable CS0618 + Assert.IsType(resolved); +#pragma warning restore CS0618 + } + + [Fact] + public void GetKnownRefPrefixes_ReturnsBothDraftPrefixes() + { +#pragma warning disable CS0618 + var sut = new JsonSchemaDraftSelector([ + new JsonSchemaDraft202012Handler(), + new LegacyDraft7JsonSchemaValidatorHandler() + ]); +#pragma warning restore CS0618 + + var prefixes = sut.GetKnownRefPrefixes(); + + Assert.Contains("#/$defs/", prefixes); + Assert.Contains("#/definitions/", prefixes); + } +} diff --git a/source/AAS.TwinEngine.DataEngine.UnitTests/Infrastructure/Providers/PluginDataProvider/Helper/JsonSchemaValidatorTests.cs b/source/AAS.TwinEngine.DataEngine.UnitTests/Infrastructure/Providers/PluginDataProvider/Helper/Validation/JsonSchemaValidatorTests.cs similarity index 95% rename from source/AAS.TwinEngine.DataEngine.UnitTests/Infrastructure/Providers/PluginDataProvider/Helper/JsonSchemaValidatorTests.cs rename to source/AAS.TwinEngine.DataEngine.UnitTests/Infrastructure/Providers/PluginDataProvider/Helper/Validation/JsonSchemaValidatorTests.cs index 7a213ef3..142b0820 100644 --- a/source/AAS.TwinEngine.DataEngine.UnitTests/Infrastructure/Providers/PluginDataProvider/Helper/JsonSchemaValidatorTests.cs +++ b/source/AAS.TwinEngine.DataEngine.UnitTests/Infrastructure/Providers/PluginDataProvider/Helper/Validation/JsonSchemaValidatorTests.cs @@ -1,5 +1,6 @@ using AAS.TwinEngine.DataEngine.ApplicationLogic.Exceptions.Application; -using AAS.TwinEngine.DataEngine.Infrastructure.Providers.PluginDataProvider.Helper; +using AAS.TwinEngine.DataEngine.Infrastructure.Providers.PluginDataProvider.Helper.LegacyV1; +using AAS.TwinEngine.DataEngine.Infrastructure.Providers.PluginDataProvider.Helper.Validation; using AAS.TwinEngine.DataEngine.ServiceConfiguration.Config; using Json.Schema; @@ -9,7 +10,7 @@ using NSubstitute; -namespace AAS.TwinEngine.DataEngine.UnitTests.Infrastructure.Providers.PluginDataProvider.Helper; +namespace AAS.TwinEngine.DataEngine.UnitTests.Infrastructure.Providers.PluginDataProvider.Helper.Validation; public class JsonSchemaValidatorTests { @@ -34,7 +35,15 @@ public JsonSchemaValidatorTests() SubmodelElementIndexContextPrefix = "_aastwinengine_" }); var logger = Substitute.For>(); - _sut = new JsonSchemaValidator(pluginsConfig, logger); + var normalizerLogger = Substitute.For>(); +#pragma warning disable CS0618 + var draftSelector = new JsonSchemaDraftSelector([ + new JsonSchemaDraft202012Handler(), + new LegacyDraft7JsonSchemaValidatorHandler() + ]); +#pragma warning restore CS0618 + var schemaNormalizer = new JsonSchemaNormalizer(pluginsConfig, draftSelector, normalizerLogger); + _sut = new JsonSchemaValidator(draftSelector, schemaNormalizer, logger); } [Fact] diff --git a/source/AAS.TwinEngine.DataEngine.UnitTests/Infrastructure/Providers/PluginDataProvider/Services/PluginDataHandlerTests.cs b/source/AAS.TwinEngine.DataEngine.UnitTests/Infrastructure/Providers/PluginDataProvider/Services/PluginDataHandlerTests.cs index fe5b731f..fa6c5369 100644 --- a/source/AAS.TwinEngine.DataEngine.UnitTests/Infrastructure/Providers/PluginDataProvider/Services/PluginDataHandlerTests.cs +++ b/source/AAS.TwinEngine.DataEngine.UnitTests/Infrastructure/Providers/PluginDataProvider/Services/PluginDataHandlerTests.cs @@ -5,8 +5,10 @@ using System.Text.Json.Serialization; using AAS.TwinEngine.DataEngine.ApplicationLogic.Exceptions.Infrastructure; +using AAS.TwinEngine.DataEngine.ApplicationLogic.Exceptions.Infrastructure.LegacyV1; using AAS.TwinEngine.DataEngine.ApplicationLogic.Services.Plugin; using AAS.TwinEngine.DataEngine.ApplicationLogic.Services.Plugin.Helper; +using AAS.TwinEngine.DataEngine.ApplicationLogic.Services.Plugin.Helper.LegacyV1; using AAS.TwinEngine.DataEngine.ApplicationLogic.Services.Plugin.Providers; using AAS.TwinEngine.DataEngine.DomainModel.AasRegistry; using AAS.TwinEngine.DataEngine.DomainModel.AasRepository; @@ -14,17 +16,16 @@ using AAS.TwinEngine.DataEngine.DomainModel.Shared; using AAS.TwinEngine.DataEngine.DomainModel.SubmodelRepository; using AAS.TwinEngine.DataEngine.Infrastructure.Providers.PluginDataProvider.Services; +using AAS.TwinEngine.DataEngine.ServiceConfiguration.Config; using Json.Schema; using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; using NSubstitute; using NSubstitute.ExceptionExtensions; -using AAS.TwinEngine.DataEngine.ServiceConfiguration.Config; -using Microsoft.Extensions.Options; - namespace AAS.TwinEngine.DataEngine.UnitTests.Infrastructure.Providers.PluginDataProvider.Services; public class PluginDataHandlerTests diff --git a/source/AAS.TwinEngine.DataEngine.UnitTests/Infrastructure/Providers/PluginDataProvider/Services/PluginDataProviderTests.cs b/source/AAS.TwinEngine.DataEngine.UnitTests/Infrastructure/Providers/PluginDataProvider/Services/PluginDataProviderTests.cs index 638b2ad4..a518aefa 100644 --- a/source/AAS.TwinEngine.DataEngine.UnitTests/Infrastructure/Providers/PluginDataProvider/Services/PluginDataProviderTests.cs +++ b/source/AAS.TwinEngine.DataEngine.UnitTests/Infrastructure/Providers/PluginDataProvider/Services/PluginDataProviderTests.cs @@ -19,6 +19,7 @@ using UnauthorizedAccessException = AAS.TwinEngine.DataEngine.ApplicationLogic.Exceptions.Infrastructure.UnauthorizedAccessException; using AAS.TwinEngine.DataEngine.ServiceConfiguration.Config; +using AAS.TwinEngine.DataEngine.ApplicationLogic.Exceptions.Infrastructure.LegacyV1; namespace AAS.TwinEngine.DataEngine.UnitTests.Infrastructure.Providers.PluginDataProvider.Services; diff --git a/source/AAS.TwinEngine.DataEngine/ApplicationLogic/Exceptions/Infrastructure/PluginSchemaRejectionException.cs b/source/AAS.TwinEngine.DataEngine/ApplicationLogic/Exceptions/Infrastructure/LegacyV1/PluginSchemaRejectionException.cs similarity index 90% rename from source/AAS.TwinEngine.DataEngine/ApplicationLogic/Exceptions/Infrastructure/PluginSchemaRejectionException.cs rename to source/AAS.TwinEngine.DataEngine/ApplicationLogic/Exceptions/Infrastructure/LegacyV1/PluginSchemaRejectionException.cs index 85c69733..67acaa7e 100644 --- a/source/AAS.TwinEngine.DataEngine/ApplicationLogic/Exceptions/Infrastructure/PluginSchemaRejectionException.cs +++ b/source/AAS.TwinEngine.DataEngine/ApplicationLogic/Exceptions/Infrastructure/LegacyV1/PluginSchemaRejectionException.cs @@ -1,4 +1,4 @@ -namespace AAS.TwinEngine.DataEngine.ApplicationLogic.Exceptions.Infrastructure; +namespace AAS.TwinEngine.DataEngine.ApplicationLogic.Exceptions.Infrastructure.LegacyV1; /// /// Thrown when a plugin indicates the request schema is rejected or incompatible, diff --git a/source/AAS.TwinEngine.DataEngine/ApplicationLogic/Services/Plugin/Helper/ILegacySchemaRetryHandler.cs b/source/AAS.TwinEngine.DataEngine/ApplicationLogic/Services/Plugin/Helper/LegacyV1/ILegacySchemaRetryHandler.cs similarity index 97% rename from source/AAS.TwinEngine.DataEngine/ApplicationLogic/Services/Plugin/Helper/ILegacySchemaRetryHandler.cs rename to source/AAS.TwinEngine.DataEngine/ApplicationLogic/Services/Plugin/Helper/LegacyV1/ILegacySchemaRetryHandler.cs index 3a93d7cc..a8464f15 100644 --- a/source/AAS.TwinEngine.DataEngine/ApplicationLogic/Services/Plugin/Helper/ILegacySchemaRetryHandler.cs +++ b/source/AAS.TwinEngine.DataEngine/ApplicationLogic/Services/Plugin/Helper/LegacyV1/ILegacySchemaRetryHandler.cs @@ -1,6 +1,6 @@ using AAS.TwinEngine.DataEngine.DomainModel.SubmodelRepository; -namespace AAS.TwinEngine.DataEngine.ApplicationLogic.Services.Plugin.Helper; +namespace AAS.TwinEngine.DataEngine.ApplicationLogic.Services.Plugin.Helper.Legacy; /// /// Retries a plugin data request using a Draft-07 JSON Schema when the plugin diff --git a/source/AAS.TwinEngine.DataEngine/ApplicationLogic/Services/Plugin/Helper/IPluginSchemaCompatibilityHandler.cs b/source/AAS.TwinEngine.DataEngine/ApplicationLogic/Services/Plugin/Helper/LegacyV1/IPluginSchemaCompatibilityHandler.cs similarity index 95% rename from source/AAS.TwinEngine.DataEngine/ApplicationLogic/Services/Plugin/Helper/IPluginSchemaCompatibilityHandler.cs rename to source/AAS.TwinEngine.DataEngine/ApplicationLogic/Services/Plugin/Helper/LegacyV1/IPluginSchemaCompatibilityHandler.cs index a4f9a199..b384a730 100644 --- a/source/AAS.TwinEngine.DataEngine/ApplicationLogic/Services/Plugin/Helper/IPluginSchemaCompatibilityHandler.cs +++ b/source/AAS.TwinEngine.DataEngine/ApplicationLogic/Services/Plugin/Helper/LegacyV1/IPluginSchemaCompatibilityHandler.cs @@ -2,7 +2,7 @@ using Json.Schema; -namespace AAS.TwinEngine.DataEngine.ApplicationLogic.Services.Plugin.Helper; +namespace AAS.TwinEngine.DataEngine.ApplicationLogic.Services.Plugin.Helper.LegacyV1; public interface IPluginSchemaCompatibilityHandler { diff --git a/source/AAS.TwinEngine.DataEngine/ApplicationLogic/Services/Plugin/Helper/Validation/IJsonSchemaDraftHandler.cs b/source/AAS.TwinEngine.DataEngine/ApplicationLogic/Services/Plugin/Helper/Validation/IJsonSchemaDraftHandler.cs new file mode 100644 index 00000000..b9e0e9c7 --- /dev/null +++ b/source/AAS.TwinEngine.DataEngine/ApplicationLogic/Services/Plugin/Helper/Validation/IJsonSchemaDraftHandler.cs @@ -0,0 +1,13 @@ +using Json.Schema; + +namespace AAS.TwinEngine.DataEngine.ApplicationLogic.Services.Plugin.Helper.Validation; + +public interface IJsonSchemaDraftHandler +{ + string DraftName { get; } + string MetaSchemaId { get; } + JsonSchema MetaSchema { get; } + IReadOnlyList SupportedRefPrefixes { get; } + + bool CanHandle(string? declaredSchema); +} diff --git a/source/AAS.TwinEngine.DataEngine/ApplicationLogic/Services/Plugin/Helper/Validation/IJsonSchemaNormalizer.cs b/source/AAS.TwinEngine.DataEngine/ApplicationLogic/Services/Plugin/Helper/Validation/IJsonSchemaNormalizer.cs new file mode 100644 index 00000000..46b36e00 --- /dev/null +++ b/source/AAS.TwinEngine.DataEngine/ApplicationLogic/Services/Plugin/Helper/Validation/IJsonSchemaNormalizer.cs @@ -0,0 +1,12 @@ +using System.Text.Json.Nodes; + +using Json.Schema; + +namespace AAS.TwinEngine.DataEngine.ApplicationLogic.Services.Plugin.Helper.Validation; + +public interface IJsonSchemaNormalizer +{ + bool TryNormalizeSchema(JsonSchema schema, string targetMetaSchemaId, out JsonSchema normalizedSchema, out string? error); + + void EscapeJsonReferencePointers(JsonNode? currentNode); +} diff --git a/source/AAS.TwinEngine.DataEngine/Infrastructure/Providers/PluginDataProvider/Helper/JsonSchemaValidator.cs b/source/AAS.TwinEngine.DataEngine/Infrastructure/Providers/PluginDataProvider/Helper/JsonSchemaValidator.cs deleted file mode 100644 index 4769d7e7..00000000 --- a/source/AAS.TwinEngine.DataEngine/Infrastructure/Providers/PluginDataProvider/Helper/JsonSchemaValidator.cs +++ /dev/null @@ -1,350 +0,0 @@ -using System.Text.Json; -using System.Text.Json.Nodes; - -using AAS.TwinEngine.DataEngine.ApplicationLogic.Exceptions.Application; -using AAS.TwinEngine.DataEngine.ApplicationLogic.Services.Plugin.Helper; -using AAS.TwinEngine.DataEngine.Infrastructure.Shared; -using AAS.TwinEngine.DataEngine.ServiceConfiguration.Config; - -using Json.Schema; - -using Microsoft.Extensions.Options; - -namespace AAS.TwinEngine.DataEngine.Infrastructure.Providers.PluginDataProvider.Helper; - -public class JsonSchemaValidator(IOptions pluginsConfig, ILogger logger) : IJsonSchemaValidator -{ - private readonly string _contextPrefix = pluginsConfig.Value.SubmodelElementIndexContextPrefix; - private static readonly IReadOnlyList KnownRefPrefixes = ["#/definitions/", "#/$defs/"]; - private static readonly JsonSchemaDraft Draft7 = new("Draft-07", MetaSchemas.Draft7Id.OriginalString, MetaSchemas.Draft7); - private static readonly JsonSchemaDraft Draft202012 = new("Draft 2020-12", MetaSchemas.Draft202012Id.OriginalString, MetaSchemas.Draft202012); - - public void ValidateRequestSchema(JsonSchema schema) - { - if (schema == null) - { - LogAndThrowException("Requested schema is null."); - } - - if (!TrySerializeSchema(schema!, out var schemaText, out var serializationError)) - { - LogAndThrowException($"Schema serialization failed: {serializationError}"); - } - - if (!TryParseSchemaNode(schemaText, out var schemaNode, out var parseError)) - { - LogAndThrowException($"Schema JSON is invalid: {parseError}"); - } - - if (schemaNode == null) - { - LogAndThrowException("Serialized schema resulted in null JsonNode."); - } - - try - { - var metaSchema = ResolveMetaSchema(schemaNode); - using var schemaDoc = JsonDocument.Parse(schemaNode!.ToJsonString()); - var result = metaSchema.Evaluate(schemaDoc.RootElement, new EvaluationOptions { OutputFormat = OutputFormat.List }); - if (!result.IsValid) - { - var details = TrySerializeEvaluationResult(result); - LogAndThrowException($"Schema is not valid against the selected JSON Schema draft. Details: {details}"); - } - } - catch (Exception ex) - { - LogAndThrowException($"Meta-schema evaluation failed: {ex.Message}", ex); - } - } - - public void ValidateResponseContent(string responseJson, JsonSchema requestSchema) - { - if (string.IsNullOrWhiteSpace(responseJson)) - { - LogAndThrowException("Response JSON is empty."); - } - - if (!TryParseJson(responseJson, out var responseDoc, out var parseError)) - { - LogAndThrowException($"Failed to parse response JSON: {parseError}"); - } - - if (!TryNormalizeSchema(requestSchema, out var preparedSchema, out var normalizeError)) - { - LogAndThrowException($"Failed to normalize request schema: {normalizeError}"); - } - - try - { - var result = preparedSchema.Evaluate(responseDoc!.RootElement, new EvaluationOptions { OutputFormat = OutputFormat.List }); - if (!result.IsValid) - { - var errorDetails = TrySerializeEvaluationResult(result); - LogAndThrowException($"Response did not validate against schema. Details: {errorDetails}"); - } - } - catch (Exception ex) - { - LogAndThrowException($"Exception occurred during response validation: {ex.Message}", ex); - } - } - - private static JsonSchemaDraft ResolveDraftFromSchemaNode(JsonNode? schemaNode) - { - var declaredSchema = schemaNode?["$schema"]?.GetValue(); - if (string.Equals(declaredSchema, Draft7.MetaSchemaId, StringComparison.OrdinalIgnoreCase)) - { - return Draft7; - } - - return Draft202012; - } - - private static string TrySerializeEvaluationResult(EvaluationResults result) - { - try - { - return JsonSerializer.Serialize(result); - } - catch - { - return "Unable to serialize evaluation details."; - } - } - - private static JsonSchema ResolveMetaSchema(JsonNode? schemaNode) - { - var declaredSchema = schemaNode?["$schema"]?.GetValue(); - if (string.Equals(declaredSchema, MetaSchemas.Draft7Id.OriginalString, StringComparison.OrdinalIgnoreCase)) - { - return MetaSchemas.Draft7; - } - - if (string.Equals(declaredSchema, MetaSchemas.Draft202012Id.OriginalString, StringComparison.OrdinalIgnoreCase)) - { - return MetaSchemas.Draft202012; - } - - return MetaSchemas.Draft202012; - } - - private void LogAndThrowException(string logMessage, Exception? ex = null) - { - if (ex != null) - { - logger.LogError(ex, "{LogMessage}", logMessage); - } - else - { - logger.LogError("{LogMessage}", logMessage); - } - - throw new InternalDataProcessingException(); - } - - private static bool TrySerializeSchema(JsonSchema schema, out string schemaText, out string? error) - { - error = null; - schemaText = string.Empty; - - try - { - schemaText = JsonSerializer.Serialize(schema, JsonSerializationOptions.SerializationWithEnum); - return true; - } - catch (Exception ex) - { - error = $"Serialization failed: {ex.Message}"; - return false; - } - } - - private static bool TryParseSchemaNode(string schemaText, out JsonNode? node, out string? error) - { - error = null; - node = null; - try - { - node = JsonNode.Parse(schemaText); - return true; - } - catch (Exception ex) - { - error = ex.Message; - return false; - } - } - - private static bool TryParseJson(string json, out JsonDocument? document, out string? error) - { - error = null; - document = null; - - try - { - document = JsonDocument.Parse(json); - return true; - } - catch (Exception ex) - { - error = $"JSON parsing failed: {ex.Message}"; - return false; - } - } - - private bool TryNormalizeSchema(JsonSchema schema, out JsonSchema normalizedSchema, out string? error) - { - error = null; - normalizedSchema = null!; - - try - { - var json = JsonSerializer.Serialize(schema, JsonSerializationOptions.SerializationWithEnum); - - var normalized = JsonNode.Parse(json)?.AsObject(); - - EscapeJsonReferencePointers(normalized); - if (normalized == null) - { - throw new InvalidDependencyException(nameof(normalized), logger); - } - - var draft = ResolveDraftFromSchemaNode(normalized); - RemoveSchemaIds(normalized); - normalized["$schema"] = draft.MetaSchemaId; - - normalizedSchema = JsonSchema.FromText(normalized.ToJsonString()); - - return true; - } - catch (Exception ex) - { - error = $"Schema normalization failed: {ex.Message}"; - return false; - } - } - - private static void RemoveSchemaIds(JsonNode? currentNode) - { - switch (currentNode) - { - case JsonObject jsonObjectNode: - _ = jsonObjectNode.Remove("$id"); - - foreach (var property in jsonObjectNode.ToList()) - { - RemoveSchemaIds(property.Value); - } - - break; - - case JsonArray jsonArrayNode: - foreach (var arrayElement in jsonArrayNode) - { - RemoveSchemaIds(arrayElement); - } - - break; - } - } - - private void EscapeJsonReferencePointers(JsonNode? currentNode) - { - switch (currentNode) - { - case JsonObject jsonObjectNode: - ProcessJsonObjectForEscaping(jsonObjectNode); - break; - - case JsonArray jsonArrayNode: - foreach (var arrayElement in jsonArrayNode) - { - EscapeJsonReferencePointers(arrayElement); - } - - break; - } - } - - private void ProcessJsonObjectForEscaping(JsonObject jsonObject) - { - var propertiesToRename = jsonObject - .Select(property => property.Key) - .Select(propertyName => (originalName: propertyName, strippedName: RemoveContextSuffix(propertyName))) - .Where(namePair => namePair.strippedName != namePair.originalName) - .ToList(); - - foreach (var (originalName, strippedName) in propertiesToRename) - { - RenameJsonProperty(jsonObject, originalName, strippedName); - } - - if (jsonObject.TryGetPropertyValue("required", out var requiredPropertiesNode) && - requiredPropertiesNode is JsonArray requiredPropertiesArray) - { - RemoveContextSuffixFromRequiredProperties(requiredPropertiesArray); - } - - foreach (var property in jsonObject.ToList()) - { - var propertyName = property.Key; - var propertyValue = property.Value; - - if (propertyName == "$ref" && - propertyValue is JsonValue referenceValue && - referenceValue.TryGetValue(out var referenceString) && - KnownRefPrefixes.Any(p => referenceString.StartsWith(p, StringComparison.OrdinalIgnoreCase))) - { - jsonObject["$ref"] = BuildEscapedReferencePath(referenceString); - } - else - { - EscapeJsonReferencePointers(propertyValue); - } - } - } - - private void RemoveContextSuffixFromRequiredProperties(JsonArray requiredProperties) - { - for (var index = 0; index < requiredProperties.Count; index++) - { - if (requiredProperties[index]?.GetValue() is { } propertyName) - { - requiredProperties[index] = RemoveContextSuffix(propertyName); - } - } - } - - private string BuildEscapedReferencePath(string originalReferencePath) - { - var prefix = KnownRefPrefixes.First(p => originalReferencePath.StartsWith(p, StringComparison.OrdinalIgnoreCase)); - var referenceWithoutPrefix = originalReferencePath[prefix.Length..]; - - var strippedReference = RemoveContextSuffix(referenceWithoutPrefix); - - var escapedReference = strippedReference.Replace("~", "~0", StringComparison.OrdinalIgnoreCase).Replace("/", "~1", StringComparison.OrdinalIgnoreCase); - - return prefix + escapedReference; - } - - private string RemoveContextSuffix(string propertyName) - { - var suffixIndex = propertyName.IndexOf(_contextPrefix, StringComparison.Ordinal); - return suffixIndex >= 0 ? propertyName[..suffixIndex] : propertyName; - } - - private static void RenameJsonProperty(JsonObject jsonObject, string oldPropertyName, string newPropertyName) - { - if (oldPropertyName == newPropertyName) - { - return; - } - - var propertyValue = jsonObject[oldPropertyName]; - _ = jsonObject.Remove(oldPropertyName); - jsonObject[newPropertyName] = propertyValue!; - } - - private sealed record JsonSchemaDraft(string Name, string MetaSchemaId, JsonSchema MetaSchema); -} diff --git a/source/AAS.TwinEngine.DataEngine/Infrastructure/Providers/PluginDataProvider/Helper/LegacyV1/LegacyDraft7JsonSchemaValidatorHandler.cs b/source/AAS.TwinEngine.DataEngine/Infrastructure/Providers/PluginDataProvider/Helper/LegacyV1/LegacyDraft7JsonSchemaValidatorHandler.cs new file mode 100644 index 00000000..b2287e83 --- /dev/null +++ b/source/AAS.TwinEngine.DataEngine/Infrastructure/Providers/PluginDataProvider/Helper/LegacyV1/LegacyDraft7JsonSchemaValidatorHandler.cs @@ -0,0 +1,28 @@ +using AAS.TwinEngine.DataEngine.ApplicationLogic.Services.Plugin.Helper.Validation; + +using Json.Schema; + +namespace AAS.TwinEngine.DataEngine.Infrastructure.Providers.PluginDataProvider.Helper.LegacyV1; + +#pragma warning disable S1133 +[Obsolete("Draft-07 schema validation is deprecated and will be removed in the next major release. All plugins should support Draft 2020-12.")] +public sealed class LegacyDraft7JsonSchemaValidatorHandler : IJsonSchemaDraftHandler +{ + private static readonly IReadOnlyList RefPrefixes = ["#/definitions/"]; + + public string DraftName => "Draft-07"; + public string MetaSchemaId => MetaSchemas.Draft7Id.OriginalString; + public JsonSchema MetaSchema => MetaSchemas.Draft7; + public IReadOnlyList SupportedRefPrefixes => RefPrefixes; + + public bool CanHandle(string? declaredSchema) + { + if (string.IsNullOrWhiteSpace(declaredSchema)) + { + return false; + } + + return string.Equals(declaredSchema, MetaSchemaId, StringComparison.OrdinalIgnoreCase) + || declaredSchema.Contains("draft-07", StringComparison.OrdinalIgnoreCase); + } +} diff --git a/source/AAS.TwinEngine.DataEngine/Infrastructure/Providers/PluginDataProvider/Helper/LegacyV1/LegacySchemaRetryHandler.cs b/source/AAS.TwinEngine.DataEngine/Infrastructure/Providers/PluginDataProvider/Helper/LegacyV1/LegacySchemaRetryHandler.cs index 07877c7e..7c0c1604 100644 --- a/source/AAS.TwinEngine.DataEngine/Infrastructure/Providers/PluginDataProvider/Helper/LegacyV1/LegacySchemaRetryHandler.cs +++ b/source/AAS.TwinEngine.DataEngine/Infrastructure/Providers/PluginDataProvider/Helper/LegacyV1/LegacySchemaRetryHandler.cs @@ -1,5 +1,5 @@ using AAS.TwinEngine.DataEngine.ApplicationLogic.Services.Plugin; -using AAS.TwinEngine.DataEngine.ApplicationLogic.Services.Plugin.Helper; +using AAS.TwinEngine.DataEngine.ApplicationLogic.Services.Plugin.Helper.Legacy; using AAS.TwinEngine.DataEngine.ApplicationLogic.Services.Plugin.Providers; using AAS.TwinEngine.DataEngine.DomainModel.SubmodelRepository; diff --git a/source/AAS.TwinEngine.DataEngine/Infrastructure/Providers/PluginDataProvider/Helper/PluginSchemaCompatibilityHandler.cs b/source/AAS.TwinEngine.DataEngine/Infrastructure/Providers/PluginDataProvider/Helper/PluginSchemaCompatibilityHandler.cs index 1850e2ed..dbbf7790 100644 --- a/source/AAS.TwinEngine.DataEngine/Infrastructure/Providers/PluginDataProvider/Helper/PluginSchemaCompatibilityHandler.cs +++ b/source/AAS.TwinEngine.DataEngine/Infrastructure/Providers/PluginDataProvider/Helper/PluginSchemaCompatibilityHandler.cs @@ -1,4 +1,6 @@ using AAS.TwinEngine.DataEngine.ApplicationLogic.Services.Plugin.Helper; +using AAS.TwinEngine.DataEngine.ApplicationLogic.Services.Plugin.Helper.Legacy; +using AAS.TwinEngine.DataEngine.ApplicationLogic.Services.Plugin.Helper.LegacyV1; using AAS.TwinEngine.DataEngine.DomainModel.SubmodelRepository; using Json.Schema; diff --git a/source/AAS.TwinEngine.DataEngine/Infrastructure/Providers/PluginDataProvider/Helper/Validation/JsonSchemaDraft202012Handler.cs b/source/AAS.TwinEngine.DataEngine/Infrastructure/Providers/PluginDataProvider/Helper/Validation/JsonSchemaDraft202012Handler.cs new file mode 100644 index 00000000..1b63d6d3 --- /dev/null +++ b/source/AAS.TwinEngine.DataEngine/Infrastructure/Providers/PluginDataProvider/Helper/Validation/JsonSchemaDraft202012Handler.cs @@ -0,0 +1,26 @@ +using AAS.TwinEngine.DataEngine.ApplicationLogic.Services.Plugin.Helper.Validation; + +using Json.Schema; + +namespace AAS.TwinEngine.DataEngine.Infrastructure.Providers.PluginDataProvider.Helper.Validation; + +public sealed class JsonSchemaDraft202012Handler : IJsonSchemaDraftHandler +{ + private static readonly IReadOnlyList RefPrefixes = ["#/$defs/"]; + + public string DraftName => "Draft 2020-12"; + public string MetaSchemaId => MetaSchemas.Draft202012Id.OriginalString; + public JsonSchema MetaSchema => MetaSchemas.Draft202012; + public IReadOnlyList SupportedRefPrefixes => RefPrefixes; + + public bool CanHandle(string? declaredSchema) + { + if (string.IsNullOrWhiteSpace(declaredSchema)) + { + return true; + } + + return string.Equals(declaredSchema, MetaSchemaId, StringComparison.OrdinalIgnoreCase) + || string.Equals(declaredSchema, MetaSchemas.Draft202012Id.ToString(), StringComparison.OrdinalIgnoreCase); + } +} diff --git a/source/AAS.TwinEngine.DataEngine/Infrastructure/Providers/PluginDataProvider/Helper/Validation/JsonSchemaDraftSelector.cs b/source/AAS.TwinEngine.DataEngine/Infrastructure/Providers/PluginDataProvider/Helper/Validation/JsonSchemaDraftSelector.cs new file mode 100644 index 00000000..a5fcc30e --- /dev/null +++ b/source/AAS.TwinEngine.DataEngine/Infrastructure/Providers/PluginDataProvider/Helper/Validation/JsonSchemaDraftSelector.cs @@ -0,0 +1,28 @@ +using AAS.TwinEngine.DataEngine.ApplicationLogic.Services.Plugin.Helper.Validation; + +namespace AAS.TwinEngine.DataEngine.Infrastructure.Providers.PluginDataProvider.Helper.Validation; + +public sealed class JsonSchemaDraftSelector +{ + private readonly IReadOnlyList _draftHandlers; + + public JsonSchemaDraftSelector(IEnumerable draftHandlers) + { + _draftHandlers = draftHandlers.ToList(); + + if (_draftHandlers.Count == 0) + { + throw new InvalidOperationException("At least one JSON Schema draft handler must be registered."); + } + } + + public IJsonSchemaDraftHandler Resolve(string? declaredSchema) + => _draftHandlers.FirstOrDefault(handler => handler.CanHandle(declaredSchema)) + ?? _draftHandlers[0]; + + public IReadOnlyList GetKnownRefPrefixes() + => _draftHandlers + .SelectMany(handler => handler.SupportedRefPrefixes) + .Distinct(StringComparer.OrdinalIgnoreCase) + .ToList(); +} diff --git a/source/AAS.TwinEngine.DataEngine/Infrastructure/Providers/PluginDataProvider/Helper/Validation/JsonSchemaNormalizer.cs b/source/AAS.TwinEngine.DataEngine/Infrastructure/Providers/PluginDataProvider/Helper/Validation/JsonSchemaNormalizer.cs new file mode 100644 index 00000000..ec55553e --- /dev/null +++ b/source/AAS.TwinEngine.DataEngine/Infrastructure/Providers/PluginDataProvider/Helper/Validation/JsonSchemaNormalizer.cs @@ -0,0 +1,176 @@ +using System.Text.Json; +using System.Text.Json.Nodes; + +using AAS.TwinEngine.DataEngine.ApplicationLogic.Exceptions.Application; +using AAS.TwinEngine.DataEngine.ApplicationLogic.Services.Plugin.Helper.Validation; +using AAS.TwinEngine.DataEngine.Infrastructure.Shared; +using AAS.TwinEngine.DataEngine.ServiceConfiguration.Config; + +using Json.Schema; + +using Microsoft.Extensions.Options; + +namespace AAS.TwinEngine.DataEngine.Infrastructure.Providers.PluginDataProvider.Helper.Validation; + +public sealed class JsonSchemaNormalizer(IOptions pluginsConfig, JsonSchemaDraftSelector draftSelector, ILogger logger) : IJsonSchemaNormalizer +{ + private readonly string _contextPrefix = pluginsConfig.Value.SubmodelElementIndexContextPrefix; + private readonly IReadOnlyList _knownRefPrefixes = draftSelector.GetKnownRefPrefixes(); + + public bool TryNormalizeSchema( + JsonSchema schema, + string targetMetaSchemaId, + out JsonSchema normalizedSchema, + out string? error) + { + error = null; + normalizedSchema = null!; + + try + { + var json = JsonSerializer.Serialize(schema, JsonSerializationOptions.SerializationWithEnum); + + var normalized = JsonNode.Parse(json)?.AsObject(); + + EscapeJsonReferencePointers(normalized); + if (normalized == null) + { + throw new InvalidDependencyException(nameof(normalized), logger); + } + + RemoveSchemaIds(normalized); + normalized["$schema"] = targetMetaSchemaId; + + normalizedSchema = JsonSchema.FromText(normalized.ToJsonString()); + + return true; + } + catch (Exception ex) + { + error = $"Schema normalization failed: {ex.Message}"; + return false; + } + } + + public void EscapeJsonReferencePointers(JsonNode? currentNode) + { + switch (currentNode) + { + case JsonObject jsonObjectNode: + ProcessJsonObjectForEscaping(jsonObjectNode); + break; + + case JsonArray jsonArrayNode: + foreach (var arrayElement in jsonArrayNode) + { + EscapeJsonReferencePointers(arrayElement); + } + + break; + } + } + + private static void RemoveSchemaIds(JsonNode? currentNode) + { + switch (currentNode) + { + case JsonObject jsonObjectNode: + _ = jsonObjectNode.Remove("$id"); + + foreach (var property in jsonObjectNode.ToList()) + { + RemoveSchemaIds(property.Value); + } + + break; + + case JsonArray jsonArrayNode: + foreach (var arrayElement in jsonArrayNode) + { + RemoveSchemaIds(arrayElement); + } + + break; + } + } + + private void ProcessJsonObjectForEscaping(JsonObject jsonObject) + { + var propertiesToRename = jsonObject + .Select(property => property.Key) + .Select(propertyName => (originalName: propertyName, strippedName: RemoveContextSuffix(propertyName))) + .Where(namePair => namePair.strippedName != namePair.originalName) + .ToList(); + + foreach (var (originalName, strippedName) in propertiesToRename) + { + RenameJsonProperty(jsonObject, originalName, strippedName); + } + + if (jsonObject.TryGetPropertyValue("required", out var requiredPropertiesNode) && + requiredPropertiesNode is JsonArray requiredPropertiesArray) + { + RemoveContextSuffixFromRequiredProperties(requiredPropertiesArray); + } + + foreach (var property in jsonObject.ToList()) + { + var propertyName = property.Key; + var propertyValue = property.Value; + + if (propertyName == "$ref" && + propertyValue is JsonValue referenceValue && + referenceValue.TryGetValue(out var referenceString) && + _knownRefPrefixes.Any(prefix => referenceString.StartsWith(prefix, StringComparison.OrdinalIgnoreCase))) + { + jsonObject["$ref"] = BuildEscapedReferencePath(referenceString); + } + else + { + EscapeJsonReferencePointers(propertyValue); + } + } + } + + private void RemoveContextSuffixFromRequiredProperties(JsonArray requiredProperties) + { + for (var index = 0; index < requiredProperties.Count; index++) + { + if (requiredProperties[index]?.GetValue() is { } propertyName) + { + requiredProperties[index] = RemoveContextSuffix(propertyName); + } + } + } + + private string BuildEscapedReferencePath(string originalReferencePath) + { + var prefix = _knownRefPrefixes.First(p => originalReferencePath.StartsWith(p, StringComparison.OrdinalIgnoreCase)); + var referenceWithoutPrefix = originalReferencePath[prefix.Length..]; + + var strippedReference = RemoveContextSuffix(referenceWithoutPrefix); + + var escapedReference = strippedReference.Replace("~", "~0", StringComparison.OrdinalIgnoreCase) + .Replace("/", "~1", StringComparison.OrdinalIgnoreCase); + + return prefix + escapedReference; + } + + private string RemoveContextSuffix(string propertyName) + { + var suffixIndex = propertyName.IndexOf(_contextPrefix, StringComparison.Ordinal); + return suffixIndex >= 0 ? propertyName[..suffixIndex] : propertyName; + } + + private static void RenameJsonProperty(JsonObject jsonObject, string oldPropertyName, string newPropertyName) + { + if (oldPropertyName == newPropertyName) + { + return; + } + + var propertyValue = jsonObject[oldPropertyName]; + _ = jsonObject.Remove(oldPropertyName); + jsonObject[newPropertyName] = propertyValue!; + } +} diff --git a/source/AAS.TwinEngine.DataEngine/Infrastructure/Providers/PluginDataProvider/Helper/Validation/JsonSchemaValidator.cs b/source/AAS.TwinEngine.DataEngine/Infrastructure/Providers/PluginDataProvider/Helper/Validation/JsonSchemaValidator.cs new file mode 100644 index 00000000..aed82d2f --- /dev/null +++ b/source/AAS.TwinEngine.DataEngine/Infrastructure/Providers/PluginDataProvider/Helper/Validation/JsonSchemaValidator.cs @@ -0,0 +1,182 @@ +using System.Text.Json; +using System.Text.Json.Nodes; + +using AAS.TwinEngine.DataEngine.ApplicationLogic.Exceptions.Application; +using AAS.TwinEngine.DataEngine.ApplicationLogic.Services.Plugin.Helper; +using AAS.TwinEngine.DataEngine.ApplicationLogic.Services.Plugin.Helper.Validation; +using AAS.TwinEngine.DataEngine.Infrastructure.Shared; + +using Json.Schema; + +namespace AAS.TwinEngine.DataEngine.Infrastructure.Providers.PluginDataProvider.Helper.Validation; + +public class JsonSchemaValidator(JsonSchemaDraftSelector draftSelector, IJsonSchemaNormalizer schemaNormalizer, ILogger logger) : IJsonSchemaValidator +{ + public void ValidateRequestSchema(JsonSchema schema) + { + if (schema == null) + { + LogAndThrowException("Requested schema is null."); + } + + if (!TrySerializeSchema(schema!, out var schemaText, out var serializationError)) + { + LogAndThrowException($"Schema serialization failed: {serializationError}"); + } + + if (!TryParseSchemaNode(schemaText, out var schemaNode, out var parseError)) + { + LogAndThrowException($"Schema JSON is invalid: {parseError}"); + } + + if (schemaNode == null) + { + LogAndThrowException("Serialized schema resulted in null JsonNode."); + } + + var nonNullSchemaNode = schemaNode!; + + try + { + var declaredSchema = (nonNullSchemaNode as JsonObject)?["$schema"]?.GetValue(); + var draftHandler = draftSelector.Resolve(declaredSchema); + using var schemaDoc = JsonDocument.Parse(nonNullSchemaNode.ToJsonString()); + var result = draftHandler.MetaSchema.Evaluate(schemaDoc.RootElement, new EvaluationOptions { OutputFormat = OutputFormat.List }); + if (!result.IsValid) + { + var details = TrySerializeEvaluationResult(result); + LogAndThrowException($"Schema is not valid against the selected JSON Schema draft. Details: {details}"); + } + } + catch (Exception ex) + { + LogAndThrowException($"Meta-schema evaluation failed: {ex.Message}", ex); + } + } + + public void ValidateResponseContent(string responseJson, JsonSchema requestSchema) + { + if (string.IsNullOrWhiteSpace(responseJson)) + { + LogAndThrowException("Response JSON is empty."); + } + + if (!TryParseJson(responseJson, out var responseDoc, out var parseError)) + { + LogAndThrowException($"Failed to parse response JSON: {parseError}"); + } + + var declaredSchema = TryGetDeclaredSchema(requestSchema); + var draftHandler = draftSelector.Resolve(declaredSchema); + + if (!schemaNormalizer.TryNormalizeSchema(requestSchema, draftHandler.MetaSchemaId, out var preparedSchema, out var normalizeError)) + { + LogAndThrowException($"Failed to normalize request schema: {normalizeError}"); + } + + try + { + var result = preparedSchema.Evaluate(responseDoc!.RootElement, new EvaluationOptions { OutputFormat = OutputFormat.List }); + if (!result.IsValid) + { + var errorDetails = TrySerializeEvaluationResult(result); + LogAndThrowException($"Response did not validate against schema. Details: {errorDetails}"); + } + } + catch (Exception ex) + { + LogAndThrowException($"Exception occurred during response validation: {ex.Message}", ex); + } + } + + private static string? TryGetDeclaredSchema(JsonSchema schema) + { + if (!TrySerializeSchema(schema, out var schemaText, out _)) + { + return null; + } + + if (!TryParseSchemaNode(schemaText, out var schemaNode, out _)) + { + return null; + } + + return schemaNode?["$schema"]?.GetValue(); + } + + private static string TrySerializeEvaluationResult(EvaluationResults result) + { + try + { + return JsonSerializer.Serialize(result); + } + catch + { + return "Unable to serialize evaluation details."; + } + } + + private void LogAndThrowException(string logMessage, Exception? ex = null) + { + if (ex != null) + { + logger.LogError(ex, "{LogMessage}", logMessage); + } + else + { + logger.LogError("{LogMessage}", logMessage); + } + + throw new InternalDataProcessingException(); + } + + private static bool TrySerializeSchema(JsonSchema schema, out string schemaText, out string? error) + { + error = null; + schemaText = string.Empty; + + try + { + schemaText = JsonSerializer.Serialize(schema, JsonSerializationOptions.SerializationWithEnum); + return true; + } + catch (Exception ex) + { + error = $"Serialization failed: {ex.Message}"; + return false; + } + } + + private static bool TryParseSchemaNode(string schemaText, out JsonNode? node, out string? error) + { + error = null; + node = null; + try + { + node = JsonNode.Parse(schemaText); + return true; + } + catch (Exception ex) + { + error = ex.Message; + return false; + } + } + + private static bool TryParseJson(string json, out JsonDocument? document, out string? error) + { + error = null; + document = null; + + try + { + document = JsonDocument.Parse(json); + return true; + } + catch (Exception ex) + { + error = $"JSON parsing failed: {ex.Message}"; + return false; + } + } +} diff --git a/source/AAS.TwinEngine.DataEngine/Infrastructure/Providers/PluginDataProvider/Services/PluginDataHandler.cs b/source/AAS.TwinEngine.DataEngine/Infrastructure/Providers/PluginDataProvider/Services/PluginDataHandler.cs index fe6147c3..6a0c113a 100644 --- a/source/AAS.TwinEngine.DataEngine/Infrastructure/Providers/PluginDataProvider/Services/PluginDataHandler.cs +++ b/source/AAS.TwinEngine.DataEngine/Infrastructure/Providers/PluginDataProvider/Services/PluginDataHandler.cs @@ -2,9 +2,11 @@ using AAS.TwinEngine.DataEngine.ApplicationLogic.Exceptions.Application; using AAS.TwinEngine.DataEngine.ApplicationLogic.Exceptions.Infrastructure; +using AAS.TwinEngine.DataEngine.ApplicationLogic.Exceptions.Infrastructure.LegacyV1; using AAS.TwinEngine.DataEngine.ApplicationLogic.Extensions; using AAS.TwinEngine.DataEngine.ApplicationLogic.Services.Plugin; using AAS.TwinEngine.DataEngine.ApplicationLogic.Services.Plugin.Helper; +using AAS.TwinEngine.DataEngine.ApplicationLogic.Services.Plugin.Helper.LegacyV1; using AAS.TwinEngine.DataEngine.ApplicationLogic.Services.Plugin.Providers; using AAS.TwinEngine.DataEngine.DomainModel.AasRegistry; using AAS.TwinEngine.DataEngine.DomainModel.AasRepository; diff --git a/source/AAS.TwinEngine.DataEngine/Infrastructure/Providers/PluginDataProvider/Services/PluginDataProvider.cs b/source/AAS.TwinEngine.DataEngine/Infrastructure/Providers/PluginDataProvider/Services/PluginDataProvider.cs index 5e34382d..fe5f8947 100644 --- a/source/AAS.TwinEngine.DataEngine/Infrastructure/Providers/PluginDataProvider/Services/PluginDataProvider.cs +++ b/source/AAS.TwinEngine.DataEngine/Infrastructure/Providers/PluginDataProvider/Services/PluginDataProvider.cs @@ -1,6 +1,7 @@ using System.Text.Json; using AAS.TwinEngine.DataEngine.ApplicationLogic.Exceptions.Infrastructure; +using AAS.TwinEngine.DataEngine.ApplicationLogic.Exceptions.Infrastructure.LegacyV1; using AAS.TwinEngine.DataEngine.ApplicationLogic.Extensions; using AAS.TwinEngine.DataEngine.ApplicationLogic.Services.Plugin.Providers; using System.Net; diff --git a/source/AAS.TwinEngine.DataEngine/ServiceConfiguration/InfrastructureDependencyInjectionExtensions.cs b/source/AAS.TwinEngine.DataEngine/ServiceConfiguration/InfrastructureDependencyInjectionExtensions.cs index ad9f1bf6..d3a8d899 100644 --- a/source/AAS.TwinEngine.DataEngine/ServiceConfiguration/InfrastructureDependencyInjectionExtensions.cs +++ b/source/AAS.TwinEngine.DataEngine/ServiceConfiguration/InfrastructureDependencyInjectionExtensions.cs @@ -2,6 +2,9 @@ using AAS.TwinEngine.DataEngine.ApplicationLogic.Services.AasRegistry.Providers; using AAS.TwinEngine.DataEngine.ApplicationLogic.Services.Plugin; using AAS.TwinEngine.DataEngine.ApplicationLogic.Services.Plugin.Helper; +using AAS.TwinEngine.DataEngine.ApplicationLogic.Services.Plugin.Helper.Legacy; +using AAS.TwinEngine.DataEngine.ApplicationLogic.Services.Plugin.Helper.LegacyV1; +using AAS.TwinEngine.DataEngine.ApplicationLogic.Services.Plugin.Helper.Validation; using AAS.TwinEngine.DataEngine.ApplicationLogic.Services.Plugin.Providers; using AAS.TwinEngine.DataEngine.ApplicationLogic.Services.SubmodelRegistry.Providers; using AAS.TwinEngine.DataEngine.Infrastructure.Configuration.LegacyV1; @@ -12,6 +15,7 @@ using AAS.TwinEngine.DataEngine.Infrastructure.Providers.AasRegistryProvider.Services; using AAS.TwinEngine.DataEngine.Infrastructure.Providers.PluginDataProvider.Helper; using AAS.TwinEngine.DataEngine.Infrastructure.Providers.PluginDataProvider.Helper.LegacyV1; +using AAS.TwinEngine.DataEngine.Infrastructure.Providers.PluginDataProvider.Helper.Validation; using AAS.TwinEngine.DataEngine.Infrastructure.Providers.PluginDataProvider.Services; using AAS.TwinEngine.DataEngine.Infrastructure.Providers.SubmodelRegistryProvider.Services; using AAS.TwinEngine.DataEngine.Infrastructure.Providers.TemplateProvider.Services; @@ -121,6 +125,12 @@ public static void ConfigureInfrastructure(this IServiceCollection services, ICo _ = services.AddScoped(); _ = services.AddScoped(); _ = services.AddScoped(); + _ = services.AddScoped(); + _ = services.AddScoped(); + _ = services.AddScoped(); + #pragma warning disable CS0618 // Obsolete — intentional legacy Draft-07 validator registration + _ = services.AddScoped(); + #pragma warning restore CS0618 _ = services.AddScoped(); _ = services.AddScoped(); #pragma warning disable CS0618 // Obsolete — intentional legacy Draft-07 fallback registration From f1cbdd46edac156be4a9a211c93cab780f67cd15 Mon Sep 17 00:00:00 2001 From: Hardi Shah Date: Wed, 6 May 2026 12:00:04 +0530 Subject: [PATCH 71/71] Refactor JsonSchemaValidator to eliminate unnecessary null checks and streamline schema handling --- .../Helper/Validation/JsonSchemaValidator.cs | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/source/AAS.TwinEngine.DataEngine/Infrastructure/Providers/PluginDataProvider/Helper/Validation/JsonSchemaValidator.cs b/source/AAS.TwinEngine.DataEngine/Infrastructure/Providers/PluginDataProvider/Helper/Validation/JsonSchemaValidator.cs index aed82d2f..6452dbd6 100644 --- a/source/AAS.TwinEngine.DataEngine/Infrastructure/Providers/PluginDataProvider/Helper/Validation/JsonSchemaValidator.cs +++ b/source/AAS.TwinEngine.DataEngine/Infrastructure/Providers/PluginDataProvider/Helper/Validation/JsonSchemaValidator.cs @@ -34,13 +34,11 @@ public void ValidateRequestSchema(JsonSchema schema) LogAndThrowException("Serialized schema resulted in null JsonNode."); } - var nonNullSchemaNode = schemaNode!; - try { - var declaredSchema = (nonNullSchemaNode as JsonObject)?["$schema"]?.GetValue(); + var declaredSchema = (schemaNode as JsonObject)?["$schema"]?.GetValue(); var draftHandler = draftSelector.Resolve(declaredSchema); - using var schemaDoc = JsonDocument.Parse(nonNullSchemaNode.ToJsonString()); + using var schemaDoc = JsonDocument.Parse(schemaNode!.ToJsonString()); var result = draftHandler.MetaSchema.Evaluate(schemaDoc.RootElement, new EvaluationOptions { OutputFormat = OutputFormat.List }); if (!result.IsValid) {