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..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,12 +10,12 @@ - - - + + + - - + + 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..ed4a3b57 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,12 @@ - - + + - - - + + + 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..1a13586e 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.Providers*") .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.Service.*") .Check(_architecture); } @@ -115,7 +111,7 @@ public void ServiceInterfacesShallBeInCorrectNamespace() { Interfaces().That().HaveNameEndingWith("Service") .Should() - .ResideInNamespace($"{BaseNamespace}.ApplicationLogic.Service.*", true) + .ResideInNamespaceMatching($"{BaseNamespace}.ApplicationLogic.Service.*") .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..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,239 +1,314 @@ -using AAS.TwinEngine.DataEngine.ApplicationLogic.Exceptions.Application; +using System.Text.Json; + +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: "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 schema = _sut.Generate(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 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 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($"#/$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 Generate_WhenBranchWithZeroToManyCardinality_WrapsChildrenInItems() + { + 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 Generate_WhenLeafNodeIsOptional_DoesNotMarkPropertyAsRequired() { const string SemanticId = "http://example.com/optional"; var leaf = new SemanticLeafNode(SemanticId, "", DataType.String, Cardinality.ZeroToOne); - var schema = JsonSchemaGenerator.ConvertToJsonSchema(leaf); + var schema = _sut.Generate(leaf); + var el = ToElement(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); + Assert.True(el.GetProperty("properties").TryGetProperty(SemanticId, out _)); + Assert.False(el.TryGetProperty("required", out _)); } [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: "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 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); + public void Generate_WhenBranchHasMixedCardinality_SetsRequiredOnlyForMandatoryChildren() + { + 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_BranchNodeWithZeroToManyCardinality_ReturnsArraySchema() + [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) { - 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)); + const string SemanticId = "http://example.com/leaf"; + var leaf = new SemanticLeafNode(SemanticId, null!, dataType, Cardinality.One); - var result = JsonSchemaGenerator.ConvertToJsonSchema(branchNode); + var schema = _sut.Generate(leaf); - var typeKeyword = result.Keywords!.OfType().SingleOrDefault(); - Assert.Equal(SchemaValueType.Object, typeKeyword!.Type); + var leafEl = ToElement(schema).GetProperty("properties").GetProperty(SemanticId); + Assert.Equal(expectedJsonType, leafEl.GetProperty("type").GetString()); + } - var propKeyword = result.Keywords!.OfType().SingleOrDefault(); - Assert.True(propKeyword!.Properties.ContainsKey(SemanticId)); + [Fact] + public void Generate_WhenUnsupportedNode_ThrowsInternalDataProcessingException() + { + var unsupportedNode = new UnsupportedSemanticNode("unsupported", Cardinality.One); - var arraySchema = propKeyword.Properties[SemanticId]; - var arrayType = arraySchema.Keywords!.OfType().SingleOrDefault(); - Assert.Equal(SchemaValueType.Array, arrayType!.Type); + Assert.Throws(() => _sut.Generate(unsupportedNode)); + } - 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); + private static JsonElement ToElement(JsonSchema schema) + { + var json = JsonSerializer.Serialize(schema); + using var doc = JsonDocument.Parse(json); + return doc.RootElement.Clone(); + } - var arrayRequiredProps = arraySchema.Keywords!.OfType().SingleOrDefault(); - var contains = arrayRequiredProps!.Properties.Contains(NameId); + private sealed class UnsupportedSemanticNode(string semanticId, Cardinality cardinality) : SemanticTreeNode(semanticId, cardinality); +} - Assert.True(contains); - } +#pragma warning disable CS0618 +public class LegacyDraft7JsonSchemaGeneratorTests +{ + private readonly IJsonSchemaGenerator _sut = new LegacyDraft7JsonSchemaGenerator(); [Fact] - public void ConvertToJsonSchema_NestedBranchNodes_UsesRefAndDefinitionsCorrectly() + public void Generate_WhenLeafNode_ReturnsDraft7Schema() { - 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)); - root.AddChild(childBranch); + 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(root); + var schema = _sut.Generate(leaf); - var typeKeyword = schema.Keywords!.OfType().SingleOrDefault(); - Assert.NotNull(typeKeyword); - Assert.Equal(SchemaValueType.Object, typeKeyword.Type); + 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()); + } - var propKeyword = schema.Keywords!.OfType().SingleOrDefault(); - Assert.NotNull(propKeyword); - Assert.True(propKeyword.Properties.ContainsKey(RootSemanticId)); + [Fact] + public void Generate_WhenNestedBranches_UsesDefinitionsAndDraft7Refs() + { + 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()); + } - var rootPropSchema = propKeyword.Properties[RootSemanticId]; - var rootPropType = rootPropSchema.Keywords!.OfType().SingleOrDefault(); - Assert.NotNull(rootPropType); - Assert.Equal(SchemaValueType.Object, rootPropType.Type); + [Fact] + public void Generate_WhenBranchWithZeroToManyCardinality_ReturnsArrayWithDirectProperties() + { + 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 _)); + } - var rootProps = rootPropSchema.Keywords!.OfType().SingleOrDefault(); - Assert.NotNull(rootProps); - Assert.True(rootProps.Properties.ContainsKey(BranchSemanticId)); + [Fact] + public void Generate_WhenLeafNodeIsOptional_DoesNotMarkPropertyAsRequired() + { + const string SemanticId = "http://example.com/optional"; + var leaf = new SemanticLeafNode(SemanticId, "", DataType.String, Cardinality.ZeroToOne); - var branchRefSchema = rootProps.Properties[BranchSemanticId]; - var refKeyword = branchRefSchema.Keywords!.OfType().SingleOrDefault(); - Assert.NotNull(refKeyword); - Assert.Equal($"#/definitions/{BranchSemanticId}", refKeyword.Reference.ToString()); + var schema = _sut.Generate(leaf); + var el = ToElement(schema); - var definitionsKeyword = schema.Keywords!.OfType().SingleOrDefault(); - Assert.NotNull(definitionsKeyword); - Assert.True(definitionsKeyword.Definitions.ContainsKey(BranchSemanticId)); + Assert.True(el.GetProperty("properties").TryGetProperty(SemanticId, out _)); + Assert.False(el.TryGetProperty("required", out _)); + } - var branchDefSchema = definitionsKeyword.Definitions[BranchSemanticId]; - var branchType = branchDefSchema.Keywords!.OfType().SingleOrDefault(); - Assert.NotNull(branchType); - Assert.Equal(SchemaValueType.Object, branchType.Type); + [Fact] + public void Generate_WhenBranchHasMixedCardinality_SetsRequiredOnlyForMandatoryChildren() + { + 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); + } - var branchProps = branchDefSchema.Keywords!.OfType().SingleOrDefault(); - Assert.NotNull(branchProps); - Assert.True(branchProps.Properties.ContainsKey(NameId)); + [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) + { + const string SemanticId = "http://example.com/leaf"; + var leaf = new SemanticLeafNode(SemanticId, null!, dataType, Cardinality.One); - var nameSchema = branchProps.Properties[NameId]; - var nameType = nameSchema.Keywords!.OfType().SingleOrDefault(); - Assert.NotNull(nameType); - Assert.Equal(SchemaValueType.String, nameType.Type); + var schema = _sut.Generate(leaf); - var requiredKeyword = branchDefSchema.Keywords!.OfType().SingleOrDefault(); - Assert.NotNull(requiredKeyword); - Assert.Contains(NameId, requiredKeyword.Properties); + var leafEl = ToElement(schema).GetProperty("properties").GetProperty(SemanticId); + Assert.Equal(expectedJsonType, leafEl.GetProperty("type").GetString()); } [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)); - 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 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")); + public void Generate_WhenUnsupportedNode_ThrowsInternalDataProcessingException() + { + var unsupportedNode = new UnsupportedSemanticNode("unsupported", Cardinality.One); + + Assert.Throws(() => _sut.Generate(unsupportedNode)); } [Fact] - public void ConvertToJsonSchema_UnsupportedNode_ThrowsException() + public void Generate_WhenBranchContainsUnsupportedChild_ThrowsInternalDataProcessingException() { - var unsupportedNode = new UnsupportedSemanticNode("unsupported", Cardinality.One); + var root = new SemanticBranchNode("http://example.com/root", Cardinality.One); + root.AddChild(new UnsupportedSemanticNode("http://example.com/unsupported", Cardinality.One)); - var ex = Assert.Throws(() => - JsonSchemaGenerator.ConvertToJsonSchema(unsupportedNode)); + 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 SchemaValueType GetTypeForProperty(PropertiesKeyword props, string key) + private static JsonElement ToElement(JsonSchema schema) { - var schema = props.Properties[key]; - var typeKeyword = schema.Keywords!.OfType().SingleOrDefault(); - Assert.NotNull(typeKeyword); - return typeKeyword.Type; + var json = JsonSerializer.Serialize(schema); + using var doc = JsonDocument.Parse(json); + return doc.RootElement.Clone(); } + + 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 deleted file mode 100644 index 82a285a1..00000000 --- a/source/AAS.TwinEngine.DataEngine.UnitTests/Infrastructure/Providers/PluginDataProvider/Helper/JsonSchemaValidatorTests.cs +++ /dev/null @@ -1,161 +0,0 @@ -using AAS.TwinEngine.DataEngine.ApplicationLogic.Exceptions.Application; -using AAS.TwinEngine.DataEngine.Infrastructure.Providers.PluginDataProvider.Helper; -using AAS.TwinEngine.DataEngine.ServiceConfiguration.Config; - -using Json.Schema; - -using Microsoft.Extensions.Logging; -using Microsoft.Extensions.Options; - -using NSubstitute; - -namespace AAS.TwinEngine.DataEngine.UnitTests.Infrastructure.Providers.PluginDataProvider.Helper; - -public class JsonSchemaValidatorTests -{ - private readonly JsonSchemaValidator _sut; - - public static IEnumerable InvalidPrimitives => [ - [SchemaValueType.String, "name", 123], - [SchemaValueType.Integer, "count", 12.34], - [SchemaValueType.Number, "price", "19.99a"], - [SchemaValueType.Boolean, "flag", "flase"], - [SchemaValueType.Number, "age", "8o5"], - [SchemaValueType.Number, "age", "-10n5"], - [SchemaValueType.Integer, "name", "10o"], - [SchemaValueType.Boolean, "flag", "\"true\""] - ]; - - public JsonSchemaValidatorTests() - { - var pluginsConfig = Substitute.For>(); - pluginsConfig.Value.Returns(new PluginsConfig - { - 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)); - } - - [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(); - - _sut.ValidateRequestSchema(schema); - } - - [Fact] - public void ValidateResponseContent_EmptyResponse_ThrowsBadRequest() - { - var schema = new JsonSchemaBuilder().Type(SchemaValueType.Object).Build(); - - Assert.Throws(() => _sut.ValidateResponseContent("", schema)); - } - - [Fact] - 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(); - - const string Json = "{\"ContactInformation\": {}}"; - - _sut.ValidateResponseContent(Json, schema); - } - - [Fact] - 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(); - - const string Json = "{\"name\": \"Test\"}"; - - _sut.ValidateResponseContent(Json, schema); - } - - [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).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).Build() - }) - .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)); - } -} 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/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/Validation/JsonSchemaValidatorTests.cs b/source/AAS.TwinEngine.DataEngine.UnitTests/Infrastructure/Providers/PluginDataProvider/Helper/Validation/JsonSchemaValidatorTests.cs new file mode 100644 index 00000000..142b0820 --- /dev/null +++ b/source/AAS.TwinEngine.DataEngine.UnitTests/Infrastructure/Providers/PluginDataProvider/Helper/Validation/JsonSchemaValidatorTests.cs @@ -0,0 +1,447 @@ +using AAS.TwinEngine.DataEngine.ApplicationLogic.Exceptions.Application; +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; + +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; + +using NSubstitute; + +namespace AAS.TwinEngine.DataEngine.UnitTests.Infrastructure.Providers.PluginDataProvider.Helper.Validation; + +public class JsonSchemaValidatorTests +{ + private readonly JsonSchemaValidator _sut; + + public static IEnumerable InvalidPrimitives => [ + [SchemaValueType.String, "name", 123], + [SchemaValueType.Integer, "count", 12.34], + [SchemaValueType.Number, "price", "19.99a"], + [SchemaValueType.Boolean, "flag", "flase"], + [SchemaValueType.Number, "age", "8o5"], + [SchemaValueType.Number, "age", "-10n5"], + [SchemaValueType.Integer, "name", "10o"], + [SchemaValueType.Boolean, "flag", "\"true\""] + ]; + + public JsonSchemaValidatorTests() + { + var pluginsConfig = Substitute.For>(); + pluginsConfig.Value.Returns(new PluginsConfig + { + SubmodelElementIndexContextPrefix = "_aastwinengine_" + }); + var logger = Substitute.For>(); + 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] + public void ValidateRequestSchema_NullSchema_ThrowsBadRequest() => Assert.Throws(() => _sut.ValidateRequestSchema(null!)); + + [Fact] + public void ValidateRequestSchema_ValidSchema_DoesNotThrow() + { + var schema = new JsonSchemaBuilder() + .Schema(MetaSchemas.Draft7Id) + .Type(SchemaValueType.Object) + .Properties(new Dictionary + { + ["name"] = new JsonSchemaBuilder().Type(SchemaValueType.String) + }) + .Build(); + + _sut.ValidateRequestSchema(schema); + } + + [Fact] + public void ValidateRequestSchema_ValidDraft202012Schema_DoesNotThrow() + { + var schema = new JsonSchemaBuilder() + .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); + } + + [Fact] + public void ValidateResponseContent_EmptyResponse_ThrowsBadRequest() + { + var schema = new JsonSchemaBuilder().Type(SchemaValueType.Object).Build(); + + Assert.Throws(() => _sut.ValidateResponseContent("", schema)); + } + + [Fact] + 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(); + + const string Json = "{\"ContactInformation\": {}}"; + + _sut.ValidateResponseContent(Json, schema); + } + + [Fact] + public void ValidateResponseContent_ValidJsonAndSchema_DoesNotThrow() + { + var schema = new JsonSchemaBuilder() + .Type(SchemaValueType.Object) + .Properties(new Dictionary + { + ["name"] = new JsonSchemaBuilder().Type(SchemaValueType.String) + }) + .Required("name") + .Build(); + + const string Json = "{\"name\": \"Test\"}"; + + _sut.ValidateResponseContent(Json, schema); + } + + [Fact] + public void ValidateResponseContent_WhenSchemaDeclaresDraft202012_ValidatesSuccessfully() + { + var schema = BuildDraft202012Schema(); + const string Json = "{\"asset\": {\"details\": {\"name\": \"ok\"}}}"; + + _sut.ValidateResponseContent(Json, schema); + } + + [Fact] + public void ValidateResponseContent_WhenSchemaHasNoSchemaKeyword_DefaultsToDraft202012() + { + var schema = new JsonSchemaBuilder() + .Type(SchemaValueType.Object) + .Properties(new Dictionary + { + ["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("asset") + .Build(); + const string Json = "{\"asset\": {\"details\": {\"name\": \"ok\"}}}"; + + _sut.ValidateResponseContent(Json, schema); + } + + [Fact] + public void ValidateResponseContent_WhenSchemaHasNoSchemaKeyword_AndUsesDraft202012Keyword_DefaultsToDraft202012() + { + 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\"}}"; + + _sut.ValidateResponseContent(Json, schema); + } + + [Fact] + public void ValidateResponseContent_WhenSchemaDeclaresDraft7_ValidatesDeepHierarchy() + { + var schema = BuildDraft7Schema(); + const string Json = "{\"asset\": {\"details\": {\"name\": \"Motor\", \"tags\": [\"a\",\"b\"]}}}"; + + _sut.ValidateResponseContent(Json, schema); + } + + [Fact] + public void ValidateResponseContent_WhenRequiredNestedPropertyMissing_ThrowsBadRequest() + { + var schema = BuildDraft202012Schema(); + const string Json = "{\"asset\": {\"details\": {}}}"; + + Assert.Throws(() => + _sut.ValidateResponseContent(Json, schema)); + } + + [Fact] + public void ValidateResponseContent_WhenAdditionalPropertiesNotAllowed_ThrowsBadRequest() + { + var schema = BuildDraft7Schema(); + const string Json = "{\"asset\": {\"details\": {\"name\": \"Motor\", \"unexpected\": 1}}}"; + + Assert.Throws(() => + _sut.ValidateResponseContent(Json, schema)); + } + + [Fact] + public void ValidateResponseContent_WhenUnevaluatedPropertiesNotAllowed_ThrowsBadRequest() + { + var schema = BuildDraft202012Schema(); + const string Json = "{\"asset\": {\"details\": {\"name\": \"Motor\"}}, \"unexpected\": 1}"; + + Assert.Throws(() => + _sut.ValidateResponseContent(Json, schema)); + } + + [Fact] + public void ValidateResponseContent_WhenNullAtDeepLevel_ThrowsBadRequest() + { + var schema = BuildDraft202012Schema(); + const string Json = "{\"asset\": {\"details\": {\"name\": null}}}"; + + Assert.Throws(() => + _sut.ValidateResponseContent(Json, schema)); + } + + [Fact] + public void ValidateResponseContent_WhenPartialPayloadMissingRequired_ThrowsBadRequest() + { + var schema = BuildDraft7Schema(); + const string Json = "{\"asset\": {}}"; + + Assert.Throws(() => + _sut.ValidateResponseContent(Json, schema)); + } + + [Fact] + public void ValidateResponseContent_WhenDraft7SchemaContainsDraft202012Construct_ThrowsBadRequest() + { + 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\"}}}"; + + Assert.Throws(() => + _sut.ValidateResponseContent(Json, schema)); + } + + [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} }}"; + + 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_WhenSchemaExpectsObjectAndResponseIsArray_ThrowsBadRequest() + { + var schema = new JsonSchemaBuilder() + .Type(SchemaValueType.Object) + .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_LegacyArraySchema_WithoutItems_DoesNotThrow() + { + 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"] + } + } + } + """); + const string Json = @"{ ""contactInformation"": [{ ""name"": ""test"" }] }"; + + _sut.ValidateResponseContent(Json, malformedSchema); + } + + [Fact] + public void ValidateResponseContent_CorrectSchema_ArrayWithItems_Succeeds() + { + var correctSchema = JsonSchema.FromText( + """ + { + "$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"" }] }"; + + _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/Services/PluginDataHandlerTests.cs b/source/AAS.TwinEngine.DataEngine.UnitTests/Infrastructure/Providers/PluginDataProvider/Services/PluginDataHandlerTests.cs index 103dcd00..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,15 +16,15 @@ 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 AAS.TwinEngine.DataEngine.ServiceConfiguration.Config; -using Microsoft.Extensions.Options; +using NSubstitute.ExceptionExtensions; namespace AAS.TwinEngine.DataEngine.UnitTests.Infrastructure.Providers.PluginDataProvider.Services; @@ -31,6 +33,7 @@ public class PluginDataHandlerTests private readonly IPluginRequestBuilder _pluginRequestBuilder; private readonly IPluginDataProvider _pluginDataProvider; private readonly IJsonSchemaValidator _jsonSchemaValidator; + private readonly IPluginSchemaCompatibilityHandler _pluginSchemaCompatibilityHandler; private readonly IMultiPluginDataHandler _multiPluginDataHandler; private readonly ILogger _logger; private readonly IOptions _options; @@ -41,6 +44,7 @@ public PluginDataHandlerTests() _pluginRequestBuilder = Substitute.For(); _pluginDataProvider = Substitute.For(); _jsonSchemaValidator = Substitute.For(); + _pluginSchemaCompatibilityHandler = Substitute.For(); _multiPluginDataHandler = Substitute.For(); _logger = Substitute.For>(); _options = Options.Create(new GeneralConfig @@ -48,7 +52,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, _pluginSchemaCompatibilityHandler, _multiPluginDataHandler, _logger, _options); } private readonly JsonSerializerOptions _jsonoptions = new() @@ -66,7 +70,6 @@ public PluginDataHandlerTests() [Fact] public async Task TryGetValuesAsync_WithValidManifestAndResponse_ReturnsMergedSemanticTreeNode() { - // Arrange var inputSemanticTreeNode = new SemanticLeafNode("Contact", "", DataType.String, Cardinality.One); const string ExpectedJsonResponse = """ @@ -98,7 +101,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 } } }; @@ -107,6 +110,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(_ => { }); @@ -133,17 +139,71 @@ 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); 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] + 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 = ["Contact"], + Capabilities = new Capabilities { HasShellDescriptor = true } + } + }; + + _multiPluginDataHandler + .SplitByPluginManifests(Arg.Any(), Arg.Any>()) + .Returns(new Dictionary { { PrefixedSchemaKey, inputSemanticTreeNode } }); + + var generatedSchema = CreateGeneratedSchema(); + _pluginSchemaCompatibilityHandler.GenerateSchema(inputSemanticTreeNode).Returns(generatedSchema); + + _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>([httpResponse.Content])); + + _multiPluginDataHandler + .Merge(Arg.Any(), Arg.Any>()) + .Returns(ci => ci.ArgAt>(1).First()); + + _ = await _sut.TryGetValuesAsync(manifests, inputSemanticTreeNode, "submodelId", CancellationToken.None); + + _pluginSchemaCompatibilityHandler.Received(1).GenerateSchema(inputSemanticTreeNode); + _jsonSchemaValidator.Received(1).ValidateRequestSchema(generatedSchema); + _jsonSchemaValidator.Received(1).ValidateResponseContent(ExpectedJsonResponse, generatedSchema); } [Fact] @@ -174,19 +234,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] @@ -204,7 +264,7 @@ public async Task GetDataForAllShellDescriptorsAsync_Throws_WhenDeserializationF }; _multiPluginDataHandler.GetAvailablePlugins(manifests, Arg.Any>()) - .Returns(new List { "PluginA" }); + .Returns(["PluginA"]); var httpResponse = new HttpResponseMessage(HttpStatusCode.OK) { @@ -213,7 +273,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)); @@ -242,19 +302,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] @@ -272,8 +331,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") @@ -281,7 +339,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)); @@ -307,15 +365,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); @@ -337,7 +394,7 @@ public async Task GetDataForAssetInformationByIdAsync_ShouldThrow_WhenJsonInvali }; _multiPluginDataHandler.GetAvailablePlugins(manifests, Arg.Any>()) - .Returns(new List { "PluginA" }); + .Returns(["PluginA"]); var httpResponse = new HttpResponseMessage(HttpStatusCode.OK) { @@ -346,7 +403,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)); @@ -367,7 +424,7 @@ public async Task GetDataForAssetInformationByIdAsync_ShouldThrow_WhenDeserializ }; _multiPluginDataHandler.GetAvailablePlugins(manifests, Arg.Any>()) - .Returns(new List { "PluginA" }); + .Returns(["PluginA"]); var httpResponse = new HttpResponseMessage(HttpStatusCode.OK) { @@ -376,7 +433,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)); @@ -392,6 +449,230 @@ 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("TestPlugin", ConvertToJsonContent("{}"))]); + _pluginDataProvider + .GetDataForSemanticIdsAsync(Arg.Any>(), Arg.Any(), Arg.Any()) + .ThrowsAsync(new PluginSchemaRejectionException()); + + using var retryContent = new StringContent(ExpectedJsonResponse, Encoding.UTF8, "application/json"); + _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 = ["Contact"], + Capabilities = new Capabilities { HasShellDescriptor = false } + } + }; + + var result = await _sut.TryGetValuesAsync(manifests, inputNode, "submodelId", CancellationToken.None); + + 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"); + _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 = ["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 } }); + _pluginSchemaCompatibilityHandler.GenerateSchema(inputNode).Returns(CreateGeneratedSchema()); + _pluginRequestBuilder + .Build(Arg.Any>()) + .Returns([new("TestPlugin", ConvertToJsonContent("{}"))]); + _pluginDataProvider + .GetDataForSemanticIdsAsync(Arg.Any>(), Arg.Any(), Arg.Any()) + .ThrowsAsync(new PluginSchemaRejectionException()); + _pluginSchemaCompatibilityHandler + .RetryWithLegacySchemaAsync(Arg.Any>(), Arg.Any(), Arg.Any()) + .ThrowsAsync(new ResponseParsingException()); + var manifests = new List + { + new() + { + PluginName = "TestPlugin", + PluginUrl = new Uri("http://localhost"), + SupportedSemanticIds = new List { "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 } }); + _pluginSchemaCompatibilityHandler.GenerateSchema(inputNode).Returns(CreateGeneratedSchema()); + _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 = ["Contact"], + Capabilities = new Capabilities { HasShellDescriptor = false } + } + }; + + _ = await _sut.TryGetValuesAsync(manifests, inputNode, "submodelId", CancellationToken.None); + + await _pluginSchemaCompatibilityHandler.DidNotReceive() + .RetryWithLegacySchemaAsync(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..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 @@ -1,4 +1,4 @@ -using System.Net; +using System.Net; using System.Net.Http.Json; using System.Text; using System.Text.Json; @@ -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; @@ -84,7 +85,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 +102,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)); } @@ -344,8 +345,11 @@ await Assert.ThrowsAsync(() => _sut.GetDataForSemanticIdsAsync(pluginRequests, "asdf", CancellationToken.None)); } - [Fact] - public async Task GetDataForSemanticIdsAsync_WhenUnexpectedStatusCode_ThrowsResponseParsingException() + [Theory] + [InlineData(HttpStatusCode.NotFound)] + [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 +358,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.UnitTests/Infrastructure/Providers/TemplateProvider/Services/ShellTemplateMappingProviderTests.cs b/source/AAS.TwinEngine.DataEngine.UnitTests/Infrastructure/Providers/TemplateProvider/Services/ShellTemplateMappingProviderTests.cs index cb153591..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 @@ -288,6 +288,7 @@ public void GetProductIdFromRule_FirstRuleMatches_StopsImmediately() Assert.Equal("2000-2201/353-000", result); } + [Fact] public void GetProductIdFromRule_ThreeSagmentMatches_SuccessResult() { 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/ApplicationLogic/Exceptions/Infrastructure/LegacyV1/PluginSchemaRejectionException.cs b/source/AAS.TwinEngine.DataEngine/ApplicationLogic/Exceptions/Infrastructure/LegacyV1/PluginSchemaRejectionException.cs new file mode 100644 index 00000000..67acaa7e --- /dev/null +++ b/source/AAS.TwinEngine.DataEngine/ApplicationLogic/Exceptions/Infrastructure/LegacyV1/PluginSchemaRejectionException.cs @@ -0,0 +1,7 @@ +namespace AAS.TwinEngine.DataEngine.ApplicationLogic.Exceptions.Infrastructure.LegacyV1; + +/// +/// 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.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/LegacyV1/ILegacySchemaRetryHandler.cs b/source/AAS.TwinEngine.DataEngine/ApplicationLogic/Services/Plugin/Helper/LegacyV1/ILegacySchemaRetryHandler.cs new file mode 100644 index 00000000..a8464f15 --- /dev/null +++ b/source/AAS.TwinEngine.DataEngine/ApplicationLogic/Services/Plugin/Helper/LegacyV1/ILegacySchemaRetryHandler.cs @@ -0,0 +1,19 @@ +using AAS.TwinEngine.DataEngine.DomainModel.SubmodelRepository; + +namespace AAS.TwinEngine.DataEngine.ApplicationLogic.Services.Plugin.Helper.Legacy; + +/// +/// 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/ApplicationLogic/Services/Plugin/Helper/LegacyV1/IPluginSchemaCompatibilityHandler.cs b/source/AAS.TwinEngine.DataEngine/ApplicationLogic/Services/Plugin/Helper/LegacyV1/IPluginSchemaCompatibilityHandler.cs new file mode 100644 index 00000000..b384a730 --- /dev/null +++ b/source/AAS.TwinEngine.DataEngine/ApplicationLogic/Services/Plugin/Helper/LegacyV1/IPluginSchemaCompatibilityHandler.cs @@ -0,0 +1,15 @@ +using AAS.TwinEngine.DataEngine.DomainModel.SubmodelRepository; + +using Json.Schema; + +namespace AAS.TwinEngine.DataEngine.ApplicationLogic.Services.Plugin.Helper.LegacyV1; + +public interface IPluginSchemaCompatibilityHandler +{ + JsonSchema GenerateSchema(SemanticTreeNode semanticNode); + + Task> RetryWithLegacySchemaAsync( + IDictionary semanticNodes, + string submodelId, + CancellationToken cancellationToken); +} 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/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 623f0dcb..00000000 --- a/source/AAS.TwinEngine.DataEngine/Infrastructure/Providers/PluginDataProvider/Helper/JsonSchemaGenerator.cs +++ /dev/null @@ -1,114 +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 = "#/definitions/"; - - public static JsonSchema ConvertToJsonSchema(SemanticTreeNode rootNode) - { - var definitions = new Dictionary(); - var rootSchema = BuildNode(rootNode, definitions, isRoot: true); - - return new JsonSchemaBuilder() - .Schema(MetaSchemas.Draft7Id) - .Type(SchemaValueType.Object) - .Properties(new Dictionary - { - [rootNode?.SemanticId!] = rootSchema - }) - .Definitions(definitions) - .Build(); - } - - private static JsonSchema 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 JsonSchema 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 isArray = IsArrayCardinality(branch.Cardinality); - - var schemaBuilder = new JsonSchemaBuilder() - .Type(isArray ? SchemaValueType.Array : SchemaValueType.Object) - .Properties(children) - .Required(requiredProperties); - - if (isRoot) - { - return schemaBuilder.Build(); - } - - definitions[branch.SemanticId] = schemaBuilder.Build(); - return CreateRefSchema(branch.SemanticId); - } - - 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 JsonSchema 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).Build(); - } - - 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 JsonSchema CreateRefSchema(string semanticId) => new JsonSchemaBuilder().Ref($"{DefinitionsRefPrefix}{semanticId}").Build(); -} 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 deleted file mode 100644 index 5ab61739..00000000 --- a/source/AAS.TwinEngine.DataEngine/Infrastructure/Providers/PluginDataProvider/Helper/JsonSchemaValidator.cs +++ /dev/null @@ -1,298 +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 const string DefinitionsPrefix = "#/definitions/"; - - 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 result = MetaSchemas.Draft7.Evaluate(schemaNode, new EvaluationOptions { OutputFormat = OutputFormat.List }); - if (!result.IsValid) - { - LogAndThrowException("Schema is not valid against Draft-7."); - } - } - catch (Exception ex) - { - LogAndThrowException("Draft-7 evaluation failed.", 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 normalizedSchema, out var normalizeError)) - { - 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 }); - if (!result.IsValid) - { - LogAndThrowException("Response did not validate against schema."); - } - } - catch (Exception ex) - { - LogAndThrowException("Exception occurred during response validation.", ex); - } - } - - 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 JsonObject normalized, out string? error) - { - error = null; - normalized = []; - - try - { - var json = JsonSerializer.Serialize(schema, JsonSerializationOptions.SerializationWithEnum); - - normalized = JsonNode.Parse(json)?.AsObject(); - - EscapeJsonReferencePointers(normalized); - if (normalized == null) - { - throw new InvalidDependencyException(nameof(normalized), logger); - } - - normalized["$id"] = normalized["$id"]?.GetValue() ?? $"urn:uuid:{Guid.NewGuid():D}"; - - return true; - } - catch (Exception ex) - { - error = $"Schema normalization failed: {ex.Message}"; - return false; - } - } - - 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); - 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) && - referenceString.StartsWith(DefinitionsPrefix, 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 referenceWithoutPrefix = originalReferencePath[DefinitionsPrefix.Length..]; - - var strippedReference = RemoveContextSuffix(referenceWithoutPrefix); - - var escapedReference = strippedReference.Replace("~", "~0", StringComparison.OrdinalIgnoreCase).Replace("/", "~1", StringComparison.OrdinalIgnoreCase); - - return DefinitionsPrefix + 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/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/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 new file mode 100644 index 00000000..7c0c1604 --- /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.Legacy; +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/Helper/PluginSchemaCompatibilityHandler.cs b/source/AAS.TwinEngine.DataEngine/Infrastructure/Providers/PluginDataProvider/Helper/PluginSchemaCompatibilityHandler.cs new file mode 100644 index 00000000..dbbf7790 --- /dev/null +++ b/source/AAS.TwinEngine.DataEngine/Infrastructure/Providers/PluginDataProvider/Helper/PluginSchemaCompatibilityHandler.cs @@ -0,0 +1,22 @@ +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; + +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/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..6452dbd6 --- /dev/null +++ b/source/AAS.TwinEngine.DataEngine/Infrastructure/Providers/PluginDataProvider/Helper/Validation/JsonSchemaValidator.cs @@ -0,0 +1,180 @@ +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."); + } + + try + { + var declaredSchema = (schemaNode as JsonObject)?["$schema"]?.GetValue(); + var draftHandler = draftSelector.Resolve(declaredSchema); + using var schemaDoc = JsonDocument.Parse(schemaNode!.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 587b5574..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; @@ -24,6 +26,7 @@ public class PluginDataHandler( IPluginRequestBuilder pluginRequestBuilder, IPluginDataProvider pluginDataProvider, IJsonSchemaValidator jsonSchemaValidator, + IPluginSchemaCompatibilityHandler pluginSchemaCompatibilityHandler, IMultiPluginDataHandler multiPluginDataHandler, ILogger logger, IOptions generalConfig) : IPluginDataHandler @@ -40,14 +43,22 @@ public async Task TryGetValuesAsync(IReadOnlyList response; + try + { + response = await pluginDataProvider.GetDataForSemanticIdsAsync(pluginRequests, submodelId, cancellationToken).ConfigureAwait(false); + } + catch (PluginSchemaRejectionException) + { + response = await pluginSchemaCompatibilityHandler.RetryWithLegacySchemaAsync(dicSemanticTreeNode, submodelId, cancellationToken).ConfigureAwait(false); + } var result = new List(); 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 afe48b15..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,8 +1,10 @@ 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; using AAS.TwinEngine.DataEngine.DomainModel.Plugin; using AAS.TwinEngine.DataEngine.Infrastructure.Http.Clients; using AAS.TwinEngine.DataEngine.ServiceConfiguration.Config; @@ -115,15 +117,23 @@ private async Task ProcessResponseAsync(HttpResponseMessage respons switch (response.StatusCode) { - case System.Net.HttpStatusCode.NotFound: - logger.LogError("Requested resource could not be found. Endpoint: {Url}", url); - throw new ResourceNotFoundException(); + case HttpStatusCode.NotFound: + logger.LogWarning("Plugin could not resolve the request schema (HTTP 404). Endpoint: {Url}", url); + throw new PluginSchemaRejectionException(); - 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..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; @@ -11,6 +14,8 @@ 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.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; @@ -120,7 +125,18 @@ 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 + _ = services.AddScoped(); +#pragma warning restore CS0618 + _ = services.AddScoped(); _ = 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 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.UnitTests/AAS.TwinEngine.Plugin.TestPlugin.UnitTests.csproj b/source/AAS.TwinEngine.Plugin.TestPlugin.UnitTests/AAS.TwinEngine.Plugin.TestPlugin.UnitTests.csproj index 617925c6..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,13 +14,13 @@ - - - + + + - - - + + + diff --git a/source/AAS.TwinEngine.Plugin.TestPlugin.UnitTests/CleanArchitectureTests.cs b/source/AAS.TwinEngine.Plugin.TestPlugin.UnitTests/CleanArchitectureTests.cs index c2f60b8b..563ff721 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.Providers*") .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.Service.*") .Check(_architecture); } @@ -115,7 +111,7 @@ public void ServiceInterfacesShallBeInCorrectNamespace() { Interfaces().That().HaveNameEndingWith("Service") .Should() - .ResideInNamespace($"{BaseNamespace}.ApplicationLogic.Service.*", true) + .ResideInNamespaceMatching($"{BaseNamespace}.ApplicationLogic.Service.*") .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..82fc72ab 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/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/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..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"