From ce86abe8ac05315c1b7d93e4b40dc0aa5cd73dad Mon Sep 17 00:00:00 2001 From: Kenn Jacobsen Date: Thu, 28 Sep 2023 13:20:03 +0200 Subject: [PATCH] Add default property value converters for all value types (#14869) * Add default property value converters for all value types * Clean up some left-over stuff --- ...alueTypePropertyValueConverterAttribute.cs | 10 + .../PropertyValueConverterCollection.cs | 12 +- .../BigintValueTypeConverter.cs | 24 ++ .../DatePickerValueConverter.cs | 43 ++-- .../DateTimeValueTypeConverter.cs | 23 ++ .../ValueConverters/DecimalValueConverter.cs | 7 +- .../DecimalValueTypeConverter.cs | 23 ++ .../IntegerValueTypeConverter.cs | 24 ++ .../TextStringValueTypeConverter.cs | 23 ++ .../ValueConverters/TimeValueTypeConverter.cs | 23 ++ .../ValueTypePropertyValueConverterBase.cs | 18 ++ .../ValueConverters/XmlValueTypeConverter.cs | 51 ++++ .../PropertyEditorValueConverterTests.cs | 35 ++- .../PropertyEditorValueTypeConverterTests.cs | 221 ++++++++++++++++++ 14 files changed, 492 insertions(+), 45 deletions(-) create mode 100644 src/Umbraco.Core/PropertyEditors/DefaultValueTypePropertyValueConverterAttribute.cs create mode 100644 src/Umbraco.Core/PropertyEditors/ValueConverters/BigintValueTypeConverter.cs create mode 100644 src/Umbraco.Core/PropertyEditors/ValueConverters/DateTimeValueTypeConverter.cs create mode 100644 src/Umbraco.Core/PropertyEditors/ValueConverters/DecimalValueTypeConverter.cs create mode 100644 src/Umbraco.Core/PropertyEditors/ValueConverters/IntegerValueTypeConverter.cs create mode 100644 src/Umbraco.Core/PropertyEditors/ValueConverters/TextStringValueTypeConverter.cs create mode 100644 src/Umbraco.Core/PropertyEditors/ValueConverters/TimeValueTypeConverter.cs create mode 100644 src/Umbraco.Core/PropertyEditors/ValueConverters/ValueTypePropertyValueConverterBase.cs create mode 100644 src/Umbraco.Core/PropertyEditors/ValueConverters/XmlValueTypeConverter.cs create mode 100644 tests/Umbraco.Tests.UnitTests/Umbraco.Core/PropertyEditors/PropertyEditorValueTypeConverterTests.cs diff --git a/src/Umbraco.Core/PropertyEditors/DefaultValueTypePropertyValueConverterAttribute.cs b/src/Umbraco.Core/PropertyEditors/DefaultValueTypePropertyValueConverterAttribute.cs new file mode 100644 index 0000000000..22a0532b5a --- /dev/null +++ b/src/Umbraco.Core/PropertyEditors/DefaultValueTypePropertyValueConverterAttribute.cs @@ -0,0 +1,10 @@ +namespace Umbraco.Cms.Core.PropertyEditors; + +/// +/// Indicates that this is a default value type property value converter (shipped with Umbraco). +/// This attribute is for internal use only. It should never be applied to custom value converters. +/// +[AttributeUsage(AttributeTargets.Class, Inherited = false)] +public class DefaultValueTypePropertyValueConverterAttribute : DefaultPropertyValueConverterAttribute +{ +} diff --git a/src/Umbraco.Core/PropertyEditors/PropertyValueConverterCollection.cs b/src/Umbraco.Core/PropertyEditors/PropertyValueConverterCollection.cs index 20eb9ae4c4..9833a31ed7 100644 --- a/src/Umbraco.Core/PropertyEditors/PropertyValueConverterCollection.cs +++ b/src/Umbraco.Core/PropertyEditors/PropertyValueConverterCollection.cs @@ -44,5 +44,15 @@ public class PropertyValueConverterCollection : BuilderCollectionBase DefaultConverters.ContainsKey(converter); internal bool Shadows(IPropertyValueConverter shadowing, IPropertyValueConverter shadowed) - => DefaultConverters.TryGetValue(shadowing, out Type[]? types) && types.Contains(shadowed.GetType()); + { + Type shadowedType = shadowed.GetType(); + + // any value converter built specifically to convert purely value type bound properties can always be shadowed + if (shadowedType.GetCustomAttribute(false) is not null) + { + return true; + } + + return DefaultConverters.TryGetValue(shadowing, out Type[]? types) && types.Contains(shadowedType); + } } diff --git a/src/Umbraco.Core/PropertyEditors/ValueConverters/BigintValueTypeConverter.cs b/src/Umbraco.Core/PropertyEditors/ValueConverters/BigintValueTypeConverter.cs new file mode 100644 index 0000000000..102236350a --- /dev/null +++ b/src/Umbraco.Core/PropertyEditors/ValueConverters/BigintValueTypeConverter.cs @@ -0,0 +1,24 @@ +using Umbraco.Cms.Core.Models.PublishedContent; +using Umbraco.Extensions; + +namespace Umbraco.Cms.Core.PropertyEditors.ValueConverters; + +[DefaultValueTypePropertyValueConverter] +public class BigintValueTypeConverter : ValueTypePropertyValueConverterBase +{ + protected override string[] SupportedValueTypes => new[] { ValueTypes.Bigint }; + + public BigintValueTypeConverter(PropertyEditorCollection propertyEditors) + : base(propertyEditors) + { + } + + public override Type GetPropertyValueType(IPublishedPropertyType propertyType) + => typeof(long); + + public override PropertyCacheLevel GetPropertyCacheLevel(IPublishedPropertyType propertyType) + => PropertyCacheLevel.Element; + + public override object? ConvertSourceToIntermediate(IPublishedElement owner, IPublishedPropertyType propertyType, object? source, bool preview) + => source.TryConvertTo().Result; +} diff --git a/src/Umbraco.Core/PropertyEditors/ValueConverters/DatePickerValueConverter.cs b/src/Umbraco.Core/PropertyEditors/ValueConverters/DatePickerValueConverter.cs index 73ef424ba4..60cbc28b6c 100644 --- a/src/Umbraco.Core/PropertyEditors/ValueConverters/DatePickerValueConverter.cs +++ b/src/Umbraco.Core/PropertyEditors/ValueConverters/DatePickerValueConverter.cs @@ -17,26 +17,7 @@ public class DatePickerValueConverter : PropertyValueConverterBase => PropertyCacheLevel.Element; public override object ConvertSourceToIntermediate(IPublishedElement owner, IPublishedPropertyType propertyType, object? source, bool preview) - { - if (source == null) - { - return DateTime.MinValue; - } - - // in XML a DateTime is: string - format "yyyy-MM-ddTHH:mm:ss" - // Actually, not always sometimes it is formatted in UTC style with 'Z' suffixed on the end but that is due to this bug: - // http://issues.umbraco.org/issue/U4-4145, http://issues.umbraco.org/issue/U4-3894 - // We should just be using TryConvertTo instead. - if (source is string sourceString) - { - Attempt attempt = sourceString.TryConvertTo(); - return attempt.Success == false ? DateTime.MinValue : attempt.Result; - } - - // in the database a DateTime is: DateTime - // default value is: DateTime.MinValue - return source is DateTime ? source : DateTime.MinValue; - } + => ParseDateTimeValue(source); // default ConvertSourceToObject just returns source ie a DateTime value [Obsolete("The current implementation of XPath is suboptimal and will be removed entirely in a future version. Scheduled for removal in v14")] @@ -55,4 +36,26 @@ public class DatePickerValueConverter : PropertyValueConverterBase return XmlConvert.ToString((DateTime)inter, XmlDateTimeSerializationMode.Unspecified); } + + internal static DateTime ParseDateTimeValue(object? source) + { + if (source == null) + { + return DateTime.MinValue; + } + + // in XML a DateTime is: string - format "yyyy-MM-ddTHH:mm:ss" + // Actually, not always sometimes it is formatted in UTC style with 'Z' suffixed on the end but that is due to this bug: + // http://issues.umbraco.org/issue/U4-4145, http://issues.umbraco.org/issue/U4-3894 + // We should just be using TryConvertTo instead. + if (source is string sourceString) + { + Attempt attempt = sourceString.TryConvertTo(); + return attempt.Success == false ? DateTime.MinValue : attempt.Result; + } + + // in the database a DateTime is: DateTime + // default value is: DateTime.MinValue + return source is DateTime dateTimeValue ? dateTimeValue : DateTime.MinValue; + } } diff --git a/src/Umbraco.Core/PropertyEditors/ValueConverters/DateTimeValueTypeConverter.cs b/src/Umbraco.Core/PropertyEditors/ValueConverters/DateTimeValueTypeConverter.cs new file mode 100644 index 0000000000..f9c8caa50c --- /dev/null +++ b/src/Umbraco.Core/PropertyEditors/ValueConverters/DateTimeValueTypeConverter.cs @@ -0,0 +1,23 @@ +using Umbraco.Cms.Core.Models.PublishedContent; + +namespace Umbraco.Cms.Core.PropertyEditors.ValueConverters; + +[DefaultValueTypePropertyValueConverter] +public class DateTimeValueTypeConverter : ValueTypePropertyValueConverterBase +{ + protected override string[] SupportedValueTypes => new[] { ValueTypes.Date, ValueTypes.DateTime }; + + public DateTimeValueTypeConverter(PropertyEditorCollection propertyEditors) + : base(propertyEditors) + { + } + + public override Type GetPropertyValueType(IPublishedPropertyType propertyType) + => typeof(DateTime); + + public override PropertyCacheLevel GetPropertyCacheLevel(IPublishedPropertyType propertyType) + => PropertyCacheLevel.Element; + + public override object? ConvertSourceToIntermediate(IPublishedElement owner, IPublishedPropertyType propertyType, object? source, bool preview) + => DatePickerValueConverter.ParseDateTimeValue(source); // reuse the value conversion from the default "Umbraco.DateTime" value converter +} diff --git a/src/Umbraco.Core/PropertyEditors/ValueConverters/DecimalValueConverter.cs b/src/Umbraco.Core/PropertyEditors/ValueConverters/DecimalValueConverter.cs index 5a7f0a4adc..ba3a1e552b 100644 --- a/src/Umbraco.Core/PropertyEditors/ValueConverters/DecimalValueConverter.cs +++ b/src/Umbraco.Core/PropertyEditors/ValueConverters/DecimalValueConverter.cs @@ -16,6 +16,9 @@ public class DecimalValueConverter : PropertyValueConverterBase => PropertyCacheLevel.Element; public override object ConvertSourceToIntermediate(IPublishedElement owner, IPublishedPropertyType propertyType, object? source, bool preview) + => ParseDecimalValue(source); + + internal static decimal ParseDecimalValue(object? source) { if (source == null) { @@ -23,9 +26,9 @@ public class DecimalValueConverter : PropertyValueConverterBase } // is it already a decimal? - if (source is decimal) + if (source is decimal sourceDecimal) { - return source; + return sourceDecimal; } // is it a double? diff --git a/src/Umbraco.Core/PropertyEditors/ValueConverters/DecimalValueTypeConverter.cs b/src/Umbraco.Core/PropertyEditors/ValueConverters/DecimalValueTypeConverter.cs new file mode 100644 index 0000000000..37ab859994 --- /dev/null +++ b/src/Umbraco.Core/PropertyEditors/ValueConverters/DecimalValueTypeConverter.cs @@ -0,0 +1,23 @@ +using Umbraco.Cms.Core.Models.PublishedContent; + +namespace Umbraco.Cms.Core.PropertyEditors.ValueConverters; + +[DefaultValueTypePropertyValueConverter] +public class DecimalValueTypeConverter : ValueTypePropertyValueConverterBase +{ + protected override string[] SupportedValueTypes => new[] { ValueTypes.Decimal }; + + public DecimalValueTypeConverter(PropertyEditorCollection propertyEditors) + : base(propertyEditors) + { + } + + public override Type GetPropertyValueType(IPublishedPropertyType propertyType) + => typeof(decimal); + + public override PropertyCacheLevel GetPropertyCacheLevel(IPublishedPropertyType propertyType) + => PropertyCacheLevel.Element; + + public override object? ConvertSourceToIntermediate(IPublishedElement owner, IPublishedPropertyType propertyType, object? source, bool preview) + => DecimalValueConverter.ParseDecimalValue(source); // reuse the value conversion from the default "Umbraco.Decimal" value converter +} diff --git a/src/Umbraco.Core/PropertyEditors/ValueConverters/IntegerValueTypeConverter.cs b/src/Umbraco.Core/PropertyEditors/ValueConverters/IntegerValueTypeConverter.cs new file mode 100644 index 0000000000..78789b3953 --- /dev/null +++ b/src/Umbraco.Core/PropertyEditors/ValueConverters/IntegerValueTypeConverter.cs @@ -0,0 +1,24 @@ +using Umbraco.Cms.Core.Models.PublishedContent; +using Umbraco.Extensions; + +namespace Umbraco.Cms.Core.PropertyEditors.ValueConverters; + +[DefaultValueTypePropertyValueConverter] +public class IntegerValueTypeConverter : ValueTypePropertyValueConverterBase +{ + protected override string[] SupportedValueTypes => new[] { ValueTypes.Integer }; + + public IntegerValueTypeConverter(PropertyEditorCollection propertyEditors) + : base(propertyEditors) + { + } + + public override Type GetPropertyValueType(IPublishedPropertyType propertyType) + => typeof(int); + + public override PropertyCacheLevel GetPropertyCacheLevel(IPublishedPropertyType propertyType) + => PropertyCacheLevel.Element; + + public override object? ConvertSourceToIntermediate(IPublishedElement owner, IPublishedPropertyType propertyType, object? source, bool preview) + => source.TryConvertTo().Result; +} diff --git a/src/Umbraco.Core/PropertyEditors/ValueConverters/TextStringValueTypeConverter.cs b/src/Umbraco.Core/PropertyEditors/ValueConverters/TextStringValueTypeConverter.cs new file mode 100644 index 0000000000..9a57aaf52c --- /dev/null +++ b/src/Umbraco.Core/PropertyEditors/ValueConverters/TextStringValueTypeConverter.cs @@ -0,0 +1,23 @@ +using Umbraco.Cms.Core.Models.PublishedContent; + +namespace Umbraco.Cms.Core.PropertyEditors.ValueConverters; + +[DefaultValueTypePropertyValueConverter] +public class TextStringValueTypeConverter : ValueTypePropertyValueConverterBase +{ + protected override string[] SupportedValueTypes => new[] { ValueTypes.Text, ValueTypes.String }; + + public TextStringValueTypeConverter(PropertyEditorCollection propertyEditors) + : base(propertyEditors) + { + } + + public override Type GetPropertyValueType(IPublishedPropertyType propertyType) + => typeof(string); + + public override PropertyCacheLevel GetPropertyCacheLevel(IPublishedPropertyType propertyType) + => PropertyCacheLevel.Element; + + public override object? ConvertSourceToIntermediate(IPublishedElement owner, IPublishedPropertyType propertyType, object? source, bool preview) + => source as string; +} diff --git a/src/Umbraco.Core/PropertyEditors/ValueConverters/TimeValueTypeConverter.cs b/src/Umbraco.Core/PropertyEditors/ValueConverters/TimeValueTypeConverter.cs new file mode 100644 index 0000000000..c18890e22a --- /dev/null +++ b/src/Umbraco.Core/PropertyEditors/ValueConverters/TimeValueTypeConverter.cs @@ -0,0 +1,23 @@ +using Umbraco.Cms.Core.Models.PublishedContent; + +namespace Umbraco.Cms.Core.PropertyEditors.ValueConverters; + +[DefaultValueTypePropertyValueConverter] +public class TimeValueTypeConverter : ValueTypePropertyValueConverterBase +{ + protected override string[] SupportedValueTypes => new[] { ValueTypes.Time }; + + public TimeValueTypeConverter(PropertyEditorCollection propertyEditors) + : base(propertyEditors) + { + } + + public override Type GetPropertyValueType(IPublishedPropertyType propertyType) + => typeof(TimeSpan); + + public override PropertyCacheLevel GetPropertyCacheLevel(IPublishedPropertyType propertyType) + => PropertyCacheLevel.Element; + + public override object? ConvertSourceToIntermediate(IPublishedElement owner, IPublishedPropertyType propertyType, object? source, bool preview) + => source is DateTime dateTimeValue ? dateTimeValue.ToUniversalTime().TimeOfDay : null; +} diff --git a/src/Umbraco.Core/PropertyEditors/ValueConverters/ValueTypePropertyValueConverterBase.cs b/src/Umbraco.Core/PropertyEditors/ValueConverters/ValueTypePropertyValueConverterBase.cs new file mode 100644 index 0000000000..461de31812 --- /dev/null +++ b/src/Umbraco.Core/PropertyEditors/ValueConverters/ValueTypePropertyValueConverterBase.cs @@ -0,0 +1,18 @@ +using Umbraco.Cms.Core.Models.PublishedContent; +using Umbraco.Extensions; + +namespace Umbraco.Cms.Core.PropertyEditors.ValueConverters; + +public abstract class ValueTypePropertyValueConverterBase : PropertyValueConverterBase +{ + private readonly PropertyEditorCollection _propertyEditors; + + protected abstract string[] SupportedValueTypes { get; } + + protected ValueTypePropertyValueConverterBase(PropertyEditorCollection propertyEditors) + => _propertyEditors = propertyEditors; + + public override bool IsConverter(IPublishedPropertyType propertyType) + => _propertyEditors.TryGet(propertyType.EditorAlias, out IDataEditor? editor) + && SupportedValueTypes.InvariantContains(editor.GetValueEditor().ValueType); +} diff --git a/src/Umbraco.Core/PropertyEditors/ValueConverters/XmlValueTypeConverter.cs b/src/Umbraco.Core/PropertyEditors/ValueConverters/XmlValueTypeConverter.cs new file mode 100644 index 0000000000..bab85a25d9 --- /dev/null +++ b/src/Umbraco.Core/PropertyEditors/ValueConverters/XmlValueTypeConverter.cs @@ -0,0 +1,51 @@ +using System.Xml.Linq; +using Umbraco.Cms.Core.Models.PublishedContent; +using Umbraco.Cms.Core.PropertyEditors.DeliveryApi; + +namespace Umbraco.Cms.Core.PropertyEditors.ValueConverters; + +[DefaultValueTypePropertyValueConverter] +public class XmlValueTypeConverter : ValueTypePropertyValueConverterBase, IDeliveryApiPropertyValueConverter +{ + protected override string[] SupportedValueTypes => new[] { ValueTypes.Xml }; + + public XmlValueTypeConverter(PropertyEditorCollection propertyEditors) + : base(propertyEditors) + { + } + + public override Type GetPropertyValueType(IPublishedPropertyType propertyType) + => typeof(XDocument); + + public override PropertyCacheLevel GetPropertyCacheLevel(IPublishedPropertyType propertyType) + => PropertyCacheLevel.Element; + + public override object? ConvertSourceToIntermediate(IPublishedElement owner, IPublishedPropertyType propertyType, object? source, bool preview) + { + if (source is not string stringValue) + { + return null; + } + + try + { + return XDocument.Parse(stringValue); + } + catch + { + return null; + } + } + + public PropertyCacheLevel GetDeliveryApiPropertyCacheLevel(IPublishedPropertyType propertyType) + => GetPropertyCacheLevel(propertyType); + + public Type GetDeliveryApiPropertyValueType(IPublishedPropertyType propertyType) + => typeof(string); + + // System.Text.Json does not appreciate serializing XDocument because of parent/child node references. Let's settle for outputting the raw XML as a string, then. + public object? ConvertIntermediateToDeliveryApiObject(IPublishedElement owner, IPublishedPropertyType propertyType, PropertyCacheLevel referenceCacheLevel, object? inter, bool preview, bool expanding) + => inter is XDocument xDocumentValue + ? xDocumentValue.ToString(SaveOptions.DisableFormatting) + : null; +} diff --git a/tests/Umbraco.Tests.UnitTests/Umbraco.Core/PropertyEditors/PropertyEditorValueConverterTests.cs b/tests/Umbraco.Tests.UnitTests/Umbraco.Core/PropertyEditors/PropertyEditorValueConverterTests.cs index d046064af2..f84f08d8aa 100644 --- a/tests/Umbraco.Tests.UnitTests/Umbraco.Core/PropertyEditors/PropertyEditorValueConverterTests.cs +++ b/tests/Umbraco.Tests.UnitTests/Umbraco.Core/PropertyEditors/PropertyEditorValueConverterTests.cs @@ -1,11 +1,7 @@ // Copyright (c) Umbraco. // See LICENSE for more details. -using System.Collections.Generic; -using System.Linq; -using Microsoft.Extensions.Logging; using Moq; -using Newtonsoft.Json.Linq; using NUnit.Framework; using Umbraco.Cms.Core.Models; using Umbraco.Cms.Core.Models.PublishedContent; @@ -116,18 +112,12 @@ public class PropertyEditorValueConverterTests } [TestCase("1", 1)] - [TestCase("1", 1)] - [TestCase("0", 0)] [TestCase("0", 0)] [TestCase(null, 0)] - [TestCase(null, 0)] - [TestCase("-1", -1)] [TestCase("-1", -1)] [TestCase("1.65", 1.65)] - [TestCase("1.65", 1.65)] [TestCase("-1.65", -1.65)] - [TestCase("-1.65", -1.65)] - public void CanConvertDecimalAliasPropertyEditor(object value, double expected) + public void CanConvertDecimalAliasPropertyEditor(object value, decimal expected) { var converter = new DecimalValueConverter(); var inter = converter.ConvertSourceToIntermediate(null, null, value, false); @@ -136,18 +126,19 @@ public class PropertyEditorValueConverterTests Assert.AreEqual(expected, result); } - [Test] - public void CanConvertManifestBasedPropertyWithValueTypeJson() + [TestCase("100", 100)] + [TestCase("0", 0)] + [TestCase(null, 0)] + [TestCase("-100", -100)] + [TestCase("1.65", 2)] + [TestCase("-1.65", -2)] + [TestCase("something something", 0)] + public void CanConvertIntegerAliasPropertyEditor(object value, int expected) { - var valueEditor = Mock.Of(x => x.ValueType == ValueTypes.Json); - var dataEditor = Mock.Of(x => x.GetValueEditor() == valueEditor); - var propertyEditors = new PropertyEditorCollection(new DataEditorCollection(() => new[] { dataEditor })); - var propertyType = Mock.Of(x => x.EditorAlias == "My.Custom.Json"); + var converter = new IntegerValueConverter(); + var inter = converter.ConvertSourceToIntermediate(null, null, value, false); + var result = converter.ConvertIntermediateToObject(null, null, PropertyCacheLevel.Unknown, inter, false); - var valueConverter = new JsonValueConverter(propertyEditors, Mock.Of>()); - var inter = valueConverter.ConvertSourceToIntermediate(Mock.Of(), propertyType, "{\"message\": \"Hello, JSON\"}", false); - var result = valueConverter.ConvertIntermediateToObject(Mock.Of(), propertyType, PropertyCacheLevel.Element, inter, false) as JObject; - Assert.IsNotNull(result); - Assert.AreEqual("Hello, JSON", result["message"]!.Value()); + Assert.AreEqual(expected, result); } } diff --git a/tests/Umbraco.Tests.UnitTests/Umbraco.Core/PropertyEditors/PropertyEditorValueTypeConverterTests.cs b/tests/Umbraco.Tests.UnitTests/Umbraco.Core/PropertyEditors/PropertyEditorValueTypeConverterTests.cs new file mode 100644 index 0000000000..fb4bc40ea0 --- /dev/null +++ b/tests/Umbraco.Tests.UnitTests/Umbraco.Core/PropertyEditors/PropertyEditorValueTypeConverterTests.cs @@ -0,0 +1,221 @@ +using System.Xml.Linq; +using Microsoft.Extensions.Logging; +using Moq; +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; +using NUnit.Framework; +using Umbraco.Cms.Core.Models; +using Umbraco.Cms.Core.Models.PublishedContent; +using Umbraco.Cms.Core.PropertyEditors; +using Umbraco.Cms.Core.PropertyEditors.ValueConverters; + +namespace Umbraco.Cms.Tests.UnitTests.Umbraco.Core.PropertyEditors; + +[TestFixture] +public class PropertyEditorValueTypeConverterTests +{ + [TestCase("2023-09-26 13:14:15", true)] + [TestCase("2023-09-26T13:14:15", true)] + [TestCase("2023-09-26T00:00:00", true)] + [TestCase("2023-09-26", true)] + [TestCase("", false)] + [TestCase("Hello, world!", false)] + [TestCase(123456, false)] + [TestCase(null, false)] + public void CanConvertDateValueTypePropertyEditor(object? date, bool expectedSuccess) + { + var expectedResult = expectedSuccess ? DateTime.Parse((date as string)!) : DateTime.MinValue; + var supportedValueTypes = new[] { ValueTypes.DateTime, ValueTypes.Date }; + foreach (var valueType in supportedValueTypes) + { + var converter = new DateTimeValueTypeConverter(ValueTypePropertyEditorCollection(valueType)); + var propertyType = PropertyType(); + + Assert.IsTrue(converter.IsConverter(propertyType)); + + var result = converter.ConvertSourceToIntermediate(null, propertyType, date, false); + + Assert.AreEqual(expectedResult, result); + } + } + + [TestCase("1", 1)] + [TestCase("0", 0)] + [TestCase(null, 0)] + [TestCase("", 0)] + [TestCase("Hello, world!", 0)] + [TestCase("-1", -1)] + [TestCase("1.65", 1.65)] + [TestCase("-1.65", -1.65)] + public void CanConvertDecimalValueTypePropertyEditor(object value, decimal expected) + { + var propertyEditors = ValueTypePropertyEditorCollection(ValueTypes.Decimal); + var propertyType = PropertyType(); + + var converter = new DecimalValueTypeConverter(propertyEditors); + var inter = converter.ConvertSourceToIntermediate(Mock.Of(), propertyType, value, false); + + Assert.IsTrue(converter.IsConverter(propertyType)); + + var result = converter.ConvertIntermediateToObject(Mock.Of(), propertyType, PropertyCacheLevel.Element, inter, false); + Assert.IsTrue(result is decimal); + Assert.AreEqual(expected, result); + } + + [TestCase("100", 100)] + [TestCase("0", 0)] + [TestCase(null, 0)] + [TestCase("", 0)] + [TestCase("Hello, world!", 0)] + [TestCase("-100", -100)] + [TestCase("1.65", 2)] + [TestCase("-1.65", -2)] + public void CanConvertIntegerValueTypePropertyEditor(object value, int expected) + { + var propertyEditors = ValueTypePropertyEditorCollection(ValueTypes.Integer); + var propertyType = PropertyType(); + + var converter = new IntegerValueTypeConverter(propertyEditors); + var inter = converter.ConvertSourceToIntermediate(Mock.Of(), propertyType, value, false); + + Assert.IsTrue(converter.IsConverter(propertyType)); + + var result = converter.ConvertIntermediateToObject(Mock.Of(), propertyType, PropertyCacheLevel.Element, inter, false); + Assert.IsTrue(result is int); + Assert.AreEqual(expected, result); + } + + [TestCase("100", 100)] + [TestCase("0", 0)] + [TestCase(null, 0)] + [TestCase("", 0)] + [TestCase("Hello, world!", 0)] + [TestCase("-100", -100)] + [TestCase("1.65", 2)] + [TestCase("-1.65", -2)] + public void CanConvertBigintValueTypePropertyEditor(object value, long expected) + { + var propertyEditors = ValueTypePropertyEditorCollection(ValueTypes.Bigint); + var propertyType = PropertyType(); + + var converter = new BigintValueTypeConverter(propertyEditors); + var inter = converter.ConvertSourceToIntermediate(Mock.Of(), propertyType, value, false); + + Assert.IsTrue(converter.IsConverter(propertyType)); + + var result = converter.ConvertIntermediateToObject(Mock.Of(), propertyType, PropertyCacheLevel.Element, inter, false); + Assert.IsTrue(result is long); + Assert.AreEqual(expected, result); + } + + [TestCase("100", "100")] + [TestCase("0", "0")] + [TestCase(null, null)] + [TestCase("", "")] + [TestCase("Hello, world!", "Hello, world!")] + [TestCase(-100, null)] + [TestCase(1.65, null)] + public void CanConvertTextAndStringValueTypePropertyEditor(object? value, string? expected) + { + var scenarios = new[] { ValueTypes.Text, ValueTypes.String }; + foreach (var scenario in scenarios) + { + var propertyEditors = ValueTypePropertyEditorCollection(scenario); + var propertyType = PropertyType(); + + var converter = new TextStringValueTypeConverter(propertyEditors); + var inter = converter.ConvertSourceToIntermediate(Mock.Of(), propertyType, value, false); + + Assert.IsTrue(converter.IsConverter(propertyType)); + + var result = converter.ConvertIntermediateToObject(Mock.Of(), propertyType, PropertyCacheLevel.Element, inter, false); + Assert.AreEqual(expected, result); + } + } + + [TestCase("2023-01-01T03:04:00Z","03:04:00")] + [TestCase("2023-01-01T13:14:00Z", "13:14:00")] + [TestCase("2023-01-01T13:14:15Z", "13:14:15")] + [TestCase("2023-01-01T13:14:15.678Z", "13:14:15.678")] + [TestCase("", null)] + [TestCase("Hello, world!", null)] + [TestCase(123456, null)] + [TestCase(null, null)] + public void CanConvertTimeValueTypePropertyEditor(object? value, object? expectedTime) + { + var sourceValue = expectedTime is not null ? DateTime.Parse((value as string)!) : value; + TimeSpan? expectedResult = expectedTime is not null ? TimeSpan.Parse((expectedTime as string)!) : null; + var propertyEditors = ValueTypePropertyEditorCollection(ValueTypes.Time); + var converter = new TimeValueTypeConverter(propertyEditors); + var propertyType = PropertyType(); + + Assert.IsTrue(converter.IsConverter(propertyType)); + + var result = converter.ConvertSourceToIntermediate(null, propertyType, sourceValue, false); + Assert.AreEqual(expectedResult, result); + } + + [TestCase("test", true)] + [TestCase("child 1child 2", true)] + [TestCase("malformed XML", false)] + [TestCase("", false)] + [TestCase("Hello, world!", false)] + [TestCase(123456, false)] + [TestCase(null, false)] + public void CanConvertXmlValueTypePropertyEditor(object? value, bool expectsSuccess) + { + var propertyEditors = ValueTypePropertyEditorCollection(ValueTypes.Xml); + var converter = new XmlValueTypeConverter(propertyEditors); + var propertyType = PropertyType(); + + Assert.IsTrue(converter.IsConverter(propertyType)); + + var result = converter.ConvertSourceToIntermediate(null, propertyType, value, false) as XDocument; + if (expectsSuccess) + { + Assert.IsNotNull(result); + Assert.AreEqual(value, result.ToString(SaveOptions.DisableFormatting)); + } + else + { + Assert.IsNull(result); + } + } + + [TestCase("{\"message\":\"Hello, JSON\"}", true)] + [TestCase("{\"nested\":{\"message\":\"Hello, Nested\"}}", true)] + [TestCase("{\"nested\":{\"invalid JSON", false)] + [TestCase("", false)] + [TestCase("Hello, world!", false)] + [TestCase(123456, false)] + [TestCase(null, false)] + public void CanConvertJsonValueTypePropertyEditor(object? source, bool expectsSuccess) + { + var propertyEditors = ValueTypePropertyEditorCollection(ValueTypes.Json); + var converter = new JsonValueConverter(propertyEditors, Mock.Of>()); + var propertyType = PropertyType(); + + Assert.IsTrue(converter.IsConverter(propertyType)); + + var result = converter.ConvertSourceToIntermediate(null, propertyType, source, false) as JToken; + if (expectsSuccess) + { + Assert.IsNotNull(result); + Assert.AreEqual(source, result.ToString(Formatting.None)); + } + else + { + Assert.IsNull(result); + } + } + + private static PropertyEditorCollection ValueTypePropertyEditorCollection(string valueType) + { + var valueEditor = Mock.Of(x => x.ValueType == valueType); + var dataEditor = Mock.Of(x => x.GetValueEditor() == valueEditor && x.Alias == "My.Custom.Alias" && x.Type == EditorType.PropertyValue); + var propertyEditors = new PropertyEditorCollection(new DataEditorCollection(() => new[] { dataEditor })); + return propertyEditors; + } + + private static IPublishedPropertyType PropertyType() => Mock.Of(x => x.EditorAlias == "My.Custom.Alias"); +}