-
-
Notifications
You must be signed in to change notification settings - Fork 803
Refactor NodaTime scalars #9352
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from all commits
Commits
Show all changes
31 commits
Select commit
Hold shift + click to select a range
3fa967f
Refactor NodaTime scalars
glen-84 90a7309
Merge branch 'main' into gai/refactor-noda-time-scalars
glen-84 90ef757
Remove unnecessary using directives
glen-84 d80daea
Use shared resources
glen-84 ba28122
Remove unnecessary constant
glen-84 52a191d
Update dictionary
glen-84 c865b99
Use ternary
glen-84 d166bdc
Refactor LocalDateTimeType
glen-84 938ccc5
Refactor LocalDateType
glen-84 d3c7ce3
Use single GetFormat method
glen-84 6c16ef9
Refactor LocalTimeType
glen-84 8378224
Merge branch 'main' into gai/refactor-noda-time-scalars
glen-84 35693b7
Fix BindingBehavior
glen-84 7a409a6
Implemented custom parser and formatter
michaelstaib 551b196
Align Duration types
glen-84 de469e4
Polish formatters and parsers
glen-84 c6acf45
Add tests for duration parsers and formatters
glen-84 a5c73bb
Merge branch 'main' into gai/refactor-noda-time-scalars
glen-84 081ef64
Align DurationTypeTests
glen-84 8a4a1b6
Delete NodaTime scalars from the previous implementation
glen-84 45892bd
Throw if TryFormat fails inside Format
glen-84 ae5bed5
Update StrawberryShake DurationSerializer to use new parser/formatter
glen-84 d089334
Add `IRequestExecutorBuilder` extension method
glen-84 8802de4
Fix parser bugs
glen-84 63f1765
Fix casing bug in DateTimeType
glen-84 72f9943
Simplify `IRequestExecutorBuilder` extension method
glen-84 d229c3a
Merge branch 'main' into gai/refactor-noda-time-scalars
glen-84 e5bb919
Address Copilot feedback
glen-84 e53a338
Address more Copilot feedback
glen-84 7a85ebc
Fix regression
glen-84 217eea9
Add section to migration docs
glen-84 File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
65 changes: 65 additions & 0 deletions
65
src/HotChocolate/Core/src/Types.NodaTime/DateTimeOptions.cs
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,65 @@ | ||
| using HotChocolate.Properties; | ||
|
|
||
| namespace HotChocolate.Types.NodaTime; | ||
|
|
||
| /// <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 = 9; | ||
| public const byte DefaultOutputPrecision = 9; | ||
|
|
||
| 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 9. | ||
| /// </exception> | ||
| public byte InputPrecision | ||
| { | ||
| get; | ||
| init | ||
| { | ||
| if (value > 9) | ||
| { | ||
| 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 9. | ||
| /// </exception> | ||
| public byte OutputPrecision | ||
| { | ||
| get; | ||
| init | ||
| { | ||
| if (value > 9) | ||
| { | ||
| throw new ArgumentOutOfRangeException( | ||
| nameof(OutputPrecision), | ||
| value, | ||
| NodaTimeResources.DateTimeOptions_OutputPrecision_InvalidValue); | ||
| } | ||
|
|
||
| field = value; | ||
| } | ||
| } = DefaultOutputPrecision; | ||
| } |
131 changes: 131 additions & 0 deletions
131
src/HotChocolate/Core/src/Types.NodaTime/DateTimeType.cs
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,131 @@ | ||
| using System.Globalization; | ||
| using System.Text.Json; | ||
| using HotChocolate.Features; | ||
| using HotChocolate.Language; | ||
| using HotChocolate.Properties; | ||
| using HotChocolate.Text.Json; | ||
| using NodaTime; | ||
| using NodaTime.Text; | ||
| using static HotChocolate.Utilities.ThrowHelper; | ||
|
|
||
| namespace HotChocolate.Types.NodaTime; | ||
|
|
||
| /// <summary> | ||
| /// The <c>DateTime</c> scalar type represents a date and time with time zone offset information. It | ||
| /// is intended for scenarios where representing a specific instant in time is required, such as | ||
| /// recording when an event occurred, scheduling future events across time zones, or storing | ||
| /// timestamps for auditing purposes. | ||
| /// </summary> | ||
| /// <seealso href="https://scalars.graphql.org/chillicream/date-time.html">Specification</seealso> | ||
| public class DateTimeType : ScalarType<OffsetDateTime, StringValueNode> | ||
| { | ||
| private const string SpecifiedByUri = "https://scalars.graphql.org/chillicream/date-time.html"; | ||
|
|
||
| private readonly DateTimeOptions _options; | ||
| private readonly OffsetDateTimePattern _inputPattern; | ||
| private readonly string _outputFormat; | ||
|
|
||
| /// <summary> | ||
| /// Initializes a new instance of the <see cref="DateTimeType"/> class. | ||
| /// </summary> | ||
| public DateTimeType( | ||
| string name, | ||
| string? description = null, | ||
| BindingBehavior bind = BindingBehavior.Explicit, | ||
| DateTimeOptions? options = null) | ||
| : base(name, bind) | ||
| { | ||
| _options = options ?? new DateTimeOptions(); | ||
| Description = description; | ||
| Pattern = GetPattern(); | ||
| SpecifiedBy = new Uri(SpecifiedByUri); | ||
| _inputPattern = OffsetDateTimePattern.CreateWithInvariantCulture(GetFormat(_options.InputPrecision)); | ||
| _outputFormat = GetFormat(_options.OutputPrecision); | ||
| } | ||
|
|
||
| /// <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> | ||
| [ActivatorUtilitiesConstructor] | ||
| public DateTimeType() | ||
| : this( | ||
| ScalarNames.DateTime, | ||
| TypeResources.DateTimeType_Description, | ||
| BindingBehavior.Implicit) | ||
| { | ||
| } | ||
|
|
||
| protected override OffsetDateTime OnCoerceInputLiteral(StringValueNode valueLiteral) | ||
| { | ||
| if (TryParseStringValue(valueLiteral.Value, out var value)) | ||
| { | ||
| return value; | ||
| } | ||
|
|
||
| throw Scalar_Cannot_CoerceInputLiteral(this, valueLiteral); | ||
| } | ||
|
|
||
| protected override OffsetDateTime OnCoerceInputValue(JsonElement inputValue, IFeatureProvider context) | ||
| { | ||
| if (TryParseStringValue(inputValue.GetString()!, out var value)) | ||
| { | ||
| return value; | ||
| } | ||
|
|
||
| throw Scalar_Cannot_CoerceInputValue(this, inputValue); | ||
| } | ||
|
|
||
| protected override void OnCoerceOutputValue(OffsetDateTime runtimeValue, ResultElement resultValue) | ||
| { | ||
| resultValue.SetStringValue( | ||
| runtimeValue.ToString( | ||
| _outputFormat, | ||
| CultureInfo.InvariantCulture)); | ||
| } | ||
|
|
||
| protected override StringValueNode OnValueToLiteral(OffsetDateTime runtimeValue) | ||
| { | ||
| return new StringValueNode( | ||
| runtimeValue.ToString( | ||
| _outputFormat, | ||
| CultureInfo.InvariantCulture)); | ||
| } | ||
|
|
||
| private bool TryParseStringValue(string serialized, out OffsetDateTime value) | ||
| { | ||
| var result = _inputPattern.Parse(serialized.Replace('t', 'T').Replace('z', 'Z')); | ||
|
|
||
| if (result.Success) | ||
| { | ||
| value = result.Value; | ||
| return true; | ||
| } | ||
|
|
||
| value = default; | ||
| 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 static string GetFormat(byte precision) | ||
| => precision == 0 | ||
| ? "uuuu-MM-dd'T'HH:mm:sso<Z+HH:mm>" | ||
| : $"uuuu-MM-dd'T'HH:mm:ss.{new string('F', precision)}o<Z+HH:mm>"; | ||
| } | ||
40 changes: 0 additions & 40 deletions
40
src/HotChocolate/Core/src/Types.NodaTime/DateTimeZoneType.cs
This file was deleted.
Oops, something went wrong.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,69 +1,83 @@ | ||
| using System.Diagnostics.CodeAnalysis; | ||
| using HotChocolate.Types.NodaTime.Properties; | ||
| using System.Text.Json; | ||
| using HotChocolate.Features; | ||
| using HotChocolate.Language; | ||
| using HotChocolate.Properties; | ||
| using HotChocolate.Text.Json; | ||
| using NodaTime; | ||
| using NodaTime.Text; | ||
| using static HotChocolate.Utilities.ThrowHelper; | ||
|
|
||
| namespace HotChocolate.Types.NodaTime; | ||
|
|
||
| /// <summary> | ||
| /// Represents a fixed (and calendar-independent) length of time. | ||
| /// The <c>Duration</c> scalar type represents a duration of time. It is intended for scenarios | ||
| /// where you need to represent time intervals, such as elapsed time, timeout durations, scheduling | ||
| /// intervals, or any measurement of time that is not tied to a specific date or time. | ||
| /// </summary> | ||
| public class DurationType : StringToStructBaseType<Duration> | ||
| /// <seealso href="https://scalars.graphql.org/chillicream/duration.html">Specification</seealso> | ||
| public class DurationType : ScalarType<Duration, StringValueNode> | ||
| { | ||
| private readonly IPattern<Duration>[] _allowedPatterns; | ||
| private readonly IPattern<Duration> _serializationPattern; | ||
| private const string SpecifiedByUri = "https://scalars.graphql.org/chillicream/duration.html"; | ||
|
|
||
| /// <summary> | ||
| /// Initializes a new instance of <see cref="DurationType"/>. | ||
| /// Initializes a new instance of the <see cref="DurationType"/> class. | ||
| /// </summary> | ||
| public DurationType(params IPattern<Duration>[] allowedPatterns) : base("Duration") | ||
| public DurationType( | ||
| string name, | ||
| string? description = null, | ||
| BindingBehavior bind = BindingBehavior.Explicit) | ||
| : base(name, bind) | ||
| { | ||
| if (allowedPatterns.Length == 0) | ||
| { | ||
| throw ThrowHelper.PatternCannotBeEmpty(this); | ||
| } | ||
|
|
||
| _allowedPatterns = allowedPatterns; | ||
| _serializationPattern = allowedPatterns[0]; | ||
|
|
||
| Description = CreateDescription( | ||
| allowedPatterns, | ||
| NodaTimeResources.DurationType_Description, | ||
| NodaTimeResources.DurationType_Description_Extended); | ||
| Description = description; | ||
| Pattern = | ||
| @"^-?P(?:-?\d+Y)?(?:-?\d+M)?(?:-?\d+W)?(?:-?\d+D)?(?:T(?:-?\d+H)?(?:-?\d+M)?(?:-?\d+(?:[.,]\d+)?S)?)?$"; | ||
|
glen-84 marked this conversation as resolved.
|
||
| SpecifiedBy = new Uri(SpecifiedByUri); | ||
| } | ||
|
|
||
| /// <summary> | ||
| /// Initializes a new instance of <see cref="DurationType"/>. | ||
| /// Initializes a new instance of the <see cref="DurationType"/> class. | ||
| /// </summary> | ||
| [ActivatorUtilitiesConstructor] | ||
| public DurationType() : this(DurationPattern.Roundtrip) | ||
| public DurationType() | ||
| : this( | ||
| ScalarNames.Duration, | ||
| TypeResources.DurationType_Description, | ||
| BindingBehavior.Implicit) | ||
| { | ||
| } | ||
|
|
||
| /// <inheritdoc /> | ||
| protected override bool TryCoerceRuntimeValue( | ||
| string resultValue, | ||
| [NotNullWhen(true)] out Duration? runtimeValue) | ||
| => _allowedPatterns.TryParse(resultValue, out runtimeValue); | ||
| protected override Duration OnCoerceInputLiteral(StringValueNode valueLiteral) | ||
| { | ||
| // Parse directly from UTF-8 bytes to avoid the string allocation. | ||
| if (Iso8601DurationParser.TryParse(valueLiteral.AsSpan(), out var value)) | ||
| { | ||
| return value; | ||
| } | ||
|
|
||
| throw Scalar_Cannot_CoerceInputLiteral(this, valueLiteral); | ||
| } | ||
|
|
||
| /// <inheritdoc /> | ||
| protected override bool TryCoerceOutputValue( | ||
| Duration runtimeValue, | ||
| [NotNullWhen(true)] out string? resultValue) | ||
| protected override Duration OnCoerceInputValue(JsonElement inputValue, IFeatureProvider context) | ||
| { | ||
| resultValue = _serializationPattern.Format(runtimeValue); | ||
| return true; | ||
| if (Iso8601DurationParser.TryParse(inputValue.GetString()!.AsSpan(), out var value)) | ||
| { | ||
| return value; | ||
| } | ||
|
|
||
| throw Scalar_Cannot_CoerceInputValue(this, inputValue); | ||
| } | ||
|
|
||
| protected override Dictionary<IPattern<Duration>, string> PatternMap => new() | ||
| /// <inheritdoc /> | ||
| protected override void OnCoerceOutputValue(Duration runtimeValue, ResultElement resultValue) | ||
| { | ||
| { DurationPattern.Roundtrip, "-D:hh:mm:ss.sssssssss" }, | ||
| { DurationPattern.JsonRoundtrip, "-hh:mm:ss.sssssssss" } | ||
| }; | ||
| // Format directly to UTF-8 bytes on the stack to avoid allocation. | ||
| Span<byte> buffer = stackalloc byte[64]; | ||
| Iso8601DurationFormatter.TryFormat(runtimeValue, buffer, out var bytesWritten); | ||
| resultValue.SetStringValue(buffer[..bytesWritten]); | ||
| } | ||
|
|
||
| protected override Dictionary<IPattern<Duration>, string> ExampleMap => new() | ||
| { | ||
| { DurationPattern.Roundtrip, "-1:20:00:00.999999999" }, | ||
| { DurationPattern.JsonRoundtrip, "-44:00:00.999999999" } | ||
| }; | ||
| /// <inheritdoc /> | ||
| protected override StringValueNode OnValueToLiteral(Duration runtimeValue) | ||
| => new StringValueNode(Iso8601DurationFormatter.Format(runtimeValue)); | ||
| } | ||
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.