From 7df53c706c88c544aec5647ded5384ce0615adc5 Mon Sep 17 00:00:00 2001 From: Sebastian Stehle Date: Mon, 29 Aug 2016 22:34:50 +0200 Subject: [PATCH] Dynamo Object Support --- PinkParrot.sln.DotSettings | 2 + .../PropertiesBagTests.cs | 135 +++++++++++++----- .../CollectionExtensions.cs | 11 ++ .../PropertiesBag.cs | 32 ++++- .../PropertyValue.cs | 86 +++++++---- 5 files changed, 198 insertions(+), 68 deletions(-) diff --git a/PinkParrot.sln.DotSettings b/PinkParrot.sln.DotSettings index 44e2de54f..6627add68 100644 --- a/PinkParrot.sln.DotSettings +++ b/PinkParrot.sln.DotSettings @@ -31,6 +31,8 @@ <?xml version="1.0" encoding="utf-16"?><Profile name="Namespaces"><CSOptimizeUsings><OptimizeUsings>True</OptimizeUsings><EmbraceInRegion>False</EmbraceInRegion><RegionName></RegionName></CSOptimizeUsings><CSUpdateFileHeader>True</CSUpdateFileHeader></Profile> <?xml version="1.0" encoding="utf-16"?><Profile name="Typescript"><JsInsertSemicolon>True</JsInsertSemicolon><FormatAttributeQuoteDescriptor>True</FormatAttributeQuoteDescriptor><CorrectVariableKindsDescriptor>True</CorrectVariableKindsDescriptor><VariablesToInnerScopesDescriptor>True</VariablesToInnerScopesDescriptor><StringToTemplatesDescriptor>True</StringToTemplatesDescriptor><RemoveRedundantQualifiersTs>True</RemoveRedundantQualifiersTs><OptimizeImportsTs>True</OptimizeImportsTs></Profile> + False + True SingleQuoted ========================================================================== $FILENAME$ diff --git a/src/pinkparrot_infrastructure/PinkParrot.Infrastructure.Tests/PropertiesBagTests.cs b/src/pinkparrot_infrastructure/PinkParrot.Infrastructure.Tests/PropertiesBagTests.cs index 9c8b75362..3e437edab 100644 --- a/src/pinkparrot_infrastructure/PinkParrot.Infrastructure.Tests/PropertiesBagTests.cs +++ b/src/pinkparrot_infrastructure/PinkParrot.Infrastructure.Tests/PropertiesBagTests.cs @@ -21,6 +21,12 @@ namespace PinkParrot.Infrastructure { private readonly CultureInfo c = CultureInfo.InvariantCulture; private readonly PropertiesBag bag = new PropertiesBag(); + private readonly dynamic dynamicBag; + + public PropertiesBagTest() + { + dynamicBag = bag; + } [Fact] public void Should_return_false_when_renaming_unknown_property() @@ -63,8 +69,8 @@ namespace PinkParrot.Infrastructure bag.Set("Key2", 1); Assert.Equal(2, bag.Count); - Assert.Equal(new[] { "Key1", "Key2" }, bag.PropertyNames.ToArray()); - Assert.Equal(new[] { "Key1", "Key2" }, bag.Properties.Keys.ToArray()); + Assert.Equal(new[] {"Key1", "Key2"}, bag.PropertyNames.ToArray()); + Assert.Equal(new[] {"Key1", "Key2"}, bag.Properties.Keys.ToArray()); } [Fact] @@ -93,10 +99,24 @@ namespace PinkParrot.Infrastructure Assert.False(bag.Contains("KEY")); } + [Fact] + public void Should_set_value_as_dynamic() + { + dynamicBag.Key = 456; + + Assert.Equal(456, (int)dynamicBag.Key); + } + + [Fact] + public void Should_throw_when_setting_value_with_invalid_type_dynamically() + { + Assert.Throws(() => dynamicBag.Key = (byte)123); + } + [Fact] public void Should_throw_when_setting_value_with_invalid_type() { - Assert.Throws(() => bag.Set("Key", (byte)1)); + Assert.Throws(() => bag.Set("Key", (byte)1)); } [Fact] @@ -168,7 +188,7 @@ namespace PinkParrot.Infrastructure [Fact] public void Should_convert_from_instant_string() { - var time = SystemClock.Instance.GetCurrentInstant(); + var time = Instant.FromUnixTimeSeconds(SystemClock.Instance.GetCurrentInstant().ToUnixTimeSeconds()); bag.Set("Key", time.ToString()); @@ -200,7 +220,7 @@ namespace PinkParrot.Infrastructure { bag.Set("Key", true); - AssertBoolean(); + AssertBoolean(true); } [Fact] @@ -208,7 +228,7 @@ namespace PinkParrot.Infrastructure { bag.Set("Key", "true"); - AssertBoolean(); + AssertBoolean(true); } [Fact] @@ -216,66 +236,109 @@ namespace PinkParrot.Infrastructure { bag.Set("Key", 1); - AssertBoolean(); + AssertBoolean(true); } [Fact] - public void Should_throw_when_converting_instant_to_number() + public void Should_convert_boolean_to_truthy_number_string() { - bag.Set("Key", SystemClock.Instance.GetCurrentInstant()); + bag.Set("Key", "1"); - Assert.Throws(() => bag["Key"].ToGuid(CultureInfo.InvariantCulture)); + AssertBoolean(true); } [Fact] - public void Should_return_default_when_property_value_is_null() + public void Should_convert_boolean_to_falsy_number_string() { - bag.Set("Key", null); + bag.Set("Key", "0"); - Assert.Equal(null, bag["Key"].ToString()); + AssertBoolean(false); + } + + [Fact] + public void Should_throw_when_converting_instant_to_number() + { + bag.Set("Key", SystemClock.Instance.GetCurrentInstant()); - Assert.Equal(0f, bag["Key"].ToSingle(CultureInfo.CurrentCulture)); - Assert.Equal(0d, bag["Key"].ToDouble(CultureInfo.CurrentCulture)); - Assert.Equal(0L, bag["Key"].ToInt64(CultureInfo.CurrentCulture)); - Assert.Equal(0, bag["Key"].ToInt32(CultureInfo.CurrentCulture)); + Assert.Throws(() => bag["Key"].ToGuid(CultureInfo.InvariantCulture)); + } - Assert.Equal(false, bag["Key"].ToBoolean(CultureInfo.CurrentCulture)); + private void AssertNumber() + { + AssertInt32(123); + AssertInt64(123); + AssertSingle(123); + AssertDouble(123); + } - Assert.Equal(new Guid(), bag["Key"].ToGuid(CultureInfo.CurrentCulture)); + private void AssertString(string expected) + { + Assert.Equal(expected, bag["Key"].ToString()); - Assert.Equal(new Instant(), bag["Key"].ToInstant(CultureInfo.CurrentCulture)); + Assert.Equal(expected, (string)dynamicBag.Key); } - private void AssertBoolean() + private void AssertBoolean(bool expected) { - Assert.True(bag["Key"].ToBoolean(c)); - Assert.True(bag["Key"].ToNullableBoolean(c)); + Assert.Equal(expected, bag["Key"].ToBoolean(c)); + Assert.Equal(expected, bag["Key"].ToNullableBoolean(c)); + + Assert.Equal(expected, (bool)dynamicBag.Key); + Assert.Equal(expected, (bool?)dynamicBag.Key); } private void AssertInstant(Instant expected) { - Assert.Equal(expected.ToUnixTimeSeconds(), bag["Key"].ToInstant(c).ToUnixTimeSeconds()); - Assert.Equal(expected.ToUnixTimeSeconds(), bag["Key"].ToNullableInstant(c).Value.ToUnixTimeSeconds()); + Assert.Equal(expected, bag["Key"].ToInstant(c)); + Assert.Equal(expected, bag["Key"].ToNullableInstant(c).Value); + + Assert.Equal(expected, (Instant)dynamicBag.Key); + Assert.Equal(expected, (Instant?)dynamicBag.Key); } private void AssertGuid(Guid expected) { Assert.Equal(expected, bag["Key"].ToGuid(c)); Assert.Equal(expected, bag["Key"].ToNullableGuid(c)); + + Assert.Equal(expected, (Guid)dynamicBag.Key); + Assert.Equal(expected, (Guid?)dynamicBag.Key); } - private void AssertNumber() + private void AssertDouble(double expected) + { + Assert.Equal(expected, bag["Key"].ToDouble(c)); + Assert.Equal(expected, bag["Key"].ToNullableDouble(c)); + + Assert.Equal(expected, (double)dynamicBag.Key); + Assert.Equal(expected, (double?)dynamicBag.Key); + } + + private void AssertSingle(float expected) + { + Assert.Equal(expected, bag["Key"].ToSingle(c)); + Assert.Equal(expected, bag["Key"].ToNullableSingle(c)); + + Assert.Equal(expected, (float)dynamicBag.Key); + Assert.Equal(expected, (float?)dynamicBag.Key); + } + + private void AssertInt64(long expected) + { + Assert.Equal(expected, bag["Key"].ToInt64(c)); + Assert.Equal(expected, bag["Key"].ToNullableInt64(c)); + + Assert.Equal(expected, (long)dynamicBag.Key); + Assert.Equal(expected, (long?)dynamicBag.Key); + } + + private void AssertInt32(int expected) { - Assert.Equal(123, bag["Key"].ToInt32(c)); - Assert.Equal(123, bag["Key"].ToNullableInt32(c)); - Assert.Equal(123L, bag["Key"].ToInt64(c)); - Assert.Equal(123L, bag["Key"].ToNullableInt64(c)); - Assert.Equal(123f, bag["Key"].ToSingle(c)); - Assert.Equal(123f, bag["Key"].ToNullableSingle(c)); - Assert.Equal(123d, bag["Key"].ToDouble(c)); - Assert.Equal(123d, bag["Key"].ToNullableDouble(c)); + Assert.Equal(expected, bag["Key"].ToInt32(c)); + Assert.Equal(expected, bag["Key"].ToNullableInt32(c)); - Assert.True(bag["Key"].ToBoolean(c)); + Assert.Equal(expected, (int)dynamicBag.Key); + Assert.Equal(expected, (int?)dynamicBag.Key); } } -} +} \ No newline at end of file diff --git a/src/pinkparrot_infrastructure/PinkParrot.Infrastructure/CollectionExtensions.cs b/src/pinkparrot_infrastructure/PinkParrot.Infrastructure/CollectionExtensions.cs index dc47c0a3d..5c4e1bac4 100644 --- a/src/pinkparrot_infrastructure/PinkParrot.Infrastructure/CollectionExtensions.cs +++ b/src/pinkparrot_infrastructure/PinkParrot.Infrastructure/CollectionExtensions.cs @@ -106,5 +106,16 @@ namespace PinkParrot.Infrastructure return result; } + + public static bool TryGetValueAsObject(this IReadOnlyDictionary dictionary, TKey key, out object value) + { + TValue result; + + var isFound = dictionary.TryGetValue(key, out result); + + value = result; + + return isFound; + } } } \ No newline at end of file diff --git a/src/pinkparrot_infrastructure/PinkParrot.Infrastructure/PropertiesBag.cs b/src/pinkparrot_infrastructure/PinkParrot.Infrastructure/PropertiesBag.cs index 4d773e167..612803f97 100644 --- a/src/pinkparrot_infrastructure/PinkParrot.Infrastructure/PropertiesBag.cs +++ b/src/pinkparrot_infrastructure/PinkParrot.Infrastructure/PropertiesBag.cs @@ -8,10 +8,11 @@ using System; using System.Collections.Generic; +using System.Dynamic; namespace PinkParrot.Infrastructure { - public class PropertiesBag + public class PropertiesBag : DynamicObject { private readonly Dictionary internalDictionary = new Dictionary(StringComparer.OrdinalIgnoreCase); @@ -40,6 +41,23 @@ namespace PinkParrot.Infrastructure } } + public override IEnumerable GetDynamicMemberNames() + { + return internalDictionary.Keys; + } + + public override bool TryGetMember(GetMemberBinder binder, out object result) + { + return internalDictionary.TryGetValueAsObject(binder.Name, out result); + } + + public override bool TrySetMember(SetMemberBinder binder, object value) + { + internalDictionary[binder.Name] = new PropertyValue(value); + + return true; + } + public bool Contains(string propertyName) { Guard.NotNullOrEmpty(propertyName, nameof(propertyName)); @@ -47,20 +65,20 @@ namespace PinkParrot.Infrastructure return internalDictionary.ContainsKey(propertyName); } - public PropertiesBag Set(string propertyName, object value) + public bool Remove(string propertyName) { Guard.NotNullOrEmpty(propertyName, nameof(propertyName)); - internalDictionary[propertyName] = new PropertyValue(value); - - return this; + return internalDictionary.Remove(propertyName); } - public bool Remove(string propertyName) + public PropertiesBag Set(string propertyName, object value) { Guard.NotNullOrEmpty(propertyName, nameof(propertyName)); - return internalDictionary.Remove(propertyName); + internalDictionary[propertyName] = new PropertyValue(value); + + return this; } public bool Rename(string oldPropertyName, string newPropertyName) diff --git a/src/pinkparrot_infrastructure/PinkParrot.Infrastructure/PropertyValue.cs b/src/pinkparrot_infrastructure/PinkParrot.Infrastructure/PropertyValue.cs index 404ad969e..95c13f53b 100644 --- a/src/pinkparrot_infrastructure/PinkParrot.Infrastructure/PropertyValue.cs +++ b/src/pinkparrot_infrastructure/PinkParrot.Infrastructure/PropertyValue.cs @@ -8,35 +8,37 @@ using System; using System.Collections.Generic; +using System.Dynamic; using System.Globalization; using NodaTime; using NodaTime.Text; namespace PinkParrot.Infrastructure { - public sealed class PropertyValue + public sealed class PropertyValue : DynamicObject { private readonly object rawValue; - private static readonly HashSet AllowedTypes = new HashSet - { - typeof(string), - typeof(bool), - typeof(bool?), - typeof(float), - typeof(float?), - typeof(double), - typeof(double?), - typeof(int), - typeof(int?), - typeof(long), - typeof(long?), - typeof(Instant), - typeof(Instant?), - typeof(Guid), - typeof(Guid?) - }; - + private static readonly Dictionary> Parsers = + new Dictionary> + { + { typeof(string), (p, c) => p.ToString() }, + { typeof(bool), (p, c) => p.ToBoolean(c) }, + { typeof(bool?), (p, c) => p.ToNullableBoolean(c) }, + { typeof(float), (p, c) => p.ToSingle(c) }, + { typeof(float?), (p, c) => p.ToNullableSingle(c) }, + { typeof(double), (p, c) => p.ToDouble(c) }, + { typeof(double?), (p, c) => p.ToNullableDouble(c) }, + { typeof(int), (p, c) => p.ToInt32(c) }, + { typeof(int?), (p, c) => p.ToNullableInt32(c) }, + { typeof(long), (p, c) => p.ToInt64(c) }, + { typeof(long?), (p, c) => p.ToNullableInt64(c) }, + { typeof(Instant), (p, c) => p.ToInstant(c) }, + { typeof(Instant?), (p, c) => p.ToNullableInstant(c) }, + { typeof(Guid), (p, c) => p.ToGuid(c) }, + { typeof(Guid?), (p, c) => p.ToNullableGuid(c) }, + }; + public object RawValue { get { return rawValue; } @@ -44,14 +46,35 @@ namespace PinkParrot.Infrastructure internal PropertyValue(object rawValue) { - if (rawValue != null && !AllowedTypes.Contains(rawValue.GetType())) + if (rawValue != null && !Parsers.ContainsKey(rawValue.GetType())) { - throw new ArgumentException("The type is not supported.", nameof(rawValue)); + throw new InvalidOperationException($"The type '{rawValue.GetType()}' is not supported."); } this.rawValue = rawValue; } - + + public override bool TryConvert(ConvertBinder binder, out object result) + { + result = null; + + if (binder.Type == typeof(object)) + { + result = rawValue; + } + + Func parser; + + if (!Parsers.TryGetValue(binder.Type, out parser)) + { + return false; + } + + result = parser(this, CultureInfo.InvariantCulture); + + return true; + } + public override string ToString() { return rawValue?.ToString(); @@ -59,12 +82,12 @@ namespace PinkParrot.Infrastructure public bool ToBoolean(CultureInfo culture) { - return ToOrParseValue(culture, bool.Parse); + return ToOrParseValue(culture, ParseBoolean); } public bool? ToNullableBoolean(CultureInfo culture) { - return ToNullableOrParseValue(culture, bool.Parse); + return ToNullableOrParseValue(culture, ParseBoolean); } public float ToSingle(CultureInfo culture) @@ -207,5 +230,18 @@ namespace PinkParrot.Infrastructure throw new InvalidCastException(message, e); } } + + private static bool ParseBoolean(string value) + { + switch (value) + { + case "1": + return true; + case "0": + return false; + default: + return bool.Parse(value); + } + } } }