diff --git a/Directory.Build.props b/Directory.Build.props index acf73ef..24eb9cb 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -9,7 +9,7 @@ Domain Primitives ALTA Software llc. Copyright © 2024 ALTA Software llc. - 6.0.3 + 6.0.4 diff --git a/src/AltaSoft.DomainPrimitives.Generator/Helpers/MethodGeneratorHelper.cs b/src/AltaSoft.DomainPrimitives.Generator/Helpers/MethodGeneratorHelper.cs index 75e94cc..a6256d0 100644 --- a/src/AltaSoft.DomainPrimitives.Generator/Helpers/MethodGeneratorHelper.cs +++ b/src/AltaSoft.DomainPrimitives.Generator/Helpers/MethodGeneratorHelper.cs @@ -992,6 +992,7 @@ public static void GenerateIXmlSerializableMethods(GeneratorData data, SourceCod { "string" => "ReadElementContentAsString", "bool" => "ReadElementContentAsBoolean", + "DateOnly" => "ReadElementContentAsDateOnly", _ => $"ReadElementContentAs<{data.PrimitiveTypeFriendlyName}>" }; diff --git a/src/AltaSoft.DomainPrimitives/XmlReaderExt.cs b/src/AltaSoft.DomainPrimitives/XmlReaderExt.cs index ccf0981..a396f95 100644 --- a/src/AltaSoft.DomainPrimitives/XmlReaderExt.cs +++ b/src/AltaSoft.DomainPrimitives/XmlReaderExt.cs @@ -13,13 +13,33 @@ namespace AltaSoft.DomainPrimitives; public static class XmlReaderExt { /// - /// Reads the content of the current element as a object. + /// Reads the content of the current XML element as a value. /// - /// The XmlReader instance. - /// A byte object representing the value read from the element. + /// The type of value to parse, which must implement . + /// The instance. + /// + /// A value parsed from the current element's content. + /// [MethodImpl(MethodImplOptions.AggressiveInlining)] public static T ReadElementContentAs(this XmlReader reader) where T : IParsable { return T.Parse(reader.ReadElementContentAsString(), CultureInfo.InvariantCulture); } + + /// + /// Reads the content of the current XML element as a value. + /// + /// The instance. + /// + /// A value parsed from the current element's content. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static DateOnly ReadElementContentAsDateOnly(this XmlReader reader) + { + var str = reader.ReadElementContentAsString(); + if (DateOnly.TryParse(str, CultureInfo.InvariantCulture, out var result)) + return result; + + return DateOnly.FromDateTime(DateTime.Parse(str, CultureInfo.InvariantCulture)); + } } diff --git a/tests/AltaSoft.DomainPrimitives.Generator.Tests/DomainPrimitiveGeneratorTest.cs b/tests/AltaSoft.DomainPrimitives.Generator.Tests/DomainPrimitiveGeneratorTest.cs index 87d634b..6e0e4dd 100644 --- a/tests/AltaSoft.DomainPrimitives.Generator.Tests/DomainPrimitiveGeneratorTest.cs +++ b/tests/AltaSoft.DomainPrimitives.Generator.Tests/DomainPrimitiveGeneratorTest.cs @@ -829,6 +829,44 @@ public static PrimitiveValidationResult Validate(uint value) GenerateTypeConverters = false }); } + + [Fact] + public Task DateOnlyValue_GeneratesAllInterfacesAndConvertersWithXml() + { + const string source = """ + using System; + using System.Collections.Generic; + using System.Linq; + using System.Text; + using System.Threading.Tasks; + using AltaSoft.DomainPrimitives; + + namespace AltaSoft.DomainPrimitives; + + public readonly partial struct DateOnlyXmlValue : IDomainValue + { + /// + public static PrimitiveValidationResult Validate(DateOnly value) + { + if (value == default) + return "Invalid Value"; + + return PrimitiveValidationResult.Ok; + } + } + + """; + + return TestHelper.Verify(source, (_, x, _) => Assert.Equal(1, x.Count), new DomainPrimitiveGlobalOptions() + { + GenerateEntityFrameworkCoreValueConverters = false, + GenerateJsonConverters = false, + GenerateSwaggerConverters = false, + GenerateTypeConverters = false, + GenerateXmlSerialization = true + }); + } + public static class TestHelper { internal static Task Verify(string source, Action, List, GeneratorDriver>? additionalChecks = null, DomainPrimitiveGlobalOptions? options = null) diff --git a/tests/AltaSoft.DomainPrimitives.Generator.Tests/ModuleInitializer.cs b/tests/AltaSoft.DomainPrimitives.Generator.Tests/ModuleInitializer.cs index 54d40a5..185b002 100644 --- a/tests/AltaSoft.DomainPrimitives.Generator.Tests/ModuleInitializer.cs +++ b/tests/AltaSoft.DomainPrimitives.Generator.Tests/ModuleInitializer.cs @@ -1,4 +1,5 @@ using System.Runtime.CompilerServices; +using DiffEngine; namespace AltaSoft.DomainPrimitives.Generator.Tests; @@ -8,6 +9,6 @@ public static class ModuleInitializer public static void Init() { VerifySourceGenerators.Initialize(); - + DiffTools.UseOrder(DiffTool.VisualStudioCode); } } diff --git a/tests/AltaSoft.DomainPrimitives.Generator.Tests/Snapshots/DomainPrimitiveGeneratorTest.DateOnlyValue_GeneratesAllInterfacesAndConvertersWithXml#DateOnlyXmlValue.g.verified.cs b/tests/AltaSoft.DomainPrimitives.Generator.Tests/Snapshots/DomainPrimitiveGeneratorTest.DateOnlyValue_GeneratesAllInterfacesAndConvertersWithXml#DateOnlyXmlValue.g.verified.cs new file mode 100644 index 0000000..f1697c7 --- /dev/null +++ b/tests/AltaSoft.DomainPrimitives.Generator.Tests/Snapshots/DomainPrimitiveGeneratorTest.DateOnlyValue_GeneratesAllInterfacesAndConvertersWithXml#DateOnlyXmlValue.g.verified.cs @@ -0,0 +1,331 @@ +//HintName: DateOnlyXmlValue.g.cs +//------------------------------------------------------------------------------ +// +// This code was generated by 'AltaSoft DomainPrimitives Generator'. +// Changes to this file may cause incorrect behavior and will be lost if the code is regenerated. +// +//------------------------------------------------------------------------------ + +#nullable enable + +using System; +using System.Numerics; +using System.Diagnostics; +using System.Runtime.CompilerServices; +using AltaSoft.DomainPrimitives; +using System.Diagnostics.CodeAnalysis; +using System.Xml; +using System.Xml.Schema; +using System.Xml.Serialization; + +namespace AltaSoft.DomainPrimitives; + +[UnderlyingPrimitiveType(typeof(DateOnly))] +[DebuggerDisplay("{_value}")] +public readonly partial struct DateOnlyXmlValue : IEquatable + , IComparable + , IComparable + , IComparisonOperators + , ISpanFormattable + , IParsable + , IConvertible + , IXmlSerializable +#if NET8_0_OR_GREATER + , IUtf8SpanFormattable +#endif +{ + /// + public Type GetUnderlyingPrimitiveType() => typeof(DateOnly); + /// + public object GetUnderlyingPrimitiveValue() => (DateOnly)this; + + private DateOnly _valueOrThrow => _isInitialized ? _value : throw new InvalidDomainValueException("The domain value has not been initialized", this); + [DebuggerBrowsable(DebuggerBrowsableState.Never)] + private readonly DateOnly _value; + [DebuggerBrowsable(DebuggerBrowsableState.Never)] + private readonly bool _isInitialized; + + /// + /// Initializes a new instance of the class by validating the specified value using static method. + /// + /// The value to be validated. + public DateOnlyXmlValue(DateOnly value) : this(value, true) + { + } + + private DateOnlyXmlValue(DateOnly value, bool validate) + { + if (validate) + { + ValidateOrThrow(value); + } + _value = value; + _isInitialized = true; + } + + /// + [Obsolete("Domain primitive cannot be created using empty Constructor", true)] + public DateOnlyXmlValue() + { + } + + /// + /// Tries to create an instance of AsciiString from the specified value. + /// + /// The value to create DateOnlyXmlValue from + /// When this method returns, contains the created DateOnlyXmlValue if the conversion succeeded, or null if the conversion failed. + /// true if the conversion succeeded; otherwise, false. + public static bool TryCreate(DateOnly value, [NotNullWhen(true)] out DateOnlyXmlValue? result) + { + return TryCreate(value, out result, out _); + } + + /// + /// Tries to create an instance of AsciiString from the specified value. + /// + /// The value to create DateOnlyXmlValue from + /// When this method returns, contains the created DateOnlyXmlValue if the conversion succeeded, or null if the conversion failed. + /// When this method returns, contains the error message if the conversion failed; otherwise, null. + /// true if the conversion succeeded; otherwise, false. + public static bool TryCreate(DateOnly value, [NotNullWhen(true)] out DateOnlyXmlValue? result, [NotNullWhen(false)] out string? errorMessage) + { + var validationResult = Validate(value); + if (!validationResult.IsValid) + { + result = null; + errorMessage = validationResult.ErrorMessage; + return false; + } + + result = new (value, false); + errorMessage = null; + return true; + } + + /// + /// Validates the specified value and throws an exception if it is not valid. + /// + /// The value to validate + /// Thrown when the value is not valid. + public void ValidateOrThrow(DateOnly value) + { + var result = Validate(value); + if (!result.IsValid) + throw new InvalidDomainValueException(result.ErrorMessage, this); + } + + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public override bool Equals(object? obj) => obj is DateOnlyXmlValue other && Equals(other); + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public bool Equals(DateOnlyXmlValue other) + { + if (!_isInitialized || !other._isInitialized) + return false; + return _value.Equals(other._value); + } + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static bool operator ==(DateOnlyXmlValue left, DateOnlyXmlValue right) => left.Equals(right); + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static bool operator !=(DateOnlyXmlValue left, DateOnlyXmlValue right) => !(left == right); + + /// + public int CompareTo(object? obj) + { + if (obj is null) + return 1; + + if (obj is DateOnlyXmlValue c) + return CompareTo(c); + + throw new ArgumentException("Object is not a DateOnlyXmlValue", nameof(obj)); + } + + /// + public int CompareTo(DateOnlyXmlValue other) + { + if (!other._isInitialized) + return 1; + if (!_isInitialized) + return -1; + return _value.CompareTo(other._value); + } + + /// + /// Implicit conversion from to + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static implicit operator DateOnlyXmlValue(DateOnly value) => new(value); + + /// + /// Implicit conversion from (nullable) to (nullable) + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + [return: NotNullIfNotNull(nameof(value))] + public static implicit operator DateOnlyXmlValue?(DateOnly? value) => value is null ? null : new(value.Value); + + /// + /// Implicit conversion from to + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static implicit operator DateOnly(DateOnlyXmlValue value) => (DateOnly)value._valueOrThrow; + + /// + /// Implicit conversion from (nullable) to (nullable) + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + [return: NotNullIfNotNull(nameof(value))] + public static implicit operator DateOnly?(DateOnlyXmlValue? value) => value is null ? null : (DateOnly?)value.Value._valueOrThrow; + + /// + /// Implicit conversion from to + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static implicit operator DateTime(DateOnlyXmlValue value) => ((DateOnly)value._valueOrThrow).ToDateTime(); + + /// + /// Implicit conversion from to + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static implicit operator DateOnlyXmlValue(DateTime value) => DateOnly.FromDateTime(value); + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static bool operator <(DateOnlyXmlValue left, DateOnlyXmlValue right) => left._valueOrThrow < right._valueOrThrow; + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static bool operator <=(DateOnlyXmlValue left, DateOnlyXmlValue right) => left._valueOrThrow <= right._valueOrThrow; + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static bool operator >(DateOnlyXmlValue left, DateOnlyXmlValue right) => left._valueOrThrow > right._valueOrThrow; + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static bool operator >=(DateOnlyXmlValue left, DateOnlyXmlValue right) => left._valueOrThrow >= right._valueOrThrow; + + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static DateOnlyXmlValue Parse(string s, IFormatProvider? provider) => DateOnly.Parse(s, provider); + + /// + public static bool TryParse([NotNullWhen(true)] string? s, IFormatProvider? provider, [MaybeNullWhen(false)] out DateOnlyXmlValue result) + { + if (!DateOnly.TryParse(s, provider, out var value)) + { + result = default; + return false; + } + + if (TryCreate(value, out var created)) + { + result = created.Value; + return true; + } + + result = default; + return false; + } + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public string ToString(string? format, IFormatProvider? formatProvider) => _valueOrThrow.ToString(format, formatProvider); + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public bool TryFormat(Span destination, out int charsWritten, ReadOnlySpan format, IFormatProvider? provider) + { + return ((ISpanFormattable)_valueOrThrow).TryFormat(destination, out charsWritten, format, provider); + } + + +#if NET8_0_OR_GREATER + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public bool TryFormat(Span utf8Destination, out int bytesWritten, ReadOnlySpan format, IFormatProvider? provider) + { + return ((IUtf8SpanFormattable)_valueOrThrow).TryFormat(utf8Destination, out bytesWritten, format, provider); + } +#endif + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public override int GetHashCode() => _valueOrThrow.GetHashCode(); + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + TypeCode IConvertible.GetTypeCode() => ((IConvertible)((DateOnly)_valueOrThrow).ToDateTime()).GetTypeCode(); + + /// + bool IConvertible.ToBoolean(IFormatProvider? provider) => ((IConvertible)((DateOnly)_valueOrThrow).ToDateTime()).ToBoolean(provider); + + /// + byte IConvertible.ToByte(IFormatProvider? provider) => ((IConvertible)((DateOnly)_valueOrThrow).ToDateTime()).ToByte(provider); + + /// + char IConvertible.ToChar(IFormatProvider? provider) => ((IConvertible)((DateOnly)_valueOrThrow).ToDateTime()).ToChar(provider); + + /// + DateTime IConvertible.ToDateTime(IFormatProvider? provider) => ((IConvertible)((DateOnly)_valueOrThrow).ToDateTime()).ToDateTime(provider); + + /// + decimal IConvertible.ToDecimal(IFormatProvider? provider) => ((IConvertible)((DateOnly)_valueOrThrow).ToDateTime()).ToDecimal(provider); + + /// + double IConvertible.ToDouble(IFormatProvider? provider) => ((IConvertible)((DateOnly)_valueOrThrow).ToDateTime()).ToDouble(provider); + + /// + short IConvertible.ToInt16(IFormatProvider? provider) => ((IConvertible)((DateOnly)_valueOrThrow).ToDateTime()).ToInt16(provider); + + /// + int IConvertible.ToInt32(IFormatProvider? provider) => ((IConvertible)((DateOnly)_valueOrThrow).ToDateTime()).ToInt32(provider); + + /// + long IConvertible.ToInt64(IFormatProvider? provider) => ((IConvertible)((DateOnly)_valueOrThrow).ToDateTime()).ToInt64(provider); + + /// + sbyte IConvertible.ToSByte(IFormatProvider? provider) => ((IConvertible)((DateOnly)_valueOrThrow).ToDateTime()).ToSByte(provider); + + /// + float IConvertible.ToSingle(IFormatProvider? provider) => ((IConvertible)((DateOnly)_valueOrThrow).ToDateTime()).ToSingle(provider); + + /// + string IConvertible.ToString(IFormatProvider? provider) => ((IConvertible)((DateOnly)_valueOrThrow).ToDateTime()).ToString(provider); + + /// + object IConvertible.ToType(Type conversionType, IFormatProvider? provider) => ((IConvertible)((DateOnly)_valueOrThrow).ToDateTime()).ToType(conversionType, provider); + + /// + ushort IConvertible.ToUInt16(IFormatProvider? provider) => ((IConvertible)((DateOnly)_valueOrThrow).ToDateTime()).ToUInt16(provider); + + /// + uint IConvertible.ToUInt32(IFormatProvider? provider) => ((IConvertible)((DateOnly)_valueOrThrow).ToDateTime()).ToUInt32(provider); + + /// + ulong IConvertible.ToUInt64(IFormatProvider? provider) => ((IConvertible)((DateOnly)_valueOrThrow).ToDateTime()).ToUInt64(provider); + + /// + public XmlSchema? GetSchema() => null; + + /// + public void ReadXml(XmlReader reader) + { + var value = reader.ReadElementContentAsDateOnly(); + ValidateOrThrow(value); + System.Runtime.CompilerServices.Unsafe.AsRef(in _value) = value; + System.Runtime.CompilerServices.Unsafe.AsRef(in _isInitialized) = true; + } + + /// + public void WriteXml(XmlWriter writer) => writer.WriteValue(((DateOnly)_valueOrThrow).ToXmlString()); + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public override string ToString() => _valueOrThrow.ToString(); +} diff --git a/tests/AltaSoft.DomainPrimitives.UnitTests/DateOnlyConversionTests.cs b/tests/AltaSoft.DomainPrimitives.UnitTests/DateOnlyConversionTests.cs new file mode 100644 index 0000000..bd01d39 --- /dev/null +++ b/tests/AltaSoft.DomainPrimitives.UnitTests/DateOnlyConversionTests.cs @@ -0,0 +1,31 @@ +using System.Xml; +using Xunit; + +namespace AltaSoft.DomainPrimitives.UnitTests; + +public class DateOnlyConversionTests +{ + [Fact] + public void ReadElementContentAsDateOnly_WithDateOnlyString_ReturnsDateOnly() + { + var xml = "2024-04-01"; + using var reader = XmlReader.Create(new StringReader(xml)); + reader.ReadToFollowing("d"); + + var result = reader.ReadElementContentAsDateOnly(); + + Assert.Equal(new DateOnly(2024, 4, 1), result); + } + + [Fact] + public void ReadElementContentAsDateOnly_WithTimezoneString_ReturnsDateOnly() + { + var xml = "2024-04-01T12:34:56+03:00"; + using var reader = XmlReader.Create(new System.IO.StringReader(xml)); + reader.ReadToFollowing("d"); + + var result = reader.ReadElementContentAsDateOnly(); + + Assert.Equal(new DateOnly(2024, 4, 1), result); + } +}