Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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>
71 changes: 71 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,71 @@
using HotChocolate.Properties;

namespace HotChocolate.Types;

/// <summary>
/// Defines options for configuring the behavior of date and time scalar types, such as
/// <c>DateTime</c>, <c>LocalDateTime</c>, and <c>LocalTime</c>.
/// </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;

/// <summary>
/// Gets a value indicating whether the input format of date and time values should be validated
/// against the expected format. Defaults to <c>true</c>.
/// </summary>
public bool ValidateInputFormat { get; init; } = true;
}
154 changes: 128 additions & 26 deletions src/HotChocolate/Core/src/Types/Types/Scalars/DateTimeType.cs
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,10 @@ public partial class DateTimeType : ScalarType<DateTimeOffset, StringValueNode>
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;

/// <summary>
/// Initializes a new instance of the <see cref="DateTimeType"/> class.
Expand All @@ -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();
}

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

Expand All @@ -61,7 +67,40 @@ public DateTimeType()
ScalarNames.DateTime,
TypeResources.DateTimeType_Description,
BindingBehavior.Implicit,
disableFormatCheck: false)
options: null)
{
}

/// <summary>
/// Initializes a new instance of the <see cref="DateTimeType"/> class.
/// </summary>
[Obsolete("Use the constructor that accepts DateTimeOptions instead.")]
public DateTimeType(
string name,
string? description = null,
BindingBehavior bind = BindingBehavior.Explicit,
bool disableFormatCheck = false)
Comment thread
glen-84 marked this conversation as resolved.
: base(name, bind)
{
_options = new DateTimeOptions { ValidateInputFormat = !disableFormatCheck };
Description = description;
Pattern = GetPattern();
SpecifiedBy = new Uri(SpecifiedByUri);
_utcFormat = GetUtcFormat();
_localFormat = GetLocalFormat();
_dateTimeRegex = GetDateTimeRegex();
}

/// <summary>
/// Initializes a new instance of the <see cref="DateTimeType"/> class.
/// </summary>
[Obsolete("Use the constructor that accepts DateTimeOptions instead.")]
public DateTimeType(bool disableFormatCheck)
: this(
ScalarNames.DateTime,
TypeResources.DateTimeType_Description,
BindingBehavior.Implicit,
disableFormatCheck: disableFormatCheck)
{
}

Expand All @@ -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;
Expand All @@ -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();
}
Loading
Loading