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
8 changes: 8 additions & 0 deletions src/HotChocolate/Core/src/Types.NodaTime/DateTimeOptions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -62,4 +62,12 @@ public byte OutputPrecision
field = value;
}
} = DefaultOutputPrecision;

/// <summary>
/// Gets a value indicating whether fractional seconds are always emitted in serialized output,
/// padded with trailing zeros up to <see cref="OutputPrecision"/>. When <see langword="false"/>
/// (the default), trailing zeros are stripped and the fractional component is omitted entirely
/// when zero. Has no effect when <see cref="OutputPrecision"/> is <c>0</c>.
/// </summary>
public bool AlwaysOutputFractionalSeconds { get; init; }
}
9 changes: 5 additions & 4 deletions src/HotChocolate/Core/src/Types.NodaTime/DateTimeType.cs
Original file line number Diff line number Diff line change
Expand Up @@ -39,8 +39,9 @@ public DateTimeType(
Description = description;
Pattern = GetPattern();
SpecifiedBy = new Uri(SpecifiedByUri);
_inputPattern = OffsetDateTimePattern.CreateWithInvariantCulture(GetFormat(_options.InputPrecision));
_outputFormat = GetFormat(_options.OutputPrecision);
_inputPattern = OffsetDateTimePattern.CreateWithInvariantCulture(
GetFormat(_options.InputPrecision, padFractionalSeconds: false));
_outputFormat = GetFormat(_options.OutputPrecision, _options.AlwaysOutputFractionalSeconds);
}

/// <summary>
Expand Down Expand Up @@ -124,8 +125,8 @@ private string GetPattern()
+ _options.InputPrecision
+ @"})?(?:[Zz]|[+-]\d{2}:\d{2})$";

private static string GetFormat(byte precision)
private static string GetFormat(byte precision, bool padFractionalSeconds)
=> 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>";
: $"uuuu-MM-dd'T'HH:mm:ss.{new string(padFractionalSeconds ? 'f' : 'F', precision)}o<Z+HH:mm>";
}
9 changes: 5 additions & 4 deletions src/HotChocolate/Core/src/Types.NodaTime/LocalDateTimeType.cs
Original file line number Diff line number Diff line change
Expand Up @@ -40,8 +40,9 @@ public LocalDateTimeType(
Description = description;
Pattern = GetPattern();
SpecifiedBy = new Uri(SpecifiedByUri);
_inputPattern = LocalDateTimePattern.CreateWithInvariantCulture(GetFormat(_options.InputPrecision));
_outputFormat = GetFormat(_options.OutputPrecision);
_inputPattern = LocalDateTimePattern.CreateWithInvariantCulture(
GetFormat(_options.InputPrecision, padFractionalSeconds: false));
_outputFormat = GetFormat(_options.OutputPrecision, _options.AlwaysOutputFractionalSeconds);
}

/// <summary>
Expand Down Expand Up @@ -117,8 +118,8 @@ private string GetPattern()
? @"^\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 static string GetFormat(byte precision)
private static string GetFormat(byte precision, bool padFractionalSeconds)
=> precision == 0
? @"uuuu-MM-dd'T'HH\:mm\:ss"
: @$"uuuu-MM-dd'T'HH\:mm\:ss.{new string('F', precision)}";
: @$"uuuu-MM-dd'T'HH\:mm\:ss.{new string(padFractionalSeconds ? 'f' : 'F', precision)}";
}
9 changes: 5 additions & 4 deletions src/HotChocolate/Core/src/Types.NodaTime/LocalTimeType.cs
Original file line number Diff line number Diff line change
Expand Up @@ -39,8 +39,9 @@ public LocalTimeType(
Description = description;
Pattern = GetPattern();
SpecifiedBy = new Uri(SpecifiedByUri);
_inputPattern = LocalTimePattern.CreateWithInvariantCulture(GetFormat(_options.InputPrecision));
_outputFormat = GetFormat(_options.OutputPrecision);
_inputPattern = LocalTimePattern.CreateWithInvariantCulture(
GetFormat(_options.InputPrecision, padFractionalSeconds: false));
_outputFormat = GetFormat(_options.OutputPrecision, _options.AlwaysOutputFractionalSeconds);
}

