From d8d954a17154948fbc8474958e8ad16ebac1a726 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andreas=20H=C3=A5kansson?= Date: Tue, 24 May 2016 11:13:25 +0200 Subject: [PATCH] Use GlobalizationConfiguration.DateTimeStyles in DynamicDictionaryValue DateTime parsing --- src/Nancy/DynamicDictionary.cs | 44 +++++++++---- src/Nancy/DynamicDictionaryValue.cs | 64 +++++++++++-------- src/Nancy/Extensions/StringExtensions.cs | 2 +- src/Nancy/GlobalizationConfiguration.cs | 28 ++++++-- src/Nancy/Routing/DefaultRouteResolver.cs | 4 +- .../Unit/DynamicDictionaryFixture.cs | 16 ++--- .../Unit/DynamicDictionaryValueFixture.cs | 35 +++++++++- 7 files changed, 134 insertions(+), 59 deletions(-) diff --git a/src/Nancy/DynamicDictionary.cs b/src/Nancy/DynamicDictionary.cs index 364be950e1..c81e9c3dac 100644 --- a/src/Nancy/DynamicDictionary.cs +++ b/src/Nancy/DynamicDictionary.cs @@ -14,9 +14,27 @@ [DebuggerDisplay("{DebuggerDisplay, nq}")] public class DynamicDictionary : DynamicObject, IEquatable, IHideObjectMembers, IEnumerable, IDictionary { + private readonly GlobalizationConfiguration globalizationConfiguration; + private readonly IDictionary dictionary = new Dictionary(StaticConfiguration.CaseSensitive ? StringComparer.Ordinal : StringComparer.OrdinalIgnoreCase); + /// + /// Initializes a new istance of the class. + /// + public DynamicDictionary() + : this(GlobalizationConfiguration.Default) + { + } + + /// + /// Initializes a new istance of the class. + /// + /// A instance. + public DynamicDictionary(GlobalizationConfiguration globalizationConfiguration) + { + this.globalizationConfiguration = globalizationConfiguration; + } /// /// Returns an empty dynamic dictionary. @@ -24,20 +42,18 @@ public class DynamicDictionary : DynamicObject, IEquatable, I /// A instance. public static DynamicDictionary Empty { - get - { - return new DynamicDictionary(); - } + get { return new DynamicDictionary(); } } /// /// Creates a dynamic dictionary from an instance. /// /// An instance, that the dynamic dictionary should be created from. + /// /// An instance. - public static DynamicDictionary Create(IDictionary values) + public static DynamicDictionary Create(IDictionary values, GlobalizationConfiguration globalizationConfiguration) { - var instance = new DynamicDictionary(); + var instance = new DynamicDictionary(globalizationConfiguration); foreach (var key in values.Keys) { @@ -67,7 +83,7 @@ public override bool TryGetMember(GetMemberBinder binder, out object result) { if (!dictionary.TryGetValue(binder.Name, out result)) { - result = new DynamicDictionaryValue(null); + result = new DynamicDictionaryValue(null, this.globalizationConfiguration); } return true; @@ -113,7 +129,7 @@ public dynamic this[string name] dynamic member; if (!dictionary.TryGetValue(name, out member)) { - member = new DynamicDictionaryValue(null); + member = new DynamicDictionaryValue(null, this.globalizationConfiguration); } return member; @@ -122,7 +138,7 @@ public dynamic this[string name] { name = GetNeutralKey(name); - dictionary[name] = value is DynamicDictionaryValue ? value : new DynamicDictionaryValue(value); + dictionary[name] = value is DynamicDictionaryValue ? value : new DynamicDictionaryValue(value, this.globalizationConfiguration); } } @@ -257,7 +273,7 @@ public int Count public bool Contains(KeyValuePair item) { var dynamicValueKeyValuePair = - GetDynamicKeyValuePair(item); + this.GetDynamicKeyValuePair(item); return this.dictionary.Contains(dynamicValueKeyValuePair); } @@ -300,7 +316,7 @@ public bool Remove(string key) public bool Remove(KeyValuePair item) { var dynamicValueKeyValuePair = - GetDynamicKeyValuePair(item); + this.GetDynamicKeyValuePair(item); return this.dictionary.Remove(dynamicValueKeyValuePair); } @@ -317,10 +333,10 @@ public ICollection Values } } - private static KeyValuePair GetDynamicKeyValuePair(KeyValuePair item) + private KeyValuePair GetDynamicKeyValuePair(KeyValuePair item) { var dynamicValueKeyValuePair = - new KeyValuePair(item.Key, new DynamicDictionaryValue(item.Value)); + new KeyValuePair(item.Key, new DynamicDictionaryValue(item.Value, this.globalizationConfiguration)); return dynamicValueKeyValuePair; } @@ -360,7 +376,7 @@ private string DebuggerDisplay for (var i = 0; i < maxItems; i++) { var item = this.dictionary.ElementAt(i); - + builder.AppendFormat(" {0} = {1}{2}", item.Key, item.Value, i < maxItems - 1 ? "," : string.Empty); } diff --git a/src/Nancy/DynamicDictionaryValue.cs b/src/Nancy/DynamicDictionaryValue.cs index 6d4d819c7c..1508d7c8a7 100644 --- a/src/Nancy/DynamicDictionaryValue.cs +++ b/src/Nancy/DynamicDictionaryValue.cs @@ -5,21 +5,34 @@ using System.Dynamic; using System.Globalization; using System.Linq.Expressions; - using Microsoft.CSharp.RuntimeBinder; - using Nancy.Routing.Trie.Nodes; + /// + /// A value that is stored inside a instance. + /// public class DynamicDictionaryValue : DynamicObject, IEquatable, IHideObjectMembers, IConvertible { private readonly object value; + private readonly GlobalizationConfiguration globalizationConfiguration; /// /// Initializes a new instance of the class. /// /// The value to store in the instance public DynamicDictionaryValue(object value) + : this(value, GlobalizationConfiguration.Default) + { + } + + /// + /// Initializes a new instance of the class. + /// + /// The value to store in the instance + /// A instance. + public DynamicDictionaryValue(object value, GlobalizationConfiguration globalizationConfiguration) { this.value = value; + this.globalizationConfiguration = globalizationConfiguration; } /// @@ -52,13 +65,12 @@ public object Value { try { - return (T)value; + return (T)this.value; } catch { - var typeName = value.GetType().Name; - var message = string.Format("Cannot convert value of type '{0}' to type '{1}'", - typeName, typeof(T).Name); + var typeName = this.value.GetType().Name; + var message = string.Format("Cannot convert value of type '{0}' to type '{1}'", typeName, typeof(T).Name); throw new InvalidCastException(message); } @@ -79,21 +91,21 @@ public object Value { try { - var valueType = value.GetType(); + var valueType = this.value.GetType(); var parseType = typeof(T); // check for direct cast if (valueType.IsAssignableFrom(parseType)) { - return (T)value; + return (T)this.value; } - var stringValue = value as string; + var stringValue = this.value as string; if (parseType == typeof(DateTime)) { DateTime result; - if (DateTime.TryParse(stringValue, CultureInfo.InvariantCulture, DateTimeStyles.None, out result)) + if (DateTime.TryParse(stringValue, CultureInfo.InvariantCulture, this.globalizationConfiguration.DateTimeStyles, out result)) { return (T)((object)result); } @@ -115,7 +127,7 @@ public object Value var underlyingType = Nullable.GetUnderlyingType(parseType) ?? parseType; - return (T)Convert.ChangeType(value, underlyingType, CultureInfo.InvariantCulture); + return (T)Convert.ChangeType(this.value, underlyingType, CultureInfo.InvariantCulture); } catch { @@ -209,7 +221,7 @@ public override bool TryBinaryOperation(BinaryOperationBinder binder, object arg var convert = Binder.Convert(CSharpBinderFlags.None, arg.GetType(), typeof(DynamicDictionaryValue)); - if (!TryConvert((ConvertBinder)convert, out resultOfCast)) + if (!this.TryConvert((ConvertBinder)convert, out resultOfCast)) { return false; } @@ -230,22 +242,22 @@ public override bool TryConvert(ConvertBinder binder, out object result) { result = null; - if (value == null) + if (this.value == null) { return true; } var binderType = binder.Type; - if (binderType == typeof(String)) + if (binderType == typeof(string)) { - result = Convert.ToString(value); + result = Convert.ToString(this.value); return true; } if (binderType == typeof(Guid) || binderType == typeof(Guid?)) { Guid guid; - if (Guid.TryParse(Convert.ToString(value), out guid)) + if (Guid.TryParse(Convert.ToString(this.value), out guid)) { result = guid; return true; @@ -254,7 +266,7 @@ public override bool TryConvert(ConvertBinder binder, out object result) else if (binderType == typeof(TimeSpan) || binderType == typeof(TimeSpan?)) { TimeSpan timespan; - if (TimeSpan.TryParse(Convert.ToString(value), out timespan)) + if (TimeSpan.TryParse(Convert.ToString(this.value), out timespan)) { result = timespan; return true; @@ -263,11 +275,11 @@ public override bool TryConvert(ConvertBinder binder, out object result) else if (binderType.IsEnum) { // handles enum to enum assignments - if (value.GetType().IsEnum) + if (this.value.GetType().IsEnum) { - if (binderType == value.GetType()) + if (binderType == this.value.GetType()) { - result = value; + result = this.value; return true; } @@ -275,9 +287,9 @@ public override bool TryConvert(ConvertBinder binder, out object result) } // handles number to enum assignments - if (Enum.GetUnderlyingType(binderType) == value.GetType()) + if (Enum.GetUnderlyingType(binderType) == this.value.GetType()) { - result = Enum.ToObject(binderType, value); + result = Enum.ToObject(binderType, this.value); return true; } @@ -294,9 +306,9 @@ public override bool TryConvert(ConvertBinder binder, out object result) if (typeCode == TypeCode.Object) { - if (binderType.IsAssignableFrom(value.GetType())) + if (binderType.IsAssignableFrom(this.value.GetType())) { - result = value; + result = this.value; return true; } else @@ -305,7 +317,7 @@ public override bool TryConvert(ConvertBinder binder, out object result) } } - result = Convert.ChangeType(value, typeCode); + result = Convert.ChangeType(this.value, typeCode); return true; } @@ -427,7 +439,7 @@ public static implicit operator DateTime(DynamicDictionaryValue dynamicValue) return (DateTime)dynamicValue.value; } - return DateTime.Parse(dynamicValue.ToString()); + return DateTime.Parse(dynamicValue.ToString(), CultureInfo.InvariantCulture, dynamicValue.globalizationConfiguration.DateTimeStyles); } public static implicit operator TimeSpan?(DynamicDictionaryValue dynamicValue) diff --git a/src/Nancy/Extensions/StringExtensions.cs b/src/Nancy/Extensions/StringExtensions.cs index c9b86d8f65..10305ce308 100644 --- a/src/Nancy/Extensions/StringExtensions.cs +++ b/src/Nancy/Extensions/StringExtensions.cs @@ -62,7 +62,7 @@ public static bool IsParameterized(this string segment) public static DynamicDictionary AsQueryDictionary(this string queryString) { var coll = HttpUtility.ParseQueryString(queryString); - var ret = new DynamicDictionary(); + var ret = new DynamicDictionary(GlobalizationConfiguration.Default); var found = 0; foreach (var key in coll.AllKeys.Where(key => key != null)) diff --git a/src/Nancy/GlobalizationConfiguration.cs b/src/Nancy/GlobalizationConfiguration.cs index d49420d3a0..cea59bef94 100644 --- a/src/Nancy/GlobalizationConfiguration.cs +++ b/src/Nancy/GlobalizationConfiguration.cs @@ -13,21 +13,31 @@ public class GlobalizationConfiguration /// /// A default instance of the class /// - public static readonly GlobalizationConfiguration Default = new GlobalizationConfiguration(supportedCultureNames: new[] { CultureInfo.CurrentCulture.Name }, defaultCulture: CultureInfo.CurrentCulture.Name); + public static readonly GlobalizationConfiguration Default = new GlobalizationConfiguration + { + SupportedCultureNames = new[] { CultureInfo.CurrentCulture.Name }, + DefaultCulture = CultureInfo.CurrentCulture.Name, + DateTimeStyles = DateTimeStyles.AdjustToUniversal | DateTimeStyles.AssumeUniversal + }; + + private GlobalizationConfiguration() + { + } /// /// Initializes a new instance of the class /// /// An array of supported cultures /// The default culture of the application - public GlobalizationConfiguration(IEnumerable supportedCultureNames, string defaultCulture = null) + /// The that should be used for date parsing. + public GlobalizationConfiguration(IEnumerable supportedCultureNames, string defaultCulture = null, DateTimeStyles? dateTimeStyles = null) { if (supportedCultureNames == null) { throw new ConfigurationException("Invalid Globalization configuration. You must support at least one culture"); } - supportedCultureNames = supportedCultureNames.Where(cultureName => !string.IsNullOrEmpty(cultureName)); + supportedCultureNames = supportedCultureNames.Where(cultureName => !string.IsNullOrEmpty(cultureName)).ToArray(); if (!supportedCultureNames.Any()) { @@ -44,18 +54,24 @@ public GlobalizationConfiguration(IEnumerable supportedCultureNames, str throw new ConfigurationException("Invalid Globalization configuration. " + defaultCulture + " does not exist in the supported culture names"); } - this.SupportedCultureNames = supportedCultureNames; + this.DateTimeStyles = dateTimeStyles ?? Default.DateTimeStyles; this.DefaultCulture = defaultCulture; + this.SupportedCultureNames = supportedCultureNames; } /// - /// A set of supported cultures + /// The that should be used for date parsing. /// - public IEnumerable SupportedCultureNames { get; private set; } + public DateTimeStyles DateTimeStyles { get; private set; } /// /// The default culture for the application /// public string DefaultCulture { get; private set; } + + /// + /// A set of supported cultures + /// + public IEnumerable SupportedCultureNames { get; private set; } } } diff --git a/src/Nancy/Routing/DefaultRouteResolver.cs b/src/Nancy/Routing/DefaultRouteResolver.cs index dd9cdbc775..b953a5f829 100644 --- a/src/Nancy/Routing/DefaultRouteResolver.cs +++ b/src/Nancy/Routing/DefaultRouteResolver.cs @@ -17,6 +17,7 @@ public class DefaultRouteResolver : IRouteResolver private readonly IRouteCache routeCache; private readonly IRouteResolverTrie trie; private readonly Lazy configuration; + private readonly GlobalizationConfiguration globalizationConfiguraton; /// /// Initializes a new instance of the class, using @@ -35,6 +36,7 @@ public DefaultRouteResolver(INancyModuleCatalog catalog, INancyModuleBuilder mod this.routeCache = routeCache; this.trie = trie; this.configuration = new Lazy(environment.GetValue); + this.globalizationConfiguraton = environment.GetValue(); this.BuildTrie(); } @@ -127,7 +129,7 @@ private ResolveResult BuildResult(NancyContext context, MatchResult result) context.NegotiationContext.SetModule(associatedModule); var route = associatedModule.Routes.ElementAt(result.RouteIndex); - var parameters = DynamicDictionary.Create(result.Parameters); + var parameters = DynamicDictionary.Create(result.Parameters, this.globalizationConfiguraton); return new ResolveResult { diff --git a/test/Nancy.Tests/Unit/DynamicDictionaryFixture.cs b/test/Nancy.Tests/Unit/DynamicDictionaryFixture.cs index 539645f126..aac5c040c1 100644 --- a/test/Nancy.Tests/Unit/DynamicDictionaryFixture.cs +++ b/test/Nancy.Tests/Unit/DynamicDictionaryFixture.cs @@ -32,7 +32,7 @@ public void Should_create_instance_from_dictionary() }; // When - dynamic instance = DynamicDictionary.Create(values); + dynamic instance = DynamicDictionary.Create(values, GlobalizationConfiguration.Default); // Then ((int)GetIntegerValue(instance.foo)).ShouldEqual(10); @@ -283,8 +283,8 @@ public void Should_implicitly_cast_to_datetime_when_value_is_retrieved_as_index( public void Should_implicitly_cast_to_datetime_when_value_is_string_and_is_retrieved_as_member() { // Given - var date = DateTime.Now; - this.dictionary.value = date.ToString(); + var date = new DateTime(2016, 05, 25, 10, 0, 05, DateTimeKind.Utc); + this.dictionary.value = date.ToString("O"); // When DateTime result = GetDateTimeValue(this.dictionary.value); @@ -297,8 +297,8 @@ public void Should_implicitly_cast_to_datetime_when_value_is_string_and_is_retri public void Should_implicitly_cast_to_datetime_when_value_is_string_and_is_retrieved_as_index() { // Given - var date = DateTime.Now; - this.dictionary.value = date.ToString(); + var date = new DateTime(2016, 05, 25, 10, 0, 05, DateTimeKind.Utc); + this.dictionary.value = date.ToString("O"); // When DateTime result = GetDateTimeValue(this.dictionary["value"]); @@ -795,7 +795,7 @@ public void Should_be_able_to_enumerate_keys() public void String_dictionary_values_are_Json_serialized_as_strings() { dynamic value = "42"; - var input = new DynamicDictionaryValue(value); + var input = new DynamicDictionaryValue(value, GlobalizationConfiguration.Default); var sut = new JavaScriptSerializer(); var actual = sut.Serialize(input); @@ -807,7 +807,7 @@ public void String_dictionary_values_are_Json_serialized_as_strings() public void Integer_dictionary_values_are_Json_serialized_as_integers() { dynamic value = 42; - var input = new DynamicDictionaryValue(value); + var input = new DynamicDictionaryValue(value, GlobalizationConfiguration.Default); var sut = new JavaScriptSerializer(); var actual = sut.Serialize(input); @@ -1120,7 +1120,7 @@ public void Should_remove_natural_key() input.Remove("a-b-c"); //then - input.ContainsKey("abc").ShouldBeFalse(); + input.ContainsKey("abc").ShouldBeFalse(); } [Fact] diff --git a/test/Nancy.Tests/Unit/DynamicDictionaryValueFixture.cs b/test/Nancy.Tests/Unit/DynamicDictionaryValueFixture.cs index 5b45e43492..940a837ccc 100644 --- a/test/Nancy.Tests/Unit/DynamicDictionaryValueFixture.cs +++ b/test/Nancy.Tests/Unit/DynamicDictionaryValueFixture.cs @@ -3,11 +3,8 @@ using System; using System.Dynamic; using System.Globalization; - using FakeItEasy; - using Xunit; - using Xunit.Extensions; public class DynamicDictionaryValueFixture { @@ -986,6 +983,38 @@ public void Should_return_default_value_if_implicit_convert_fails_on_datetime() Assert.Equal(expected, actual); } + [Fact] + public void Should_adjust_to_universal_time_when_globalizationconfiguration_datetimestyles_requires_it() + { + // Given + var expected = new DateTime(2016, 05, 24, 08, 41, 37, DateTimeKind.Utc); + + var config = new GlobalizationConfiguration(new [] {"en-US"}, dateTimeStyles: DateTimeStyles.AdjustToUniversal); + var value = new DynamicDictionaryValue("2016-05-24T10:41:37+02:00", config); + + // When + DateTime actual = value; + + // Then + actual.ShouldEqual(expected); + } + + [Fact] + public void Should_assume_local_time_when_globalizationconfiguration_datetimestyles_requires_it() + { + // Given + var expected = DateTime.Now; + + var config = new GlobalizationConfiguration(new[] { "en-US" }, dateTimeStyles: DateTimeStyles.AssumeLocal); + var value = new DynamicDictionaryValue(expected.ToString("O"), config); + + // When + DateTime actual = value; + + // Then + actual.ShouldEqual(expected); + } + [Fact] public void Should_implicitly_convert_from_int_based_on_given_type_of_string() {