Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions dictionary.txt
Original file line number Diff line number Diff line change
Expand Up @@ -171,6 +171,7 @@ sortings
Specwise
sqft
srid
sszzz
Staib
Starships
starwars
Expand Down

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

6 changes: 6 additions & 0 deletions src/HotChocolate/Core/src/Types/Properties/TypeResources.resx
Original file line number Diff line number Diff line change
Expand Up @@ -1032,4 +1032,10 @@ Type: `{0}`</value>
<data name="RegexType_InvalidFormat" xml:space="preserve">
<value>{0}Type cannot parse the provided value. The value does not match the required regular expression pattern.</value>
</data>
<data name="DateTimeOptions_InputPrecision_InvalidValue" xml:space="preserve">
<value>InputPrecision must be less than or equal to 7.</value>
</data>
<data name="DateTimeOptions_OutputPrecision_InvalidValue" xml:space="preserve">
<value>OutputPrecision must be less than or equal to 7.</value>
</data>
</root>
78 changes: 78 additions & 0 deletions src/HotChocolate/Core/src/Types/Types/Scalars/DateTimeOptions.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
using HotChocolate.Properties;

namespace HotChocolate.Types;

/// <summary>
/// <para>
/// Defines options for configuring the behavior of date and time scalar types, such as
/// <c>DateTime</c>, <c>LocalDateTime</c>, and <c>LocalTime</c>.
/// </para>
/// <para>
/// 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.
/// </para>
/// <para>
/// 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.
/// </para>
/// </summary>
public struct DateTimeOptions
{
public const byte DefaultInputPrecision = 7;
public const byte DefaultOutputPrecision = 7;

public DateTimeOptions()
{
}

/// <summary>
/// Gets the maximum number of fractional second digits to expect when parsing date and time
/// input values.
/// </summary>
/// <exception cref="ArgumentOutOfRangeException">
/// Thrown when the value is greater than 7.
/// </exception>
public byte InputPrecision
{
get;
init
{
if (value > 7)
{
throw new ArgumentOutOfRangeException(
nameof(InputPrecision),
value,
TypeResources.DateTimeOptions_InputPrecision_InvalidValue);
}

field = value;
}
} = DefaultInputPrecision;

/// <summary>
/// Gets the maximum number of fractional second digits to include when serializing date and
/// time output values.
/// </summary>
/// <exception cref="ArgumentOutOfRangeException">
/// Thrown when the value is greater than 7.
/// </exception>
public byte OutputPrecision
{
get;
init
{
if (value > 7)
{
throw new ArgumentOutOfRangeException(
nameof(OutputPrecision),
value,
TypeResources.DateTimeOptions_OutputPrecision_InvalidValue);
}

field = value;
}
} = DefaultOutputPrecision;
}
127 changes: 106 additions & 21 deletions src/HotChocolate/Core/src/Types/Types/Scalars/DateTimeType.cs
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,10 @@ public partial class DateTimeType : ScalarType<DateTimeOffset, StringValueNode>
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;

/// <summary>
/// Initializes a new instance of the <see cref="DateTimeType"/> class.
Expand All @@ -31,13 +35,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();
_options = options.Value;
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();
}

/// <summary>
Expand All @@ -52,6 +62,18 @@ public DateTimeType(bool disableFormatCheck)
{
}

/// <summary>
/// Initializes a new instance of the <see cref="DateTimeType"/> class.
/// </summary>
public DateTimeType(DateTimeOptions options)
: this(
ScalarNames.DateTime,
TypeResources.DateTimeType_Description,
BindingBehavior.Implicit,
options: options)
{
}

/// <summary>
/// Initializes a new instance of the <see cref="DateTimeType"/> class.
/// </summary>
Expand Down Expand Up @@ -87,32 +109,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 (_enforceSpecFormat && !_dateTimeRegex.IsMatch(serialized))
{
value = default;
return false;
Expand All @@ -132,8 +146,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();
}
Loading
Loading