/// <summary>
Expand Down Expand Up @@ -116,8 +117,8 @@ private string GetPattern()
? @"^\d{2}:\d{2}:\d{2}$"
: @"^\d{2}:\d{2}:\d{2}(?:\.\d{1," + _options.InputPrecision + "})?$";

private static string GetFormat(byte precision)
private static string GetFormat(byte precision, bool padFractionalSeconds)
=> precision == 0
? "HH:mm:ss"
: $"HH:mm:ss.{new string('F', precision)}";
: $"HH:mm:ss.{new string(padFractionalSeconds ? 'f' : 'F', precision)}";
}
Original file line number Diff line number Diff line change
Expand Up @@ -73,4 +73,12 @@ public byte OutputPrecision
/// against the expected format. Defaults to <c>true</c>.
/// </summary>
public bool ValidateInputFormat { get; init; } = true;

/// <summary>
/// Gets a value indicating whether fractional seconds are always emitted in serialized output,
/// padded with trailing zeros up to <see cref="OutputPrecision"/>. When <see langword="false"/>
/// (the default), trailing zeros are stripped and the fractional component is omitted entirely
/// when zero. Has no effect when <see cref="OutputPrecision"/> is <c>0</c>.
/// </summary>
public bool AlwaysOutputFractionalSeconds { get; init; }
}
18 changes: 12 additions & 6 deletions src/HotChocolate/Core/src/Types/Types/Scalars/DateTimeType.cs
Original file line number Diff line number Diff line change
Expand Up @@ -171,20 +171,26 @@ private string GetPattern()
+ @"})?(?:[Zz]|[+-]\d{2}:\d{2})$";

private string GetUtcFormat()
=> _options.OutputPrecision switch
{
var pad = _options.AlwaysOutputFractionalSeconds;
return _options.OutputPrecision switch
{
DateTimeOptions.DefaultOutputPrecision => UtcFormat,
DateTimeOptions.DefaultOutputPrecision when !pad => UtcFormat,
0 => @"yyyy-MM-ddTHH\:mm\:ssZ",
_ => @$"yyyy-MM-ddTHH\:mm\:ss.{new string('F', _options.OutputPrecision)}Z"
_ => @$"yyyy-MM-ddTHH\:mm\:ss.{new string(pad ? 'f' : 'F', _options.OutputPrecision)}Z"
};
}

private string GetLocalFormat()
=> _options.OutputPrecision switch
{
var pad = _options.AlwaysOutputFractionalSeconds;
return _options.OutputPrecision switch
{
DateTimeOptions.DefaultOutputPrecision => LocalFormat,
DateTimeOptions.DefaultOutputPrecision when !pad => LocalFormat,
0 => @"yyyy-MM-ddTHH\:mm\:sszzz",
_ => @$"yyyy-MM-ddTHH\:mm\:ss.{new string('F', _options.OutputPrecision)}zzz"
_ => @$"yyyy-MM-ddTHH\:mm\:ss.{new string(pad ? 'f' : 'F', _options.OutputPrecision)}zzz"
};
}

private Regex GetDateTimeRegex()
=> _options.InputPrecision switch
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -159,12 +159,15 @@ private string GetPattern()
: @"^\d{4}-\d{2}-\d{2}[Tt]\d{2}:\d{2}:\d{2}(?:\.\d{1," + _options.InputPrecision + "})?$";

private string GetLocalFormat()
=> _options.OutputPrecision switch
{
var pad = _options.AlwaysOutputFractionalSeconds;
return _options.OutputPrecision switch
{
DateTimeOptions.DefaultOutputPrecision => LocalFormat,
DateTimeOptions.DefaultOutputPrecision when !pad => LocalFormat,
0 => @"yyyy-MM-ddTHH\:mm\:ss",
_ => @$"yyyy-MM-ddTHH\:mm\:ss.{new string('F', _options.OutputPrecision)}"
_ => @$"yyyy-MM-ddTHH\:mm\:ss.{new string(pad ? 'f' : 'F', _options.OutputPrecision)}"
};
}

