diff --git a/dictionary.txt b/dictionary.txt
index eb32caed457..0ec1c23941f 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 bb50e74cddd..620fac42dba 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 643feeecf10..a09a2d08b04 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..32bd19d18b6
--- /dev/null
+++ b/src/HotChocolate/Core/src/Types/Types/Scalars/DateTimeOptions.cs
@@ -0,0 +1,71 @@
+using HotChocolate.Properties;
+
+namespace HotChocolate.Types;
+
+///
+/// Defines options for configuring the behavior of date and time scalar types, such as
+/// DateTime, LocalDateTime, and LocalTime.
+///
+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;
+
+ ///
+ /// 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 b92aeda462c..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,10 @@ 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;
+ private readonly Regex _dateTimeRegex;
///
/// Initializes a new instance of the class.
@@ -31,24 +34,27 @@ public DateTimeType(
string name,
string? description = null,
BindingBehavior bind = BindingBehavior.Explicit,
- bool disableFormatCheck = false)
+ DateTimeOptions? options = null)
: base(name, bind)
{
+ _options = 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 = GetPattern();
SpecifiedBy = new Uri(SpecifiedByUri);
- _enforceSpecFormat = !disableFormatCheck;
+ _utcFormat = GetUtcFormat();
+ _localFormat = GetLocalFormat();
+ _dateTimeRegex = GetDateTimeRegex();
}
///
/// 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)
{
}
@@ -61,7 +67,40 @@ public DateTimeType()
ScalarNames.DateTime,
TypeResources.DateTimeType_Description,
BindingBehavior.Implicit,
- disableFormatCheck: false)
+ options: null)
+ {
+ }
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ [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: disableFormatCheck)
{
}
@@ -87,32 +126,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 ? _utcFormat : _localFormat,
+ 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 ? _utcFormat : _localFormat,
+ CultureInfo.InvariantCulture));
}
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;
@@ -132,8 +163,79 @@ 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
+ {
+ 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..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,9 @@ 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;
///
/// Initializes a new instance of the class.
@@ -31,24 +33,26 @@ public LocalDateTimeType(
string name,
string? description = null,
BindingBehavior bind = BindingBehavior.Explicit,
- bool disableFormatCheck = false)
+ DateTimeOptions? options = null)
: base(name, bind)
{
+ _options = options ?? new DateTimeOptions();
Description = description;
- Pattern = @"^\d{4}-\d{2}-\d{2}[Tt]\d{2}:\d{2}:\d{2}(?:\.\d{1,9})?$";
+ Pattern = GetPattern();
SpecifiedBy = new Uri(SpecifiedByUri);
- _enforceSpecFormat = !disableFormatCheck;
+ _localFormat = GetLocalFormat();
+ _localDateTimeRegex = GetLocalDateTimeRegex();
}
///
/// 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)
{
}
@@ -59,7 +63,40 @@ public LocalDateTimeType(bool disableFormatCheck)
public LocalDateTimeType()
: this(
ScalarNames.LocalDateTime,
- TypeResources.LocalDateTimeType_Description)
+ TypeResources.LocalDateTimeType_Description,
+ options: null)
+ {
+ }
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ [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,
+ BindingBehavior.Implicit,
+ disableFormatCheck: disableFormatCheck)
{
}
@@ -87,16 +124,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(_localFormat, CultureInfo.InvariantCulture));
///
protected override StringValueNode OnValueToLiteral(DateTime runtimeValue)
- => new StringValueNode(runtimeValue.ToString(LocalFormat, CultureInfo.InvariantCulture));
+ => new StringValueNode(runtimeValue.ToString(_localFormat, CultureInfo.InvariantCulture));
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;
@@ -116,7 +153,61 @@ 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 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
+ {
+ 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..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,9 @@ 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;
///
/// Initializes a new instance of the class.
@@ -30,24 +32,26 @@ public LocalTimeType(
string name,
string? description = null,
BindingBehavior bind = BindingBehavior.Explicit,
- bool disableFormatCheck = false)
+ DateTimeOptions? options = null)
: base(name, bind)
{
+ _options = options ?? new DateTimeOptions();
Description = description;
- Pattern = @"^\d{2}:\d{2}:\d{2}(?:\.\d{1,9})?$";
+ Pattern = GetPattern();
SpecifiedBy = new Uri(SpecifiedByUri);
- _enforceSpecFormat = !disableFormatCheck;
+ _localFormat = GetLocalFormat();
+ _localTimeRegex = GetLocalTimeRegex();
}
///
/// 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)
{
}
@@ -58,7 +62,40 @@ public LocalTimeType(bool disableFormatCheck)
public LocalTimeType()
: this(
ScalarNames.LocalTime,
- TypeResources.LocalTimeType_Description)
+ TypeResources.LocalTimeType_Description,
+ options: null)
+ {
+ }
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ [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,
+ BindingBehavior.Implicit,
+ disableFormatCheck: disableFormatCheck)
{
}
@@ -86,16 +123,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(_localFormat, CultureInfo.InvariantCulture));
///
protected override StringValueNode OnValueToLiteral(TimeOnly runtimeValue)
- => new StringValueNode(runtimeValue.ToString(LocalFormat, CultureInfo.InvariantCulture));
+ => new StringValueNode(runtimeValue.ToString(_localFormat, CultureInfo.InvariantCulture));
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;
@@ -114,6 +151,53 @@ 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 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
+ {
+ 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..2ba79dc582b 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()
{
@@ -295,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!);
@@ -303,49 +319,139 @@ 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;
}
- 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)
},
{
- "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",
- // More than 9 fractional second digits.
- "2023-12-24T15:30:00.1234567890Z",
+ { DateTimeOptions.DefaultInputPrecision, "2023-02-30T15:30:00Z" },
+ // More than 7 fractional second digits.
+ { DateTimeOptions.DefaultInputPrecision, "2023-12-24T15:30:00.12345678Z" },
// 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 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..10b3016814d 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()
{
@@ -289,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!);
@@ -297,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]
@@ -328,42 +363,114 @@ 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)
},
{
- "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",
- // More than 9 fractional second digits.
- "2023-12-24T15:30:00.1234567890"
- ];
+ { DateTimeOptions.DefaultInputPrecision, "2023-02-30T15:30:00" },
+ // 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.
+ { 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..1b41874f8cb 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()
{
@@ -289,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!);
@@ -297,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))]
@@ -325,41 +360,113 @@ 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)
},
{
- "07:30:00.500",
- new TimeOnly(7, 30, 0, 500)
+ 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",
- // More than 9 fractional second digits.
- "15:30:00.1234567890"
- ];
+ { DateTimeOptions.DefaultInputPrecision, "15:60:00" },
+ // 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.
+ { 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"
+ }
+ };
}
}