From d7201ce4424208dd2e6291ca919f5b4ba1bae77e Mon Sep 17 00:00:00 2001 From: Sebastian Stehle Date: Sat, 28 Aug 2021 16:06:02 +0200 Subject: [PATCH] Feature/component resolvement (#750) * Improve how components are resolved. * Refactorings. --- .../ValidateContent/JsonValueConverter.cs | 78 +++++++++++------- .../ValidateContent/ComponentFieldTests.cs | 71 +++++++++++++---- .../ValidateContent/ComponentsFieldTests.cs | 79 ++++++++++++++----- 3 files changed, 165 insertions(+), 63 deletions(-) diff --git a/backend/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/JsonValueConverter.cs b/backend/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/JsonValueConverter.cs index 34e05b913..8c4e54f2d 100644 --- a/backend/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/JsonValueConverter.cs +++ b/backend/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/JsonValueConverter.cs @@ -6,10 +6,12 @@ // ========================================================================== using System.Collections.Generic; +using System.Linq; using NodaTime.Text; using Squidex.Domain.Apps.Core.Contents; using Squidex.Domain.Apps.Core.Schemas; using Squidex.Infrastructure; +using Squidex.Infrastructure.Collections; using Squidex.Infrastructure.Json; using Squidex.Infrastructure.Json.Objects; using Squidex.Infrastructure.Translations; @@ -39,6 +41,11 @@ namespace Squidex.Domain.Apps.Core.ValidateContent return field.Accept(Instance, args); } + public (object? Result, JsonError? Error) Visit(IField field, Args args) + { + return (args.Value, null); + } + public (object? Result, JsonError? Error) Visit(IArrayField field, Args args) { return ConvertToObjectList(args.Value); @@ -51,12 +58,12 @@ namespace Squidex.Domain.Apps.Core.ValidateContent public (object? Result, JsonError? Error) Visit(IField field, Args args) { - return ConvertToComponent(args.Value, args.Components); + return ConvertToComponent(args.Value, args.Components, field.Properties.SchemaIds); } public (object? Result, JsonError? Error) Visit(IField field, Args args) { - return ConvertToComponentList(args.Value, args.Components); + return ConvertToComponentList(args.Value, args.Components, field.Properties.SchemaIds); } public (object? Result, JsonError? Error) Visit(IField field, Args args) @@ -138,11 +145,6 @@ namespace Squidex.Domain.Apps.Core.ValidateContent } } - public (object? Result, JsonError? Error) Visit(IField field, Args args) - { - return (args.Value, null); - } - private static (object? Result, JsonError? Error) ConvertToIdList(IJsonValue value) { if (value is JsonArray array) @@ -191,25 +193,21 @@ namespace Squidex.Domain.Apps.Core.ValidateContent return (null, new JsonError(T.Get("contents.invalidArrayOfStrings"))); } - private static (object? Result, JsonError? Error) ConvertToComponentList(IJsonValue value, - ResolvedComponents components) + private static (object? Result, JsonError? Error) ConvertToObjectList(IJsonValue value) { if (value is JsonArray array) { - var result = new List(array.Count); + var result = new List(array.Count); for (var i = 0; i < array.Count; i++) { - var (item, error) = ConvertToComponent(array[i], components); - - if (error != null) + if (array[i] is JsonObject obj) { - return (null, error); + result.Add(obj); } - - if (item != null) + else { - result.Add(item); + return (null, new JsonError(T.Get("contents.invalidArrayOfObjects"))); } } @@ -219,21 +217,25 @@ namespace Squidex.Domain.Apps.Core.ValidateContent return (null, new JsonError(T.Get("contents.invalidArrayOfObjects"))); } - private static (object? Result, JsonError? Error) ConvertToObjectList(IJsonValue value) + private static (object? Result, JsonError? Error) ConvertToComponentList(IJsonValue value, + ResolvedComponents components, ImmutableList? allowedIds) { if (value is JsonArray array) { - var result = new List(array.Count); + var result = new List(array.Count); for (var i = 0; i < array.Count; i++) { - if (array[i] is JsonObject obj) + var (item, error) = ConvertToComponent(array[i], components, allowedIds); + + if (error != null) { - result.Add(obj); + return (null, error); } - else + + if (item != null) { - return (null, new JsonError(T.Get("contents.invalidArrayOfObjects"))); + result.Add(item); } } @@ -244,21 +246,39 @@ namespace Squidex.Domain.Apps.Core.ValidateContent } private static (object? Result, JsonError? Error) ConvertToComponent(IJsonValue value, - ResolvedComponents components) + ResolvedComponents components, ImmutableList? allowedIds) { if (value is not JsonObject obj) { return (null, new JsonError(T.Get("contents.invalidComponentNoObject"))); } - if (!obj.TryGetValue(Component.Discriminator, out var type)) + var id = default(DomainId); + + if (obj.TryGetValue("schemaName", out var schemaName)) { - return (null, new JsonError(T.Get("contents.invalidComponentNoType"))); + id = components.FirstOrDefault(x => x.Value.Name == schemaName.Value).Key; + + obj.Remove("schemaName"); + obj[Component.Discriminator] = JsonValue.Create(id); + } + else if (obj.TryGetValue(Component.Discriminator, out var discriminator)) + { + id = DomainId.Create(discriminator.Value); + } + else if (allowedIds?.Count == 1) + { + id = allowedIds[0]; + + obj[Component.Discriminator] = JsonValue.Create(id); } - var id = DomainId.Create(type.Value); + if (id == default) + { + return (null, new JsonError(T.Get("contents.invalidComponentNoType"))); + } - if (!components.TryGetValue(id, out var schema)) + if (allowedIds?.Contains(id) == false || !components.TryGetValue(id, out var schema)) { return (null, new JsonError(T.Get("contents.invalidComponentUnknownSchema"))); } @@ -267,7 +287,7 @@ namespace Squidex.Domain.Apps.Core.ValidateContent data.Remove(Component.Discriminator); - return (new Component(type.Value, data, schema), null); + return (new Component(id.ToString(), data, schema), null); } } } diff --git a/backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/ValidateContent/ComponentFieldTests.cs b/backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/ValidateContent/ComponentFieldTests.cs index eae29365e..ad71eace5 100644 --- a/backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/ValidateContent/ComponentFieldTests.cs +++ b/backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/ValidateContent/ComponentFieldTests.cs @@ -12,6 +12,7 @@ using Squidex.Domain.Apps.Core.Contents; using Squidex.Domain.Apps.Core.Schemas; using Squidex.Domain.Apps.Core.TestHelpers; using Squidex.Infrastructure; +using Squidex.Infrastructure.Collections; using Squidex.Infrastructure.Json.Objects; using Xunit; @@ -19,6 +20,8 @@ namespace Squidex.Domain.Apps.Core.Operations.ValidateContent { public class ComponentFieldTests : IClassFixture { + private readonly DomainId schemaId1 = DomainId.NewGuid(); + private readonly DomainId schemaId2 = DomainId.NewGuid(); private readonly List errors = new List(); [Fact] @@ -32,7 +35,7 @@ namespace Squidex.Domain.Apps.Core.Operations.ValidateContent [Fact] public async Task Should_not_add_error_if_component_is_null_and_valid() { - var (_, sut, components) = Field(new ComponentFieldProperties()); + var (_, sut, components) = Field(new ComponentFieldProperties { SchemaId = schemaId1 }); await sut.ValidateAsync(null, errors, components: components); @@ -42,7 +45,7 @@ namespace Squidex.Domain.Apps.Core.Operations.ValidateContent [Fact] public async Task Should_not_add_error_if_component_is_valid() { - var (id, sut, components) = Field(new ComponentFieldProperties()); + var (id, sut, components) = Field(new ComponentFieldProperties { SchemaId = schemaId1 }); await sut.ValidateAsync(CreateValue(id.ToString(), "component-field", 1), errors, components: components); @@ -52,7 +55,7 @@ namespace Squidex.Domain.Apps.Core.Operations.ValidateContent [Fact] public async Task Should_add_error_if_component_is_required() { - var (_, sut, components) = Field(new ComponentFieldProperties { IsRequired = true }); + var (_, sut, components) = Field(new ComponentFieldProperties { SchemaId = schemaId1, IsRequired = true }); await sut.ValidateAsync(null, errors, components: components); @@ -63,7 +66,7 @@ namespace Squidex.Domain.Apps.Core.Operations.ValidateContent [Fact] public async Task Should_add_error_if_component_value_is_required() { - var (id, sut, components) = Field(new ComponentFieldProperties { IsRequired = true }, true); + var (id, sut, components) = Field(new ComponentFieldProperties { SchemaId = schemaId1, IsRequired = true }, true); await sut.ValidateAsync(CreateValue(id.ToString(), "component-field", null), errors, components: components); @@ -74,7 +77,7 @@ namespace Squidex.Domain.Apps.Core.Operations.ValidateContent [Fact] public async Task Should_add_error_if_value_is_not_valid() { - var (_, sut, components) = Field(new ComponentFieldProperties()); + var (_, sut, components) = Field(new ComponentFieldProperties { SchemaId = schemaId1 }); await sut.ValidateAsync(JsonValue.Create("Invalid"), errors, components: components); @@ -85,7 +88,7 @@ namespace Squidex.Domain.Apps.Core.Operations.ValidateContent [Fact] public async Task Should_add_error_if_value_has_no_discriminator() { - var (_, sut, components) = Field(new ComponentFieldProperties()); + var (_, sut, components) = Field(new ComponentFieldProperties { SchemaIds = ImmutableList.Create(schemaId1, schemaId2) }); await sut.ValidateAsync(CreateValue(null, "field", 1), errors, components: components); @@ -94,9 +97,9 @@ namespace Squidex.Domain.Apps.Core.Operations.ValidateContent } [Fact] - public async Task Should_add_error_if_value_has_invalid_discriminator() + public async Task Should_add_error_if_value_has_invalid_discriminator_format() { - var (_, sut, components) = Field(new ComponentFieldProperties()); + var (_, sut, components) = Field(new ComponentFieldProperties { SchemaId = schemaId1 }); await sut.ValidateAsync(CreateValue("invalid", "field", 1), errors, components: components); @@ -104,13 +107,52 @@ namespace Squidex.Domain.Apps.Core.Operations.ValidateContent new[] { "Invalid component. Cannot find schema." }); } - private static IJsonValue CreateValue(string? type, string key, object? value) + [Fact] + public async Task Should_add_error_if_value_has_invalid_discriminator_schema() + { + var (_, sut, components) = Field(new ComponentFieldProperties { SchemaId = schemaId2 }); + + await sut.ValidateAsync(CreateValue(schemaId1.ToString(), "field", 1), errors, components: components); + + errors.Should().BeEquivalentTo( + new[] { "Invalid component. Cannot find schema." }); + } + + [Fact] + public async Task Should_resolve_schema_id_from_name() + { + var (_, sut, components) = Field(new ComponentFieldProperties { SchemaId = schemaId1 }); + + var value = CreateValue("my-component", "component-field", 1, "schemaName"); + + await sut.ValidateAsync(value, errors, components: components); + + Assert.Empty(errors); + Assert.Equal(value[Component.Discriminator].ToString(), schemaId1.ToString()); + } + + [Fact] + public async Task Should_resolve_schema_from_single_component() + { + var (_, sut, components) = Field(new ComponentFieldProperties { SchemaId = schemaId1 }); + + var value = CreateValue(null, "component-field", 1); + + await sut.ValidateAsync(value, errors, components: components); + + Assert.Empty(errors); + Assert.Equal(value[Component.Discriminator].ToString(), schemaId1.ToString()); + } + + private static JsonObject CreateValue(string? type, string key, object? value, string? discriminator = null) { var obj = JsonValue.Object(); if (type != null) { - obj[Component.Discriminator] = JsonValue.Create(type); + discriminator ??= Component.Discriminator; + + obj[discriminator] = JsonValue.Create(type); } obj.Add(key, value); @@ -118,23 +160,22 @@ namespace Squidex.Domain.Apps.Core.Operations.ValidateContent return obj; } - private static (DomainId, RootField, ResolvedComponents) Field(ComponentFieldProperties properties, bool isRequired = false) + private (DomainId, RootField, ResolvedComponents) Field(ComponentFieldProperties properties, bool isRequired = false) { var schema = new Schema("my-component") .AddNumber(1, "component-field", Partitioning.Invariant, new NumberFieldProperties { IsRequired = isRequired }); - var id = DomainId.NewGuid(); - var field = Fields.Component(1, "my-component", Partitioning.Invariant, properties); var components = new ResolvedComponents(new Dictionary { - [id] = schema + [schemaId1] = schema, + [schemaId2] = schema, }); - return (id, field, components); + return (schemaId1, field, components); } } } diff --git a/backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/ValidateContent/ComponentsFieldTests.cs b/backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/ValidateContent/ComponentsFieldTests.cs index 43d1abd23..93c8d56ff 100644 --- a/backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/ValidateContent/ComponentsFieldTests.cs +++ b/backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/ValidateContent/ComponentsFieldTests.cs @@ -12,6 +12,7 @@ using Squidex.Domain.Apps.Core.Contents; using Squidex.Domain.Apps.Core.Schemas; using Squidex.Domain.Apps.Core.TestHelpers; using Squidex.Infrastructure; +using Squidex.Infrastructure.Collections; using Squidex.Infrastructure.Json.Objects; using Xunit; @@ -19,6 +20,8 @@ namespace Squidex.Domain.Apps.Core.Operations.ValidateContent { public class ComponentsFieldTests : IClassFixture { + private readonly DomainId schemaId1 = DomainId.NewGuid(); + private readonly DomainId schemaId2 = DomainId.NewGuid(); private readonly List errors = new List(); [Fact] @@ -32,7 +35,7 @@ namespace Squidex.Domain.Apps.Core.Operations.ValidateContent [Fact] public async Task Should_not_add_error_if_components_are_null_and_valid() { - var (_, sut, components) = Field(new ComponentsFieldProperties()); + var (_, sut, components) = Field(new ComponentsFieldProperties { SchemaId = schemaId1 }); await sut.ValidateAsync(null, errors, components: components); @@ -42,7 +45,7 @@ namespace Squidex.Domain.Apps.Core.Operations.ValidateContent [Fact] public async Task Should_not_add_error_if_components_is_valid() { - var (id, sut, components) = Field(new ComponentsFieldProperties()); + var (id, sut, components) = Field(new ComponentsFieldProperties { SchemaId = schemaId1 }); await sut.ValidateAsync(CreateValue(1, id.ToString(), "component-field", 1), errors, components: components); @@ -52,7 +55,7 @@ namespace Squidex.Domain.Apps.Core.Operations.ValidateContent [Fact] public async Task Should_not_add_error_if_number_of_components_is_equal_to_min_and_max_components() { - var (id, sut, components) = Field(new ComponentsFieldProperties { MinItems = 2, MaxItems = 2 }); + var (id, sut, components) = Field(new ComponentsFieldProperties { SchemaId = schemaId1, MinItems = 2, MaxItems = 2 }); await sut.ValidateAsync(CreateValue(2, id.ToString(), "component-field", 1), errors, components: components); @@ -62,7 +65,7 @@ namespace Squidex.Domain.Apps.Core.Operations.ValidateContent [Fact] public async Task Should_add_error_if_components_are_required() { - var (_, sut, components) = Field(new ComponentsFieldProperties { IsRequired = true }); + var (_, sut, components) = Field(new ComponentsFieldProperties { SchemaId = schemaId1, IsRequired = true }); await sut.ValidateAsync(null, errors, components: components); @@ -73,7 +76,7 @@ namespace Squidex.Domain.Apps.Core.Operations.ValidateContent [Fact] public async Task Should_add_error_if_components_value_is_required() { - var (id, sut, components) = Field(new ComponentsFieldProperties { IsRequired = true }, true); + var (id, sut, components) = Field(new ComponentsFieldProperties { SchemaId = schemaId1, IsRequired = true }, true); await sut.ValidateAsync(CreateValue(1, id.ToString(), "component-field", null), errors, components: components); @@ -84,7 +87,7 @@ namespace Squidex.Domain.Apps.Core.Operations.ValidateContent [Fact] public async Task Should_add_error_if_value_is_not_valid() { - var (_, sut, components) = Field(new ComponentsFieldProperties()); + var (_, sut, components) = Field(new ComponentsFieldProperties { SchemaId = schemaId1 }); await sut.ValidateAsync(JsonValue.Create("Invalid"), errors, components: components); @@ -95,7 +98,7 @@ namespace Squidex.Domain.Apps.Core.Operations.ValidateContent [Fact] public async Task Should_add_error_if_component_is_not_valid() { - var (_, sut, components) = Field(new ComponentsFieldProperties()); + var (_, sut, components) = Field(new ComponentsFieldProperties { SchemaId = schemaId1 }); await sut.ValidateAsync(JsonValue.Array(JsonValue.Create("Invalid")), errors, components: components); @@ -106,7 +109,7 @@ namespace Squidex.Domain.Apps.Core.Operations.ValidateContent [Fact] public async Task Should_add_error_if_component_has_no_discriminator() { - var (_, sut, components) = Field(new ComponentsFieldProperties()); + var (_, sut, components) = Field(new ComponentsFieldProperties { SchemaIds = ImmutableList.Create(schemaId1, schemaId2) }); await sut.ValidateAsync(CreateValue(1, null, "field", 1), errors, components: components); @@ -115,9 +118,9 @@ namespace Squidex.Domain.Apps.Core.Operations.ValidateContent } [Fact] - public async Task Should_add_error_if_value_has_invalid_discriminator() + public async Task Should_add_error_if_value_has_invalid_discriminator_format() { - var (_, sut, components) = Field(new ComponentsFieldProperties()); + var (_, sut, components) = Field(new ComponentsFieldProperties { SchemaId = schemaId1 }); await sut.ValidateAsync(CreateValue(1, "invalid", "field", 1), errors, components: components); @@ -125,10 +128,21 @@ namespace Squidex.Domain.Apps.Core.Operations.ValidateContent new[] { "Invalid component. Cannot find schema." }); } + [Fact] + public async Task Should_add_error_if_value_has_invalid_discriminator_schema() + { + var (_, sut, components) = Field(new ComponentsFieldProperties { SchemaId = schemaId2 }); + + await sut.ValidateAsync(CreateValue(1, schemaId1.ToString(), "field", 1), errors, components: components); + + errors.Should().BeEquivalentTo( + new[] { "Invalid component. Cannot find schema." }); + } + [Fact] public async Task Should_add_error_if_value_has_not_enough_components() { - var (id, sut, components) = Field(new ComponentsFieldProperties { MinItems = 3 }); + var (id, sut, components) = Field(new ComponentsFieldProperties { SchemaId = schemaId1, MinItems = 3 }); await sut.ValidateAsync(CreateValue(2, id.ToString(), "component-field", 1), errors, components: components); @@ -139,7 +153,7 @@ namespace Squidex.Domain.Apps.Core.Operations.ValidateContent [Fact] public async Task Should_add_error_if_value_has_too_much_components() { - var (id, sut, components) = Field(new ComponentsFieldProperties { MaxItems = 1 }); + var (id, sut, components) = Field(new ComponentsFieldProperties { SchemaId = schemaId1, MaxItems = 1 }); await sut.ValidateAsync(CreateValue(2, id.ToString(), "component-field", 1), errors, components: components); @@ -147,7 +161,33 @@ namespace Squidex.Domain.Apps.Core.Operations.ValidateContent new[] { "Must not have more than 1 item(s)." }); } - private static IJsonValue CreateValue(int count, string? type, string key, object? value) + [Fact] + public async Task Should_resolve_schema_id_from_name() + { + var (_, sut, components) = Field(new ComponentsFieldProperties { SchemaId = schemaId1 }); + + var value = CreateValue(1, "my-component", "component-field", 1, "schemaName"); + + await sut.ValidateAsync(value, errors, components: components); + + Assert.Empty(errors); + Assert.Equal(((JsonObject)value[0])[Component.Discriminator].ToString(), schemaId1.ToString()); + } + + [Fact] + public async Task Should_resolve_schema_from_single_component() + { + var (_, sut, components) = Field(new ComponentsFieldProperties { SchemaId = schemaId1 }); + + var value = CreateValue(1, null, "component-field", 1); + + await sut.ValidateAsync(value, errors, components: components); + + Assert.Empty(errors); + Assert.Equal(((JsonObject)value[0])[Component.Discriminator].ToString(), schemaId1.ToString()); + } + + private static JsonArray CreateValue(int count, string? type, string key, object? value, string? discriminator = null) { var result = JsonValue.Array(); @@ -157,7 +197,9 @@ namespace Squidex.Domain.Apps.Core.Operations.ValidateContent if (type != null) { - obj[Component.Discriminator] = JsonValue.Create(type); + discriminator ??= Component.Discriminator; + + obj[discriminator] = JsonValue.Create(type); } obj.Add(key, value); @@ -168,23 +210,22 @@ namespace Squidex.Domain.Apps.Core.Operations.ValidateContent return result; } - private static (DomainId, RootField, ResolvedComponents) Field(ComponentsFieldProperties properties, bool isRequired = false) + private (DomainId, RootField, ResolvedComponents) Field(ComponentsFieldProperties properties, bool isRequired = false) { var schema = new Schema("my-component") .AddNumber(1, "component-field", Partitioning.Invariant, new NumberFieldProperties { IsRequired = isRequired }); - var id = DomainId.NewGuid(); - var field = Fields.Components(1, "my-components", Partitioning.Invariant, properties); var components = new ResolvedComponents(new Dictionary { - [id] = schema + [schemaId1] = schema, + [schemaId2] = schema }); - return (id, field, components); + return (schemaId1, field, components); } } }