From 9a0d1f34c3a0e4fb681edc968f422b7a27c601a1 Mon Sep 17 00:00:00 2001 From: Glen Date: Fri, 20 Feb 2026 16:43:12 +0200 Subject: [PATCH 1/5] Add precision options to DateTime/LocalDateTime/LocalTime scalars --- dictionary.txt | 1 + .../Properties/TypeResources.Designer.cs | 18 +++ .../src/Types/Properties/TypeResources.resx | 6 + .../Types/Types/Scalars/DateTimeOptions.cs | 78 ++++++++++ .../src/Types/Types/Scalars/DateTimeType.cs | 117 ++++++++++++--- .../Types/Types/Scalars/LocalDateTimeType.cs | 79 +++++++++- .../src/Types/Types/Scalars/LocalTimeType.cs | 72 ++++++++- .../Types/Scalars/DateTimeOptionsTests.cs | 91 ++++++++++++ .../Types/Scalars/DateTimeTypeTests.cs | 138 +++++++++++++++--- .../Types/Scalars/LocalDateTimeTypeTests.cs | 135 ++++++++++++++--- .../Types/Scalars/LocalTimeTypeTests.cs | 136 ++++++++++++++--- 11 files changed, 773 insertions(+), 98 deletions(-) create mode 100644 src/HotChocolate/Core/src/Types/Types/Scalars/DateTimeOptions.cs create mode 100644 src/HotChocolate/Core/test/Types.Tests/Types/Scalars/DateTimeOptionsTests.cs diff --git a/dictionary.txt b/dictionary.txt index d2ae6befe51..b5937c167b0 100644 --- a/dictionary.txt +++ b/dictionary.txt @@ -171,6 +171,7 @@ sortings Specwise sqft srid +sszzz Staib Starships starwars diff --git a/src/HotChocolate/Core/src/Types/Properties/TypeResources.Designer.cs b/src/HotChocolate/Core/src/Types/Properties/TypeResources.Designer.cs index 31f1c63043f..997ca0d6ce4 100644 --- a/src/HotChocolate/Core/src/Types/Properties/TypeResources.Designer.cs +++ b/src/HotChocolate/Core/src/Types/Properties/TypeResources.Designer.cs @@ -293,6 +293,24 @@ internal static string DataLoaderResolverContextExtensions_UnableToRegister { } } + /// + /// Looks up a localized string similar to InputPrecision must be less than or equal to 7.. + /// + internal static string DateTimeOptions_InputPrecision_InvalidValue { + get { + return ResourceManager.GetString("DateTimeOptions_InputPrecision_InvalidValue", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to OutputPrecision must be less than or equal to 7.. + /// + internal static string DateTimeOptions_OutputPrecision_InvalidValue { + get { + return ResourceManager.GetString("DateTimeOptions_OutputPrecision_InvalidValue", resourceCulture); + } + } + /// /// Looks up a localized string similar to The `DateTime` scalar type represents a date and time with time zone offset information.. /// diff --git a/src/HotChocolate/Core/src/Types/Properties/TypeResources.resx b/src/HotChocolate/Core/src/Types/Properties/TypeResources.resx index 65d5b3f8abf..d0c68b25a46 100644 --- a/src/HotChocolate/Core/src/Types/Properties/TypeResources.resx +++ b/src/HotChocolate/Core/src/Types/Properties/TypeResources.resx @@ -1032,4 +1032,10 @@ Type: `{0}` {0}Type cannot parse the provided value. The value does not match the required regular expression pattern. + + InputPrecision must be less than or equal to 7. + + + OutputPrecision must be less than or equal to 7. + diff --git a/src/HotChocolate/Core/src/Types/Types/Scalars/DateTimeOptions.cs b/src/HotChocolate/Core/src/Types/Types/Scalars/DateTimeOptions.cs new file mode 100644 index 00000000000..4e036c4a2ca --- /dev/null +++ b/src/HotChocolate/Core/src/Types/Types/Scalars/DateTimeOptions.cs @@ -0,0 +1,78 @@ +using HotChocolate.Properties; + +namespace HotChocolate.Types; + +/// +/// +/// Defines options for configuring the behavior of date and time scalar types, such as +/// DateTime, LocalDateTime, and LocalTime. +/// +/// +/// These options allow you to specify the precision of fractional seconds for both input parsing +/// and output serialization, ensuring that the date and time values are handled with the desired +/// level of detail. +/// +/// +/// The default precision is set to 7, which corresponds to the maximum precision supported by +/// .NET's date and time types. Adjusting these options can help optimize performance and storage +/// when high precision is not required, while still adhering to the GraphQL specification for date +/// and time formats. +/// +/// +public struct DateTimeOptions +{ + public const byte DefaultInputPrecision = 7; + public const byte DefaultOutputPrecision = 7; + + public DateTimeOptions() + { + } + + /// + /// Gets the maximum number of fractional second digits to expect when parsing date and time + /// input values. + /// + /// + /// Thrown when the value is greater than 7. + /// + public byte InputPrecision + { + get; + init + { + if (value > 7) + { + throw new ArgumentOutOfRangeException( + nameof(InputPrecision), + value, + TypeResources.DateTimeOptions_InputPrecision_InvalidValue); + } + + field = value; + } + } = DefaultInputPrecision; + + /// + /// Gets the maximum number of fractional second digits to include when serializing date and + /// time output values. + /// + /// + /// Thrown when the value is greater than 7. + /// + public byte OutputPrecision + { + get; + init + { + if (value > 7) + { + throw new ArgumentOutOfRangeException( + nameof(OutputPrecision), + value, + TypeResources.DateTimeOptions_OutputPrecision_InvalidValue); + } + + field = value; + } + } = DefaultOutputPrecision; +} diff --git a/src/HotChocolate/Core/src/Types/Types/Scalars/DateTimeType.cs b/src/HotChocolate/Core/src/Types/Types/Scalars/DateTimeType.cs index b92aeda462c..36f7e416289 100644 --- a/src/HotChocolate/Core/src/Types/Types/Scalars/DateTimeType.cs +++ b/src/HotChocolate/Core/src/Types/Types/Scalars/DateTimeType.cs @@ -23,6 +23,7 @@ public partial class DateTimeType : ScalarType private const string SpecifiedByUri = "https://scalars.graphql.org/chillicream/date-time.html"; private readonly bool _enforceSpecFormat; + private readonly DateTimeOptions _options; /// /// Initializes a new instance of the class. @@ -31,13 +32,19 @@ public DateTimeType( string name, string? description = null, BindingBehavior bind = BindingBehavior.Explicit, - bool disableFormatCheck = false) + bool disableFormatCheck = false, + DateTimeOptions? options = null) : base(name, bind) { + options ??= new DateTimeOptions(); Description = description; - Pattern = @"^\d{4}-\d{2}-\d{2}[Tt]\d{2}:\d{2}:\d{2}(?:\.\d{1,9})?(?:[Zz]|[+-]\d{2}:\d{2})$"; + Pattern = + @"^\d{4}-\d{2}-\d{2}[Tt]\d{2}:\d{2}:\d{2}(?:\.\d{1," + + options.Value.InputPrecision + + @"})?(?:[Zz]|[+-]\d{2}:\d{2})$"; SpecifiedBy = new Uri(SpecifiedByUri); _enforceSpecFormat = !disableFormatCheck; + _options = options.Value; } /// @@ -52,6 +59,18 @@ public DateTimeType(bool disableFormatCheck) { } + /// + /// Initializes a new instance of the class. + /// + public DateTimeType(DateTimeOptions options) + : this( + ScalarNames.DateTime, + TypeResources.DateTimeType_Description, + BindingBehavior.Implicit, + options: options) + { + } + /// /// Initializes a new instance of the class. /// @@ -87,32 +106,24 @@ protected override DateTimeOffset OnCoerceInputValue(JsonElement inputValue, IFe protected override void OnCoerceOutputValue(DateTimeOffset runtimeValue, ResultElement resultValue) { - if (runtimeValue.Offset == TimeSpan.Zero) - { - resultValue.SetStringValue(runtimeValue.ToString(UtcFormat, CultureInfo.InvariantCulture)); - } - else - { - resultValue.SetStringValue(runtimeValue.ToString(LocalFormat, CultureInfo.InvariantCulture)); - } + resultValue.SetStringValue( + runtimeValue.ToString( + runtimeValue.Offset == TimeSpan.Zero ? GetUtcFormat() : GetLocalFormat(), + CultureInfo.InvariantCulture)); } protected override StringValueNode OnValueToLiteral(DateTimeOffset runtimeValue) { - if (runtimeValue.Offset == TimeSpan.Zero) - { - return new StringValueNode(runtimeValue.ToString(UtcFormat, CultureInfo.InvariantCulture)); - } - else - { - return new StringValueNode(runtimeValue.ToString(LocalFormat, CultureInfo.InvariantCulture)); - } + return new StringValueNode( + runtimeValue.ToString( + runtimeValue.Offset == TimeSpan.Zero ? GetUtcFormat() : GetLocalFormat(), + CultureInfo.InvariantCulture)); } private bool TryParseStringValue(string serialized, out DateTimeOffset value) { // Check format. - if (_enforceSpecFormat && !DateTimeRegex().IsMatch(serialized)) + if (_enforceSpecFormat && !GetDateTimeRegex().IsMatch(serialized)) { value = default; return false; @@ -132,8 +143,72 @@ private bool TryParseStringValue(string serialized, out DateTimeOffset value) return false; } + private string GetUtcFormat() + => _options.OutputPrecision switch + { + DateTimeOptions.DefaultOutputPrecision => UtcFormat, + 0 => @"yyyy-MM-ddTHH\:mm\:ssZ", + _ => @$"yyyy-MM-ddTHH\:mm\:ss.{new string('F', _options.OutputPrecision)}Z" + }; + + private string GetLocalFormat() + => _options.OutputPrecision switch + { + DateTimeOptions.DefaultOutputPrecision => LocalFormat, + 0 => @"yyyy-MM-ddTHH\:mm\:sszzz", + _ => @$"yyyy-MM-ddTHH\:mm\:ss.{new string('F', _options.OutputPrecision)}zzz" + }; + + private Regex GetDateTimeRegex() + => _options.InputPrecision switch + { + 0 => DateTimeRegex0(), + 1 => DateTimeRegex1(), + 2 => DateTimeRegex2(), + 3 => DateTimeRegex3(), + 4 => DateTimeRegex4(), + 5 => DateTimeRegex5(), + 6 => DateTimeRegex6(), + _ => DateTimeRegex7() + }; + + [GeneratedRegex( + @"^[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2}(Z|[+-][0-9]{2}:[0-9]{2})\z", + RegexOptions.ExplicitCapture | RegexOptions.IgnoreCase)] + private static partial Regex DateTimeRegex0(); + + [GeneratedRegex( + @"^[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2}(\.[0-9])?(Z|[+-][0-9]{2}:[0-9]{2})\z", + RegexOptions.ExplicitCapture | RegexOptions.IgnoreCase)] + private static partial Regex DateTimeRegex1(); + + [GeneratedRegex( + @"^[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2}(\.[0-9]{1,2})?(Z|[+-][0-9]{2}:[0-9]{2})\z", + RegexOptions.ExplicitCapture | RegexOptions.IgnoreCase)] + private static partial Regex DateTimeRegex2(); + + [GeneratedRegex( + @"^[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2}(\.[0-9]{1,3})?(Z|[+-][0-9]{2}:[0-9]{2})\z", + RegexOptions.ExplicitCapture | RegexOptions.IgnoreCase)] + private static partial Regex DateTimeRegex3(); + + [GeneratedRegex( + @"^[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2}(\.[0-9]{1,4})?(Z|[+-][0-9]{2}:[0-9]{2})\z", + RegexOptions.ExplicitCapture | RegexOptions.IgnoreCase)] + private static partial Regex DateTimeRegex4(); + + [GeneratedRegex( + @"^[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2}(\.[0-9]{1,5})?(Z|[+-][0-9]{2}:[0-9]{2})\z", + RegexOptions.ExplicitCapture | RegexOptions.IgnoreCase)] + private static partial Regex DateTimeRegex5(); + + [GeneratedRegex( + @"^[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2}(\.[0-9]{1,6})?(Z|[+-][0-9]{2}:[0-9]{2})\z", + RegexOptions.ExplicitCapture | RegexOptions.IgnoreCase)] + private static partial Regex DateTimeRegex6(); + [GeneratedRegex( - @"^[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2}(\.[0-9]{1,9})?(Z|[+-][0-9]{2}:[0-9]{2})\z", + @"^[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2}(\.[0-9]{1,7})?(Z|[+-][0-9]{2}:[0-9]{2})\z", RegexOptions.ExplicitCapture | RegexOptions.IgnoreCase)] - private static partial Regex DateTimeRegex(); + private static partial Regex DateTimeRegex7(); } diff --git a/src/HotChocolate/Core/src/Types/Types/Scalars/LocalDateTimeType.cs b/src/HotChocolate/Core/src/Types/Types/Scalars/LocalDateTimeType.cs index 49d0dfaa1b8..977fa363937 100644 --- a/src/HotChocolate/Core/src/Types/Types/Scalars/LocalDateTimeType.cs +++ b/src/HotChocolate/Core/src/Types/Types/Scalars/LocalDateTimeType.cs @@ -23,6 +23,7 @@ public partial class LocalDateTimeType : ScalarType private const string SpecifiedByUri = "https://scalars.graphql.org/chillicream/local-date-time.html"; private readonly bool _enforceSpecFormat; + private readonly DateTimeOptions _options; /// /// Initializes a new instance of the class. @@ -31,13 +32,16 @@ public LocalDateTimeType( string name, string? description = null, BindingBehavior bind = BindingBehavior.Explicit, - bool disableFormatCheck = false) + bool disableFormatCheck = false, + DateTimeOptions? options = null) : base(name, bind) { + options ??= new DateTimeOptions(); Description = description; - Pattern = @"^\d{4}-\d{2}-\d{2}[Tt]\d{2}:\d{2}:\d{2}(?:\.\d{1,9})?$"; + Pattern = @"^\d{4}-\d{2}-\d{2}[Tt]\d{2}:\d{2}:\d{2}(?:\.\d{1," + options.Value.InputPrecision + "})?$"; SpecifiedBy = new Uri(SpecifiedByUri); _enforceSpecFormat = !disableFormatCheck; + _options = options.Value; } /// @@ -52,6 +56,18 @@ public LocalDateTimeType(bool disableFormatCheck) { } + /// + /// Initializes a new instance of the class. + /// + public LocalDateTimeType(DateTimeOptions options) + : this( + ScalarNames.LocalDateTime, + TypeResources.LocalDateTimeType_Description, + BindingBehavior.Implicit, + options: options) + { + } + /// /// Initializes a new instance of the class. /// @@ -87,16 +103,16 @@ protected override DateTime OnCoerceInputValue(JsonElement inputValue, IFeatureP /// protected override void OnCoerceOutputValue(DateTime runtimeValue, ResultElement resultValue) - => resultValue.SetStringValue(runtimeValue.ToString(LocalFormat, CultureInfo.InvariantCulture)); + => resultValue.SetStringValue(runtimeValue.ToString(GetLocalFormat(), CultureInfo.InvariantCulture)); /// protected override StringValueNode OnValueToLiteral(DateTime runtimeValue) - => new StringValueNode(runtimeValue.ToString(LocalFormat, CultureInfo.InvariantCulture)); + => new StringValueNode(runtimeValue.ToString(GetLocalFormat(), CultureInfo.InvariantCulture)); private bool TryParseStringValue(string serialized, out DateTime value) { // Check format. - if (_enforceSpecFormat && !LocalDateTimeRegex().IsMatch(serialized)) + if (_enforceSpecFormat && !GetLocalDateTimeRegex().IsMatch(serialized)) { value = default; return false; @@ -116,7 +132,56 @@ private bool TryParseStringValue(string serialized, out DateTime value) return false; } - [GeneratedRegex(@"^[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2}(\.[0-9]{1,9})?\z", + private string GetLocalFormat() + => _options.OutputPrecision switch + { + DateTimeOptions.DefaultOutputPrecision => LocalFormat, + 0 => @"yyyy-MM-ddTHH\:mm\:ss", + _ => @$"yyyy-MM-ddTHH\:mm\:ss.{new string('F', _options.OutputPrecision)}" + }; + + private Regex GetLocalDateTimeRegex() + => _options.InputPrecision switch + { + 0 => LocalDateTimeRegex0(), + 1 => LocalDateTimeRegex1(), + 2 => LocalDateTimeRegex2(), + 3 => LocalDateTimeRegex3(), + 4 => LocalDateTimeRegex4(), + 5 => LocalDateTimeRegex5(), + 6 => LocalDateTimeRegex6(), + _ => LocalDateTimeRegex7() + }; + + [GeneratedRegex(@"^[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2}\z", + RegexOptions.ExplicitCapture | RegexOptions.IgnoreCase)] + private static partial Regex LocalDateTimeRegex0(); + + [GeneratedRegex(@"^[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2}(\.[0-9])?\z", + RegexOptions.ExplicitCapture | RegexOptions.IgnoreCase)] + private static partial Regex LocalDateTimeRegex1(); + + [GeneratedRegex(@"^[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2}(\.[0-9]{1,2})?\z", + RegexOptions.ExplicitCapture | RegexOptions.IgnoreCase)] + private static partial Regex LocalDateTimeRegex2(); + + [GeneratedRegex(@"^[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2}(\.[0-9]{1,3})?\z", + RegexOptions.ExplicitCapture | RegexOptions.IgnoreCase)] + private static partial Regex LocalDateTimeRegex3(); + + [GeneratedRegex(@"^[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2}(\.[0-9]{1,4})?\z", + RegexOptions.ExplicitCapture | RegexOptions.IgnoreCase)] + private static partial Regex LocalDateTimeRegex4(); + + [GeneratedRegex(@"^[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2}(\.[0-9]{1,5})?\z", + RegexOptions.ExplicitCapture | RegexOptions.IgnoreCase)] + private static partial Regex LocalDateTimeRegex5(); + + [GeneratedRegex(@"^[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2}(\.[0-9]{1,6})?\z", + RegexOptions.ExplicitCapture | RegexOptions.IgnoreCase)] + private static partial Regex LocalDateTimeRegex6(); + + [GeneratedRegex(@"^[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2}(\.[0-9]{1,7})?\z", RegexOptions.ExplicitCapture | RegexOptions.IgnoreCase)] - private static partial Regex LocalDateTimeRegex(); + private static partial Regex LocalDateTimeRegex7(); } diff --git a/src/HotChocolate/Core/src/Types/Types/Scalars/LocalTimeType.cs b/src/HotChocolate/Core/src/Types/Types/Scalars/LocalTimeType.cs index b7e709dbc34..1bbbd770e0b 100644 --- a/src/HotChocolate/Core/src/Types/Types/Scalars/LocalTimeType.cs +++ b/src/HotChocolate/Core/src/Types/Types/Scalars/LocalTimeType.cs @@ -22,6 +22,7 @@ public partial class LocalTimeType : ScalarType private const string SpecifiedByUri = "https://scalars.graphql.org/chillicream/local-time.html"; private readonly bool _enforceSpecFormat; + private readonly DateTimeOptions _options; /// /// Initializes a new instance of the class. @@ -30,13 +31,16 @@ public LocalTimeType( string name, string? description = null, BindingBehavior bind = BindingBehavior.Explicit, - bool disableFormatCheck = false) + bool disableFormatCheck = false, + DateTimeOptions? options = null) : base(name, bind) { + options ??= new DateTimeOptions(); Description = description; - Pattern = @"^\d{2}:\d{2}:\d{2}(?:\.\d{1,9})?$"; + Pattern = @"^\d{2}:\d{2}:\d{2}(?:\.\d{1," + options.Value.InputPrecision + "})?$"; SpecifiedBy = new Uri(SpecifiedByUri); _enforceSpecFormat = !disableFormatCheck; + _options = options.Value; } /// @@ -51,6 +55,18 @@ public LocalTimeType(bool disableFormatCheck) { } + /// + /// Initializes a new instance of the class. + /// + public LocalTimeType(DateTimeOptions options) + : this( + ScalarNames.LocalTime, + TypeResources.LocalTimeType_Description, + BindingBehavior.Implicit, + options: options) + { + } + /// /// Initializes a new instance of the class. /// @@ -86,16 +102,16 @@ protected override TimeOnly OnCoerceInputValue(JsonElement inputValue, IFeatureP /// protected override void OnCoerceOutputValue(TimeOnly runtimeValue, ResultElement resultValue) - => resultValue.SetStringValue(runtimeValue.ToString(LocalFormat, CultureInfo.InvariantCulture)); + => resultValue.SetStringValue(runtimeValue.ToString(GetLocalFormat(), CultureInfo.InvariantCulture)); /// protected override StringValueNode OnValueToLiteral(TimeOnly runtimeValue) - => new StringValueNode(runtimeValue.ToString(LocalFormat, CultureInfo.InvariantCulture)); + => new StringValueNode(runtimeValue.ToString(GetLocalFormat(), CultureInfo.InvariantCulture)); private bool TryParseStringValue(string serialized, out TimeOnly value) { // Check format. - if (_enforceSpecFormat && !LocalTimeRegex().IsMatch(serialized)) + if (_enforceSpecFormat && !GetLocalTimeRegex().IsMatch(serialized)) { value = default; return false; @@ -114,6 +130,48 @@ private bool TryParseStringValue(string serialized, out TimeOnly value) return false; } - [GeneratedRegex(@"^[0-9]{2}:[0-9]{2}:[0-9]{2}(\.[0-9]{1,9})?\z", RegexOptions.ExplicitCapture)] - private static partial Regex LocalTimeRegex(); + private string GetLocalFormat() + => _options.OutputPrecision switch + { + DateTimeOptions.DefaultOutputPrecision => LocalFormat, + 0 => "HH:mm:ss", + _ => $"HH:mm:ss.{new string('F', _options.OutputPrecision)}" + }; + + private Regex GetLocalTimeRegex() + => _options.InputPrecision switch + { + 0 => LocalTimeRegex0(), + 1 => LocalTimeRegex1(), + 2 => LocalTimeRegex2(), + 3 => LocalTimeRegex3(), + 4 => LocalTimeRegex4(), + 5 => LocalTimeRegex5(), + 6 => LocalTimeRegex6(), + _ => LocalTimeRegex7() + }; + + [GeneratedRegex(@"^[0-9]{2}:[0-9]{2}:[0-9]{2}\z", RegexOptions.ExplicitCapture)] + private static partial Regex LocalTimeRegex0(); + + [GeneratedRegex(@"^[0-9]{2}:[0-9]{2}:[0-9]{2}(\.[0-9])?\z", RegexOptions.ExplicitCapture)] + private static partial Regex LocalTimeRegex1(); + + [GeneratedRegex(@"^[0-9]{2}:[0-9]{2}:[0-9]{2}(\.[0-9]{1,2})?\z", RegexOptions.ExplicitCapture)] + private static partial Regex LocalTimeRegex2(); + + [GeneratedRegex(@"^[0-9]{2}:[0-9]{2}:[0-9]{2}(\.[0-9]{1,3})?\z", RegexOptions.ExplicitCapture)] + private static partial Regex LocalTimeRegex3(); + + [GeneratedRegex(@"^[0-9]{2}:[0-9]{2}:[0-9]{2}(\.[0-9]{1,4})?\z", RegexOptions.ExplicitCapture)] + private static partial Regex LocalTimeRegex4(); + + [GeneratedRegex(@"^[0-9]{2}:[0-9]{2}:[0-9]{2}(\.[0-9]{1,5})?\z", RegexOptions.ExplicitCapture)] + private static partial Regex LocalTimeRegex5(); + + [GeneratedRegex(@"^[0-9]{2}:[0-9]{2}:[0-9]{2}(\.[0-9]{1,6})?\z", RegexOptions.ExplicitCapture)] + private static partial Regex LocalTimeRegex6(); + + [GeneratedRegex(@"^[0-9]{2}:[0-9]{2}:[0-9]{2}(\.[0-9]{1,7})?\z", RegexOptions.ExplicitCapture)] + private static partial Regex LocalTimeRegex7(); } diff --git a/src/HotChocolate/Core/test/Types.Tests/Types/Scalars/DateTimeOptionsTests.cs b/src/HotChocolate/Core/test/Types.Tests/Types/Scalars/DateTimeOptionsTests.cs new file mode 100644 index 00000000000..3c35caed5ed --- /dev/null +++ b/src/HotChocolate/Core/test/Types.Tests/Types/Scalars/DateTimeOptionsTests.cs @@ -0,0 +1,91 @@ +namespace HotChocolate.Types; + +public class DateTimeOptionsTests +{ + [Fact] + public void DefaultConstructor_ShouldSetDefaultPrecisions() + { + // arrange & act + var options = new DateTimeOptions(); + + // assert + Assert.Equal(DateTimeOptions.DefaultInputPrecision, options.InputPrecision); + Assert.Equal(DateTimeOptions.DefaultOutputPrecision, options.OutputPrecision); + } + + [Fact] + public void DefaultConstants_ShouldBeCorrect() + { + // assert + Assert.Equal(7, DateTimeOptions.DefaultInputPrecision); + Assert.Equal(7, DateTimeOptions.DefaultOutputPrecision); + } + + [Theory] + [InlineData(0)] + [InlineData(1)] + [InlineData(2)] + [InlineData(3)] + [InlineData(4)] + [InlineData(5)] + [InlineData(6)] + [InlineData(7)] + public void InputPrecision_ValidValues_ShouldSet(byte precision) + { + // arrange & act + var options = new DateTimeOptions { InputPrecision = precision }; + + // assert + Assert.Equal(precision, options.InputPrecision); + } + + [Theory] + [InlineData(0)] + [InlineData(1)] + [InlineData(2)] + [InlineData(3)] + [InlineData(4)] + [InlineData(5)] + [InlineData(6)] + [InlineData(7)] + public void OutputPrecision_ValidValues_ShouldSet(byte precision) + { + // arrange & act + var options = new DateTimeOptions { OutputPrecision = precision }; + + // assert + Assert.Equal(precision, options.OutputPrecision); + } + + [Theory] + [InlineData(8)] + [InlineData(9)] + [InlineData(10)] + [InlineData(255)] + public void InputPrecision_InvalidValues_ShouldThrow(byte precision) + { + // arrange & act + var exception = Assert.Throws(() + => new DateTimeOptions { InputPrecision = precision }); + + // assert + Assert.Equal("InputPrecision", exception.ParamName); + Assert.Equal(precision, exception.ActualValue); + } + + [Theory] + [InlineData(8)] + [InlineData(9)] + [InlineData(10)] + [InlineData(255)] + public void OutputPrecision_InvalidValues_ShouldThrow(byte precision) + { + // arrange & act + var exception = Assert.Throws(() + => new DateTimeOptions { OutputPrecision = precision }); + + // assert + Assert.Equal("OutputPrecision", exception.ParamName); + Assert.Equal(precision, exception.ActualValue); + } +} diff --git a/src/HotChocolate/Core/test/Types.Tests/Types/Scalars/DateTimeTypeTests.cs b/src/HotChocolate/Core/test/Types.Tests/Types/Scalars/DateTimeTypeTests.cs index 7083d427bfc..6a1dc6385d2 100644 --- a/src/HotChocolate/Core/test/Types.Tests/Types/Scalars/DateTimeTypeTests.cs +++ b/src/HotChocolate/Core/test/Types.Tests/Types/Scalars/DateTimeTypeTests.cs @@ -2,7 +2,6 @@ using System.Text.Json; using HotChocolate.Execution; using HotChocolate.Language; -using HotChocolate.Tests; using HotChocolate.Text.Json; using Microsoft.Extensions.DependencyInjection; @@ -39,11 +38,11 @@ public void CoerceInputLiteral() } [Theory] - [MemberData(nameof(ValidDateTimeScalarStrings))] - public void CoerceInputLiteral_Valid(string dateTime, DateTimeOffset result) + [MemberData(nameof(ValidInput))] + public void CoerceInputLiteral_Valid(byte precision, string dateTime, DateTimeOffset result) { // arrange - var type = new DateTimeType(); + var type = new DateTimeType(new DateTimeOptions { InputPrecision = precision }); var literal = new StringValueNode(dateTime); // act @@ -54,11 +53,11 @@ public void CoerceInputLiteral_Valid(string dateTime, DateTimeOffset result) } [Theory] - [MemberData(nameof(InvalidDateTimeScalarStrings))] - public void CoerceInputLiteral_Invalid(string dateTime) + [MemberData(nameof(InvalidInput))] + public void CoerceInputLiteral_Invalid(byte precision, string dateTime) { // arrange - var type = new DateTimeType(); + var type = new DateTimeType(new DateTimeOptions { InputPrecision = precision }); var literal = new StringValueNode(dateTime); // act @@ -143,6 +142,23 @@ public void CoerceInputValue_Invalid_Format() Assert.Throws(Action); } + [Theory] + [MemberData(nameof(ValidOutput))] + public void CoerceOutputValue_Valid(byte precision, DateTimeOffset dateTime, string result) + { + // arrange + var type = new DateTimeType(new DateTimeOptions { OutputPrecision = precision }); + + // act + var operation = CommonTestExtensions.CreateOperation(); + var resultDocument = new ResultDocument(operation, 0); + var resultValue = resultDocument.Data.GetProperty("first"); + type.CoerceOutputValue(dateTime, resultValue); + + // assert + resultValue.MatchInlineSnapshot($"\"{result}\""); + } + [Fact] public void CoerceOutputValue_Utc_DateTimeOffset() { @@ -308,44 +324,120 @@ public class DefaultDateTime public DateTime Test => default; } - public static TheoryData ValidDateTimeScalarStrings() + public static TheoryData ValidInput() { - return new TheoryData + return new TheoryData { // https://scalars.graphql.org/chillicream/date-time.html#sec-Input-spec.Examples (Valid input values) { + DateTimeOptions.DefaultInputPrecision, "2023-12-24T15:30:00Z", new DateTimeOffset(2023, 12, 24, 15, 30, 0, 0, TimeSpan.Zero) }, + // Additional cases. + // Up to 7 fractional second digits. { - "2023-12-24T15:30:00.123456789+01:00", // Rounded to ".1234568". - new DateTimeOffset(2023, 12, 24, 15, 30, 0, 123, 456, TimeSpan.FromHours(1)).AddTicks(8) + DateTimeOptions.DefaultInputPrecision, + "2023-12-24T15:30:00.1234567+01:00", + new DateTimeOffset(2023, 12, 24, 15, 30, 0, 123, 456, TimeSpan.FromHours(1)).AddTicks(7) } }; } - public static TheoryData InvalidDateTimeScalarStrings() + public static TheoryData InvalidInput() { - return - [ + return new TheoryData + { // https://scalars.graphql.org/chillicream/date-time.html#sec-Input-spec.Examples (Invalid input values) // Missing time zone offset. - "2023-12-24T15:30:00", + { DateTimeOptions.DefaultInputPrecision, "2023-12-24T15:30:00" }, // Space instead of T or t separator. - "2023-12-24 15:30:00Z", + { DateTimeOptions.DefaultInputPrecision, "2023-12-24 15:30:00Z" }, // Invalid hour (25). - "2023-12-24T25:00:00Z", + { DateTimeOptions.DefaultInputPrecision, "2023-12-24T25:00:00Z" }, // Invalid minute (60). - "2023-12-24T15:60:00Z", + { DateTimeOptions.DefaultInputPrecision, "2023-12-24T15:60:00Z" }, // ReSharper disable once GrammarMistakeInComment // Invalid date (February 30th). - "2023-02-30T15:30:00Z", + { DateTimeOptions.DefaultInputPrecision, "2023-02-30T15:30:00Z" }, // More than 9 fractional second digits. - "2023-12-24T15:30:00.1234567890Z", + { DateTimeOptions.DefaultInputPrecision, "2023-12-24T15:30:00.1234567890Z" }, // Invalid offset (exceeds maximum). - "2023-12-24T15:30:00+25:00", + { DateTimeOptions.DefaultInputPrecision, "2023-12-24T15:30:00+25:00" }, // Invalid offset format. - "2023-12-24T15:30:00 UTC" - ]; + { DateTimeOptions.DefaultInputPrecision, "2023-12-24T15:30:00 UTC" }, + // Additional cases. + // More than 7 fractional second digits with default precision. + { DateTimeOptions.DefaultInputPrecision, "2023-12-24T15:30:00.12345678Z" }, + // More than 6 fractional second digits with precision set to 6. + { 6, "2023-12-24T15:30:00.1234567Z" }, + // More than 5 fractional second digits with precision set to 5. + { 5, "2023-12-24T15:30:00.123456Z" }, + // More than 4 fractional second digits with precision set to 4. + { 4, "2023-12-24T15:30:00.12345Z" }, + // More than 3 fractional second digits with precision set to 3. + { 3, "2023-12-24T15:30:00.1234Z" }, + // More than 2 fractional second digits with precision set to 2. + { 2, "2023-12-24T15:30:00.123Z" }, + // More than 1 fractional second digit with precision set to 1. + { 1, "2023-12-24T15:30:00.12Z" }, + // Fractional second digits with precision set to 0. + { 0, "2023-12-24T15:30:00.1Z" } + }; + } + + public static TheoryData ValidOutput() + { + return new TheoryData + { + // Up to 7 fractional second digits with default precision. + { + DateTimeOptions.DefaultOutputPrecision, + new DateTimeOffset(2023, 12, 24, 15, 30, 0, 123, 456, TimeSpan.Zero).AddTicks(7), + "2023-12-24T15:30:00.1234567Z" + }, + // Up to 6 fractional second digits with precision set to 6. + { + 6, + new DateTimeOffset(2023, 12, 24, 15, 30, 0, 123, 456, TimeSpan.Zero).AddTicks(7), + "2023-12-24T15:30:00.123456Z" + }, + // Up to 5 fractional second digits with precision set to 5. + { + 5, + new DateTimeOffset(2023, 12, 24, 15, 30, 0, 123, 456, TimeSpan.Zero).AddTicks(7), + "2023-12-24T15:30:00.12345Z" + }, + // Up to 4 fractional second digits with precision set to 4. + { + 4, + new DateTimeOffset(2023, 12, 24, 15, 30, 0, 123, 456, TimeSpan.Zero).AddTicks(7), + "2023-12-24T15:30:00.1234Z" + }, + // Up to 3 fractional second digits with precision set to 3. + { + 3, + new DateTimeOffset(2023, 12, 24, 15, 30, 0, 123, 456, TimeSpan.Zero).AddTicks(7), + "2023-12-24T15:30:00.123Z" + }, + // Up to 2 fractional second digits with precision set to 2. + { + 2, + new DateTimeOffset(2023, 12, 24, 15, 30, 0, 123, 456, TimeSpan.Zero).AddTicks(7), + "2023-12-24T15:30:00.12Z" + }, + // Up to 1 fractional second digit with precision set to 1. + { + 1, + new DateTimeOffset(2023, 12, 24, 15, 30, 0, 123, 456, TimeSpan.Zero).AddTicks(7), + "2023-12-24T15:30:00.1Z" + }, + // No fractional second digits with precision set to 0. + { + 0, + new DateTimeOffset(2023, 12, 24, 15, 30, 0, 123, 456, TimeSpan.Zero).AddTicks(7), + "2023-12-24T15:30:00Z" + } + }; } } diff --git a/src/HotChocolate/Core/test/Types.Tests/Types/Scalars/LocalDateTimeTypeTests.cs b/src/HotChocolate/Core/test/Types.Tests/Types/Scalars/LocalDateTimeTypeTests.cs index 1ef00a51716..ff5bcc8a9fe 100644 --- a/src/HotChocolate/Core/test/Types.Tests/Types/Scalars/LocalDateTimeTypeTests.cs +++ b/src/HotChocolate/Core/test/Types.Tests/Types/Scalars/LocalDateTimeTypeTests.cs @@ -37,11 +37,11 @@ public void CoerceInputLiteral() } [Theory] - [MemberData(nameof(ValidLocalDateTimeScalarStrings))] - public void CoerceInputLiteral_Valid(string dateTimeString, DateTime result) + [MemberData(nameof(ValidInput))] + public void CoerceInputLiteral_Valid(byte precision, string dateTimeString, DateTime result) { // arrange - var type = new LocalDateTimeType(); + var type = new LocalDateTimeType(new DateTimeOptions { InputPrecision = precision }); var literal = new StringValueNode(dateTimeString); // act @@ -52,11 +52,11 @@ public void CoerceInputLiteral_Valid(string dateTimeString, DateTime result) } [Theory] - [MemberData(nameof(InvalidLocalDateTimeScalarStrings))] - public void CoerceInputLiteral_Invalid(string dateTime) + [MemberData(nameof(InvalidInput))] + public void CoerceInputLiteral_Invalid(byte precision, string dateTime) { // arrange - var type = new LocalDateTimeType(); + var type = new LocalDateTimeType(new DateTimeOptions { InputPrecision = precision }); var literal = new StringValueNode(dateTime); // act @@ -134,6 +134,23 @@ public void CoerceInputValue_Invalid_Format() Assert.Throws(Action); } + [Theory] + [MemberData(nameof(ValidOutput))] + public void CoerceOutputValue_Valid(byte precision, DateTime dateTime, string result) + { + // arrange + var type = new LocalDateTimeType(new DateTimeOptions { OutputPrecision = precision }); + + // act + var operation = CommonTestExtensions.CreateOperation(); + var resultDocument = new ResultDocument(operation, 0); + var resultValue = resultDocument.Data.GetProperty("first"); + type.CoerceOutputValue(dateTime, resultValue); + + // assert + resultValue.MatchInlineSnapshot($"\"{result}\""); + } + [Fact] public void CoerceOutputValue() { @@ -328,42 +345,118 @@ public class Bar public DateTime GetLocalDateTime() => DateTime.MaxValue; } - public static TheoryData ValidLocalDateTimeScalarStrings() + public static TheoryData ValidInput() { - return new TheoryData + return new TheoryData { // https://scalars.graphql.org/chillicream/local-date-time.html#sec-Input-spec.Examples (Valid input values) { + DateTimeOptions.DefaultInputPrecision, "2023-12-24T15:30:00", new DateTime(2023, 12, 24, 15, 30, 0, 0) }, + // Additional cases. + // Up to 7 fractional second digits. { - "2023-12-24t15:30:00.123456789", // Rounded to ".1234568". - new DateTime(2023, 12, 24, 15, 30, 0, 123, 456).AddTicks(8) + DateTimeOptions.DefaultInputPrecision, + "2023-12-24t15:30:00.1234567", + new DateTime(2023, 12, 24, 15, 30, 0, 123, 456).AddTicks(7) } }; } - public static TheoryData InvalidLocalDateTimeScalarStrings() + public static TheoryData InvalidInput() { - return - [ + return new TheoryData + { // https://scalars.graphql.org/chillicream/local-date-time.html#sec-Input-spec.Examples (Invalid input values) // Contains time zone indicator Z. - "2023-12-24T15:30:00Z", + { DateTimeOptions.DefaultInputPrecision, "2023-12-24T15:30:00Z" }, // Contains time zone offset. - "2023-12-24T15:30:00+05:30", + { DateTimeOptions.DefaultInputPrecision, "2023-12-24T15:30:00+05:30" }, // Invalid separator (space instead of T or t). - "2023-12-24 15:30:00", + { DateTimeOptions.DefaultInputPrecision, "2023-12-24 15:30:00" }, // Invalid hour (25). - "2023-12-24T25:00:00", + { DateTimeOptions.DefaultInputPrecision, "2023-12-24T25:00:00" }, // Invalid minute (60). - "2023-12-24T15:60:00", + { DateTimeOptions.DefaultInputPrecision, "2023-12-24T15:60:00" }, // ReSharper disable once GrammarMistakeInComment // Invalid date (February 30th). - "2023-02-30T15:30:00", + { DateTimeOptions.DefaultInputPrecision, "2023-02-30T15:30:00" }, // More than 9 fractional second digits. - "2023-12-24T15:30:00.1234567890" - ]; + { DateTimeOptions.DefaultInputPrecision, "2023-12-24T15:30:00.1234567890" }, + // Additional cases. + // More than 7 fractional second digits with default precision. + { DateTimeOptions.DefaultInputPrecision, "2023-12-24T15:30:00.12345678" }, + // More than 6 fractional second digits with precision set to 6. + { 6, "2023-12-24T15:30:00.1234567" }, + // More than 5 fractional second digits with precision set to 5. + { 5, "2023-12-24T15:30:00.123456" }, + // More than 4 fractional second digits with precision set to 4. + { 4, "2023-12-24T15:30:00.12345" }, + // More than 3 fractional second digits with precision set to 3. + { 3, "2023-12-24T15:30:00.1234" }, + // More than 2 fractional second digits with precision set to 2. + { 2, "2023-12-24T15:30:00.123" }, + // More than 1 fractional second digit with precision set to 1. + { 1, "2023-12-24T15:30:00.12" }, + // Fractional second digits with precision set to 0. + { 0, "2023-12-24T15:30:00.1" } + }; + } + + public static TheoryData ValidOutput() + { + return new TheoryData + { + // Up to 7 fractional second digits with default precision. + { + DateTimeOptions.DefaultOutputPrecision, + new DateTime(2023, 12, 24, 15, 30, 0, 123, 456).AddTicks(7), + "2023-12-24T15:30:00.1234567" + }, + // Up to 6 fractional second digits with precision set to 6. + { + 6, + new DateTime(2023, 12, 24, 15, 30, 0, 123, 456).AddTicks(7), + "2023-12-24T15:30:00.123456" + }, + // Up to 5 fractional second digits with precision set to 5. + { + 5, + new DateTime(2023, 12, 24, 15, 30, 0, 123, 456).AddTicks(7), + "2023-12-24T15:30:00.12345" + }, + // Up to 4 fractional second digits with precision set to 4. + { + 4, + new DateTime(2023, 12, 24, 15, 30, 0, 123, 456).AddTicks(7), + "2023-12-24T15:30:00.1234" + }, + // Up to 3 fractional second digits with precision set to 3. + { + 3, + new DateTime(2023, 12, 24, 15, 30, 0, 123, 456).AddTicks(7), + "2023-12-24T15:30:00.123" + }, + // Up to 2 fractional second digits with precision set to 2. + { + 2, + new DateTime(2023, 12, 24, 15, 30, 0, 123, 456).AddTicks(7), + "2023-12-24T15:30:00.12" + }, + // Up to 1 fractional second digit with precision set to 1. + { + 1, + new DateTime(2023, 12, 24, 15, 30, 0, 123, 456).AddTicks(7), + "2023-12-24T15:30:00.1" + }, + // No fractional second digits with precision set to 0. + { + 0, + new DateTime(2023, 12, 24, 15, 30, 0, 123, 456).AddTicks(7), + "2023-12-24T15:30:00" + } + }; } } diff --git a/src/HotChocolate/Core/test/Types.Tests/Types/Scalars/LocalTimeTypeTests.cs b/src/HotChocolate/Core/test/Types.Tests/Types/Scalars/LocalTimeTypeTests.cs index 0b7cb3b3ab9..1df0bd1512d 100644 --- a/src/HotChocolate/Core/test/Types.Tests/Types/Scalars/LocalTimeTypeTests.cs +++ b/src/HotChocolate/Core/test/Types.Tests/Types/Scalars/LocalTimeTypeTests.cs @@ -37,11 +37,11 @@ public void CoerceInputLiteral() } [Theory] - [MemberData(nameof(ValidLocalTimeScalarStrings))] - public void CoerceInputLiteral_Valid(string time, TimeOnly result) + [MemberData(nameof(ValidInput))] + public void CoerceInputLiteral_Valid(byte precision, string time, TimeOnly result) { // arrange - var type = new LocalTimeType(); + var type = new LocalTimeType(new DateTimeOptions { InputPrecision = precision }); var literal = new StringValueNode(time); // act @@ -52,11 +52,11 @@ public void CoerceInputLiteral_Valid(string time, TimeOnly result) } [Theory] - [MemberData(nameof(InvalidLocalTimeScalarStrings))] - public void CoerceInputLiteral_Invalid(string time) + [MemberData(nameof(InvalidInput))] + public void CoerceInputLiteral_Invalid(byte precision, string time) { // arrange - var type = new LocalTimeType(); + var type = new LocalTimeType(new DateTimeOptions { InputPrecision = precision }); var literal = new StringValueNode(time); // act @@ -134,6 +134,23 @@ public void CoerceInputValue_Invalid_Format() Assert.Throws(Action); } + [Theory] + [MemberData(nameof(ValidOutput))] + public void CoerceOutputValue_Valid(byte precision, TimeOnly time, string result) + { + // arrange + var type = new LocalTimeType(new DateTimeOptions { OutputPrecision = precision }); + + // act + var operation = CommonTestExtensions.CreateOperation(); + var resultDocument = new ResultDocument(operation, 0); + var resultValue = resultDocument.Data.GetProperty("first"); + type.CoerceOutputValue(time, resultValue); + + // assert + resultValue.MatchInlineSnapshot($"\"{result}\""); + } + [Fact] public void CoerceOutputValue() { @@ -325,41 +342,122 @@ public class Bar public TimeOnly GetTime() => TimeOnly.MaxValue; } - public static TheoryData ValidLocalTimeScalarStrings() + public static TheoryData ValidInput() { - return new TheoryData + return new TheoryData { // https://scalars.graphql.org/chillicream/local-time.html#sec-Input-spec.Examples (Valid input values) { + DateTimeOptions.DefaultInputPrecision, "09:00:00", new TimeOnly(9, 0, 0) }, { + DateTimeOptions.DefaultInputPrecision, "07:30:00.500", new TimeOnly(7, 30, 0, 500) + }, + // Additional cases. + // Up to 7 fractional second digits. + { + DateTimeOptions.DefaultInputPrecision, + "07:30:00.1234567", + new TimeOnly(7, 30, 0, 123, 456).Add(TimeSpan.FromTicks(7)) } }; } - public static TheoryData InvalidLocalTimeScalarStrings() + public static TheoryData InvalidInput() { - return - [ + return new TheoryData + { // https://scalars.graphql.org/chillicream/local-time.html#sec-Input-spec.Examples (Invalid input values) // Contains time zone indicator Z. - "15:30:00Z", + { DateTimeOptions.DefaultInputPrecision, "15:30:00Z" }, // Contains time zone offset. - "15:30:00+05:30", + { DateTimeOptions.DefaultInputPrecision, "15:30:00+05:30" }, // Contains date component. - "2023-12-24T15:30:00", + { DateTimeOptions.DefaultInputPrecision, "2023-12-24T15:30:00" }, // Missing seconds component. - "15:30", + { DateTimeOptions.DefaultInputPrecision, "15:30" }, // Invalid hour (24). - "24:00:00", + { DateTimeOptions.DefaultInputPrecision, "24:00:00" }, // Invalid minute (60). - "15:60:00", + { DateTimeOptions.DefaultInputPrecision, "15:60:00" }, // More than 9 fractional second digits. - "15:30:00.1234567890" - ]; + { DateTimeOptions.DefaultInputPrecision, "15:30:00.1234567890" }, + // Additional cases. + // More than 7 fractional second digits with default precision. + { DateTimeOptions.DefaultInputPrecision, "15:30:00.12345678" }, + // More than 6 fractional second digits with precision set to 6. + { 6, "15:30:00.1234567" }, + // More than 5 fractional second digits with precision set to 5. + { 5, "15:30:00.123456" }, + // More than 4 fractional second digits with precision set to 4. + { 4, "15:30:00.12345" }, + // More than 3 fractional second digits with precision set to 3. + { 3, "15:30:00.1234" }, + // More than 2 fractional second digits with precision set to 2. + { 2, "15:30:00.123" }, + // More than 1 fractional second digit with precision set to 1. + { 1, "15:30:00.12" }, + // Fractional second digits with precision set to 0. + { 0, "15:30:00.1" } + }; + } + + public static TheoryData ValidOutput() + { + return new TheoryData + { + // Up to 7 fractional second digits with default precision. + { + DateTimeOptions.DefaultOutputPrecision, + new TimeOnly(15, 30, 0, 123, 456).Add(TimeSpan.FromTicks(7)), + "15:30:00.1234567" + }, + // Up to 6 fractional second digits with precision set to 6. + { + 6, + new TimeOnly(15, 30, 0, 123, 456).Add(TimeSpan.FromTicks(7)), + "15:30:00.123456" + }, + // Up to 5 fractional second digits with precision set to 5. + { + 5, + new TimeOnly(15, 30, 0, 123, 456).Add(TimeSpan.FromTicks(7)), + "15:30:00.12345" + }, + // Up to 4 fractional second digits with precision set to 4. + { + 4, + new TimeOnly(15, 30, 0, 123, 456).Add(TimeSpan.FromTicks(7)), + "15:30:00.1234" + }, + // Up to 3 fractional second digits with precision set to 3. + { + 3, + new TimeOnly(15, 30, 0, 123, 456).Add(TimeSpan.FromTicks(7)), + "15:30:00.123" + }, + // Up to 2 fractional second digits with precision set to 2. + { + 2, + new TimeOnly(15, 30, 0, 123, 456).Add(TimeSpan.FromTicks(7)), + "15:30:00.12" + }, + // Up to 1 fractional second digit with precision set to 1. + { + 1, + new TimeOnly(15, 30, 0, 123, 456).Add(TimeSpan.FromTicks(7)), + "15:30:00.1" + }, + // No fractional second digits with precision set to 0. + { + 0, + new TimeOnly(15, 30, 0, 123, 456).Add(TimeSpan.FromTicks(7)), + "15:30:00" + } + }; } } From 387e8eb781c2729c1846088bea0e00822cc815e7 Mon Sep 17 00:00:00 2001 From: Glen Date: Fri, 20 Feb 2026 17:43:31 +0200 Subject: [PATCH 2/5] Address Copilot feedback --- .../src/Types/Types/Scalars/DateTimeType.cs | 26 +++++++++++++------ .../Types/Types/Scalars/LocalDateTimeType.cs | 19 ++++++++++---- .../src/Types/Types/Scalars/LocalTimeType.cs | 19 ++++++++++---- .../Types/Scalars/DateTimeTypeTests.cs | 18 +++++++++++++ .../Types/Scalars/LocalDateTimeTypeTests.cs | 18 +++++++++++++ .../Types/Scalars/LocalTimeTypeTests.cs | 18 +++++++++++++ 6 files changed, 100 insertions(+), 18 deletions(-) diff --git a/src/HotChocolate/Core/src/Types/Types/Scalars/DateTimeType.cs b/src/HotChocolate/Core/src/Types/Types/Scalars/DateTimeType.cs index 36f7e416289..0af55df6298 100644 --- a/src/HotChocolate/Core/src/Types/Types/Scalars/DateTimeType.cs +++ b/src/HotChocolate/Core/src/Types/Types/Scalars/DateTimeType.cs @@ -24,6 +24,9 @@ public partial class DateTimeType : ScalarType private readonly bool _enforceSpecFormat; private readonly DateTimeOptions _options; + private readonly string _utcFormat; + private readonly string _localFormat; + private readonly Regex _dateTimeRegex; /// /// Initializes a new instance of the class. @@ -37,14 +40,14 @@ public DateTimeType( : base(name, bind) { options ??= new DateTimeOptions(); + _options = options.Value; Description = description; - Pattern = - @"^\d{4}-\d{2}-\d{2}[Tt]\d{2}:\d{2}:\d{2}(?:\.\d{1," - + options.Value.InputPrecision - + @"})?(?:[Zz]|[+-]\d{2}:\d{2})$"; + Pattern = GetPattern(); SpecifiedBy = new Uri(SpecifiedByUri); _enforceSpecFormat = !disableFormatCheck; - _options = options.Value; + _utcFormat = GetUtcFormat(); + _localFormat = GetLocalFormat(); + _dateTimeRegex = GetDateTimeRegex(); } /// @@ -108,7 +111,7 @@ protected override void OnCoerceOutputValue(DateTimeOffset runtimeValue, ResultE { resultValue.SetStringValue( runtimeValue.ToString( - runtimeValue.Offset == TimeSpan.Zero ? GetUtcFormat() : GetLocalFormat(), + runtimeValue.Offset == TimeSpan.Zero ? _utcFormat : _localFormat, CultureInfo.InvariantCulture)); } @@ -116,14 +119,14 @@ protected override StringValueNode OnValueToLiteral(DateTimeOffset runtimeValue) { return new StringValueNode( runtimeValue.ToString( - runtimeValue.Offset == TimeSpan.Zero ? GetUtcFormat() : GetLocalFormat(), + runtimeValue.Offset == TimeSpan.Zero ? _utcFormat : _localFormat, CultureInfo.InvariantCulture)); } private bool TryParseStringValue(string serialized, out DateTimeOffset value) { // Check format. - if (_enforceSpecFormat && !GetDateTimeRegex().IsMatch(serialized)) + if (_enforceSpecFormat && !_dateTimeRegex.IsMatch(serialized)) { value = default; return false; @@ -143,6 +146,13 @@ private bool TryParseStringValue(string serialized, out DateTimeOffset value) return false; } + private string GetPattern() + => _options.InputPrecision == 0 + ? @"^\d{4}-\d{2}-\d{2}[Tt]\d{2}:\d{2}:\d{2}(?:[Zz]|[+-]\d{2}:\d{2})$" + : @"^\d{4}-\d{2}-\d{2}[Tt]\d{2}:\d{2}:\d{2}(?:\.\d{1," + + _options.InputPrecision + + @"})?(?:[Zz]|[+-]\d{2}:\d{2})$"; + private string GetUtcFormat() => _options.OutputPrecision switch { diff --git a/src/HotChocolate/Core/src/Types/Types/Scalars/LocalDateTimeType.cs b/src/HotChocolate/Core/src/Types/Types/Scalars/LocalDateTimeType.cs index 977fa363937..903a03ab71b 100644 --- a/src/HotChocolate/Core/src/Types/Types/Scalars/LocalDateTimeType.cs +++ b/src/HotChocolate/Core/src/Types/Types/Scalars/LocalDateTimeType.cs @@ -24,6 +24,8 @@ public partial class LocalDateTimeType : ScalarType private readonly bool _enforceSpecFormat; private readonly DateTimeOptions _options; + private readonly string _localFormat; + private readonly Regex _localDateTimeRegex; /// /// Initializes a new instance of the class. @@ -37,11 +39,13 @@ public LocalDateTimeType( : base(name, bind) { options ??= new DateTimeOptions(); + _options = options.Value; Description = description; - Pattern = @"^\d{4}-\d{2}-\d{2}[Tt]\d{2}:\d{2}:\d{2}(?:\.\d{1," + options.Value.InputPrecision + "})?$"; + Pattern = GetPattern(); SpecifiedBy = new Uri(SpecifiedByUri); _enforceSpecFormat = !disableFormatCheck; - _options = options.Value; + _localFormat = GetLocalFormat(); + _localDateTimeRegex = GetLocalDateTimeRegex(); } /// @@ -103,16 +107,16 @@ protected override DateTime OnCoerceInputValue(JsonElement inputValue, IFeatureP /// protected override void OnCoerceOutputValue(DateTime runtimeValue, ResultElement resultValue) - => resultValue.SetStringValue(runtimeValue.ToString(GetLocalFormat(), CultureInfo.InvariantCulture)); + => resultValue.SetStringValue(runtimeValue.ToString(_localFormat, CultureInfo.InvariantCulture)); /// protected override StringValueNode OnValueToLiteral(DateTime runtimeValue) - => new StringValueNode(runtimeValue.ToString(GetLocalFormat(), CultureInfo.InvariantCulture)); + => new StringValueNode(runtimeValue.ToString(_localFormat, CultureInfo.InvariantCulture)); private bool TryParseStringValue(string serialized, out DateTime value) { // Check format. - if (_enforceSpecFormat && !GetLocalDateTimeRegex().IsMatch(serialized)) + if (_enforceSpecFormat && !_localDateTimeRegex.IsMatch(serialized)) { value = default; return false; @@ -132,6 +136,11 @@ private bool TryParseStringValue(string serialized, out DateTime value) return false; } + private string GetPattern() + => _options.InputPrecision == 0 + ? @"^\d{4}-\d{2}-\d{2}[Tt]\d{2}:\d{2}:\d{2}$" + : @"^\d{4}-\d{2}-\d{2}[Tt]\d{2}:\d{2}:\d{2}(?:\.\d{1," + _options.InputPrecision + "})?$"; + private string GetLocalFormat() => _options.OutputPrecision switch { diff --git a/src/HotChocolate/Core/src/Types/Types/Scalars/LocalTimeType.cs b/src/HotChocolate/Core/src/Types/Types/Scalars/LocalTimeType.cs index 1bbbd770e0b..160e6e271b3 100644 --- a/src/HotChocolate/Core/src/Types/Types/Scalars/LocalTimeType.cs +++ b/src/HotChocolate/Core/src/Types/Types/Scalars/LocalTimeType.cs @@ -23,6 +23,8 @@ public partial class LocalTimeType : ScalarType private readonly bool _enforceSpecFormat; private readonly DateTimeOptions _options; + private readonly string _localFormat; + private readonly Regex _localTimeRegex; /// /// Initializes a new instance of the class. @@ -36,11 +38,13 @@ public LocalTimeType( : base(name, bind) { options ??= new DateTimeOptions(); + _options = options.Value; Description = description; - Pattern = @"^\d{2}:\d{2}:\d{2}(?:\.\d{1," + options.Value.InputPrecision + "})?$"; + Pattern = GetPattern(); SpecifiedBy = new Uri(SpecifiedByUri); _enforceSpecFormat = !disableFormatCheck; - _options = options.Value; + _localFormat = GetLocalFormat(); + _localTimeRegex = GetLocalTimeRegex(); } /// @@ -102,16 +106,16 @@ protected override TimeOnly OnCoerceInputValue(JsonElement inputValue, IFeatureP /// protected override void OnCoerceOutputValue(TimeOnly runtimeValue, ResultElement resultValue) - => resultValue.SetStringValue(runtimeValue.ToString(GetLocalFormat(), CultureInfo.InvariantCulture)); + => resultValue.SetStringValue(runtimeValue.ToString(_localFormat, CultureInfo.InvariantCulture)); /// protected override StringValueNode OnValueToLiteral(TimeOnly runtimeValue) - => new StringValueNode(runtimeValue.ToString(GetLocalFormat(), CultureInfo.InvariantCulture)); + => new StringValueNode(runtimeValue.ToString(_localFormat, CultureInfo.InvariantCulture)); private bool TryParseStringValue(string serialized, out TimeOnly value) { // Check format. - if (_enforceSpecFormat && !GetLocalTimeRegex().IsMatch(serialized)) + if (_enforceSpecFormat && !_localTimeRegex.IsMatch(serialized)) { value = default; return false; @@ -130,6 +134,11 @@ private bool TryParseStringValue(string serialized, out TimeOnly value) return false; } + private string GetPattern() + => _options.InputPrecision == 0 + ? @"^\d{2}:\d{2}:\d{2}$" + : @"^\d{2}:\d{2}:\d{2}(?:\.\d{1," + _options.InputPrecision + "})?$"; + private string GetLocalFormat() => _options.OutputPrecision switch { diff --git a/src/HotChocolate/Core/test/Types.Tests/Types/Scalars/DateTimeTypeTests.cs b/src/HotChocolate/Core/test/Types.Tests/Types/Scalars/DateTimeTypeTests.cs index 6a1dc6385d2..aadc3b8e5fc 100644 --- a/src/HotChocolate/Core/test/Types.Tests/Types/Scalars/DateTimeTypeTests.cs +++ b/src/HotChocolate/Core/test/Types.Tests/Types/Scalars/DateTimeTypeTests.cs @@ -319,6 +319,24 @@ public void DateTime_Relaxed_Format_Check() Assert.IsType(result); } + [Theory] + [InlineData(0, @"^\d{4}-\d{2}-\d{2}[Tt]\d{2}:\d{2}:\d{2}(?:[Zz]|[+-]\d{2}:\d{2})$")] + [InlineData(1, @"^\d{4}-\d{2}-\d{2}[Tt]\d{2}:\d{2}:\d{2}(?:\.\d{1,1})?(?:[Zz]|[+-]\d{2}:\d{2})$")] + [InlineData(2, @"^\d{4}-\d{2}-\d{2}[Tt]\d{2}:\d{2}:\d{2}(?:\.\d{1,2})?(?:[Zz]|[+-]\d{2}:\d{2})$")] + [InlineData(3, @"^\d{4}-\d{2}-\d{2}[Tt]\d{2}:\d{2}:\d{2}(?:\.\d{1,3})?(?:[Zz]|[+-]\d{2}:\d{2})$")] + [InlineData(4, @"^\d{4}-\d{2}-\d{2}[Tt]\d{2}:\d{2}:\d{2}(?:\.\d{1,4})?(?:[Zz]|[+-]\d{2}:\d{2})$")] + [InlineData(5, @"^\d{4}-\d{2}-\d{2}[Tt]\d{2}:\d{2}:\d{2}(?:\.\d{1,5})?(?:[Zz]|[+-]\d{2}:\d{2})$")] + [InlineData(6, @"^\d{4}-\d{2}-\d{2}[Tt]\d{2}:\d{2}:\d{2}(?:\.\d{1,6})?(?:[Zz]|[+-]\d{2}:\d{2})$")] + [InlineData(7, @"^\d{4}-\d{2}-\d{2}[Tt]\d{2}:\d{2}:\d{2}(?:\.\d{1,7})?(?:[Zz]|[+-]\d{2}:\d{2})$")] + public void Pattern_Should_Match_InputPrecision(byte precision, string expectedPattern) + { + // arrange & act + var type = new DateTimeType(new DateTimeOptions { InputPrecision = precision }); + + // assert + Assert.Equal(expectedPattern, type.Pattern); + } + public class DefaultDateTime { public DateTime Test => default; diff --git a/src/HotChocolate/Core/test/Types.Tests/Types/Scalars/LocalDateTimeTypeTests.cs b/src/HotChocolate/Core/test/Types.Tests/Types/Scalars/LocalDateTimeTypeTests.cs index ff5bcc8a9fe..c9f680f1657 100644 --- a/src/HotChocolate/Core/test/Types.Tests/Types/Scalars/LocalDateTimeTypeTests.cs +++ b/src/HotChocolate/Core/test/Types.Tests/Types/Scalars/LocalDateTimeTypeTests.cs @@ -314,6 +314,24 @@ public void LocalDateTime_Relaxed_Format_Check() Assert.IsType(result); } + [Theory] + [InlineData(0, @"^\d{4}-\d{2}-\d{2}[Tt]\d{2}:\d{2}:\d{2}$")] + [InlineData(1, @"^\d{4}-\d{2}-\d{2}[Tt]\d{2}:\d{2}:\d{2}(?:\.\d{1,1})?$")] + [InlineData(2, @"^\d{4}-\d{2}-\d{2}[Tt]\d{2}:\d{2}:\d{2}(?:\.\d{1,2})?$")] + [InlineData(3, @"^\d{4}-\d{2}-\d{2}[Tt]\d{2}:\d{2}:\d{2}(?:\.\d{1,3})?$")] + [InlineData(4, @"^\d{4}-\d{2}-\d{2}[Tt]\d{2}:\d{2}:\d{2}(?:\.\d{1,4})?$")] + [InlineData(5, @"^\d{4}-\d{2}-\d{2}[Tt]\d{2}:\d{2}:\d{2}(?:\.\d{1,5})?$")] + [InlineData(6, @"^\d{4}-\d{2}-\d{2}[Tt]\d{2}:\d{2}:\d{2}(?:\.\d{1,6})?$")] + [InlineData(7, @"^\d{4}-\d{2}-\d{2}[Tt]\d{2}:\d{2}:\d{2}(?:\.\d{1,7})?$")] + public void Pattern_Should_Match_InputPrecision(byte precision, string expectedPattern) + { + // arrange & act + var type = new LocalDateTimeType(new DateTimeOptions { InputPrecision = precision }); + + // assert + Assert.Equal(expectedPattern, type.Pattern); + } + public class Query { [GraphQLType] diff --git a/src/HotChocolate/Core/test/Types.Tests/Types/Scalars/LocalTimeTypeTests.cs b/src/HotChocolate/Core/test/Types.Tests/Types/Scalars/LocalTimeTypeTests.cs index 1df0bd1512d..038ae62c17d 100644 --- a/src/HotChocolate/Core/test/Types.Tests/Types/Scalars/LocalTimeTypeTests.cs +++ b/src/HotChocolate/Core/test/Types.Tests/Types/Scalars/LocalTimeTypeTests.cs @@ -314,6 +314,24 @@ public void LocalTime_Relaxed_Format_Check() Assert.IsType(result); } + [Theory] + [InlineData(0, @"^\d{2}:\d{2}:\d{2}$")] + [InlineData(1, @"^\d{2}:\d{2}:\d{2}(?:\.\d{1,1})?$")] + [InlineData(2, @"^\d{2}:\d{2}:\d{2}(?:\.\d{1,2})?$")] + [InlineData(3, @"^\d{2}:\d{2}:\d{2}(?:\.\d{1,3})?$")] + [InlineData(4, @"^\d{2}:\d{2}:\d{2}(?:\.\d{1,4})?$")] + [InlineData(5, @"^\d{2}:\d{2}:\d{2}(?:\.\d{1,5})?$")] + [InlineData(6, @"^\d{2}:\d{2}:\d{2}(?:\.\d{1,6})?$")] + [InlineData(7, @"^\d{2}:\d{2}:\d{2}(?:\.\d{1,7})?$")] + public void Pattern_Should_Match_InputPrecision(byte precision, string expectedPattern) + { + // arrange & act + var type = new LocalTimeType(new DateTimeOptions { InputPrecision = precision }); + + // assert + Assert.Equal(expectedPattern, type.Pattern); + } + public class Query { [GraphQLType(typeof(LocalTimeType))] From a0a7d176caa19ab45d32d23f1cd1e9076cca4713 Mon Sep 17 00:00:00 2001 From: Glen Date: Fri, 20 Feb 2026 20:28:26 +0200 Subject: [PATCH 3/5] Update test cases --- .../Types.Tests/Types/Scalars/DateTimeTypeTests.cs | 8 ++------ .../Types/Scalars/LocalDateTimeTypeTests.cs | 8 ++------ .../Types.Tests/Types/Scalars/LocalTimeTypeTests.cs | 13 ++----------- 3 files changed, 6 insertions(+), 23 deletions(-) diff --git a/src/HotChocolate/Core/test/Types.Tests/Types/Scalars/DateTimeTypeTests.cs b/src/HotChocolate/Core/test/Types.Tests/Types/Scalars/DateTimeTypeTests.cs index aadc3b8e5fc..6296bea79b8 100644 --- a/src/HotChocolate/Core/test/Types.Tests/Types/Scalars/DateTimeTypeTests.cs +++ b/src/HotChocolate/Core/test/Types.Tests/Types/Scalars/DateTimeTypeTests.cs @@ -352,8 +352,6 @@ public static TheoryData ValidInput() "2023-12-24T15:30:00Z", new DateTimeOffset(2023, 12, 24, 15, 30, 0, 0, TimeSpan.Zero) }, - // Additional cases. - // Up to 7 fractional second digits. { DateTimeOptions.DefaultInputPrecision, "2023-12-24T15:30:00.1234567+01:00", @@ -378,15 +376,13 @@ public static TheoryData InvalidInput() // ReSharper disable once GrammarMistakeInComment // Invalid date (February 30th). { DateTimeOptions.DefaultInputPrecision, "2023-02-30T15:30:00Z" }, - // More than 9 fractional second digits. - { DateTimeOptions.DefaultInputPrecision, "2023-12-24T15:30:00.1234567890Z" }, + // More than 7 fractional second digits. + { DateTimeOptions.DefaultInputPrecision, "2023-12-24T15:30:00.12345678Z" }, // Invalid offset (exceeds maximum). { DateTimeOptions.DefaultInputPrecision, "2023-12-24T15:30:00+25:00" }, // Invalid offset format. { DateTimeOptions.DefaultInputPrecision, "2023-12-24T15:30:00 UTC" }, // Additional cases. - // More than 7 fractional second digits with default precision. - { DateTimeOptions.DefaultInputPrecision, "2023-12-24T15:30:00.12345678Z" }, // More than 6 fractional second digits with precision set to 6. { 6, "2023-12-24T15:30:00.1234567Z" }, // More than 5 fractional second digits with precision set to 5. diff --git a/src/HotChocolate/Core/test/Types.Tests/Types/Scalars/LocalDateTimeTypeTests.cs b/src/HotChocolate/Core/test/Types.Tests/Types/Scalars/LocalDateTimeTypeTests.cs index c9f680f1657..6949d4761f7 100644 --- a/src/HotChocolate/Core/test/Types.Tests/Types/Scalars/LocalDateTimeTypeTests.cs +++ b/src/HotChocolate/Core/test/Types.Tests/Types/Scalars/LocalDateTimeTypeTests.cs @@ -373,8 +373,6 @@ public static TheoryData ValidInput() "2023-12-24T15:30:00", new DateTime(2023, 12, 24, 15, 30, 0, 0) }, - // Additional cases. - // Up to 7 fractional second digits. { DateTimeOptions.DefaultInputPrecision, "2023-12-24t15:30:00.1234567", @@ -401,11 +399,9 @@ public static TheoryData InvalidInput() // ReSharper disable once GrammarMistakeInComment // Invalid date (February 30th). { DateTimeOptions.DefaultInputPrecision, "2023-02-30T15:30:00" }, - // More than 9 fractional second digits. - { DateTimeOptions.DefaultInputPrecision, "2023-12-24T15:30:00.1234567890" }, - // Additional cases. - // More than 7 fractional second digits with default precision. + // More than 7 fractional second digits. { DateTimeOptions.DefaultInputPrecision, "2023-12-24T15:30:00.12345678" }, + // Additional cases. // More than 6 fractional second digits with precision set to 6. { 6, "2023-12-24T15:30:00.1234567" }, // More than 5 fractional second digits with precision set to 5. diff --git a/src/HotChocolate/Core/test/Types.Tests/Types/Scalars/LocalTimeTypeTests.cs b/src/HotChocolate/Core/test/Types.Tests/Types/Scalars/LocalTimeTypeTests.cs index 038ae62c17d..b3361ea548b 100644 --- a/src/HotChocolate/Core/test/Types.Tests/Types/Scalars/LocalTimeTypeTests.cs +++ b/src/HotChocolate/Core/test/Types.Tests/Types/Scalars/LocalTimeTypeTests.cs @@ -370,13 +370,6 @@ public static TheoryData ValidInput() "09:00:00", new TimeOnly(9, 0, 0) }, - { - DateTimeOptions.DefaultInputPrecision, - "07:30:00.500", - new TimeOnly(7, 30, 0, 500) - }, - // Additional cases. - // Up to 7 fractional second digits. { DateTimeOptions.DefaultInputPrecision, "07:30:00.1234567", @@ -402,11 +395,9 @@ public static TheoryData InvalidInput() { DateTimeOptions.DefaultInputPrecision, "24:00:00" }, // Invalid minute (60). { DateTimeOptions.DefaultInputPrecision, "15:60:00" }, - // More than 9 fractional second digits. - { DateTimeOptions.DefaultInputPrecision, "15:30:00.1234567890" }, - // Additional cases. - // More than 7 fractional second digits with default precision. + // More than 7 fractional second digits. { DateTimeOptions.DefaultInputPrecision, "15:30:00.12345678" }, + // Additional cases. // More than 6 fractional second digits with precision set to 6. { 6, "15:30:00.1234567" }, // More than 5 fractional second digits with precision set to 5. From aad8f82cd7a7936df08771c40b6c08274cab4aa4 Mon Sep 17 00:00:00 2001 From: Glen Date: Fri, 6 Mar 2026 10:38:32 +0200 Subject: [PATCH 4/5] Add ValidateInputFormat option --- .../Types/Types/Scalars/DateTimeOptions.cs | 6 +++ .../src/Types/Types/Scalars/DateTimeType.cs | 43 ++++++++++++------ .../Types/Types/Scalars/LocalDateTimeType.cs | 45 +++++++++++++------ .../src/Types/Types/Scalars/LocalTimeType.cs | 45 +++++++++++++------ .../Types/Scalars/DateTimeTypeTests.cs | 2 +- .../Types/Scalars/LocalDateTimeTypeTests.cs | 2 +- .../Types/Scalars/LocalTimeTypeTests.cs | 2 +- 7 files changed, 101 insertions(+), 44 deletions(-) diff --git a/src/HotChocolate/Core/src/Types/Types/Scalars/DateTimeOptions.cs b/src/HotChocolate/Core/src/Types/Types/Scalars/DateTimeOptions.cs index 4e036c4a2ca..dd9ebd739ed 100644 --- a/src/HotChocolate/Core/src/Types/Types/Scalars/DateTimeOptions.cs +++ b/src/HotChocolate/Core/src/Types/Types/Scalars/DateTimeOptions.cs @@ -75,4 +75,10 @@ public byte OutputPrecision field = value; } } = DefaultOutputPrecision; + + /// + /// Gets a value indicating whether the input format of date and time values should be validated + /// against the expected format. Defaults to true. + /// + public bool ValidateInputFormat { get; init; } = true; } diff --git a/src/HotChocolate/Core/src/Types/Types/Scalars/DateTimeType.cs b/src/HotChocolate/Core/src/Types/Types/Scalars/DateTimeType.cs index 0af55df6298..7451fdf4375 100644 --- a/src/HotChocolate/Core/src/Types/Types/Scalars/DateTimeType.cs +++ b/src/HotChocolate/Core/src/Types/Types/Scalars/DateTimeType.cs @@ -22,7 +22,6 @@ public partial class DateTimeType : ScalarType private const string LocalFormat = "yyyy-MM-ddTHH\\:mm\\:ss.FFFFFFFzzz"; private const string SpecifiedByUri = "https://scalars.graphql.org/chillicream/date-time.html"; - private readonly bool _enforceSpecFormat; private readonly DateTimeOptions _options; private readonly string _utcFormat; private readonly string _localFormat; @@ -35,16 +34,13 @@ public DateTimeType( string name, string? description = null, BindingBehavior bind = BindingBehavior.Explicit, - bool disableFormatCheck = false, DateTimeOptions? options = null) : base(name, bind) { - options ??= new DateTimeOptions(); - _options = options.Value; + _options = options ?? new DateTimeOptions(); Description = description; Pattern = GetPattern(); SpecifiedBy = new Uri(SpecifiedByUri); - _enforceSpecFormat = !disableFormatCheck; _utcFormat = GetUtcFormat(); _localFormat = GetLocalFormat(); _dateTimeRegex = GetDateTimeRegex(); @@ -53,37 +49,58 @@ public DateTimeType( /// /// Initializes a new instance of the class. /// - public DateTimeType(bool disableFormatCheck) + public DateTimeType(DateTimeOptions options) : this( ScalarNames.DateTime, TypeResources.DateTimeType_Description, BindingBehavior.Implicit, - disableFormatCheck: disableFormatCheck) + options: options) { } /// /// Initializes a new instance of the class. /// - public DateTimeType(DateTimeOptions options) + [ActivatorUtilitiesConstructor] + public DateTimeType() : this( ScalarNames.DateTime, TypeResources.DateTimeType_Description, BindingBehavior.Implicit, - options: options) + options: null) { } /// /// Initializes a new instance of the class. /// - [ActivatorUtilitiesConstructor] - public DateTimeType() + [Obsolete("Use the constructor that accepts DateTimeOptions instead.")] + public DateTimeType( + string name, + string? description = null, + BindingBehavior bind = BindingBehavior.Explicit, + bool disableFormatCheck = false) + : base(name, bind) + { + _options = new DateTimeOptions { ValidateInputFormat = !disableFormatCheck }; + Description = description; + Pattern = GetPattern(); + SpecifiedBy = new Uri(SpecifiedByUri); + _utcFormat = GetUtcFormat(); + _localFormat = GetLocalFormat(); + _dateTimeRegex = GetDateTimeRegex(); + } + + /// + /// Initializes a new instance of the class. + /// + [Obsolete("Use the constructor that accepts DateTimeOptions instead.")] + public DateTimeType(bool disableFormatCheck) : this( ScalarNames.DateTime, TypeResources.DateTimeType_Description, BindingBehavior.Implicit, - disableFormatCheck: false) + disableFormatCheck: disableFormatCheck) { } @@ -126,7 +143,7 @@ protected override StringValueNode OnValueToLiteral(DateTimeOffset runtimeValue) private bool TryParseStringValue(string serialized, out DateTimeOffset value) { // Check format. - if (_enforceSpecFormat && !_dateTimeRegex.IsMatch(serialized)) + if (_options.ValidateInputFormat && !_dateTimeRegex.IsMatch(serialized)) { value = default; return false; diff --git a/src/HotChocolate/Core/src/Types/Types/Scalars/LocalDateTimeType.cs b/src/HotChocolate/Core/src/Types/Types/Scalars/LocalDateTimeType.cs index 903a03ab71b..cdd54992016 100644 --- a/src/HotChocolate/Core/src/Types/Types/Scalars/LocalDateTimeType.cs +++ b/src/HotChocolate/Core/src/Types/Types/Scalars/LocalDateTimeType.cs @@ -22,7 +22,6 @@ public partial class LocalDateTimeType : ScalarType private const string LocalFormat = "yyyy-MM-ddTHH\\:mm\\:ss.FFFFFFF"; private const string SpecifiedByUri = "https://scalars.graphql.org/chillicream/local-date-time.html"; - private readonly bool _enforceSpecFormat; private readonly DateTimeOptions _options; private readonly string _localFormat; private readonly Regex _localDateTimeRegex; @@ -34,16 +33,13 @@ public LocalDateTimeType( string name, string? description = null, BindingBehavior bind = BindingBehavior.Explicit, - bool disableFormatCheck = false, DateTimeOptions? options = null) : base(name, bind) { - options ??= new DateTimeOptions(); - _options = options.Value; + _options = options ?? new DateTimeOptions(); Description = description; Pattern = GetPattern(); SpecifiedBy = new Uri(SpecifiedByUri); - _enforceSpecFormat = !disableFormatCheck; _localFormat = GetLocalFormat(); _localDateTimeRegex = GetLocalDateTimeRegex(); } @@ -51,35 +47,56 @@ public LocalDateTimeType( /// /// Initializes a new instance of the class. /// - public LocalDateTimeType(bool disableFormatCheck) + public LocalDateTimeType(DateTimeOptions options) : this( ScalarNames.LocalDateTime, TypeResources.LocalDateTimeType_Description, BindingBehavior.Implicit, - disableFormatCheck: disableFormatCheck) + options: options) { } /// /// Initializes a new instance of the class. /// - public LocalDateTimeType(DateTimeOptions options) + [ActivatorUtilitiesConstructor] + public LocalDateTimeType() : this( ScalarNames.LocalDateTime, TypeResources.LocalDateTimeType_Description, - BindingBehavior.Implicit, - options: options) + options: null) { } /// /// Initializes a new instance of the class. /// - [ActivatorUtilitiesConstructor] - public LocalDateTimeType() + [Obsolete("Use the constructor that accepts DateTimeOptions instead.")] + public LocalDateTimeType( + string name, + string? description = null, + BindingBehavior bind = BindingBehavior.Explicit, + bool disableFormatCheck = false) + : base(name, bind) + { + _options = new DateTimeOptions { ValidateInputFormat = !disableFormatCheck }; + Description = description; + Pattern = GetPattern(); + SpecifiedBy = new Uri(SpecifiedByUri); + _localFormat = GetLocalFormat(); + _localDateTimeRegex = GetLocalDateTimeRegex(); + } + + /// + /// Initializes a new instance of the class. + /// + [Obsolete("Use the constructor that accepts DateTimeOptions instead.")] + public LocalDateTimeType(bool disableFormatCheck) : this( ScalarNames.LocalDateTime, - TypeResources.LocalDateTimeType_Description) + TypeResources.LocalDateTimeType_Description, + BindingBehavior.Implicit, + disableFormatCheck: disableFormatCheck) { } @@ -116,7 +133,7 @@ protected override StringValueNode OnValueToLiteral(DateTime runtimeValue) private bool TryParseStringValue(string serialized, out DateTime value) { // Check format. - if (_enforceSpecFormat && !_localDateTimeRegex.IsMatch(serialized)) + if (_options.ValidateInputFormat && !_localDateTimeRegex.IsMatch(serialized)) { value = default; return false; diff --git a/src/HotChocolate/Core/src/Types/Types/Scalars/LocalTimeType.cs b/src/HotChocolate/Core/src/Types/Types/Scalars/LocalTimeType.cs index 160e6e271b3..42ed4e0b2d6 100644 --- a/src/HotChocolate/Core/src/Types/Types/Scalars/LocalTimeType.cs +++ b/src/HotChocolate/Core/src/Types/Types/Scalars/LocalTimeType.cs @@ -21,7 +21,6 @@ public partial class LocalTimeType : ScalarType private const string LocalFormat = "HH:mm:ss.FFFFFFF"; private const string SpecifiedByUri = "https://scalars.graphql.org/chillicream/local-time.html"; - private readonly bool _enforceSpecFormat; private readonly DateTimeOptions _options; private readonly string _localFormat; private readonly Regex _localTimeRegex; @@ -33,16 +32,13 @@ public LocalTimeType( string name, string? description = null, BindingBehavior bind = BindingBehavior.Explicit, - bool disableFormatCheck = false, DateTimeOptions? options = null) : base(name, bind) { - options ??= new DateTimeOptions(); - _options = options.Value; + _options = options ?? new DateTimeOptions(); Description = description; Pattern = GetPattern(); SpecifiedBy = new Uri(SpecifiedByUri); - _enforceSpecFormat = !disableFormatCheck; _localFormat = GetLocalFormat(); _localTimeRegex = GetLocalTimeRegex(); } @@ -50,35 +46,56 @@ public LocalTimeType( /// /// Initializes a new instance of the class. /// - public LocalTimeType(bool disableFormatCheck) + public LocalTimeType(DateTimeOptions options) : this( ScalarNames.LocalTime, TypeResources.LocalTimeType_Description, BindingBehavior.Implicit, - disableFormatCheck: disableFormatCheck) + options: options) { } /// /// Initializes a new instance of the class. /// - public LocalTimeType(DateTimeOptions options) + [ActivatorUtilitiesConstructor] + public LocalTimeType() : this( ScalarNames.LocalTime, TypeResources.LocalTimeType_Description, - BindingBehavior.Implicit, - options: options) + options: null) { } /// /// Initializes a new instance of the class. /// - [ActivatorUtilitiesConstructor] - public LocalTimeType() + [Obsolete("Use the constructor that accepts DateTimeOptions instead.")] + public LocalTimeType( + string name, + string? description = null, + BindingBehavior bind = BindingBehavior.Explicit, + bool disableFormatCheck = false) + : base(name, bind) + { + _options = new DateTimeOptions { ValidateInputFormat = !disableFormatCheck }; + Description = description; + Pattern = GetPattern(); + SpecifiedBy = new Uri(SpecifiedByUri); + _localFormat = GetLocalFormat(); + _localTimeRegex = GetLocalTimeRegex(); + } + + /// + /// Initializes a new instance of the class. + /// + [Obsolete("Use the constructor that accepts DateTimeOptions instead.")] + public LocalTimeType(bool disableFormatCheck) : this( ScalarNames.LocalTime, - TypeResources.LocalTimeType_Description) + TypeResources.LocalTimeType_Description, + BindingBehavior.Implicit, + disableFormatCheck: disableFormatCheck) { } @@ -115,7 +132,7 @@ protected override StringValueNode OnValueToLiteral(TimeOnly runtimeValue) private bool TryParseStringValue(string serialized, out TimeOnly value) { // Check format. - if (_enforceSpecFormat && !_localTimeRegex.IsMatch(serialized)) + if (_options.ValidateInputFormat && !_localTimeRegex.IsMatch(serialized)) { value = default; return false; diff --git a/src/HotChocolate/Core/test/Types.Tests/Types/Scalars/DateTimeTypeTests.cs b/src/HotChocolate/Core/test/Types.Tests/Types/Scalars/DateTimeTypeTests.cs index 6296bea79b8..2ba79dc582b 100644 --- a/src/HotChocolate/Core/test/Types.Tests/Types/Scalars/DateTimeTypeTests.cs +++ b/src/HotChocolate/Core/test/Types.Tests/Types/Scalars/DateTimeTypeTests.cs @@ -311,7 +311,7 @@ public void DateTime_Relaxed_Format_Check() const string s = "2011-08-30"; // act - var type = new DateTimeType(disableFormatCheck: true); + var type = new DateTimeType(new DateTimeOptions { ValidateInputFormat = false }); var inputValue = JsonDocument.Parse($"\"{s}\"").RootElement; var result = type.CoerceInputValue(inputValue, null!); diff --git a/src/HotChocolate/Core/test/Types.Tests/Types/Scalars/LocalDateTimeTypeTests.cs b/src/HotChocolate/Core/test/Types.Tests/Types/Scalars/LocalDateTimeTypeTests.cs index 6949d4761f7..10b3016814d 100644 --- a/src/HotChocolate/Core/test/Types.Tests/Types/Scalars/LocalDateTimeTypeTests.cs +++ b/src/HotChocolate/Core/test/Types.Tests/Types/Scalars/LocalDateTimeTypeTests.cs @@ -306,7 +306,7 @@ public void LocalDateTime_Relaxed_Format_Check() const string s = "2011-08-30"; // act - var type = new LocalDateTimeType(disableFormatCheck: true); + var type = new LocalDateTimeType(new DateTimeOptions { ValidateInputFormat = false }); var inputValue = JsonDocument.Parse($"\"{s}\"").RootElement; var result = type.CoerceInputValue(inputValue, null!); diff --git a/src/HotChocolate/Core/test/Types.Tests/Types/Scalars/LocalTimeTypeTests.cs b/src/HotChocolate/Core/test/Types.Tests/Types/Scalars/LocalTimeTypeTests.cs index b3361ea548b..1b41874f8cb 100644 --- a/src/HotChocolate/Core/test/Types.Tests/Types/Scalars/LocalTimeTypeTests.cs +++ b/src/HotChocolate/Core/test/Types.Tests/Types/Scalars/LocalTimeTypeTests.cs @@ -306,7 +306,7 @@ public void LocalTime_Relaxed_Format_Check() const string s = "15:30"; // act - var type = new LocalTimeType(disableFormatCheck: true); + var type = new LocalTimeType(new DateTimeOptions { ValidateInputFormat = false }); var inputValue = JsonDocument.Parse($"\"{s}\"").RootElement; var result = type.CoerceInputValue(inputValue, null!); From e2ced9e6f9612ebdf74bf3d8061648ed13c7c61d Mon Sep 17 00:00:00 2001 From: Glen Date: Fri, 6 Mar 2026 10:48:26 +0200 Subject: [PATCH 5/5] Update options summary --- .../Core/src/Types/Types/Scalars/DateTimeOptions.cs | 13 ------------- 1 file changed, 13 deletions(-) diff --git a/src/HotChocolate/Core/src/Types/Types/Scalars/DateTimeOptions.cs b/src/HotChocolate/Core/src/Types/Types/Scalars/DateTimeOptions.cs index dd9ebd739ed..32bd19d18b6 100644 --- a/src/HotChocolate/Core/src/Types/Types/Scalars/DateTimeOptions.cs +++ b/src/HotChocolate/Core/src/Types/Types/Scalars/DateTimeOptions.cs @@ -3,21 +3,8 @@ namespace HotChocolate.Types; /// -/// /// Defines options for configuring the behavior of date and time scalar types, such as /// DateTime, LocalDateTime, and LocalTime. -/// -/// -/// These options allow you to specify the precision of fractional seconds for both input parsing -/// and output serialization, ensuring that the date and time values are handled with the desired -/// level of detail. -/// -/// -/// The default precision is set to 7, which corresponds to the maximum precision supported by -/// .NET's date and time types. Adjusting these options can help optimize performance and storage -/// when high precision is not required, while still adhering to the GraphQL specification for date -/// and time formats. -/// /// public struct DateTimeOptions {