mirror of https://github.com/dapr/dotnet-sdk.git
Actor reminder deserialization bugfix (#1483)
Signed-off-by: Whit Waldo <whit.waldo@innovian.net>
This commit is contained in:
parent
bb47132f98
commit
c14fcea0d4
|
@ -0,0 +1,124 @@
|
|||
// ------------------------------------------------------------------------
|
||||
// Copyright 2025 The Dapr Authors
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
// ------------------------------------------------------------------------
|
||||
|
||||
using System;
|
||||
using System.Globalization;
|
||||
using System.Linq;
|
||||
using System.Text.RegularExpressions;
|
||||
|
||||
namespace Dapr.Actors.Extensions;
|
||||
|
||||
internal static class DurationExtensions
|
||||
{
|
||||
/// <summary>
|
||||
/// Used to parse the duration string accompanying an @every expression.
|
||||
/// </summary>
|
||||
private static readonly Regex durationRegex = new(@"(?<value>\d+(\.\d+)?)(?<unit>ns|us|µs|ms|s|m|h)", RegexOptions.Compiled | RegexOptions.IgnoreCase);
|
||||
/// <summary>
|
||||
/// A regular expression used to evaluate whether a given prefix period embodies an @every statement.
|
||||
/// </summary>
|
||||
private static readonly Regex isEveryExpression = new(@"^@every (\d+(\.\d+)?(ns|us|µs|ms|s|m|h))+$");
|
||||
/// <summary>
|
||||
/// The various acceptable duration values for a period expression.
|
||||
/// </summary>
|
||||
private static readonly string[] acceptablePeriodValues =
|
||||
{
|
||||
"yearly", "monthly", "weekly", "daily", "midnight", "hourly"
|
||||
};
|
||||
|
||||
private const string YearlyPrefixPeriod = "@yearly";
|
||||
private const string MonthlyPrefixPeriod = "@monthly";
|
||||
private const string WeeklyPrefixPeriod = "@weekly";
|
||||
private const string DailyPrefixPeriod = "@daily";
|
||||
private const string MidnightPrefixPeriod = "@midnight";
|
||||
private const string HourlyPrefixPeriod = "@hourly";
|
||||
private const string EveryPrefixPeriod = "@every";
|
||||
|
||||
/// <summary>
|
||||
/// Indicates that the schedule represents a prefixed period expression.
|
||||
/// </summary>
|
||||
/// <param name="expression"></param>
|
||||
/// <returns></returns>
|
||||
public static bool IsDurationExpression(this string expression) => expression.StartsWith('@') &&
|
||||
(isEveryExpression.IsMatch(expression) ||
|
||||
expression.EndsWithAny(acceptablePeriodValues, StringComparison.InvariantCulture));
|
||||
|
||||
/// <summary>
|
||||
/// Creates a TimeSpan value from the prefixed period value.
|
||||
/// </summary>
|
||||
/// <param name="period">The prefixed period value to parse.</param>
|
||||
/// <returns>A TimeSpan value matching the provided period.</returns>
|
||||
public static TimeSpan FromPrefixedPeriod(this string period)
|
||||
{
|
||||
if (period.StartsWith(YearlyPrefixPeriod))
|
||||
{
|
||||
var dateTime = DateTime.UtcNow;
|
||||
return dateTime.AddYears(1) - dateTime;
|
||||
}
|
||||
|
||||
if (period.StartsWith(MonthlyPrefixPeriod))
|
||||
{
|
||||
var dateTime = DateTime.UtcNow;
|
||||
return dateTime.AddMonths(1) - dateTime;
|
||||
}
|
||||
|
||||
if (period.StartsWith(MidnightPrefixPeriod))
|
||||
{
|
||||
return new TimeSpan();
|
||||
}
|
||||
|
||||
if (period.StartsWith(WeeklyPrefixPeriod))
|
||||
{
|
||||
return TimeSpan.FromDays(7);
|
||||
}
|
||||
|
||||
if (period.StartsWith(DailyPrefixPeriod) || period.StartsWith(MidnightPrefixPeriod))
|
||||
{
|
||||
return TimeSpan.FromDays(1);
|
||||
}
|
||||
|
||||
if (period.StartsWith(HourlyPrefixPeriod))
|
||||
{
|
||||
return TimeSpan.FromHours(1);
|
||||
}
|
||||
|
||||
if (period.StartsWith(EveryPrefixPeriod))
|
||||
{
|
||||
//A sequence of decimal numbers each with an optional fraction and unit suffix
|
||||
//Valid time units are: 'ns', 'us'/'µs', 'ms', 's', 'm', and 'h'
|
||||
double totalMilliseconds = 0;
|
||||
var durationString = period.Split(' ').Last().Trim();
|
||||
|
||||
foreach (Match match in durationRegex.Matches(durationString))
|
||||
{
|
||||
var value = double.Parse(match.Groups["value"].Value, CultureInfo.InvariantCulture);
|
||||
var unit = match.Groups["unit"].Value.ToLower();
|
||||
|
||||
totalMilliseconds += unit switch
|
||||
{
|
||||
"ns" => value / 1_000_000,
|
||||
"us" or "µs" => value / 1_000,
|
||||
"ms" => value,
|
||||
"s" => value * 1_000,
|
||||
"m" => value * 1_000 * 60,
|
||||
"h" => value * 1_000 * 60 * 60,
|
||||
_ => throw new ArgumentException($"Unknown duration unit: {unit}")
|
||||
};
|
||||
}
|
||||
|
||||
return TimeSpan.FromMilliseconds(totalMilliseconds);
|
||||
}
|
||||
|
||||
throw new ArgumentException($"Unknown prefix period expression: {period}");
|
||||
}
|
||||
}
|
|
@ -0,0 +1,32 @@
|
|||
// ------------------------------------------------------------------------
|
||||
// Copyright 2025 The Dapr Authors
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
// ------------------------------------------------------------------------
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text.RegularExpressions;
|
||||
|
||||
namespace Dapr.Actors.Extensions;
|
||||
|
||||
internal static class StringExtensions
|
||||
{
|
||||
/// <summary>
|
||||
/// Extension method that validates a string against a list of possible matches.
|
||||
/// </summary>
|
||||
/// <param name="value">The string value to evaluate.</param>
|
||||
/// <param name="possibleValues">The possible values to look for a match within.</param>
|
||||
/// <param name="comparisonType">The type of string comparison to perform.</param>
|
||||
/// <returns>True if the value ends with any of the possible values; otherwise false.</returns>
|
||||
public static bool EndsWithAny(this string value, IReadOnlyList<string> possibleValues, StringComparison comparisonType )
|
||||
=> possibleValues.Any(val => value.EndsWith(val, comparisonType));
|
||||
}
|
|
@ -11,15 +11,17 @@
|
|||
// limitations under the License.
|
||||
// ------------------------------------------------------------------------
|
||||
|
||||
namespace Dapr.Actors.Runtime
|
||||
{
|
||||
using Dapr.Actors.Extensions;
|
||||
|
||||
namespace Dapr.Actors.Runtime;
|
||||
|
||||
using System;
|
||||
using System.Text;
|
||||
using System.Text.RegularExpressions;
|
||||
|
||||
internal class ConverterUtils
|
||||
internal static class ConverterUtils
|
||||
{
|
||||
private static Regex regex = new Regex("^(R(?<repetition>\\d+)/)?P((?<year>\\d+)Y)?((?<month>\\d+)M)?((?<week>\\d+)W)?((?<day>\\d+)D)?(T((?<hour>\\d+)H)?((?<minute>\\d+)M)?((?<second>\\d+)S)?)?$", RegexOptions.Compiled);
|
||||
private static Regex regex = new("^(R(?<repetition>\\d+)/)?P((?<year>\\d+)Y)?((?<month>\\d+)M)?((?<week>\\d+)W)?((?<day>\\d+)D)?(T((?<hour>\\d+)H)?((?<minute>\\d+)M)?((?<second>\\d+)S)?)?$", RegexOptions.Compiled);
|
||||
public static TimeSpan ConvertTimeSpanFromDaprFormat(string valueString)
|
||||
{
|
||||
if (string.IsNullOrEmpty(valueString))
|
||||
|
@ -28,6 +30,11 @@ namespace Dapr.Actors.Runtime
|
|||
return never;
|
||||
}
|
||||
|
||||
if (valueString.IsDurationExpression())
|
||||
{
|
||||
return valueString.FromPrefixedPeriod();
|
||||
}
|
||||
|
||||
// TimeSpan is a string. Format returned by Dapr is: 1h4m5s4ms4us4ns
|
||||
// acceptable values are: m, s, ms, us(micro), ns
|
||||
var spanOfValue = valueString.AsSpan();
|
||||
|
@ -63,6 +70,9 @@ namespace Dapr.Actors.Runtime
|
|||
{
|
||||
// write in format expected by Dapr, it only accepts h, m, s, ms, us(micro), ns
|
||||
var stringValue = string.Empty;
|
||||
if (value is null)
|
||||
return stringValue;
|
||||
|
||||
if (value.Value >= TimeSpan.Zero)
|
||||
{
|
||||
var hours = (value.Value.Days * 24) + value.Value.Hours;
|
||||
|
@ -86,28 +96,28 @@ namespace Dapr.Actors.Runtime
|
|||
throw new ArgumentException("The TimeSpan value, combined with repetition cannot be in milliseconds.", nameof(value));
|
||||
}
|
||||
|
||||
builder.AppendFormat("R{0}/P", repetitions);
|
||||
builder.Append($"R{repetitions}/P");
|
||||
|
||||
if(value.Days > 0)
|
||||
{
|
||||
builder.AppendFormat("{0}D", value.Days);
|
||||
builder.Append($"{value.Days}D");
|
||||
}
|
||||
|
||||
builder.Append("T");
|
||||
|
||||
if(value.Hours > 0)
|
||||
{
|
||||
builder.AppendFormat("{0}H", value.Hours);
|
||||
builder.Append($"{value.Hours}H");
|
||||
}
|
||||
|
||||
if(value.Minutes > 0)
|
||||
{
|
||||
builder.AppendFormat("{0}M", value.Minutes);
|
||||
builder.Append($"{value.Minutes}M");
|
||||
}
|
||||
|
||||
if(value.Seconds > 0)
|
||||
{
|
||||
builder.AppendFormat("{0}S", value.Seconds);
|
||||
builder.Append($"{value.Seconds}S");
|
||||
}
|
||||
return builder.ToString();
|
||||
}
|
||||
|
@ -125,6 +135,7 @@ namespace Dapr.Actors.Runtime
|
|||
|
||||
var repetition = matches.Groups["repetition"].Success ? int.Parse(matches.Groups["repetition"].Value) : (int?)null;
|
||||
|
||||
|
||||
var days = 0;
|
||||
var year = matches.Groups["year"].Success ? int.Parse(matches.Groups["year"].Value) : 0;
|
||||
days = year * 365;
|
||||
|
@ -145,4 +156,3 @@ namespace Dapr.Actors.Runtime
|
|||
return (new TimeSpan(days, hour, minute, second), repetition);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -12,6 +12,7 @@
|
|||
// ------------------------------------------------------------------------
|
||||
|
||||
#nullable enable
|
||||
|
||||
namespace Dapr.Actors.Runtime;
|
||||
|
||||
using System;
|
||||
|
|
|
@ -0,0 +1,35 @@
|
|||
// ------------------------------------------------------------------------
|
||||
// Copyright 2025 The Dapr Authors
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
// ------------------------------------------------------------------------
|
||||
|
||||
using System;
|
||||
using Dapr.Actors.Runtime;
|
||||
using Xunit;
|
||||
|
||||
namespace Dapr.Actors;
|
||||
|
||||
public class ConverterUtilsTests
|
||||
{
|
||||
[Fact]
|
||||
public void Deserialize_Period_Duration1()
|
||||
{
|
||||
var result = ConverterUtils.ConvertTimeSpanValueFromISO8601Format("@every 15m");
|
||||
Assert.Equal(TimeSpan.FromMinutes(15), result.Item1);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Deserialize_Period_Duration2()
|
||||
{
|
||||
var result = ConverterUtils.ConvertTimeSpanValueFromISO8601Format("@hourly");
|
||||
Assert.Equal(TimeSpan.FromHours(1), result.Item1);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,90 @@
|
|||
// ------------------------------------------------------------------------
|
||||
// Copyright 2025 The Dapr Authors
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
// ------------------------------------------------------------------------
|
||||
|
||||
using System;
|
||||
using Xunit;
|
||||
|
||||
namespace Dapr.Actors.Extensions;
|
||||
|
||||
public sealed class DurationExtensionsTests
|
||||
{
|
||||
[Theory]
|
||||
[InlineData("@yearly", 364, 0, 0, 0, 0)]
|
||||
[InlineData("@monthly", 28, 0, 0, 0, 0 )]
|
||||
[InlineData("@weekly", 7, 0, 0, 0, 0 )]
|
||||
[InlineData("@daily", 1, 0, 0, 0, 0)]
|
||||
[InlineData("@midnight", 0, 0, 0, 0, 0 )]
|
||||
[InlineData("@hourly", 0, 1, 0, 0, 0)]
|
||||
[InlineData("@every 1h", 0, 1, 0, 0, 0)]
|
||||
[InlineData("@every 30m", 0, 0, 30, 0, 0)]
|
||||
[InlineData("@every 45s", 0, 0, 0, 45, 0)]
|
||||
[InlineData("@every 1.5h", 0, 1, 30, 0, 0)]
|
||||
[InlineData("@every 1h30m", 0, 1, 30, 0, 0)]
|
||||
[InlineData("@every 1h30m45s", 0, 1, 30, 45, 0)]
|
||||
[InlineData("@every 1h30m45.3s", 0, 1, 30, 45, 300)]
|
||||
[InlineData("@every 100ms", 0, 0, 0, 0, 100)]
|
||||
[InlineData("@every 1s500ms", 0, 0, 0, 1, 500)]
|
||||
[InlineData("@every 1m1s", 0, 0, 1, 1, 0)]
|
||||
[InlineData("@every 1.1m", 0, 0, 1, 6, 0)]
|
||||
[InlineData("@every 1.5h30m45s100ms", 0, 2, 0, 45, 100)]
|
||||
public void ValidatePrefixedPeriodParsing(string input, int expectedDays, int expectedHours, int expectedMinutes, int expectedSeconds, int expectedMilliseconds)
|
||||
{
|
||||
var result = input.FromPrefixedPeriod();
|
||||
|
||||
if (input is "@yearly" or "@monthly")
|
||||
{
|
||||
Assert.True(result.Days >= expectedDays);
|
||||
return;
|
||||
}
|
||||
|
||||
Assert.Equal(expectedDays, result.Days);
|
||||
Assert.Equal(expectedHours, result.Hours);
|
||||
Assert.Equal(expectedMinutes, result.Minutes);
|
||||
Assert.Equal(expectedSeconds, result.Seconds);
|
||||
Assert.Equal(expectedMilliseconds, result.Milliseconds);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData("@yearly", true)]
|
||||
[InlineData("@monthly", true)]
|
||||
[InlineData("@weekly", true)]
|
||||
[InlineData("@daily", true)]
|
||||
[InlineData("@midnight", true)]
|
||||
[InlineData("@hourly", true)]
|
||||
[InlineData("@every 1h", true)]
|
||||
[InlineData("@every 30m", true)]
|
||||
[InlineData("@every 45s", true)]
|
||||
[InlineData("@every 1.5h", true)]
|
||||
[InlineData("@every 1h30m", true)]
|
||||
[InlineData("@every 1h30m45s", true)]
|
||||
[InlineData("@every 1h30m45.3s", true)]
|
||||
[InlineData("@every 100ms", true)]
|
||||
[InlineData("@every 1s500ms", true)]
|
||||
[InlineData("@every 1m1s", true)]
|
||||
[InlineData("@every 1.1m", true)]
|
||||
[InlineData("@every 1.5h30m45s100ms", true)]
|
||||
public void TestIsDurationExpression(string input, bool expectedResult)
|
||||
{
|
||||
var actualResult = input.IsDurationExpression();
|
||||
Assert.Equal(expectedResult, actualResult);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ValidateExceptionForUnknownExpression()
|
||||
{
|
||||
Assert.Throws<ArgumentException>(() =>
|
||||
{
|
||||
var result = "every 100s".FromPrefixedPeriod();
|
||||
});
|
||||
}
|
||||
}
|
|
@ -0,0 +1,41 @@
|
|||
// ------------------------------------------------------------------------
|
||||
// Copyright 2025 The Dapr Authors
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
// ------------------------------------------------------------------------
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using Xunit;
|
||||
|
||||
namespace Dapr.Actors.Extensions;
|
||||
|
||||
public sealed class StringExtensionsTests
|
||||
{
|
||||
[Fact]
|
||||
public void ValidateMatchesValue()
|
||||
{
|
||||
var matchingValues = new List<string> { "apples", "bananas", "cherries", };
|
||||
const string value = "I have four cherries";
|
||||
|
||||
var result = value.EndsWithAny(matchingValues, StringComparison.InvariantCulture);
|
||||
Assert.True(result);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ValidateDoesNotMatchValue()
|
||||
{
|
||||
var matchingValues = new List<string> { "apples", "bananas", "cherries", };
|
||||
const string value = "I have four grapes";
|
||||
|
||||
var result = value.EndsWithAny(matchingValues, StringComparison.InvariantCulture);
|
||||
Assert.False(result);
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue