Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with
or
.
Download ZIP
Browse files

Merge pull request #871 from jchannon/CultureModelBinding

Culture model binding
  • Loading branch information...
commit 9bbe5b3f9f425fe31232a82f1c9ab3719b39434b 2 parents 2df1c00 + de4bf2c
@grumpydev grumpydev authored
View
1  src/Nancy.Tests/Nancy.Tests.csproj
@@ -137,6 +137,7 @@
<Compile Include="Unit\Diagnostics\ConcurrentLimitedCollectionFixture.cs" />
<Compile Include="Unit\Diagnostics\DiagnosticsHookFixture.cs" />
<Compile Include="Unit\ErrorHandling\DefaultStatusCodeHandlerFixture.cs" />
+ <Compile Include="Unit\Extensions\TypeExtensionsFixture.cs" />
<Compile Include="Unit\FormatterExtensionsFixture.cs" />
<Compile Include="Unit\MimeTypesFixture.cs" />
<Compile Include="Unit\ModelBinding\ModelBindingExceptionFixture.cs" />
View
161 src/Nancy.Tests/Unit/Extensions/TypeExtensionsFixture.cs
@@ -0,0 +1,161 @@
+namespace Nancy.Tests.Unit.Extensions
+{
+ using System;
+ using Nancy.Extensions;
+ using Xunit;
+
+ public class TypeExtensionsFixture
+ {
+ [Fact]
+ public void Should_test_non_numeric_types()
+ {
+ Assert.False(TypeExtensions.IsNumeric(null));
+ Assert.False(typeof(object).IsNumeric());
+ Assert.False(typeof(DBNull).IsNumeric());
+ Assert.False(typeof(bool).IsNumeric());
+ Assert.False(typeof(char).IsNumeric());
+ Assert.False(typeof(DateTime).IsNumeric());
+ Assert.False(typeof(string).IsNumeric());
+ }
+
+ [Fact]
+ public void Should_test_Arrays_of_numeric_and_non_numeric_types()
+ {
+ Assert.False(typeof(object[]).IsNumeric());
+ Assert.False(typeof(DBNull[]).IsNumeric());
+ Assert.False(typeof(bool[]).IsNumeric());
+ Assert.False(typeof(char[]).IsNumeric());
+ Assert.False(typeof(DateTime[]).IsNumeric());
+ Assert.False(typeof(string[]).IsNumeric());
+ Assert.False(typeof(byte[]).IsNumeric());
+ Assert.False(typeof(decimal[]).IsNumeric());
+ Assert.False(typeof(double[]).IsNumeric());
+ Assert.False(typeof(short[]).IsNumeric());
+ Assert.False(typeof(int[]).IsNumeric());
+ Assert.False(typeof(long[]).IsNumeric());
+ Assert.False(typeof(sbyte[]).IsNumeric());
+ Assert.False(typeof(float[]).IsNumeric());
+ Assert.False(typeof(ushort[]).IsNumeric());
+ Assert.False(typeof(uint[]).IsNumeric());
+ Assert.False(typeof(ulong[]).IsNumeric());
+ }
+
+ [Fact]
+ public void Should_test_numeric_types()
+ {
+ Assert.True(typeof(byte).IsNumeric());
+ Assert.True(typeof(decimal).IsNumeric());
+ Assert.True(typeof(double).IsNumeric());
+ Assert.True(typeof(short).IsNumeric());
+ Assert.True(typeof(int).IsNumeric());
+ Assert.True(typeof(long).IsNumeric());
+ Assert.True(typeof(sbyte).IsNumeric());
+ Assert.True(typeof(float).IsNumeric());
+ Assert.True(typeof(ushort).IsNumeric());
+ Assert.True(typeof(uint).IsNumeric());
+ Assert.True(typeof(ulong).IsNumeric());
+ }
+
+ [Fact]
+ public void Should_test_nullable_non_numeric_types()
+ {
+ Assert.False(typeof(bool?).IsNumeric());
+ Assert.False(typeof(char?).IsNumeric());
+ Assert.False(typeof(DateTime?).IsNumeric());
+ }
+
+ [Fact]
+ public void Should_test_nullable_numeric_types()
+ {
+ Assert.True(typeof(byte?).IsNumeric());
+ Assert.True(typeof(decimal?).IsNumeric());
+ Assert.True(typeof(double?).IsNumeric());
+ Assert.True(typeof(short?).IsNumeric());
+ Assert.True(typeof(int?).IsNumeric());
+ Assert.True(typeof(long?).IsNumeric());
+ Assert.True(typeof(sbyte?).IsNumeric());
+ Assert.True(typeof(float?).IsNumeric());
+ Assert.True(typeof(ushort?).IsNumeric());
+ Assert.True(typeof(uint?).IsNumeric());
+ Assert.True(typeof(ulong?).IsNumeric());
+ }
+
+ [Fact]
+ public void Should_test_non_numeric_gettype()
+ {
+ Assert.False((new object()).GetType().IsNumeric());
+ Assert.False(DBNull.Value.GetType().IsNumeric());
+ Assert.False(true.GetType().IsNumeric());
+ Assert.False('a'.GetType().IsNumeric());
+ Assert.False((new DateTime(2009, 1, 1)).GetType().IsNumeric());
+ Assert.False(string.Empty.GetType().IsNumeric());
+ }
+
+ [Fact]
+ public void Should_test_numeric_gettype()
+ {
+ Assert.True((new byte()).GetType().IsNumeric());
+ Assert.True(43.2m.GetType().IsNumeric());
+ Assert.True(43.2d.GetType().IsNumeric());
+ Assert.True(((short)2).GetType().IsNumeric());
+ Assert.True(((int)2).GetType().IsNumeric());
+ Assert.True(((long)2).GetType().IsNumeric());
+ Assert.True(((sbyte)2).GetType().IsNumeric());
+ Assert.True(2f.GetType().IsNumeric());
+ Assert.True(((ushort)2).GetType().IsNumeric());
+ Assert.True(((uint)2).GetType().IsNumeric());
+ Assert.True(((ulong)2).GetType().IsNumeric());
+ }
+
+ [Fact]
+ public void Should_test_nullable_non_numeric_types_gettype()
+ {
+ bool? nullableBool = true;
+ Assert.False(nullableBool.GetType().IsNumeric());
+
+ char? nullableChar = ' ';
+ Assert.False(nullableChar.GetType().IsNumeric());
+
+ DateTime? nullableDateTime = new DateTime(2009, 1, 1);
+ Assert.False(nullableDateTime.GetType().IsNumeric());
+ }
+
+ [Fact]
+ public void Should_test_nullable_numeric_types_gettype()
+ {
+ byte? nullableByte = 12;
+ Assert.True(nullableByte.GetType().IsNumeric());
+
+ decimal? nullableDecimal = 12.2m;
+ Assert.True(nullableDecimal.GetType().IsNumeric());
+
+ double? nullableDouble = 12.32;
+ Assert.True(nullableDouble.GetType().IsNumeric());
+
+ short? nullableInt16 = 12;
+ Assert.True(nullableInt16.GetType().IsNumeric());
+
+ short? nullableInt32 = 12;
+ Assert.True(nullableInt32.GetType().IsNumeric());
+
+ short? nullableInt64 = 12;
+ Assert.True(nullableInt64.GetType().IsNumeric());
+
+ sbyte? nullableSByte = 12;
+ Assert.True(nullableSByte.GetType().IsNumeric());
+
+ float? nullableSingle = 3.2f;
+ Assert.True(nullableSingle.GetType().IsNumeric());
+
+ ushort? nullableUInt16 = 12;
+ Assert.True(nullableUInt16.GetType().IsNumeric());
+
+ ushort? nullableUInt32 = 12;
+ Assert.True(nullableUInt32.GetType().IsNumeric());
+
+ ushort? nullableUInt64 = 12;
+ Assert.True(nullableUInt64.GetType().IsNumeric());
+
+ }
+ }
+}
View
70 src/Nancy.Tests/Unit/ModelBinding/DefaultBinderFixture.cs
@@ -5,7 +5,7 @@ namespace Nancy.Tests.Unit.ModelBinding
using System.IO;
using System.Linq;
using System.Text;
-
+ using System.Globalization;
using FakeItEasy;
using Nancy.IO;
@@ -15,6 +15,7 @@ namespace Nancy.Tests.Unit.ModelBinding
using Nancy.ModelBinding.DefaultBodyDeserializers;
using Nancy.ModelBinding.DefaultConverters;
using Nancy.Tests.Unit.ModelBinding.DefaultBodyDeserializers;
+ using Xunit.Extensions;
using Xunit;
@@ -171,20 +172,20 @@ public void Should_use_object_from_deserializer_if_one_returned_and_overwrite_wh
public void Should_use_object_from_deserializer_if_one_returned_and_not_overwrite_when_not_allowed()
{
// Given
- var modelObject = new TestModel {StringPropertyWithDefaultValue = "Hello!"};
+ var modelObject = new TestModel { StringPropertyWithDefaultValue = "Hello!" };
var deserializer = A.Fake<IBodyDeserializer>();
A.CallTo(() => deserializer.CanDeserialize(null)).WithAnyArguments().Returns(true);
A.CallTo(() => deserializer.Deserialize(null, null, null)).WithAnyArguments().Returns(modelObject);
- var binder = this.GetBinder(bodyDeserializers: new[] {deserializer});
+ var binder = this.GetBinder(bodyDeserializers: new[] { deserializer });
- var context = CreateContextWithHeader("Content-Type", new[] {"application/xml"});
+ var context = CreateContextWithHeader("Content-Type", new[] { "application/xml" });
// When
- var result = binder.Bind(context, typeof (TestModel), null, BindingConfig.NoOverwrite);
+ var result = binder.Bind(context, typeof(TestModel), null, BindingConfig.NoOverwrite);
// Then
result.ShouldBeOfType<TestModel>();
- ((TestModel) result).StringPropertyWithDefaultValue.ShouldEqual("Default Value");
+ ((TestModel)result).StringPropertyWithDefaultValue.ShouldEqual("Default Value");
}
[Fact]
@@ -290,7 +291,7 @@ public void Should_ignore_indexer_properties()
binder.Bind(context, typeof(TestModel), null, new BindingConfig());
// Then
- validProperties.ShouldEqual(6);
+ validProperties.ShouldEqual(7);
}
[Fact]
@@ -528,6 +529,57 @@ public void Should_be_able_to_bind_from_form_and_request_simultaneously()
result.IntProperty.ShouldEqual(12);
}
+ [Theory]
+ [InlineData("de-DE", 4.50)]
+ [InlineData("en-GB", 450)]
+ [InlineData("en-US", 450)]
+ [InlineData("se-SE", 4.50)]
+ [InlineData("ru-RU", 4.50)]
+ [InlineData("zh-TW", 450)]
+ public void Should_be_able_to_bind_culturally_aware_form_properties_if_numeric(string culture, double expected)
+ {
+ // Given
+ var binder = this.GetBinder();
+
+ var context = CreateContextWithHeader("Content-Type", new[] { "application/xml" });
+ context.Culture = new CultureInfo(culture);
+ context.Request.Form["DoubleProperty"] = "4,50";
+
+ // When
+ var result = (TestModel)binder.Bind(context, typeof(TestModel), null, new BindingConfig());
+
+ // Then
+ result.DoubleProperty.ShouldEqual(expected);
+ }
+
+ [Theory]
+ [InlineData("12/25/2012", 12, 25, 2012, "en-US")]
+ [InlineData("12/12/2012", 12, 12, 2012, "en-US")]
+ [InlineData("25/12/2012", 12, 25, 2012, "en-GB")]
+ [InlineData("12/12/2012", 12, 12, 2012, "en-GB")]
+ [InlineData("12/12/2012", 12, 12, 2012, "ru-RU")]
+ [InlineData("25/12/2012", 12, 25, 2012, "ru-RU")]
+ [InlineData("2012-12-25", 12, 25, 2012, "zh-TW")]
+ [InlineData("2012-12-12", 12, 12, 2012, "zh-TW")]
+ public void Should_be_able_to_bind_culturally_aware_form_properties_if_datetime(string date, int month, int day, int year, string culture)
+ {
+ // Given
+ var binder = this.GetBinder();
+
+ var context = CreateContextWithHeader("Content-Type", new[] { "application/xml" });
+ context.Culture = new CultureInfo(culture);
+ context.Request.Form["DateProperty"] = date;
+
+ // When
+ var result = (TestModel)binder.Bind(context, typeof(TestModel), null, new BindingConfig());
+ // Then
+ result.DateProperty.Date.Month.ShouldEqual(month);
+ result.DateProperty.Date.Day.ShouldEqual(day);
+ result.DateProperty.Date.Year.ShouldEqual(year);
+ }
+
+
+
[Fact]
public void Should_be_able_to_bind_from_request_and_context_simultaneously()
{
@@ -687,7 +739,7 @@ public void Should_ignore_existing_instance_if_type_doesnt_match()
private IBinder GetBinder(IEnumerable<ITypeConverter> typeConverters = null, IEnumerable<IBodyDeserializer> bodyDeserializers = null, IFieldNameConverter nameConverter = null, BindingDefaults bindingDefaults = null)
{
- var converters = typeConverters ?? new ITypeConverter[] { new FallbackConverter(), };
+ var converters = typeConverters ?? new ITypeConverter[] { new DateTimeConverter(), new NumericConverter(), new FallbackConverter(), };
var deserializers = bodyDeserializers ?? new IBodyDeserializer[] { };
var converter = nameConverter ?? this.passthroughNameConverter;
var defaults = bindingDefaults ?? this.emptyDefaults;
@@ -745,6 +797,8 @@ public TestModel()
public string StringPropertyWithDefaultValue { get; set; }
+ public double DoubleProperty { get; set; }
+
public int this[int index]
{
get { return 0; }
View
2  src/Nancy.sln
@@ -103,7 +103,7 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Nancy.Demo.Authentication.S
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Nancy.Demo.Authentication.Stateless", "Nancy.Demo.Authentication.Stateless\Nancy.Demo.Authentication.Stateless.csproj", "{BAE74CD5-57C2-40E3-8F7A-EDE5721C2ACC}"
EndProject
-Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Nancy.Demo.Razor.Localization", "Nancy.Demo.Razor.Localization\Nancy.Demo.Razor.Localization.csproj", "{396F0BCE-5B51-4B6A-931E-312880C24725}"
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Nancy.Demo.Razor.Localization", "Nancy.Demo.Localization\Nancy.Demo.Razor.Localization.csproj", "{396F0BCE-5B51-4B6A-931E-312880C24725}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
View
37 src/Nancy/Extensions/TypeExtensions.cs
@@ -34,5 +34,42 @@ public static bool IsEnumerable(this Type source)
return source.IsGenericType && source.GetGenericTypeDefinition() == enumerableType;
}
+
+ /// <summary>
+ /// Determines if a type is numeric. Nullable numeric types are considered numeric.
+ /// </summary>
+ /// <remarks>
+ /// Boolean is not considered numeric.
+ /// </remarks>
+ public static bool IsNumeric(this Type source)
+ {
+ if (source == null)
+ {
+ return false;
+ }
+
+ switch (Type.GetTypeCode(source))
+ {
+ case TypeCode.Byte:
+ case TypeCode.Decimal:
+ case TypeCode.Double:
+ case TypeCode.Int16:
+ case TypeCode.Int32:
+ case TypeCode.Int64:
+ case TypeCode.SByte:
+ case TypeCode.Single:
+ case TypeCode.UInt16:
+ case TypeCode.UInt32:
+ case TypeCode.UInt64:
+ return true;
+ case TypeCode.Object:
+ if (source.IsGenericType && source.GetGenericTypeDefinition() == typeof(Nullable<>))
+ {
+ return IsNumeric(Nullable.GetUnderlyingType(source));
+ }
+ return false;
+ }
+ return false;
+ }
}
}
View
38 src/Nancy/ModelBinding/DefaultConverters/DateTimeConverter.cs
@@ -0,0 +1,38 @@
+namespace Nancy.ModelBinding.DefaultConverters
+{
+ using System;
+
+ /// <summary>
+ /// Converter for datetime types
+ /// </summary>
+ public class DateTimeConverter : ITypeConverter
+ {
+ /// <summary>
+ /// Whether the converter can convert to the destination type
+ /// </summary>
+ /// <param name="destinationType">Destination type</param>
+ /// <param name="context">The current binding context</param>
+ /// <returns>True if conversion supported, false otherwise</returns>
+ public bool CanConvertTo(Type destinationType, BindingContext context)
+ {
+ return destinationType == typeof(DateTime);
+ }
+
+ /// <summary>
+ /// Convert the string representation to the destination type
+ /// </summary>
+ /// <param name="input">Input string</param>
+ /// <param name="destinationType">Destination type</param>
+ /// <param name="context">Current context</param>
+ /// <returns>Converted object of the destination type</returns>
+ public object Convert(string input, Type destinationType, BindingContext context)
+ {
+ if (string.IsNullOrEmpty(input))
+ {
+ return null;
+ }
+
+ return System.Convert.ChangeType(input, destinationType, context.Context.Culture);
+ }
+ }
+}
View
40 src/Nancy/ModelBinding/DefaultConverters/NumericConverter.cs
@@ -0,0 +1,40 @@
+namespace Nancy.ModelBinding.DefaultConverters
+{
+ using System;
+ using Extensions;
+
+ /// <summary>
+ /// Converter for numeric types
+ /// </summary>
+ public class NumericConverter : ITypeConverter
+ {
+ /// <summary>
+ /// Whether the converter can convert to the destination type
+ /// </summary>
+ /// <param name="destinationType">Destination type</param>
+ /// <param name="context">The current binding context</param>
+ /// <returns>True if conversion supported, false otherwise</returns>
+ public bool CanConvertTo(Type destinationType, BindingContext context)
+ {
+ return destinationType.IsNumeric();
+ }
+
+ /// <summary>
+ /// Convert the string representation to the destination type
+ /// </summary>
+ /// <param name="input">Input string</param>
+ /// <param name="destinationType">Destination type</param>
+ /// <param name="context">Current context</param>
+ /// <returns>Converted object of the destination type</returns>
+ public object Convert(string input, Type destinationType, BindingContext context)
+ {
+ if (string.IsNullOrEmpty(input))
+ {
+ return null;
+ }
+
+
+ return System.Convert.ChangeType(input, destinationType, context.Context.Culture);
+ }
+ }
+}
View
2  src/Nancy/Nancy.csproj
@@ -147,6 +147,8 @@
<Compile Include="Diagnostics\DefaultDiagnostics.cs" />
<Compile Include="Diagnostics\DiagnosticsViewRenderer.cs" />
<Compile Include="ModelBinding\BindingConfig.cs" />
+ <Compile Include="ModelBinding\DefaultConverters\DateTimeConverter.cs" />
+ <Compile Include="ModelBinding\DefaultConverters\NumericConverter.cs" />
<Compile Include="ModelBinding\PropertyBindingException.cs" />
<Compile Include="NegotiatorExtensions.cs" />
<Compile Include="Responses\EmbeddedFileResponse.cs" />
Please sign in to comment.
Something went wrong with that request. Please try again.