From 0e587dec82d5fb7997865ea26a1b6265138fbb68 Mon Sep 17 00:00:00 2001 From: Sebastian Date: Sat, 11 Mar 2017 20:02:52 +0100 Subject: [PATCH] Geolocation Field in API --- src/Squidex.Core/Schemas/BooleanField.cs | 3 +- src/Squidex.Core/Schemas/DateTimeField.cs | 2 +- .../Schemas/DateTimeFieldProperties.cs | 2 +- src/Squidex.Core/Schemas/Field.cs | 4 +- src/Squidex.Core/Schemas/FieldRegistry.cs | 3 + src/Squidex.Core/Schemas/GeolocationField.cs | 89 +++++++++++++++++ .../Schemas/GeolocationFieldEditor.cs | 15 +++ .../Schemas/GeolocationFieldProperties.cs | 44 +++++++++ src/Squidex.Core/Schemas/JsonField.cs | 3 +- src/Squidex.Core/Schemas/NumberField.cs | 3 +- src/Squidex.Core/Schemas/StringField.cs | 3 +- .../Models/Converters/SchemaConverter.cs | 11 +++ .../Api/Schemas/Models/FieldPropertiesDto.cs | 1 + .../Models/GeolocationPropertiesDto.cs | 38 +++++++ .../Schemas/GeolocationFieldTests.cs | 98 +++++++++++++++++++ .../Schemas/GeolocationPropertiesTests.cs | 79 +++++++++++++++ .../Schemas/Json/JsonSerializerTests.cs | 12 ++- .../Squidex.Core.Tests/Schemas/SchemaTests.cs | 4 +- 18 files changed, 401 insertions(+), 13 deletions(-) create mode 100644 src/Squidex.Core/Schemas/GeolocationField.cs create mode 100644 src/Squidex.Core/Schemas/GeolocationFieldEditor.cs create mode 100644 src/Squidex.Core/Schemas/GeolocationFieldProperties.cs create mode 100644 src/Squidex/Controllers/Api/Schemas/Models/GeolocationPropertiesDto.cs create mode 100644 tests/Squidex.Core.Tests/Schemas/GeolocationFieldTests.cs create mode 100644 tests/Squidex.Core.Tests/Schemas/GeolocationPropertiesTests.cs diff --git a/src/Squidex.Core/Schemas/BooleanField.cs b/src/Squidex.Core/Schemas/BooleanField.cs index 5944b4a72..a0a9f0d2a 100644 --- a/src/Squidex.Core/Schemas/BooleanField.cs +++ b/src/Squidex.Core/Schemas/BooleanField.cs @@ -6,6 +6,7 @@ // All rights reserved. // ========================================================================== +using System; using System.Collections.Generic; using Microsoft.OData.Edm; using Microsoft.OData.Edm.Library; @@ -35,7 +36,7 @@ namespace Squidex.Core.Schemas return (bool?)value; } - protected override void PrepareJsonSchema(JsonProperty jsonProperty) + protected override void PrepareJsonSchema(JsonProperty jsonProperty, Func schemaResolver) { jsonProperty.Type = JsonObjectType.Boolean; } diff --git a/src/Squidex.Core/Schemas/DateTimeField.cs b/src/Squidex.Core/Schemas/DateTimeField.cs index 7670cfd66..8ec99b9d5 100644 --- a/src/Squidex.Core/Schemas/DateTimeField.cs +++ b/src/Squidex.Core/Schemas/DateTimeField.cs @@ -65,7 +65,7 @@ namespace Squidex.Core.Schemas throw new InvalidCastException("Invalid json type, expected string."); } - protected override void PrepareJsonSchema(JsonProperty jsonProperty) + protected override void PrepareJsonSchema(JsonProperty jsonProperty, Func schemaResolver) { jsonProperty.Type = JsonObjectType.String; diff --git a/src/Squidex.Core/Schemas/DateTimeFieldProperties.cs b/src/Squidex.Core/Schemas/DateTimeFieldProperties.cs index eefcca87d..189911847 100644 --- a/src/Squidex.Core/Schemas/DateTimeFieldProperties.cs +++ b/src/Squidex.Core/Schemas/DateTimeFieldProperties.cs @@ -67,7 +67,7 @@ namespace Squidex.Core.Schemas public override JToken GetDefaultValue() { - return DefaultValue != null ? DefaultValue.ToString() : null; + return DefaultValue?.ToString(); } protected override IEnumerable ValidateCore() diff --git a/src/Squidex.Core/Schemas/Field.cs b/src/Squidex.Core/Schemas/Field.cs index 3d09dd8b0..10717765d 100644 --- a/src/Squidex.Core/Schemas/Field.cs +++ b/src/Squidex.Core/Schemas/Field.cs @@ -196,7 +196,7 @@ namespace Squidex.Core.Schemas { var languageProperty = new JsonProperty { Description = language.EnglishName }; - PrepareJsonSchema(languageProperty); + PrepareJsonSchema(languageProperty, schemaResolver); languagesObject.Properties.Add(language.Iso2Code, languageProperty); } @@ -231,7 +231,7 @@ namespace Squidex.Core.Schemas protected abstract IEdmTypeReference CreateEdmType(); - protected abstract void PrepareJsonSchema(JsonProperty jsonProperty); + protected abstract void PrepareJsonSchema(JsonProperty jsonProperty, Func schemaResolver); protected abstract object ConvertValue(JToken value); } diff --git a/src/Squidex.Core/Schemas/FieldRegistry.cs b/src/Squidex.Core/Schemas/FieldRegistry.cs index 51613499a..573928988 100644 --- a/src/Squidex.Core/Schemas/FieldRegistry.cs +++ b/src/Squidex.Core/Schemas/FieldRegistry.cs @@ -70,6 +70,9 @@ namespace Squidex.Core.Schemas Add( (id, name, p) => new JsonField(id, name, (JsonFieldProperties)p)); + + Add( + (id, name, p) => new GeolocationField(id, name, (GeolocationFieldProperties)p)); } public void Add(FactoryFunction fieldFactory) diff --git a/src/Squidex.Core/Schemas/GeolocationField.cs b/src/Squidex.Core/Schemas/GeolocationField.cs new file mode 100644 index 000000000..6738954c4 --- /dev/null +++ b/src/Squidex.Core/Schemas/GeolocationField.cs @@ -0,0 +1,89 @@ +// ========================================================================== +// GeolocationField.cs +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex Group +// All rights reserved. +// ========================================================================== + +using System; +using System.Collections.Generic; +using Microsoft.OData.Edm; +using Newtonsoft.Json.Linq; +using NJsonSchema; +using Squidex.Core.Schemas.Validators; +using Squidex.Infrastructure; + +namespace Squidex.Core.Schemas +{ + public sealed class GeolocationField : Field + { + public GeolocationField(long id, string name, GeolocationFieldProperties properties) + : base(id, name, properties) + { + } + + protected override IEnumerable CreateValidators() + { + if (Properties.IsRequired) + { + yield return new RequiredValidator(); + } + } + + protected override object ConvertValue(JToken value) + { + var geolocation = (JObject)value; + + foreach (var property in geolocation.Properties()) + { + if (!string.Equals(property.Name, "latitude", StringComparison.OrdinalIgnoreCase) && + !string.Equals(property.Name, "longitude", StringComparison.OrdinalIgnoreCase)) + { + throw new InvalidCastException("Geolocation can only have latitude and longitude property."); + } + } + + var lat = (double)geolocation["latitude"]; + var lon = (double)geolocation["longitude"]; + + Guard.Between(lat, -90, 90, "latitude"); + Guard.Between(lon, -180, 180, "longitude"); + + return value; + } + + protected override void PrepareJsonSchema(JsonProperty jsonProperty, Func schemaResolver) + { + jsonProperty.Type = JsonObjectType.Object; + + var geolocationSchema = new JsonSchema4(); + + geolocationSchema.Properties.Add("latitude", new JsonProperty + { + Type = JsonObjectType.Number, + Minimum = -90, + Maximum = 90, + IsRequired = true + }); + geolocationSchema.Properties.Add("longitude", new JsonProperty + { + Type = JsonObjectType.Number, + Minimum = -180, + Maximum = 180, + IsRequired = true + }); + + geolocationSchema.AllowAdditionalProperties = false; + + var schemaReference = schemaResolver("GeolocationDto", geolocationSchema); + + jsonProperty.SchemaReference = schemaReference; + } + + protected override IEdmTypeReference CreateEdmType() + { + return null; + } + } +} diff --git a/src/Squidex.Core/Schemas/GeolocationFieldEditor.cs b/src/Squidex.Core/Schemas/GeolocationFieldEditor.cs new file mode 100644 index 000000000..dd2a347a6 --- /dev/null +++ b/src/Squidex.Core/Schemas/GeolocationFieldEditor.cs @@ -0,0 +1,15 @@ +// ========================================================================== +// GeolocationFieldEditor.cs +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex Group +// All rights reserved. +// ========================================================================== + +namespace Squidex.Core.Schemas +{ + public enum GeolocationFieldEditor + { + Map + } +} diff --git a/src/Squidex.Core/Schemas/GeolocationFieldProperties.cs b/src/Squidex.Core/Schemas/GeolocationFieldProperties.cs new file mode 100644 index 000000000..ffb4cbca4 --- /dev/null +++ b/src/Squidex.Core/Schemas/GeolocationFieldProperties.cs @@ -0,0 +1,44 @@ +// ========================================================================== +// GeolocationFieldProperties.cs +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex Group +// All rights reserved. +// ========================================================================== + +using System.Collections.Generic; +using Newtonsoft.Json.Linq; +using Squidex.Infrastructure; + +namespace Squidex.Core.Schemas +{ + [TypeName("GeolocationField")] + public sealed class GeolocationFieldProperties : FieldProperties + { + private GeolocationFieldEditor editor; + + public GeolocationFieldEditor Editor + { + get { return editor; } + set + { + ThrowIfFrozen(); + + editor = value; + } + } + + public override JToken GetDefaultValue() + { + return null; + } + + protected override IEnumerable ValidateCore() + { + if (!Editor.IsEnumValue()) + { + yield return new ValidationError("Editor ist not a valid value", nameof(Editor)); + } + } + } +} diff --git a/src/Squidex.Core/Schemas/JsonField.cs b/src/Squidex.Core/Schemas/JsonField.cs index f12703c9b..e64a1ed30 100644 --- a/src/Squidex.Core/Schemas/JsonField.cs +++ b/src/Squidex.Core/Schemas/JsonField.cs @@ -6,6 +6,7 @@ // All rights reserved. // ========================================================================== +using System; using System.Collections.Generic; using Microsoft.OData.Edm; using Newtonsoft.Json.Linq; @@ -34,7 +35,7 @@ namespace Squidex.Core.Schemas return value; } - protected override void PrepareJsonSchema(JsonProperty jsonProperty) + protected override void PrepareJsonSchema(JsonProperty jsonProperty, Func schemaResolver) { jsonProperty.Type = JsonObjectType.Object; } diff --git a/src/Squidex.Core/Schemas/NumberField.cs b/src/Squidex.Core/Schemas/NumberField.cs index aeae44b63..902e3269d 100644 --- a/src/Squidex.Core/Schemas/NumberField.cs +++ b/src/Squidex.Core/Schemas/NumberField.cs @@ -6,6 +6,7 @@ // All rights reserved. // ========================================================================== +using System; using System.Collections.Generic; using System.Linq; using Microsoft.OData.Edm; @@ -41,7 +42,7 @@ namespace Squidex.Core.Schemas } } - protected override void PrepareJsonSchema(JsonProperty jsonProperty) + protected override void PrepareJsonSchema(JsonProperty jsonProperty, Func schemaResolver) { jsonProperty.Type = JsonObjectType.Number; diff --git a/src/Squidex.Core/Schemas/StringField.cs b/src/Squidex.Core/Schemas/StringField.cs index 5114bf439..c4dfbd2bb 100644 --- a/src/Squidex.Core/Schemas/StringField.cs +++ b/src/Squidex.Core/Schemas/StringField.cs @@ -6,6 +6,7 @@ // All rights reserved. // ========================================================================== +using System; using System.Collections.Generic; using System.Collections.ObjectModel; using System.Linq; @@ -47,7 +48,7 @@ namespace Squidex.Core.Schemas } } - protected override void PrepareJsonSchema(JsonProperty jsonProperty) + protected override void PrepareJsonSchema(JsonProperty jsonProperty, Func schemaResolver) { jsonProperty.Type = JsonObjectType.String; diff --git a/src/Squidex/Controllers/Api/Schemas/Models/Converters/SchemaConverter.cs b/src/Squidex/Controllers/Api/Schemas/Models/Converters/SchemaConverter.cs index 98df82247..d0c4e0a2c 100644 --- a/src/Squidex/Controllers/Api/Schemas/Models/Converters/SchemaConverter.cs +++ b/src/Squidex/Controllers/Api/Schemas/Models/Converters/SchemaConverter.cs @@ -38,6 +38,10 @@ namespace Squidex.Controllers.Api.Schemas.Models.Converters { typeof(BooleanFieldProperties), p => Convert((BooleanFieldProperties)p) + }, + { + typeof(GeolocationFieldProperties), + p => Convert((GeolocationFieldProperties)p) } }; @@ -85,6 +89,13 @@ namespace Squidex.Controllers.Api.Schemas.Models.Converters return result; } + private static FieldPropertiesDto Convert(GeolocationFieldProperties source) + { + var result = SimpleMapper.Map(source, new GeolocationFieldPropertiesDto()); + + return result; + } + private static FieldPropertiesDto Convert(StringFieldProperties source) { var result = SimpleMapper.Map(source, new StringFieldPropertiesDto()); diff --git a/src/Squidex/Controllers/Api/Schemas/Models/FieldPropertiesDto.cs b/src/Squidex/Controllers/Api/Schemas/Models/FieldPropertiesDto.cs index 1bd51e93f..1c8009a8a 100644 --- a/src/Squidex/Controllers/Api/Schemas/Models/FieldPropertiesDto.cs +++ b/src/Squidex/Controllers/Api/Schemas/Models/FieldPropertiesDto.cs @@ -17,6 +17,7 @@ namespace Squidex.Controllers.Api.Schemas.Models [JsonConverter(typeof(JsonInheritanceConverter), "fieldType")] [KnownType(typeof(BooleanFieldPropertiesDto))] [KnownType(typeof(DateTimeFieldPropertiesDto))] + [KnownType(typeof(GeolocationFieldPropertiesDto))] [KnownType(typeof(JsonFieldPropertiesDto))] [KnownType(typeof(NumberFieldPropertiesDto))] [KnownType(typeof(StringFieldPropertiesDto))] diff --git a/src/Squidex/Controllers/Api/Schemas/Models/GeolocationPropertiesDto.cs b/src/Squidex/Controllers/Api/Schemas/Models/GeolocationPropertiesDto.cs new file mode 100644 index 000000000..a6f0333a9 --- /dev/null +++ b/src/Squidex/Controllers/Api/Schemas/Models/GeolocationPropertiesDto.cs @@ -0,0 +1,38 @@ +// ========================================================================== +// GeolocationFieldPropertiesDto.cs +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex Group +// All rights reserved. +// ========================================================================== + +using Newtonsoft.Json; +using Newtonsoft.Json.Converters; +using NJsonSchema.Annotations; +using Squidex.Core.Schemas; +using Squidex.Infrastructure.Reflection; + +namespace Squidex.Controllers.Api.Schemas.Models +{ + [JsonSchema("Geolocation")] + public sealed class GeolocationFieldPropertiesDto : FieldPropertiesDto + { + /// + /// The default value for the field value. + /// + public bool? DefaultValue { get; set; } + + /// + /// The editor that is used to manage this field. + /// + [JsonConverter(typeof(StringEnumConverter))] + public GeolocationFieldEditor Editor { get; set; } + + public override FieldProperties ToProperties() + { + var result = SimpleMapper.Map(this, new GeolocationFieldProperties()); + + return result; + } + } +} diff --git a/tests/Squidex.Core.Tests/Schemas/GeolocationFieldTests.cs b/tests/Squidex.Core.Tests/Schemas/GeolocationFieldTests.cs new file mode 100644 index 000000000..acbc14928 --- /dev/null +++ b/tests/Squidex.Core.Tests/Schemas/GeolocationFieldTests.cs @@ -0,0 +1,98 @@ +// ========================================================================== +// GeolocationFieldTests.cs +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex Group +// All rights reserved. +// ========================================================================== + +using System.Collections.Generic; +using System.Threading.Tasks; +using FluentAssertions; +using Newtonsoft.Json.Linq; +using Xunit; + +namespace Squidex.Core.Schemas +{ + public class GeolocationFieldTests + { + private readonly List errors = new List(); + + [Fact] + public void Should_instantiate_field() + { + var sut = new GeolocationField(1, "my-geolocation", new GeolocationFieldProperties()); + + Assert.Equal("my-geolocation", sut.Name); + } + + [Fact] + public void Should_clone_object() + { + var sut = new GeolocationField(1, "my-geolocation", new GeolocationFieldProperties()); + + Assert.NotEqual(sut, sut.Enable()); + } + + [Fact] + public async Task Should_not_add_error_if_geolocation_is_valid() + { + var sut = new GeolocationField(1, "my-geolocation", new GeolocationFieldProperties { Label = "my-geolocation" }); + + var geolocation = new JObject( + new JProperty("latitude", 0), + new JProperty("longitude", 0)); + + await sut.ValidateAsync(CreateValue(geolocation), errors); + + Assert.Empty(errors); + } + + [Fact] + public async Task Should_add_errors_if_geolocation_has_invalid_properties() + { + var sut = new GeolocationField(1, "my-geolocation", new GeolocationFieldProperties { Label = "my-geolocation", IsRequired = true }); + + var geolocation = new JObject( + new JProperty("latitude", 200), + new JProperty("longitude", 0)); + + await sut.ValidateAsync(CreateValue(geolocation), errors); + + errors.ShouldBeEquivalentTo( + new[] { "my-geolocation is not a valid value" }); + } + + [Fact] + public async Task Should_add_errors_if_geolocation_has_too_many_properties() + { + var sut = new GeolocationField(1, "my-geolocation", new GeolocationFieldProperties { Label = "my-geolocation", IsRequired = true }); + + var geolocation = new JObject( + new JProperty("invalid", 0), + new JProperty("latitude", 0), + new JProperty("longitude", 0)); + + await sut.ValidateAsync(CreateValue(geolocation), errors); + + errors.ShouldBeEquivalentTo( + new[] { "my-geolocation is not a valid value" }); + } + + [Fact] + public async Task Should_add_errors_if_geolocation_is_required() + { + var sut = new GeolocationField(1, "my-geolocation", new GeolocationFieldProperties { Label = "my-geolocation", IsRequired = true }); + + await sut.ValidateAsync(CreateValue(JValue.CreateNull()), errors); + + errors.ShouldBeEquivalentTo( + new[] { "my-geolocation is required" }); + } + + private static JToken CreateValue(JToken v) + { + return v; + } + } +} diff --git a/tests/Squidex.Core.Tests/Schemas/GeolocationPropertiesTests.cs b/tests/Squidex.Core.Tests/Schemas/GeolocationPropertiesTests.cs new file mode 100644 index 000000000..d858f704e --- /dev/null +++ b/tests/Squidex.Core.Tests/Schemas/GeolocationPropertiesTests.cs @@ -0,0 +1,79 @@ +// ========================================================================== +// GeolocationFieldTests.cs +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex Group +// All rights reserved. +// ========================================================================== + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Reflection; +using FluentAssertions; +using Squidex.Infrastructure; +using Xunit; + +namespace Squidex.Core.Schemas +{ + public class GeolocationFieldPropertiesTests + { + private readonly List errors = new List(); + + [Fact] + public void Should_add_error_if_editor_is_not_valid() + { + var sut = new GeolocationFieldProperties { Editor = (GeolocationFieldEditor)123 }; + + sut.Validate(errors); + + errors.ShouldBeEquivalentTo( + new List + { + new ValidationError("Editor ist not a valid value", "Editor") + }); + } + + [Fact] + public void Should_set_or_freeze_sut() + { + var sut = new GeolocationFieldProperties(); + + foreach (var property in sut.GetType().GetRuntimeProperties().Where(x => x.Name != "IsFrozen")) + { + var value = + property.PropertyType.GetTypeInfo().IsValueType ? + Activator.CreateInstance(property.PropertyType) : + null; + + property.SetValue(sut, value); + + var result = property.GetValue(sut); + + Assert.Equal(value, result); + } + + sut.Freeze(); + + foreach (var property in sut.GetType().GetRuntimeProperties().Where(x => x.Name != "IsFrozen")) + { + var value = + property.PropertyType.GetTypeInfo().IsValueType ? + Activator.CreateInstance(property.PropertyType) : + null; + + Assert.Throws(() => + { + try + { + property.SetValue(sut, value); + } + catch (Exception ex) + { + throw ex.InnerException; + } + }); + } + } + } +} diff --git a/tests/Squidex.Core.Tests/Schemas/Json/JsonSerializerTests.cs b/tests/Squidex.Core.Tests/Schemas/Json/JsonSerializerTests.cs index 41069bcac..c4ea8cac8 100644 --- a/tests/Squidex.Core.Tests/Schemas/Json/JsonSerializerTests.cs +++ b/tests/Squidex.Core.Tests/Schemas/Json/JsonSerializerTests.cs @@ -34,13 +34,17 @@ namespace Squidex.Core.Schemas.Json var schema = Schema.Create("my-schema", new SchemaProperties()) .AddOrUpdateField(new StringField(1, "my-string", - new StringFieldProperties { Label = "My-String", Pattern = "[0-9]{3}" })).DisableField(1) + new StringFieldProperties { Label = "My-String", Pattern = "[0-9]{3}" })) .AddOrUpdateField(new NumberField(2, "my-number", new NumberFieldProperties { Hints = "My-Number" })) .AddOrUpdateField(new BooleanField(3, "my-boolean", - new BooleanFieldProperties())).HideField(2) - .AddOrUpdateField(new DateTimeField(4, "my-datetime", - new DateTimeFieldProperties())).HideField(2) + new BooleanFieldProperties())).HideField(3) + .AddOrUpdateField(new JsonField(4, "my-json", + new JsonFieldProperties())).DisableField(4) + .AddOrUpdateField(new DateTimeField(5, "my-datetime", + new DateTimeFieldProperties())) + .AddOrUpdateField(new GeolocationField(6, "my-geolocation", + new GeolocationFieldProperties())) .Publish(); var deserialized = sut.Deserialize(sut.Serialize(schema)); diff --git a/tests/Squidex.Core.Tests/Schemas/SchemaTests.cs b/tests/Squidex.Core.Tests/Schemas/SchemaTests.cs index cdb9fc223..5173de0e9 100644 --- a/tests/Squidex.Core.Tests/Schemas/SchemaTests.cs +++ b/tests/Squidex.Core.Tests/Schemas/SchemaTests.cs @@ -290,7 +290,9 @@ namespace Squidex.Core.Schemas .AddOrUpdateField(new DateTimeField(6, "my-datetime", new DateTimeFieldProperties { Editor = DateTimeFieldEditor.DateTime })) .AddOrUpdateField(new DateTimeField(7, "my-date", - new DateTimeFieldProperties { Editor = DateTimeFieldEditor.Date })); + new DateTimeFieldProperties { Editor = DateTimeFieldEditor.Date })) + .AddOrUpdateField(new GeolocationField(8, "my-geolocation", + new GeolocationFieldProperties())); return schema; }