From 6111db39b0f4f5de49e125308966293609bf3720 Mon Sep 17 00:00:00 2001 From: Stijn Herreman Date: Thu, 24 Aug 2023 22:25:15 +0200 Subject: [PATCH 1/2] Implement DateOnly and TimeOnly converters. --- .../Serialization/DateOnlyConverterTests.cs | 347 ++++++++++++++++++ .../Serialization/TimeOnlyConverterTests.cs | 335 +++++++++++++++++ .../Converters/DateOnlyConverter.cs | 96 +++++ .../Converters/TimeOnlyConverter.cs | 96 +++++ YamlDotNet/Serialization/SerializerBuilder.cs | 4 + 5 files changed, 878 insertions(+) create mode 100644 YamlDotNet.Test/Serialization/DateOnlyConverterTests.cs create mode 100644 YamlDotNet.Test/Serialization/TimeOnlyConverterTests.cs create mode 100644 YamlDotNet/Serialization/Converters/DateOnlyConverter.cs create mode 100644 YamlDotNet/Serialization/Converters/TimeOnlyConverter.cs diff --git a/YamlDotNet.Test/Serialization/DateOnlyConverterTests.cs b/YamlDotNet.Test/Serialization/DateOnlyConverterTests.cs new file mode 100644 index 00000000..dbfce3b0 --- /dev/null +++ b/YamlDotNet.Test/Serialization/DateOnlyConverterTests.cs @@ -0,0 +1,347 @@ +// This file is part of YamlDotNet - A .NET library for YAML. +// Copyright (c) Antoine Aubry and contributors +// +// Permission is hereby granted, free of charge, to any person obtaining a copy of +// this software and associated documentation files (the "Software"), to deal in +// the Software without restriction, including without limitation the rights to +// use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies +// of the Software, and to permit persons to whom the Software is furnished to do +// so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. + +#if NET6_0_OR_GREATER +using System; +using System.Globalization; +using FakeItEasy; +using FluentAssertions; +using Xunit; +using YamlDotNet.Core; +using YamlDotNet.Core.Events; +using YamlDotNet.Serialization; +using YamlDotNet.Serialization.Converters; +using YamlDotNet.Serialization.NamingConventions; + +namespace YamlDotNet.Test.Serialization +{ + /// + /// This represents the test entity for the class. + /// + public class DateOnlyConverterTests + { + /// + /// Tests whether the Accepts() method should return expected result or not. + /// + /// to check. + /// Expected result. + [Theory] + [InlineData(typeof(DateOnly), true)] + [InlineData(typeof(string), false)] + public void Given_Type_Accepts_ShouldReturn_Result(Type type, bool expected) + { + var converter = new DateOnlyConverter(); + + var result = converter.Accepts(type); + + result.Should().Be(expected); + } + + /// + /// Tests whether the ReadYaml() method should throw or not. + /// + /// Year value. + /// Month value. + /// Day value. + /// The converter instance uses its default parameter of "d". + [Theory] + [InlineData(2016, 12, 31)] + public void Given_Yaml_WithInvalidDateTimeFormat_WithDefaultParameter_ReadYaml_ShouldThrow_Exception(int year, int month, int day) + { + var yaml = $"{year}-{month:00}-{day:00}"; + + var parser = A.Fake(); + A.CallTo(() => parser.Current).ReturnsLazily(() => new Scalar(yaml)); + + var converter = new DateOnlyConverter(); + + Action action = () => { converter.ReadYaml(parser, typeof(DateOnly)); }; + + action.ShouldThrow(); + } + + /// + /// Tests whether the ReadYaml() method should return expected result or not. + /// + /// Year value. + /// Month value. + /// Day value. + /// The converter instance uses its default parameter of "d". + [Theory] + [InlineData(2016, 12, 31)] + public void Given_Yaml_WithValidDateTimeFormat_WithDefaultParameter_ReadYaml_ShouldReturn_Result(int year, int month, int day) + { + var yaml = $"{month:00}/{day:00}/{year}"; // This is the DateOnly format of "d" + + var parser = A.Fake(); + A.CallTo(() => parser.Current).ReturnsLazily(() => new Scalar(yaml)); + + var converter = new DateOnlyConverter(); + + var result = converter.ReadYaml(parser, typeof(DateOnly)); + + result.Should().BeOfType(); + ((DateOnly)result).Year.Should().Be(year); + ((DateOnly)result).Month.Should().Be(month); + ((DateOnly)result).Day.Should().Be(day); + } + + /// + /// Tests whether the ReadYaml() method should return expected result or not. + /// + /// Year value. + /// Month value. + /// Day value. + /// Designated date/time format 1. + /// Designated date/time format 2. + [Theory] + [InlineData(2016, 12, 31, "yyyy-MM-dd", "yyyy/MM/dd")] + public void Given_Yaml_WithValidDateTimeFormat_ReadYaml_ShouldReturn_Result(int year, int month, int day, string format1, string format2) + { + var yaml = $"{year}-{month:00}-{day:00}"; + + var parser = A.Fake(); + A.CallTo(() => parser.Current).ReturnsLazily(() => new Scalar(yaml)); + + var converter = new DateOnlyConverter(formats: new[] { format1, format2 }); + + var result = converter.ReadYaml(parser, typeof(DateOnly)); + + result.Should().BeOfType(); + ((DateOnly)result).Year.Should().Be(year); + ((DateOnly)result).Month.Should().Be(month); + ((DateOnly)result).Day.Should().Be(day); + } + + /// + /// Tests whether the ReadYaml() method should return expected result or not. + /// + /// Year value. + /// Month value. + /// Day value. + /// Designated date/time format 1. + /// Designated date/time format 2. + [Theory] + [InlineData(2016, 12, 31, "yyyy-MM-dd", "yyyy/MM/dd")] + public void Given_Yaml_WithSpecificCultureAndValidDateTimeFormat_ReadYaml_ShouldReturn_Result(int year, int month, int day, string format1, string format2) + { + var yaml = $"{year}-{month:00}-{day:00}"; + + var parser = A.Fake(); + A.CallTo(() => parser.Current).ReturnsLazily(() => new Scalar(yaml)); + + var culture = new CultureInfo("ko-KR"); // Sample specific culture + var converter = new DateOnlyConverter(provider: culture, formats: new[] { format1, format2 }); + + var result = converter.ReadYaml(parser, typeof(DateOnly)); + + result.Should().BeOfType(); + ((DateOnly)result).Year.Should().Be(year); + ((DateOnly)result).Month.Should().Be(month); + ((DateOnly)result).Day.Should().Be(day); + } + + /// + /// Tests whether the ReadYaml() method should return expected result or not. + /// + /// Date/Time format. + /// Date/Time value. + [Theory] + [InlineData("d", "01/11/2017")] + [InlineData("D", "Wednesday, 11 January 2017")] + [InlineData("f", "Wednesday, 11 January 2017 02:36")] + [InlineData("F", "Wednesday, 11 January 2017 02:36:16")] + [InlineData("g", "01/11/2017 02:36")] + [InlineData("G", "01/11/2017 02:36:16")] + [InlineData("M", "January 11")] + [InlineData("s", "2017-01-11T02:36:16")] + [InlineData("u", "2017-01-11 02:36:16Z")] + [InlineData("Y", "2017 January")] + public void Given_Yaml_WithDateTimeFormat_ReadYaml_ShouldReturn_Result(string format, string value) + { + var expected = DateOnly.ParseExact(value, format, CultureInfo.InvariantCulture); + var converter = new DateOnlyConverter(formats: new[] { "d", "D", "f", "F", "g", "G", "M", "s", "u", "Y" }); + + var parser = A.Fake(); + A.CallTo(() => parser.Current).ReturnsLazily(() => new Scalar(value)); + + var result = converter.ReadYaml(parser, typeof(DateOnly)); + + result.Should().Be(expected); + } + + /// + /// Tests whether the ReadYaml() method should return expected result or not. + /// + /// Date/Time format. + /// Locale value. + /// Date/Time value. + [Theory] + [InlineData("d", "fr-FR", "13/01/2017")] + [InlineData("D", "fr-FR", "vendredi 13 janvier 2017")] + [InlineData("f", "fr-FR", "vendredi 13 janvier 2017 05:25")] + [InlineData("F", "fr-FR", "vendredi 13 janvier 2017 05:25:08")] + [InlineData("g", "fr-FR", "13/01/2017 05:25")] + [InlineData("G", "fr-FR", "13/01/2017 05:25:08")] + [InlineData("M", "fr-FR", "13 janvier")] + [InlineData("s", "fr-FR", "2017-01-13T05:25:08")] + [InlineData("u", "fr-FR", "2017-01-13 05:25:08Z")] + [InlineData("Y", "fr-FR", "janvier 2017")] + // [InlineData("d", "ko-KR", "2017-01-13")] + [InlineData("D", "ko-KR", "2017년 1월 13일 금요일")] + // [InlineData("f", "ko-KR", "2017년 1월 13일 금요일 오전 5:32")] + // [InlineData("F", "ko-KR", "2017년 1월 13일 금요일 오전 5:32:06")] + // [InlineData("g", "ko-KR", "2017-01-13 오전 5:32")] + // [InlineData("G", "ko-KR", "2017-01-13 오전 5:32:06")] + [InlineData("M", "ko-KR", "1월 13일")] + [InlineData("s", "ko-KR", "2017-01-13T05:32:06")] + [InlineData("u", "ko-KR", "2017-01-13 05:32:06Z")] + [InlineData("Y", "ko-KR", "2017년 1월")] + public void Given_Yaml_WithLocaleAndDateTimeFormat_ReadYaml_ShouldReturn_Result(string format, string locale, string value) + { + var culture = new CultureInfo(locale); + + var expected = default(DateOnly); + try + { + expected = DateOnly.ParseExact(value, format, culture); + } + catch (Exception ex) + { + var message = string.Format("Failed to parse the test argument to DateOnly. The expected date/time format should look like this: '{0}'", DateTime.Now.ToString(format, culture)); + throw new Exception(message, ex); + } + + var converter = new DateOnlyConverter(provider: culture, formats: new[] { "d", "D", "f", "F", "g", "G", "M", "s", "u", "Y" }); + + var parser = A.Fake(); + A.CallTo(() => parser.Current).ReturnsLazily(() => new Scalar(value)); + + var result = converter.ReadYaml(parser, typeof(DateOnly)); + + result.Should().Be(expected); + } + + /// + /// Tests whether the WriteYaml method should return expected result or not. + /// + /// Year value. + /// Month value. + /// Day value. + /// The converter instance uses its default parameter of "d". + [Theory] + [InlineData(2016, 12, 31)] + public void Given_Values_WriteYaml_ShouldReturn_Result(int year, int month, int day) + { + var dateOnly = new DateOnly(year, month, day); + var formatted = dateOnly.ToString("d", CultureInfo.InvariantCulture); + var obj = new TestObject() { DateOnly = dateOnly }; + + var builder = new SerializerBuilder(); + builder.WithNamingConvention(CamelCaseNamingConvention.Instance); + builder.WithTypeConverter(new DateOnlyConverter()); + + var serialiser = builder.Build(); + + var serialised = serialiser.Serialize(obj); + + serialised.Should().ContainEquivalentOf($"dateonly: {formatted}"); + } + + /// + /// Tests whether the WriteYaml method should return expected result or not. + /// + /// Year value. + /// Month value. + /// Day value. + /// Locale value. + /// The converter instance uses its default parameter of "d". + [Theory] + [InlineData(2016, 12, 31, "es-ES")] + [InlineData(2016, 12, 31, "ko-KR")] + public void Given_Values_WithLocale_WriteYaml_ShouldReturn_Result(int year, int month, int day, string locale) + { + var dateOnly = new DateOnly(year, month, day); + var culture = new CultureInfo(locale); + var formatted = dateOnly.ToString("d", culture); + var obj = new TestObject() { DateOnly = dateOnly }; + + var builder = new SerializerBuilder(); + builder.WithNamingConvention(CamelCaseNamingConvention.Instance); + builder.WithTypeConverter(new DateOnlyConverter(provider: culture)); + + var serialiser = builder.Build(); + + var serialised = serialiser.Serialize(obj); + + serialised.Should().ContainEquivalentOf($"dateonly: {formatted}"); + } + + /// + /// Tests whether the WriteYaml method should return expected result or not. + /// + /// Year value. + /// Month value. + /// Day value. + /// The converter instance uses its default parameter of "d". + [Theory] + [InlineData(2016, 12, 31)] + public void Given_Values_WithFormats_WriteYaml_ShouldReturn_Result_WithFirstFormat(int year, int month, int day) + { + var dateOnly = new DateOnly(year, month, day); + var format = "yyyy-MM-dd"; + var formatted = dateOnly.ToString(format, CultureInfo.InvariantCulture); + var obj = new TestObject() { DateOnly = dateOnly }; + + var builder = new SerializerBuilder(); + builder.WithNamingConvention(CamelCaseNamingConvention.Instance); + builder.WithTypeConverter(new DateOnlyConverter(formats: new[] { format, "d" })); + + var serialiser = builder.Build(); + + var serialised = serialiser.Serialize(obj); + + serialised.Should().ContainEquivalentOf($"dateonly: {formatted}"); + } + + [Fact] + public void JsonCompatible_EncaseDateOnlyInDoubleQuotes() + { + var serializer = new SerializerBuilder().JsonCompatible().Build(); + var testObject = new TestObject { DateOnly = new DateOnly(2023, 01, 14) }; + var actual = serializer.Serialize(testObject); + + actual.TrimNewLines().Should().ContainEquivalentOf("{\"DateOnly\": \"01/14/2023\"}"); + } + + /// + /// This represents the test object entity. + /// + private class TestObject + { + /// + /// Gets or sets the value. + /// + public DateOnly DateOnly { get; set; } + } + } +} +#endif diff --git a/YamlDotNet.Test/Serialization/TimeOnlyConverterTests.cs b/YamlDotNet.Test/Serialization/TimeOnlyConverterTests.cs new file mode 100644 index 00000000..7d1be635 --- /dev/null +++ b/YamlDotNet.Test/Serialization/TimeOnlyConverterTests.cs @@ -0,0 +1,335 @@ +// This file is part of YamlDotNet - A .NET library for YAML. +// Copyright (c) Antoine Aubry and contributors +// +// Permission is hereby granted, free of charge, to any person obtaining a copy of +// this software and associated documentation files (the "Software"), to deal in +// the Software without restriction, including without limitation the rights to +// use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies +// of the Software, and to permit persons to whom the Software is furnished to do +// so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. + +#if NET6_0_OR_GREATER +using System; +using System.Globalization; +using FakeItEasy; +using FluentAssertions; +using Xunit; +using YamlDotNet.Core; +using YamlDotNet.Core.Events; +using YamlDotNet.Serialization; +using YamlDotNet.Serialization.Converters; +using YamlDotNet.Serialization.NamingConventions; + +namespace YamlDotNet.Test.Serialization +{ + /// + /// This represents the test entity for the class. + /// + public class TimeOnlyConverterTests + { + /// + /// Tests whether the Accepts() method should return expected result or not. + /// + /// to check. + /// Expected result. + [Theory] + [InlineData(typeof(TimeOnly), true)] + [InlineData(typeof(string), false)] + public void Given_Type_Accepts_ShouldReturn_Result(Type type, bool expected) + { + var converter = new TimeOnlyConverter(); + + var result = converter.Accepts(type); + + result.Should().Be(expected); + } + + /// + /// Tests whether the ReadYaml() method should throw or not. + /// + /// Hour value. + /// Minute value. + /// Second value. + /// The converter instance uses its default parameter of "T". + [Theory] + [InlineData(6, 12, 31)] + public void Given_Yaml_WithInvalidDateTimeFormat_WithDefaultParameters_ReadYaml_ShouldThrow_Exception(int hour, int minute, int second) + { + var yaml = $"{hour:00}-{minute:00}-{second:00}"; + + var parser = A.Fake(); + A.CallTo(() => parser.Current).ReturnsLazily(() => new Scalar(yaml)); + + var converter = new TimeOnlyConverter(); + + Action action = () => { converter.ReadYaml(parser, typeof(TimeOnly)); }; + + action.ShouldThrow(); + } + + /// + /// Tests whether the ReadYaml() method should return expected result or not. + /// + /// Hour value. + /// Minute value. + /// Second value. + /// The converter instance uses its default parameter of "T". + [Theory] + [InlineData(6, 12, 31)] + public void Given_Yaml_WithValidDateTimeFormat_WithDefaultParameters_ReadYaml_ShouldReturn_Result(int hour, int minute, int second) + { + var yaml = $"{hour:00}:{minute:00}:{second:00}"; // This is the DateTime format of "T" + + var parser = A.Fake(); + A.CallTo(() => parser.Current).ReturnsLazily(() => new Scalar(yaml)); + + var converter = new TimeOnlyConverter(); + + var result = converter.ReadYaml(parser, typeof(TimeOnly)); + + result.Should().BeOfType(); + ((TimeOnly)result).Hour.Should().Be(hour); + ((TimeOnly)result).Minute.Should().Be(minute); + ((TimeOnly)result).Second.Should().Be(second); + } + + /// + /// Tests whether the ReadYaml() method should return expected result or not. + /// + /// Hour value. + /// Minute value. + /// Second value. + /// Designated date/time format 1. + /// Designated date/time format 2. + [Theory] + [InlineData(6, 12, 31, "HH-mm-ss", "HH:mm:ss")] + public void Given_Yaml_WithValidDateTimeFormat_ReadYaml_ShouldReturn_Result(int hour, int minute, int second, string format1, string format2) + { + var yaml = $"{hour:00}-{minute:00}-{second:00}"; + + var parser = A.Fake(); + A.CallTo(() => parser.Current).ReturnsLazily(() => new Scalar(yaml)); + + var converter = new TimeOnlyConverter(formats: new[] { format1, format2 }); + + var result = converter.ReadYaml(parser, typeof(TimeOnly)); + + result.Should().BeOfType(); + ((TimeOnly)result).Hour.Should().Be(6); + ((TimeOnly)result).Minute.Should().Be(12); + ((TimeOnly)result).Second.Should().Be(31); + } + + /// + /// Tests whether the ReadYaml() method should return expected result or not. + /// + /// Hour value. + /// Minute value. + /// Second value. + /// Designated date/time format 1. + /// Designated date/time format 2. + [Theory] + [InlineData(6, 12, 31, "HH-mm-ss", "HH:mm:ss")] + public void Given_Yaml_WithSpecificCultureAndValidDateTimeFormat_ReadYaml_ShouldReturn_Result(int hour, int minute, int second, string format1, string format2) + { + var yaml = $"{hour:00}-{minute:00}-{second:00}"; + + var parser = A.Fake(); + A.CallTo(() => parser.Current).ReturnsLazily(() => new Scalar(yaml)); + + var culture = new CultureInfo("ko-KR"); // Sample specific culture + var converter = new TimeOnlyConverter(provider: culture, formats: new[] { format1, format2 }); + + var result = converter.ReadYaml(parser, typeof(TimeOnly)); + + result.Should().BeOfType(); + ((TimeOnly)result).Hour.Should().Be(6); + ((TimeOnly)result).Minute.Should().Be(12); + ((TimeOnly)result).Second.Should().Be(31); + } + + /// + /// Tests whether the ReadYaml() method should return expected result or not. + /// + /// Date/Time format. + /// Date/Time value. + [Theory] + [InlineData("g", "01/11/2017 02:36")] + [InlineData("G", "01/11/2017 02:36:16")] + [InlineData("s", "2017-01-11T02:36:16")] + [InlineData("t", "02:36")] + [InlineData("T", "02:36:16")] + [InlineData("u", "2017-01-11 02:36:16Z")] + public void Given_Yaml_WithTimeFormat_ReadYaml_ShouldReturn_Result(string format, string value) + { + var expected = TimeOnly.ParseExact(value, format, CultureInfo.InvariantCulture); + var converter = new TimeOnlyConverter(formats: new[] { "g", "G", "s", "t", "T", "u" }); + + var parser = A.Fake(); + A.CallTo(() => parser.Current).ReturnsLazily(() => new Scalar(value)); + + var result = converter.ReadYaml(parser, typeof(TimeOnly)); + + result.Should().Be(expected); + } + + /// + /// Tests whether the ReadYaml() method should return expected result or not. + /// + /// Date/Time format. + /// Locale value. + /// Date/Time value. + [Theory] + [InlineData("g", "fr-FR", "13/01/2017 05:25")] + [InlineData("G", "fr-FR", "13/01/2017 05:25:08")] + [InlineData("s", "fr-FR", "2017-01-13T05:25:08")] + [InlineData("t", "fr-FR", "05:25")] + [InlineData("T", "fr-FR", "05:25:08")] + [InlineData("u", "fr-FR", "2017-01-13 05:25:08Z")] + // [InlineData("g", "ko-KR", "2017-01-13 오전 5:32")] + // [InlineData("G", "ko-KR", "2017-01-13 오전 5:32:06")] + [InlineData("s", "ko-KR", "2017-01-13T05:32:06")] + // [InlineData("t", "ko-KR", "오전 5:32")] + // [InlineData("T", "ko-KR", "오전 5:32:06")] + [InlineData("u", "ko-KR", "2017-01-13 05:32:06Z")] + public void Given_Yaml_WithLocaleAndTimeFormat_ReadYaml_ShouldReturn_Result(string format, string locale, string value) + { + var culture = new CultureInfo(locale); + + var expected = default(TimeOnly); + try + { + expected = TimeOnly.ParseExact(value, format, culture); + } + catch (Exception ex) + { + var message = string.Format("Failed to parse the test argument to TimeOnly. The expected date/time format should look like this: '{0}'", DateTime.Now.ToString(format, culture)); + throw new Exception(message, ex); + } + + var converter = new TimeOnlyConverter(provider: culture, formats: new[] { "g", "G", "s", "t", "T", "u" }); + + var parser = A.Fake(); + A.CallTo(() => parser.Current).ReturnsLazily(() => new Scalar(value)); + + var result = converter.ReadYaml(parser, typeof(TimeOnly)); + + result.Should().Be(expected); + } + + /// + /// Tests whether the WriteYaml method should return expected result or not. + /// + /// Hour value. + /// Minute value. + /// Second value. + /// The converter instance uses its default parameter of "T". + [Theory] + [InlineData(6, 12, 31)] + public void Given_Values_WriteYaml_ShouldReturn_Result(int hour, int minute, int second) + { + var timeOnly = new TimeOnly(hour, minute, second); + var formatted = timeOnly.ToString("T", CultureInfo.InvariantCulture); + var obj = new TestObject() { TimeOnly = timeOnly }; + + var builder = new SerializerBuilder(); + builder.WithNamingConvention(CamelCaseNamingConvention.Instance); + builder.WithTypeConverter(new TimeOnlyConverter()); + + var serialiser = builder.Build(); + + var serialised = serialiser.Serialize(obj); + + serialised.Should().ContainEquivalentOf($"timeonly: {formatted}"); + } + + /// + /// Tests whether the WriteYaml method should return expected result or not. + /// + /// Hour value. + /// Minute value. + /// Second value. + /// Locale value. + /// The converter instance uses its default parameter of "T". + [Theory] + [InlineData(6, 12, 31, "es-ES")] + [InlineData(6, 12, 31, "ko-KR")] + public void Given_Values_WithLocale_WriteYaml_ShouldReturn_Result(int hour, int minute, int second, string locale) + { + var timeOnly = new TimeOnly(hour, minute, second); + var culture = new CultureInfo(locale); + var formatted = timeOnly.ToString("T", culture); + var obj = new TestObject() { TimeOnly = timeOnly }; + + var builder = new SerializerBuilder(); + builder.WithNamingConvention(CamelCaseNamingConvention.Instance); + builder.WithTypeConverter(new TimeOnlyConverter(provider: culture)); + + var serialiser = builder.Build(); + + var serialised = serialiser.Serialize(obj); + + serialised.Should().ContainEquivalentOf($"timeonly: {formatted}"); + } + + /// + /// Tests whether the WriteYaml method should return expected result or not. + /// + /// Hour value. + /// Minute value. + /// Second value. + /// The converter instance uses its default parameter of "T". + [Theory] + [InlineData(6, 12, 31)] + public void Given_Values_WithFormats_WriteYaml_ShouldReturn_Result_WithFirstFormat(int hour, int minute, int second) + { + var timeOnly = new TimeOnly(hour, minute, second); + var format = "HH:mm:ss"; + var formatted = timeOnly.ToString(format, CultureInfo.InvariantCulture); + var obj = new TestObject() { TimeOnly = timeOnly }; + + var builder = new SerializerBuilder(); + builder.WithNamingConvention(CamelCaseNamingConvention.Instance); + builder.WithTypeConverter(new TimeOnlyConverter(formats: new[] { format, "T" })); + + var serialiser = builder.Build(); + + var serialised = serialiser.Serialize(obj); + + serialised.Should().ContainEquivalentOf($"timeonly: {formatted}"); + } + + [Fact] + public void JsonCompatible_EncaseTimeOnlyInDoubleQuotes() + { + var serializer = new SerializerBuilder().JsonCompatible().Build(); + var testObject = new TestObject { TimeOnly = new TimeOnly(6, 12, 31) }; + var actual = serializer.Serialize(testObject); + + actual.TrimNewLines().Should().ContainEquivalentOf("{\"TimeOnly\": \"06:12:31\"}"); + } + + /// + /// This represents the test object entity. + /// + private class TestObject + { + /// + /// Gets or sets the value. + /// + public TimeOnly TimeOnly { get; set; } + } + } +} +#endif diff --git a/YamlDotNet/Serialization/Converters/DateOnlyConverter.cs b/YamlDotNet/Serialization/Converters/DateOnlyConverter.cs new file mode 100644 index 00000000..6bf04d47 --- /dev/null +++ b/YamlDotNet/Serialization/Converters/DateOnlyConverter.cs @@ -0,0 +1,96 @@ +// This file is part of YamlDotNet - A .NET library for YAML. +// Copyright (c) Antoine Aubry and contributors +// +// Permission is hereby granted, free of charge, to any person obtaining a copy of +// this software and associated documentation files (the "Software"), to deal in +// the Software without restriction, including without limitation the rights to +// use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies +// of the Software, and to permit persons to whom the Software is furnished to do +// so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. + +#if NET6_0_OR_GREATER +using System; +using System.Globalization; +using System.Linq; + +using YamlDotNet.Core; +using YamlDotNet.Core.Events; + +namespace YamlDotNet.Serialization.Converters +{ + /// + /// This represents the YAML converter entity for . + /// + public class DateOnlyConverter : IYamlTypeConverter + { + private readonly IFormatProvider provider; + private readonly bool doubleQuotes; + private readonly string[] formats; + + /// + /// Initializes a new instance of the class. + /// + /// instance. Default value is . + /// If true, will use double quotes when writing the value to the stream. + /// List of date/time formats for parsing. Default value is "d". + /// On deserializing, all formats in the list are used for conversion, while on serializing, the first format in the list is used. + public DateOnlyConverter(IFormatProvider? provider = null, bool doubleQuotes = false, params string[] formats) + { + this.provider = provider ?? CultureInfo.InvariantCulture; + this.doubleQuotes = doubleQuotes; + this.formats = formats.DefaultIfEmpty("d").ToArray(); + } + + /// + /// Gets a value indicating whether the current converter supports converting the specified type. + /// + /// to check. + /// Returns True, if the current converter supports; otherwise returns False. + public bool Accepts(Type type) + { + return type == typeof(DateOnly); + } + + /// + /// Reads an object's state from a YAML parser. + /// + /// instance. + /// to convert. + /// Returns the instance converted. + /// On deserializing, all formats in the list are used for conversion. + public object ReadYaml(IParser parser, Type type) + { + var value = parser.Consume().Value; + + var dateOnly = DateOnly.ParseExact(value, this.formats, this.provider); + return dateOnly; + } + + /// + /// Writes the specified object's state to a YAML emitter. + /// + /// instance. + /// Value to write. + /// to convert. + /// On serializing, the first format in the list is used. + public void WriteYaml(IEmitter emitter, object? value, Type type) + { + var dateOnly = (DateOnly)value!; + var formatted = dateOnly.ToString(this.formats.First(), this.provider); // Always take the first format of the list. + + emitter.Emit(new Scalar(AnchorName.Empty, TagName.Empty, formatted, doubleQuotes ? ScalarStyle.DoubleQuoted : ScalarStyle.Any, true, false)); + } + } +} +#endif diff --git a/YamlDotNet/Serialization/Converters/TimeOnlyConverter.cs b/YamlDotNet/Serialization/Converters/TimeOnlyConverter.cs new file mode 100644 index 00000000..05e53a2d --- /dev/null +++ b/YamlDotNet/Serialization/Converters/TimeOnlyConverter.cs @@ -0,0 +1,96 @@ +// This file is part of YamlDotNet - A .NET library for YAML. +// Copyright (c) Antoine Aubry and contributors +// +// Permission is hereby granted, free of charge, to any person obtaining a copy of +// this software and associated documentation files (the "Software"), to deal in +// the Software without restriction, including without limitation the rights to +// use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies +// of the Software, and to permit persons to whom the Software is furnished to do +// so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. + +#if NET6_0_OR_GREATER +using System; +using System.Globalization; +using System.Linq; + +using YamlDotNet.Core; +using YamlDotNet.Core.Events; + +namespace YamlDotNet.Serialization.Converters +{ + /// + /// This represents the YAML converter entity for . + /// + public class TimeOnlyConverter : IYamlTypeConverter + { + private readonly IFormatProvider provider; + private readonly bool doubleQuotes; + private readonly string[] formats; + + /// + /// Initializes a new instance of the class. + /// + /// instance. Default value is . + /// If true, will use double quotes when writing the value to the stream. + /// List of date/time formats for parsing. Default value is "T". + /// On deserializing, all formats in the list are used for conversion, while on serializing, the first format in the list is used. + public TimeOnlyConverter(IFormatProvider? provider = null, bool doubleQuotes = false, params string[] formats) + { + this.provider = provider ?? CultureInfo.InvariantCulture; + this.doubleQuotes = doubleQuotes; + this.formats = formats.DefaultIfEmpty("T").ToArray(); + } + + /// + /// Gets a value indicating whether the current converter supports converting the specified type. + /// + /// to check. + /// Returns True, if the current converter supports; otherwise returns False. + public bool Accepts(Type type) + { + return type == typeof(TimeOnly); + } + + /// + /// Reads an object's state from a YAML parser. + /// + /// instance. + /// to convert. + /// Returns the instance converted. + /// On deserializing, all formats in the list are used for conversion. + public object ReadYaml(IParser parser, Type type) + { + var value = parser.Consume().Value; + + var timeOnly = TimeOnly.ParseExact(value, this.formats, this.provider); + return timeOnly; + } + + /// + /// Writes the specified object's state to a YAML emitter. + /// + /// instance. + /// Value to write. + /// to convert. + /// On serializing, the first format in the list is used. + public void WriteYaml(IEmitter emitter, object? value, Type type) + { + var timeOnly = (TimeOnly)value!; + var formatted = timeOnly.ToString(this.formats.First(), this.provider); // Always take the first format of the list. + + emitter.Emit(new Scalar(AnchorName.Empty, TagName.Empty, formatted, doubleQuotes ? ScalarStyle.DoubleQuoted : ScalarStyle.Any, true, false)); + } + } +} +#endif diff --git a/YamlDotNet/Serialization/SerializerBuilder.cs b/YamlDotNet/Serialization/SerializerBuilder.cs index df462672..8b817f7c 100755 --- a/YamlDotNet/Serialization/SerializerBuilder.cs +++ b/YamlDotNet/Serialization/SerializerBuilder.cs @@ -335,6 +335,10 @@ public SerializerBuilder JsonCompatible() return this .WithTypeConverter(new GuidConverter(true), w => w.InsteadOf()) .WithTypeConverter(new DateTimeConverter(doubleQuotes: true)) +#if NET6_0_OR_GREATER + .WithTypeConverter(new DateOnlyConverter(doubleQuotes: true)) + .WithTypeConverter(new TimeOnlyConverter(doubleQuotes: true)) +#endif .WithEventEmitter(inner => new JsonEventEmitter(inner, yamlFormatter), loc => loc.InsteadOf()); } From 03bf14e33ea26cceba515b5e35e49b90f6b48587 Mon Sep 17 00:00:00 2001 From: Stijn Herreman Date: Wed, 20 Sep 2023 21:12:24 +0200 Subject: [PATCH 2/2] Add date/time converters to StaticSerializerBuilder. --- YamlDotNet/Serialization/StaticSerializerBuilder.cs | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/YamlDotNet/Serialization/StaticSerializerBuilder.cs b/YamlDotNet/Serialization/StaticSerializerBuilder.cs index 973c7a5b..99b332fa 100644 --- a/YamlDotNet/Serialization/StaticSerializerBuilder.cs +++ b/YamlDotNet/Serialization/StaticSerializerBuilder.cs @@ -340,6 +340,11 @@ public StaticSerializerBuilder JsonCompatible() return this .WithTypeConverter(new GuidConverter(true), w => w.InsteadOf()) + .WithTypeConverter(new DateTimeConverter(doubleQuotes: true)) +#if NET6_0_OR_GREATER + .WithTypeConverter(new DateOnlyConverter(doubleQuotes: true)) + .WithTypeConverter(new TimeOnlyConverter(doubleQuotes: true)) +#endif .WithEventEmitter(inner => new JsonEventEmitter(inner, yamlFormatter), loc => loc.InsteadOf()); }