From 86cb51ff269bfcab802c061ae8131174f0848f5c Mon Sep 17 00:00:00 2001 From: Mike Stall Date: Thu, 18 May 2017 11:11:59 -0700 Subject: [PATCH] Add UtcNow, RandGuid support to SystemBindingData --- .../Bindings/BindingDataPathHelper.cs | 20 ++++++++- .../Bindings/Path/BindingParameterResolver.cs | 32 ++++++-------- .../Bindings/Path/BindingTemplateToken.cs | 29 ++++++++++--- .../Bindings/SystemBindingData.cs | 11 +++++ .../GlobalSuppressions.cs | 4 +- .../Bindings/BindingDataPathHelperTests.cs | 4 +- .../Bindings/Path/BindingTemplateTests.cs | 43 +++++++++++++++++++ .../Common/BindToGenericItemTests.cs | 20 ++++++++- 8 files changed, 130 insertions(+), 33 deletions(-) diff --git a/src/Microsoft.Azure.WebJobs.Host/Bindings/BindingDataPathHelper.cs b/src/Microsoft.Azure.WebJobs.Host/Bindings/BindingDataPathHelper.cs index 38de02e78..92d149a0b 100644 --- a/src/Microsoft.Azure.WebJobs.Host/Bindings/BindingDataPathHelper.cs +++ b/src/Microsoft.Azure.WebJobs.Host/Bindings/BindingDataPathHelper.cs @@ -18,9 +18,14 @@ internal static class BindingDataPathHelper /// strings, JToken, and Guid (which is translated in canonical form without curly braces). /// /// The parameter value to convert + /// Optional format string /// Path compatible string representation of the given parameter or null if its type is not supported. - public static string ConvertParameterValueToString(object parameterValue) + public static string ConvertParameterValueToString(object parameterValue, string format = null) { + if (string.IsNullOrWhiteSpace(format)) + { + format = null; // normalize. + } if (parameterValue != null) { switch (Type.GetTypeCode(parameterValue.GetType())) @@ -45,10 +50,21 @@ public static string ConvertParameterValueToString(object parameterValue) return ((Byte)parameterValue).ToString(CultureInfo.InvariantCulture); case TypeCode.SByte: return ((SByte)parameterValue).ToString(CultureInfo.InvariantCulture); + case TypeCode.DateTime: + format = format ?? "yyyy-MM-ddTHH-mm-ssK"; // default to ISO 8601 + var dateTime = (DateTime)parameterValue; + return dateTime.ToString(format, CultureInfo.InvariantCulture); case TypeCode.Object: if (parameterValue is Guid) { - return parameterValue.ToString(); + if (format == null) + { + return parameterValue.ToString(); + } + else + { + return ((Guid)parameterValue).ToString(format, CultureInfo.InvariantCulture); + } } if (parameterValue is Newtonsoft.Json.Linq.JToken) { diff --git a/src/Microsoft.Azure.WebJobs.Host/Bindings/Path/BindingParameterResolver.cs b/src/Microsoft.Azure.WebJobs.Host/Bindings/Path/BindingParameterResolver.cs index d31063041..85085216b 100644 --- a/src/Microsoft.Azure.WebJobs.Host/Bindings/Path/BindingParameterResolver.cs +++ b/src/Microsoft.Azure.WebJobs.Host/Bindings/Path/BindingParameterResolver.cs @@ -8,6 +8,11 @@ namespace Microsoft.Azure.WebJobs.Host.Bindings.Path { + /// + /// Support for adding built-in values to binding data. + /// Don't add new resolvers here. Instead, add it to + /// + [Obsolete("Use SystemBindingData instead")] internal abstract class BindingParameterResolver { private static Collection _resolvers; @@ -69,6 +74,7 @@ protected string GetFormatOrNull(string value) return null; } + // This is an alias for 'sys.randguid' private class RandGuidResolver : BindingParameterResolver { public override string Name @@ -82,18 +88,14 @@ public override string Name public override string Resolve(string value) { string format = GetFormatOrNull(value); - - if (!string.IsNullOrEmpty(format)) - { - return Guid.NewGuid().ToString(format, CultureInfo.InvariantCulture); - } - else - { - return Guid.NewGuid().ToString(); - } + var val = new SystemBindingData().RandGuid; + return BindingDataPathHelper.ConvertParameterValueToString(val, format); } } + // This can't be aliases to 'sys.UtcNow' because + // 'sys.UtcNow' always resolves to DateTime.UtcNow. + // But 'datetime' may either resolve to user bidning data or to DateTime.UtcNow. private class DateTimeResolver : BindingParameterResolver { public override string Name @@ -107,16 +109,8 @@ public override string Name public override string Resolve(string value) { string format = GetFormatOrNull(value); - - if (!string.IsNullOrEmpty(format)) - { - return DateTime.UtcNow.ToString(format, CultureInfo.InvariantCulture); - } - else - { - // default to ISO 8601 - return DateTime.UtcNow.ToString("yyyy-MM-ddTHH-mm-ssK", CultureInfo.InvariantCulture); - } + var val = new SystemBindingData().UtcNow; + return BindingDataPathHelper.ConvertParameterValueToString(val, format); } } } diff --git a/src/Microsoft.Azure.WebJobs.Host/Bindings/Path/BindingTemplateToken.cs b/src/Microsoft.Azure.WebJobs.Host/Bindings/Path/BindingTemplateToken.cs index 31d28ec20..2dbaece1e 100644 --- a/src/Microsoft.Azure.WebJobs.Host/Bindings/Path/BindingTemplateToken.cs +++ b/src/Microsoft.Azure.WebJobs.Host/Bindings/Path/BindingTemplateToken.cs @@ -37,15 +37,27 @@ public static BindingTemplateToken NewLiteral(string literalValue) public static BindingTemplateToken NewExpression(string expression) { + // BindingData takes precedence over builtins. + BindingParameterResolver builtin; + BindingParameterResolver.TryGetResolver(expression, out builtin); + + // check for formatter, which is applied to finale results. + string format = null; + if (builtin == null) + { + int indexColon = expression.IndexOf(':'); + if (indexColon > 0) + { + format = expression.Substring(indexColon + 1); + expression = expression.Substring(0, indexColon); + } + } + if (!BindingTemplateParser.IsValidIdentifier(expression)) { throw new FormatException($"Invalid template expression '{expression}"); } - // BindingData takes precedence over builtins. - BindingParameterResolver builtin; - BindingParameterResolver.TryGetResolver(expression, out builtin); - // Expression is just a series of dot operators like: a.b.c var parts = expression.Split('.'); @@ -66,7 +78,7 @@ public static BindingTemplateToken NewExpression(string expression) } } - return new ExpressionToken(parts, builtin); + return new ExpressionToken(parts, format, builtin); } public abstract string Evaluate(IReadOnlyDictionary bindingData); @@ -134,13 +146,16 @@ private class ExpressionToken : BindingTemplateToken // If non-null, then this could be a builtin object. private readonly BindingParameterResolver _builtin; + private readonly string _format; + // The parts of an expression, like a.b.c. private readonly string[] _expressionParts; - public ExpressionToken(string[] expressionParts, BindingParameterResolver builtin) + public ExpressionToken(string[] expressionParts, string format, BindingParameterResolver builtin) { _expressionParts = expressionParts; _builtin = builtin; + _format = format; } public override string AsLiteral => null; @@ -198,7 +213,7 @@ public override string Evaluate(IReadOnlyDictionary bindingData) } } - var strValue = BindingDataPathHelper.ConvertParameterValueToString(current); + var strValue = BindingDataPathHelper.ConvertParameterValueToString(current, _format); return strValue; } diff --git a/src/Microsoft.Azure.WebJobs.Host/Bindings/SystemBindingData.cs b/src/Microsoft.Azure.WebJobs.Host/Bindings/SystemBindingData.cs index 21be188e4..4d5c60a9f 100644 --- a/src/Microsoft.Azure.WebJobs.Host/Bindings/SystemBindingData.cs +++ b/src/Microsoft.Azure.WebJobs.Host/Bindings/SystemBindingData.cs @@ -20,6 +20,7 @@ internal class SystemBindingData public const string Name = "sys"; // An internal name for this binding that uses characters that gaurantee it can't be overwritten by a user. + // This is never seen by the user. // This ensures that we can always unambiguously retrieve this later. private const string InternalKeyName = "$sys"; @@ -34,6 +35,16 @@ internal class SystemBindingData /// public string MethodName { get; set; } + /// + /// Get the current UTC date. + /// + public DateTime UtcNow => DateTime.UtcNow; + + /// + /// Return a new random guid. This create a new guid each time it's called. + /// + public Guid RandGuid => Guid.NewGuid(); + // Given a full bindingData, create a binding data with just the system object . // This can be used when resolving default contracts that shouldn't be using an instance binding data. internal static IReadOnlyDictionary GetSystemBindingData(IReadOnlyDictionary bindingData) diff --git a/src/Microsoft.Azure.WebJobs.Host/GlobalSuppressions.cs b/src/Microsoft.Azure.WebJobs.Host/GlobalSuppressions.cs index fd53e8e62..d0e3c7439 100644 --- a/src/Microsoft.Azure.WebJobs.Host/GlobalSuppressions.cs +++ b/src/Microsoft.Azure.WebJobs.Host/GlobalSuppressions.cs @@ -78,4 +78,6 @@ [assembly: System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Design", "CA1062:Validate arguments of public methods", MessageId = "2", Scope = "member", Target = "Microsoft.Azure.WebJobs.DefaultResolutionPolicy.#TemplateBind(System.Reflection.PropertyInfo,System.Attribute,Microsoft.Azure.WebJobs.Host.Bindings.Path.BindingTemplate,System.Collections.Generic.IReadOnlyDictionary`2)")] [assembly: System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Usage", "CA2214:DoNotCallOverridableMethodsInConstructors", Scope = "member", Target = "Microsoft.Azure.WebJobs.Host.PropertyHelper.#.ctor(System.Reflection.PropertyInfo)")] [assembly: System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Performance", "CA1800:DoNotCastUnnecessarily", Scope = "member", Target = "Microsoft.Azure.WebJobs.Host.Bindings.SysBindingData.#GetFromData(System.Collections.Generic.IReadOnlyDictionary`2)")] -[assembly: System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Performance", "CA1800:DoNotCastUnnecessarily", Scope = "member", Target = "Microsoft.Azure.WebJobs.Host.Bindings.SystemBindingData.#GetFromData(System.Collections.Generic.IReadOnlyDictionary`2)")] \ No newline at end of file +[assembly: System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Performance", "CA1800:DoNotCastUnnecessarily", Scope = "member", Target = "Microsoft.Azure.WebJobs.Host.Bindings.SystemBindingData.#GetFromData(System.Collections.Generic.IReadOnlyDictionary`2)")] +[assembly: System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Performance", "CA1822:MarkMembersAsStatic", Scope = "member", Target = "Microsoft.Azure.WebJobs.Host.Bindings.SystemBindingData.#UtcNow")] +[assembly: System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Performance", "CA1822:MarkMembersAsStatic", Scope = "member", Target = "Microsoft.Azure.WebJobs.Host.Bindings.SystemBindingData.#RandGuid")] \ No newline at end of file diff --git a/test/Microsoft.Azure.WebJobs.Host.UnitTests/Bindings/BindingDataPathHelperTests.cs b/test/Microsoft.Azure.WebJobs.Host.UnitTests/Bindings/BindingDataPathHelperTests.cs index 64b543434..41623db09 100644 --- a/test/Microsoft.Azure.WebJobs.Host.UnitTests/Bindings/BindingDataPathHelperTests.cs +++ b/test/Microsoft.Azure.WebJobs.Host.UnitTests/Bindings/BindingDataPathHelperTests.cs @@ -70,10 +70,10 @@ public void ConvertParamValueToString_IfGuidParam_ReturnsStringValue() public void ConvertParamValueToString_IfUnupportedType_ReturnsNull() { // Arrange - DateTime dateTimeParam = DateTime.Now; + var obj = new { value = 12 }; // Act - string stringParamValue = BindingDataPathHelper.ConvertParameterValueToString(dateTimeParam); + string stringParamValue = BindingDataPathHelper.ConvertParameterValueToString(obj); // Assert Assert.Null(stringParamValue); diff --git a/test/Microsoft.Azure.WebJobs.Host.UnitTests/Bindings/Path/BindingTemplateTests.cs b/test/Microsoft.Azure.WebJobs.Host.UnitTests/Bindings/Path/BindingTemplateTests.cs index 193fe239c..e8f20005a 100644 --- a/test/Microsoft.Azure.WebJobs.Host.UnitTests/Bindings/Path/BindingTemplateTests.cs +++ b/test/Microsoft.Azure.WebJobs.Host.UnitTests/Bindings/Path/BindingTemplateTests.cs @@ -1,6 +1,7 @@ // Copyright (c) .NET Foundation. All rights reserved. // Licensed under the MIT License. See License.txt in the project root for license information. +using System; using System.Collections.Generic; using System.Linq; using System.Text.RegularExpressions; @@ -9,6 +10,7 @@ using Xunit; using Xunit.Extensions; using Newtonsoft.Json.Linq; +using System.Globalization; namespace Microsoft.Azure.WebJobs.Host.UnitTests.Bindings.Path { @@ -319,5 +321,46 @@ public void FromString_IgnoreCase_CreatesCaseInsensitiveTemplate() result = template.Bind(parameters); Assert.Equal("A/TestB/TestC", result); } + + + [Fact] + public void GuidFormats() + { + var g = Guid.NewGuid(); + var parameters = new Dictionary + { + { "g", g } + }; + + foreach (var format in new string[] { "N", "D", "B", "P", "X", "" }) + { + + BindingTemplate template = BindingTemplate.FromString(@"{g:" + format + "}"); + + string expected = g.ToString(format, CultureInfo.InvariantCulture); + string result = template.Bind(parameters); + + Assert.Equal(expected, result); + } + } + + [Fact] + public void DateTimeFormats() + { + var dt = DateTime.UtcNow; + var parameters = new Dictionary + { + { "dt",dt } + }; + + + var format = "YYYYMMdd"; + BindingTemplate template = BindingTemplate.FromString(@"{dt:" + format + "}"); + + string expected = dt.ToString(format, CultureInfo.InvariantCulture); + string result = template.Bind(parameters); + + Assert.Equal(expected, result); + } } } diff --git a/test/Microsoft.Azure.WebJobs.Host.UnitTests/Common/BindToGenericItemTests.cs b/test/Microsoft.Azure.WebJobs.Host.UnitTests/Common/BindToGenericItemTests.cs index dd9214348..74b4309ec 100644 --- a/test/Microsoft.Azure.WebJobs.Host.UnitTests/Common/BindToGenericItemTests.cs +++ b/test/Microsoft.Azure.WebJobs.Host.UnitTests/Common/BindToGenericItemTests.cs @@ -79,7 +79,7 @@ public void Initialize(ExtensionConfigContext context) public void Test(TestJobHost host) { host.Call("Func", new { k = 1 }); - Assert.Equal("1", _log); + Assert.NotNull(_log); host.Call("Func2", new { k = 1 }); Assert.Equal("Func2", _log); @@ -87,8 +87,24 @@ public void Test(TestJobHost host) string _log; - public void Func([Test2(Path = "{k}")] string w) + public void Func([Test2(Path = "{k}*{sys.randGuid:N}*{sys.randGuid:B}*{sys.UtcNow:yyyy}")] string w) { + var parts = w.Split('*'); + string k = parts[0]; + Assert.Equal("1", k); + + string guidstr1 = parts[1]; + var guid1 = Guid.Parse(guidstr1); + string guidstr2 = parts[2]; + var guid2 = Guid.Parse(guidstr2); + + Assert.Equal(guid1.ToString("N"), guidstr1); + Assert.Equal(guid2.ToString("B"), guidstr2); + Assert.NotEqual(guid1, guid2); // each sys.RandGuid is a different value + + string date = parts[3]; + Assert.Equal(DateTime.UtcNow.Year.ToString(), date); + _log = w; }