private Regex GetLocalDateTimeRegex()
=> _options.InputPrecision switch
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -157,12 +157,15 @@ private string GetPattern()
: @"^\d{2}:\d{2}:\d{2}(?:\.\d{1," + _options.InputPrecision + "})?$";

private string GetLocalFormat()
=> _options.OutputPrecision switch
{
var pad = _options.AlwaysOutputFractionalSeconds;
return _options.OutputPrecision switch
{
DateTimeOptions.DefaultOutputPrecision => LocalFormat,
DateTimeOptions.DefaultOutputPrecision when !pad => LocalFormat,
0 => "HH:mm:ss",
_ => $"HH:mm:ss.{new string('F', _options.OutputPrecision)}"
_ => $"HH:mm:ss.{new string(pad ? 'f' : 'F', _options.OutputPrecision)}"
};
}

private Regex GetLocalTimeRegex()
=> _options.InputPrecision switch
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -160,6 +160,80 @@ public void CoerceOutputValue_Valid(byte precision, OffsetDateTime dateTime, str
resultValue.MatchInlineSnapshot($"\"{result}\"");
}

[Theory]
[InlineData(9, "2023-12-24T15:30:00.123456789Z")]
[InlineData(3, "2023-12-24T15:30:00.123Z")]
public void CoerceOutputValue_AlwaysOutputFractionalSeconds_Pads(byte precision, string expected)
{
// arrange
var type = new DateTimeType(
new DateTimeOptions
{
OutputPrecision = precision,
AlwaysOutputFractionalSeconds = true
});
var dateTime = new OffsetDateTime(
new LocalDateTime(2023, 12, 24, 15, 30, 0, 123),
Offset.Zero).PlusNanoseconds(456_789);

// act
var operation = CommonTestExtensions.CreateOperation();
var resultDocument = new ResultDocument(operation, 0);
var resultValue = resultDocument.Data.GetProperty("first");
type.CoerceOutputValue(dateTime, resultValue);

// assert
resultValue.MatchInlineSnapshot($"\"{expected}\"");
}

[Fact]
public void CoerceOutputValue_AlwaysOutputFractionalSeconds_EmitsZerosForWholeSecond()
{
// arrange
var type = new DateTimeType(
new DateTimeOptions
{
OutputPrecision = 3,
AlwaysOutputFractionalSeconds = true
});
var dateTime = new OffsetDateTime(
new LocalDateTime(2023, 12, 24, 15, 30, 0),
Offset.Zero);

// act
var operation = CommonTestExtensions.CreateOperation();
var resultDocument = new ResultDocument(operation, 0);
var resultValue = resultDocument.Data.GetProperty("first");
type.CoerceOutputValue(dateTime, resultValue);

// assert
resultValue.MatchInlineSnapshot("\"2023-12-24T15:30:00.000Z\"");
}

[Fact]
public void CoerceOutputValue_AlwaysOutputFractionalSeconds_NoOpWhenPrecisionZero()
{
// arrange
var type = new DateTimeType(
new DateTimeOptions
{
OutputPrecision = 0,
AlwaysOutputFractionalSeconds = true
});
var dateTime = new OffsetDateTime(
new LocalDateTime(2023, 12, 24, 15, 30, 0, 123),
Offset.Zero);

// act
var operation = CommonTestExtensions.CreateOperation();
var resultDocument = new ResultDocument(operation, 0);
var resultValue = resultDocument.Data.GetProperty("first");
type.CoerceOutputValue(dateTime, resultValue);

// assert
resultValue.MatchInlineSnapshot("\"2023-12-24T15:30:00Z\"");
}

[Fact]
public void CoerceOutputValue_OffsetDateTime()
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -159,6 +159,96 @@ public void CoerceOutputValue_Valid(byte precision, DateTimeOffset dateTime, str
resultValue.MatchInlineSnapshot($"\"{result}\"");
}

[Theory]
[InlineData(DateTimeOptions.DefaultOutputPrecision, "2023-12-24T15:30:00.1234567Z")]
[InlineData(3, "2023-12-24T15:30:00.123Z")]
public void CoerceOutputValue_AlwaysOutputFractionalSeconds_Pads(byte precision, string expected)
{
// arrange
var type = new DateTimeType(
new DateTimeOptions
{
OutputPrecision = precision,
AlwaysOutputFractionalSeconds = true
});
var dateTime = new DateTimeOffset(2023, 12, 24, 15, 30, 0, 123, 456, TimeSpan.Zero).AddTicks(7);

// act
var operation = CommonTestExtensions.CreateOperation();
var resultDocument = new ResultDocument(operation, 0);
var resultValue = resultDocument.Data.GetProperty("first");
type.CoerceOutputValue(dateTime, resultValue);

// assert
resultValue.MatchInlineSnapshot($"\"{expected}\"");
}

[Fact]
public void CoerceOutputValue_AlwaysOutputFractionalSeconds_EmitsZerosForWholeSecond()
{
// arrange
var type = new DateTimeType(
new DateTimeOptions
{
OutputPrecision = 3,
AlwaysOutputFractionalSeconds = true
});
var dateTime = new DateTimeOffset(2023, 12, 24, 15, 30, 0, TimeSpan.Zero);

// act
var operation = CommonTestExtensions.CreateOperation();
var resultDocument = new ResultDocument(operation, 0);
var resultValue = resultDocument.Data.GetProperty("first");
type.CoerceOutputValue(dateTime, resultValue);

// assert
resultValue.MatchInlineSnapshot("\"2023-12-24T15:30:00.000Z\"");
}

[Fact]
public void CoerceOutputValue_AlwaysOutputFractionalSeconds_NoOpWhenPrecisionZero()
{
// arrange
var type = new DateTimeType(
new DateTimeOptions
{
OutputPrecision = 0,
AlwaysOutputFractionalSeconds = true
});
var dateTime = new DateTimeOffset(2023, 12, 24, 15, 30, 0, 123, TimeSpan.Zero);

// act
var operation = CommonTestExtensions.CreateOperation();
var resultDocument = new ResultDocument(operation, 0);
var resultValue = resultDocument.Data.GetProperty("first");
type.CoerceOutputValue(dateTime, resultValue);

// assert
resultValue.MatchInlineSnapshot("\"2023-12-24T15:30:00Z\"");
}

[Fact]
public void CoerceOutputValue_AlwaysOutputFractionalSeconds_LocalOffset()
{
// arrange
var type = new DateTimeType(
new DateTimeOptions
{
OutputPrecision = 3,
AlwaysOutputFractionalSeconds = true
});
var dateTime = new DateTimeOffset(2023, 12, 24, 15, 30, 0, TimeSpan.FromHours(4));

// act
var operation = CommonTestExtensions.CreateOperation();
var resultDocument = new ResultDocument(operation, 0);
var resultValue = resultDocument.Data.GetProperty("first");
type.CoerceOutputValue(dateTime, resultValue);

// assert
resultValue.MatchInlineSnapshot("\"2023-12-24T15:30:00.000+04:00\"");
}

[Fact]
public void CoerceOutputValue_Utc_DateTimeOffset()
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -151,6 +151,29 @@ public void CoerceOutputValue_Valid(byte precision, DateTime dateTime, string re
resultValue.MatchInlineSnapshot($"\"{result}\"");
}

[Fact]
public void CoerceOutputValue_AlwaysOutputFractionalSeconds_PadsAtDefaultPrecision()
{
// arrange
// exercises the bypass of the `LocalFormat` const-string shortcut at default precision
var type = new LocalDateTimeType(
new DateTimeOptions
{
OutputPrecision = DateTimeOptions.DefaultOutputPrecision,
AlwaysOutputFractionalSeconds = true
});
var dateTime = new DateTime(2023, 12, 24, 15, 30, 0);

// act
var operation = CommonTestExtensions.CreateOperation();
var resultDocument = new ResultDocument(operation, 0);
var resultValue = resultDocument.Data.GetProperty("first");
type.CoerceOutputValue(dateTime, resultValue);

// assert
resultValue.MatchInlineSnapshot("\"2023-12-24T15:30:00.0000000\"");
}

[Fact]
public void CoerceOutputValue()
{
Expand Down
Loading
Loading