From 5e912e5fcf80c8ae730ac2b4871f77ea444ed5ee Mon Sep 17 00:00:00 2001 From: Sebastian Date: Sun, 28 Jul 2019 00:43:28 +0200 Subject: [PATCH] Json reader. --- .../Json/Newtonsoft/JsonClassConverter.cs | 2 +- .../Json/Newtonsoft/LanguageConverter.cs | 7 +- .../Json/Newtonsoft/NamedGuidIdConverter.cs | 7 +- .../Json/Newtonsoft/NamedLongIdConverter.cs | 7 +- .../Json/Newtonsoft/NamedStringIdConverter.cs | 7 +- .../Json/Newtonsoft/RefTokenConverter.cs | 7 +- .../Queries/Json/FilterConverter.cs | 162 ++++++++++++++++ .../Queries/Json/PropertyPathConverter.cs | 29 +++ .../Queries/PropertyPath.cs | 4 +- .../Queries/QueryJsonTests.cs | 181 ++++++++++++++++++ ...nTests.cs => QueryODataConversionTests.cs} | 4 +- .../TestHelpers/JsonHelper.cs | 3 + 12 files changed, 391 insertions(+), 29 deletions(-) create mode 100644 src/Squidex.Infrastructure/Queries/Json/FilterConverter.cs create mode 100644 src/Squidex.Infrastructure/Queries/Json/PropertyPathConverter.cs create mode 100644 tests/Squidex.Infrastructure.Tests/Queries/QueryJsonTests.cs rename tests/Squidex.Infrastructure.Tests/Queries/{ODataConversionTests.cs => QueryODataConversionTests.cs} (99%) diff --git a/src/Squidex.Infrastructure/Json/Newtonsoft/JsonClassConverter.cs b/src/Squidex.Infrastructure/Json/Newtonsoft/JsonClassConverter.cs index 03acfc2f5..5bce0855f 100644 --- a/src/Squidex.Infrastructure/Json/Newtonsoft/JsonClassConverter.cs +++ b/src/Squidex.Infrastructure/Json/Newtonsoft/JsonClassConverter.cs @@ -13,7 +13,7 @@ namespace Squidex.Infrastructure.Json.Newtonsoft { public abstract class JsonClassConverter : JsonConverter, ISupportedTypes where T : class { - public IEnumerable SupportedTypes + public virtual IEnumerable SupportedTypes { get { yield return typeof(T); } } diff --git a/src/Squidex.Infrastructure/Json/Newtonsoft/LanguageConverter.cs b/src/Squidex.Infrastructure/Json/Newtonsoft/LanguageConverter.cs index 11d45a037..dacf54841 100644 --- a/src/Squidex.Infrastructure/Json/Newtonsoft/LanguageConverter.cs +++ b/src/Squidex.Infrastructure/Json/Newtonsoft/LanguageConverter.cs @@ -19,12 +19,9 @@ namespace Squidex.Infrastructure.Json.Newtonsoft protected override Language ReadValue(JsonReader reader, Type objectType, JsonSerializer serializer) { - if (reader.TokenType != JsonToken.String) - { - throw new JsonException($"Expected String, but got {reader.TokenType}."); - } + var value = serializer.Deserialize(reader); - return Language.GetLanguage(reader.Value.ToString()); + return Language.GetLanguage(value); } } } diff --git a/src/Squidex.Infrastructure/Json/Newtonsoft/NamedGuidIdConverter.cs b/src/Squidex.Infrastructure/Json/Newtonsoft/NamedGuidIdConverter.cs index f1b141060..f5f753d79 100644 --- a/src/Squidex.Infrastructure/Json/Newtonsoft/NamedGuidIdConverter.cs +++ b/src/Squidex.Infrastructure/Json/Newtonsoft/NamedGuidIdConverter.cs @@ -19,12 +19,9 @@ namespace Squidex.Infrastructure.Json.Newtonsoft protected override NamedId ReadValue(JsonReader reader, Type objectType, JsonSerializer serializer) { - if (reader.TokenType != JsonToken.String) - { - throw new JsonException($"Expected String, but got {reader.TokenType}."); - } + var value = serializer.Deserialize(reader); - if (!NamedId.TryParse(reader.Value.ToString(), Guid.TryParse, out var result)) + if (!NamedId.TryParse(value, Guid.TryParse, out var result)) { throw new JsonException("Named id must have more than 2 parts divided by commata."); } diff --git a/src/Squidex.Infrastructure/Json/Newtonsoft/NamedLongIdConverter.cs b/src/Squidex.Infrastructure/Json/Newtonsoft/NamedLongIdConverter.cs index d984e572b..494bce251 100644 --- a/src/Squidex.Infrastructure/Json/Newtonsoft/NamedLongIdConverter.cs +++ b/src/Squidex.Infrastructure/Json/Newtonsoft/NamedLongIdConverter.cs @@ -19,12 +19,9 @@ namespace Squidex.Infrastructure.Json.Newtonsoft protected override NamedId ReadValue(JsonReader reader, Type objectType, JsonSerializer serializer) { - if (reader.TokenType != JsonToken.String) - { - throw new JsonException($"Expected String, but got {reader.TokenType}."); - } + var value = serializer.Deserialize(reader); - if (!NamedId.TryParse(reader.Value.ToString(), long.TryParse, out var result)) + if (!NamedId.TryParse(value, long.TryParse, out var result)) { throw new JsonException("Named id must have at least 2 parts divided by commata."); } diff --git a/src/Squidex.Infrastructure/Json/Newtonsoft/NamedStringIdConverter.cs b/src/Squidex.Infrastructure/Json/Newtonsoft/NamedStringIdConverter.cs index 573cbaa57..8eba29cfd 100644 --- a/src/Squidex.Infrastructure/Json/Newtonsoft/NamedStringIdConverter.cs +++ b/src/Squidex.Infrastructure/Json/Newtonsoft/NamedStringIdConverter.cs @@ -19,12 +19,9 @@ namespace Squidex.Infrastructure.Json.Newtonsoft protected override NamedId ReadValue(JsonReader reader, Type objectType, JsonSerializer serializer) { - if (reader.TokenType != JsonToken.String) - { - throw new JsonException($"Expected String, but got {reader.TokenType}."); - } + var value = serializer.Deserialize(reader); - if (!NamedId.TryParse(reader.Value.ToString(), ParseString, out var result)) + if (!NamedId.TryParse(value, ParseString, out var result)) { throw new JsonException("Named id must have at least 2 parts divided by commata."); } diff --git a/src/Squidex.Infrastructure/Json/Newtonsoft/RefTokenConverter.cs b/src/Squidex.Infrastructure/Json/Newtonsoft/RefTokenConverter.cs index 988f3ff95..3017fce97 100644 --- a/src/Squidex.Infrastructure/Json/Newtonsoft/RefTokenConverter.cs +++ b/src/Squidex.Infrastructure/Json/Newtonsoft/RefTokenConverter.cs @@ -19,12 +19,9 @@ namespace Squidex.Infrastructure.Json.Newtonsoft protected override RefToken ReadValue(JsonReader reader, Type objectType, JsonSerializer serializer) { - if (reader.TokenType != JsonToken.String) - { - throw new JsonException($"Expected String, but got {reader.TokenType}."); - } + var value = serializer.Deserialize(reader); - if (!RefToken.TryParse(reader.Value.ToString(), out var result)) + if (!RefToken.TryParse(value, out var result)) { throw new JsonException("Named id must have at least 2 parts divided by colon."); } diff --git a/src/Squidex.Infrastructure/Queries/Json/FilterConverter.cs b/src/Squidex.Infrastructure/Queries/Json/FilterConverter.cs new file mode 100644 index 000000000..76a1ce0af --- /dev/null +++ b/src/Squidex.Infrastructure/Queries/Json/FilterConverter.cs @@ -0,0 +1,162 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System; +using System.Collections.Generic; +using System.Linq; +using Newtonsoft.Json; +using Squidex.Infrastructure.Json.Newtonsoft; +using Squidex.Infrastructure.Json.Objects; + +namespace Squidex.Infrastructure.Queries.Json +{ + public sealed class FilterConverter : JsonClassConverter> + { + public override IEnumerable SupportedTypes + { + get + { + yield return typeof(CompareFilter); + yield return typeof(FilterNode); + yield return typeof(LogicalFilter); + yield return typeof(NegateFilter); + } + } + + public override bool CanConvert(Type objectType) + { + return SupportedTypes.Contains(objectType); + } + + protected override FilterNode ReadValue(JsonReader reader, Type objectType, JsonSerializer serializer) + { + if (reader.TokenType != JsonToken.StartObject) + { + throw new JsonException($"Expected StartObject, but got {reader.TokenType}."); + } + + FilterNode result = null; + + var comparePath = (PropertyPath)null; + var compareOperator = (CompareOperator)99; + var compareValue = (IJsonValue)null; + + while (reader.Read()) + { + switch (reader.TokenType) + { + case JsonToken.PropertyName: + string propertyName = reader.Value.ToString(); + + if (!reader.Read()) + { + throw new JsonSerializationException("Unexpected end when reading filter."); + } + + if (result != null) + { + throw new JsonSerializationException($"Unexpected property {propertyName}"); + } + + switch (propertyName.ToLowerInvariant()) + { + case "not": + var filter = serializer.Deserialize>(reader); + + result = new NegateFilter(filter); + break; + case "and": + var andFilters = serializer.Deserialize>>(reader); + + result = new LogicalFilter(LogicalFilterType.And, andFilters); + break; + case "or": + var orFilters = serializer.Deserialize>>(reader); + + result = new LogicalFilter(LogicalFilterType.Or, orFilters); + break; + case "path": + comparePath = serializer.Deserialize(reader); + break; + case "op": + compareOperator = ReadOperator(reader, serializer); + break; + case "value": + compareValue = serializer.Deserialize(reader); + break; + } + + break; + case JsonToken.Comment: + break; + case JsonToken.EndObject: + if (result != null) + { + return result; + } + + if (comparePath == null) + { + throw new JsonSerializationException("Path not defined."); + } + + if (compareValue == null && compareOperator != CompareOperator.Empty) + { + throw new JsonSerializationException("Value not defined."); + } + + if (!compareOperator.IsEnumValue()) + { + throw new JsonSerializationException("Operator not defined."); + } + + return new CompareFilter(comparePath, compareOperator, compareValue ?? JsonValue.Null); + } + } + + throw new JsonSerializationException("Unexpected end when reading filter."); + } + + private static CompareOperator ReadOperator(JsonReader reader, JsonSerializer serializer) + { + var value = serializer.Deserialize(reader); + + switch (value.ToLowerInvariant()) + { + case "eq": + return CompareOperator.Equals; + case "ne": + return CompareOperator.NotEquals; + case "lt": + return CompareOperator.LessThan; + case "le": + return CompareOperator.LessThanOrEqual; + case "gt": + return CompareOperator.GreaterThan; + case "ge": + return CompareOperator.GreaterThanOrEqual; + case "empty": + return CompareOperator.Empty; + case "contains": + return CompareOperator.Contains; + case "endswith": + return CompareOperator.EndsWith; + case "startswith": + return CompareOperator.StartsWith; + case "in": + return CompareOperator.In; + } + + throw new JsonSerializationException($"Unexpected compare operator, got {value}."); + } + + protected override void WriteValue(JsonWriter writer, FilterNode value, JsonSerializer serializer) + { + throw new NotSupportedException(); + } + } +} diff --git a/src/Squidex.Infrastructure/Queries/Json/PropertyPathConverter.cs b/src/Squidex.Infrastructure/Queries/Json/PropertyPathConverter.cs new file mode 100644 index 000000000..15994b713 --- /dev/null +++ b/src/Squidex.Infrastructure/Queries/Json/PropertyPathConverter.cs @@ -0,0 +1,29 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System; +using System.Collections.Generic; +using Newtonsoft.Json; +using Squidex.Infrastructure.Json.Newtonsoft; + +namespace Squidex.Infrastructure.Queries.Json +{ + public sealed class PropertyPathConverter : JsonClassConverter + { + protected override PropertyPath ReadValue(JsonReader reader, Type objectType, JsonSerializer serializer) + { + var value = serializer.Deserialize(reader); + + return value; + } + + protected override void WriteValue(JsonWriter writer, PropertyPath value, JsonSerializer serializer) + { + serializer.Serialize(writer, (IEnumerable)value); + } + } +} diff --git a/src/Squidex.Infrastructure/Queries/PropertyPath.cs b/src/Squidex.Infrastructure/Queries/PropertyPath.cs index 71af576c2..176bccf3d 100644 --- a/src/Squidex.Infrastructure/Queries/PropertyPath.cs +++ b/src/Squidex.Infrastructure/Queries/PropertyPath.cs @@ -15,6 +15,8 @@ namespace Squidex.Infrastructure.Queries { public sealed class PropertyPath : ReadOnlyCollection { + private static readonly char[] Separators = { '.', '/' }; + public PropertyPath(IList items) : base(items) { @@ -26,7 +28,7 @@ namespace Squidex.Infrastructure.Queries public static implicit operator PropertyPath(string path) { - return new PropertyPath(path?.Split('.', '/')?.ToList()); + return new PropertyPath(path?.Split(Separators, StringSplitOptions.RemoveEmptyEntries)?.ToList()); } public static implicit operator PropertyPath(string[] path) diff --git a/tests/Squidex.Infrastructure.Tests/Queries/QueryJsonTests.cs b/tests/Squidex.Infrastructure.Tests/Queries/QueryJsonTests.cs new file mode 100644 index 000000000..508c3b676 --- /dev/null +++ b/tests/Squidex.Infrastructure.Tests/Queries/QueryJsonTests.cs @@ -0,0 +1,181 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using Newtonsoft.Json; +using Squidex.Infrastructure.Json.Objects; +using Squidex.Infrastructure.TestHelpers; +using Xunit; + +namespace Squidex.Infrastructure.Queries +{ + public class QueryJsonTests + { + [Theory] + [InlineData("eq", "property == 12")] + [InlineData("ne", "property != 12")] + [InlineData("le", "property <= 12")] + [InlineData("lt", "property < 12")] + [InlineData("ge", "property >= 12")] + [InlineData("gt", "property > 12")] + [InlineData("contains", "contains(property, 12)")] + [InlineData("endswith", "endsWith(property, 12)")] + [InlineData("startswith", "startsWith(property, 12)")] + public void Should_convert_comparison(string op, string expected) + { + var json = new + { + path = "property", + op, + value = 12 + }; + + var filter = SerializeAndDeserialize(json); + + Assert.Equal(expected, filter.ToString()); + } + + [Fact] + public void Should_convert_comparison_empty() + { + var json = new { path = "property", op = "empty" }; + + var filter = SerializeAndDeserialize(json); + + Assert.Equal("empty(property)", filter.ToString()); + } + + [Fact] + public void Should_convert_comparison_in() + { + var json = new { path = "property", op = "in", value = new[] { 12, 13 } }; + + var filter = SerializeAndDeserialize(json); + + Assert.Equal("property in [12, 13]", filter.ToString()); + } + + [Fact] + public void Should_convert_comparison_with_deep_path() + { + var json = new { path = "property.nested", op = "eq", value = 12 }; + + var filter = SerializeAndDeserialize(json); + + Assert.Equal("property.nested == 12", filter.ToString()); + } + + [Fact] + public void Should_convert_logical_and() + { + var json = new + { + and = new[] + { + new { path = "property", op = "ge", value = 10 }, + new { path = "property", op = "lt", value = 20 } + } + }; + + var filter = SerializeAndDeserialize(json); + + Assert.Equal("(property >= 10 && property < 20)", filter.ToString()); + } + + [Fact] + public void Should_convert_logical_or() + { + var json = new + { + or = new[] + { + new { path = "property", op = "ge", value = 10 }, + new { path = "property", op = "lt", value = 20 } + } + }; + + var filter = SerializeAndDeserialize(json); + + Assert.Equal("(property >= 10 || property < 20)", filter.ToString()); + } + + [Fact] + public void Should_convert_logical_not() + { + var json = new + { + not = new { path = "property", op = "ge", value = 10 } + }; + + var filter = SerializeAndDeserialize(json); + + Assert.Equal("!(property >= 10)", filter.ToString()); + } + + [Fact] + public void Should_throw_exception_for_invalid_operator() + { + var json = new { path = "property", op = "invalid", value = 12 }; + + Assert.ThrowsAny(() => SerializeAndDeserialize(json)); + } + + [Fact] + public void Should_throw_exception_for_missing_path() + { + var json = new { op = "invalid", value = 12 }; + + Assert.ThrowsAny(() => SerializeAndDeserialize(json)); + } + + [Fact] + public void Should_throw_exception_for_missing_operator() + { + var json = new { path = "property", value = 12 }; + + Assert.ThrowsAny(() => SerializeAndDeserialize(json)); + } + + [Fact] + public void Should_throw_exception_for_missing_value() + { + var json = new { path = "property", op = "invalid" }; + + Assert.ThrowsAny(() => SerializeAndDeserialize(json)); + } + + [Fact] + public void Should_throw_exception_for_invalid_property() + { + var json = new { path = "property", op = "invalid", value = 12, other = 4 }; + + Assert.ThrowsAny(() => SerializeAndDeserialize(json)); + } + + [Fact] + public void Should_throw_exception_for_invalid_property_after_filter() + { + var json = new + { + and = new[] + { + new { path = "property", op = "ge", value = 10 }, + new { path = "property", op = "lt", value = 20 } + }, + additional = 1 + }; + + Assert.ThrowsAny(() => SerializeAndDeserialize(json)); + } + + private static FilterNode SerializeAndDeserialize(T value) + { + var json = JsonHelper.DefaultSerializer.Serialize(value, true); + + return JsonHelper.DefaultSerializer.Deserialize>(json); + } + } +} diff --git a/tests/Squidex.Infrastructure.Tests/Queries/ODataConversionTests.cs b/tests/Squidex.Infrastructure.Tests/Queries/QueryODataConversionTests.cs similarity index 99% rename from tests/Squidex.Infrastructure.Tests/Queries/ODataConversionTests.cs rename to tests/Squidex.Infrastructure.Tests/Queries/QueryODataConversionTests.cs index 90154a9c5..bdac075d4 100644 --- a/tests/Squidex.Infrastructure.Tests/Queries/ODataConversionTests.cs +++ b/tests/Squidex.Infrastructure.Tests/Queries/QueryODataConversionTests.cs @@ -11,11 +11,11 @@ using Xunit; namespace Squidex.Infrastructure.Queries { - public class ODataConversionTests + public class QueryODataConversionTests { private static readonly IEdmModel EdmModel; - static ODataConversionTests() + static QueryODataConversionTests() { var entityType = new EdmEntityType("Squidex", "Users"); diff --git a/tests/Squidex.Infrastructure.Tests/TestHelpers/JsonHelper.cs b/tests/Squidex.Infrastructure.Tests/TestHelpers/JsonHelper.cs index bef611a12..ad9db8569 100644 --- a/tests/Squidex.Infrastructure.Tests/TestHelpers/JsonHelper.cs +++ b/tests/Squidex.Infrastructure.Tests/TestHelpers/JsonHelper.cs @@ -10,6 +10,7 @@ using Newtonsoft.Json; using Newtonsoft.Json.Converters; using Squidex.Infrastructure.Json; using Squidex.Infrastructure.Json.Newtonsoft; +using Squidex.Infrastructure.Queries.Json; namespace Squidex.Infrastructure.TestHelpers { @@ -34,11 +35,13 @@ namespace Squidex.Infrastructure.TestHelpers new ClaimsPrincipalConverter(), new InstantConverter(), new EnvelopeHeadersConverter(), + new FilterConverter(), new JsonValueConverter(), new LanguageConverter(), new NamedGuidIdConverter(), new NamedLongIdConverter(), new NamedStringIdConverter(), + new PropertyPathConverter(), new RefTokenConverter(), new StringEnumConverter()),