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