From 03d4cb7b5fc10e06be7c9d4f73d8a3a4e7457772 Mon Sep 17 00:00:00 2001 From: David Arno Date: Tue, 5 Dec 2017 13:45:19 +0000 Subject: [PATCH] #44 Various updates to the Copy and With methods: Renamed to TryWith and TryCopy With (version that throws exceptions rather than using an Option type) added Various code changes to support edge-case badly behaved types --- .../Functional/CachedConstructorInfo.cs | 18 ++ src/SuccincT/Functional/CachedTypeInfo.cs | 30 ++ src/SuccincT/Functional/CopyException.cs | 11 + src/SuccincT/Functional/WithExtensions.cs | 299 +++++++++++------- src/SuccincT/SuccincT.csproj | 1 + .../WithExtensionsAdverseConditionsTests.cs | 206 ++++++++++++ .../Functional/WithExtensionsTests.cs | 166 ++++++++-- .../PatternMatchers/TypeMatcherTests.cs | 3 + 8 files changed, 604 insertions(+), 130 deletions(-) create mode 100644 src/SuccincT/Functional/CachedConstructorInfo.cs create mode 100644 src/SuccincT/Functional/CachedTypeInfo.cs create mode 100644 src/SuccincT/Functional/CopyException.cs create mode 100644 tests/SuccincT.Tests/SuccincT/Functional/WithExtensionsAdverseConditionsTests.cs diff --git a/src/SuccincT/Functional/CachedConstructorInfo.cs b/src/SuccincT/Functional/CachedConstructorInfo.cs new file mode 100644 index 0000000..b530f51 --- /dev/null +++ b/src/SuccincT/Functional/CachedConstructorInfo.cs @@ -0,0 +1,18 @@ +using System.Collections.Generic; +using System.Linq; +using System.Reflection; + +namespace SuccincT.Functional +{ + internal class CachedConstructorInfo + { + public ConstructorInfo Constructor { get; } + public List Parameters { get; } + + public CachedConstructorInfo(ConstructorInfo constructorInfo) + { + Constructor = constructorInfo; + Parameters = constructorInfo.GetParameters().ToList(); + } + } +} \ No newline at end of file diff --git a/src/SuccincT/Functional/CachedTypeInfo.cs b/src/SuccincT/Functional/CachedTypeInfo.cs new file mode 100644 index 0000000..d43158a --- /dev/null +++ b/src/SuccincT/Functional/CachedTypeInfo.cs @@ -0,0 +1,30 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Reflection; + +namespace SuccincT.Functional +{ + internal class CachedTypeInfo + { + public List CachedPublicConstructors { get; } + public List Properties { get; } + public List ReadOnlyProperties { get; } + public List WriteOnlyProperties { get; } + + public CachedTypeInfo(Type type) + { + var typeInfo = type.GetTypeInfo(); + + var cachedConstructors = + typeInfo.DeclaredConstructors.Select(c => new CachedConstructorInfo(c)).ToList(); + + CachedPublicConstructors = cachedConstructors + .Where(cc => cc.Constructor.IsPublic && !cc.Constructor.IsStatic).ToList(); + + Properties = type.GetRuntimeProperties().ToList(); + ReadOnlyProperties = Properties.Where(p => p.CanRead && !p.CanWrite).ToList(); + WriteOnlyProperties = Properties.Where(p => !p.CanRead && p.CanWrite).ToList(); + } + } +} \ No newline at end of file diff --git a/src/SuccincT/Functional/CopyException.cs b/src/SuccincT/Functional/CopyException.cs new file mode 100644 index 0000000..11125cc --- /dev/null +++ b/src/SuccincT/Functional/CopyException.cs @@ -0,0 +1,11 @@ +using System; + +namespace SuccincT.Functional +{ + public class CopyException : Exception + { + public CopyException(string message) : base(message) {} + + public CopyException(string message, Exception innerException) : base(message, innerException) {} + } +} \ No newline at end of file diff --git a/src/SuccincT/Functional/WithExtensions.cs b/src/SuccincT/Functional/WithExtensions.cs index c123f36..acc52c5 100644 --- a/src/SuccincT/Functional/WithExtensions.cs +++ b/src/SuccincT/Functional/WithExtensions.cs @@ -22,22 +22,15 @@ public static Option Copy(this T @object) where T : class if (!constructorToUse.HasValue) return Option.None(); - var sourceReadProperties = cachedTypeInfo.Properties.Except(cachedTypeInfo.WriteOnlyProperties); - + var sourceReadProperties = cachedTypeInfo.Properties.Except(cachedTypeInfo.WriteOnlyProperties).ToList(); var constructorParameters = constructorToUse.Value.Parameters; - var @params = constructorParameters - .Select(p => sourceReadProperties.TryFirst(x => AreLinked(x, p))) - .Where(x => x.HasValue) - .Select(x => x.Value) - .Select(sourceReadProperty => sourceReadProperty.GetValue(@object, null)) - .ToArray(); - - if (@params.Length != constructorParameters.Count) return Option.None(); + var constructorParameterValues = + GetConstructorParameterValuesForCopy(@object, sourceReadProperties, constructorParameters); - var newObject = Activator.CreateInstance(typeof(T), @params) as T; + if (constructorParameterValues.Length != constructorParameters.Count) return Option.None(); - // Overwrite properties on the new created object + var newObject = Activator.CreateInstance(typeof(T), constructorParameterValues) as T; var destWriteProperties = cachedTypeInfo.Properties.Except(cachedTypeInfo.ReadOnlyProperties); var propertiesToOverwrite = sourceReadProperties @@ -53,87 +46,206 @@ public static Option Copy(this T @object) where T : class return Option.Some(newObject); } - public static Option With(this T @object, TProps propertiesToUpdate) + private static object[] GetConstructorParameterValuesForCopy(T @object, + IEnumerable sourceReadProperties, + List constructorParameters) + { + return constructorParameters + .Select(p => sourceReadProperties.TryFirst(x => AreLinked(x, p))) + .Where(x => x.HasValue) + .Select(x => x.Value) + .Select(sourceReadProperty => sourceReadProperty.GetValue(@object, null)) + .ToArray(); + } + + public static Option TryWith(this T itemToCopy, TProps propertiesToUpdate) where T : class where TProps : class { - // Do nothing if `propertiesToUpdate` is null if (propertiesToUpdate == null) return Option.None(); var cachedTypeInfo = GetCachedTypeInfo(typeof(T)); - - // Create the new object using the most specialized constructor based on props to update var sourceReadProperties = cachedTypeInfo.Properties.Except(cachedTypeInfo.WriteOnlyProperties).ToList(); var updateProperties = typeof(TProps).GetRuntimeProperties().Where(x => x.CanRead).ToList(); - - var constructorToUse = cachedTypeInfo.CachedPublicConstructors - .OrderByDescending( - cc => cc.Parameters.Count( - p => updateProperties.Any(ptu => AreLinked(ptu, p)))) - .TryFirst(); + var constructorToUse = ConstructorToUseForWith(cachedTypeInfo, updateProperties, sourceReadProperties); if (!constructorToUse.HasValue) return Option.None(); var constructorParameters = constructorToUse.Value.Parameters; + var constructorParameterValues = MapUpdateValuesToConstructorParameters(itemToCopy, + propertiesToUpdate, + constructorParameters, + updateProperties, + sourceReadProperties); - var @params = constructorParameters - .Select(p => - { - return updateProperties - .TryFirst(ptu => AreLinked(ptu, p)) - .Match>() - .Some().Do(ptu => ptu.GetValue(propertiesToUpdate, null).ToOption()) - .None().Do(() => - { - return sourceReadProperties - .TryFirst(sp => AreLinked(sp, p)) - .Match>() - .Some().Do(sp => sp.GetValue(@object, null).ToOption()) - .None().Do(Option.None) - .Result(); - }) - .Result(); - }) - .Where(x => x.HasValue) - .Select(x => x.Value) - .ToArray(); - - if (@params.Length != constructorParameters.Count) return Option.None(); - - var newObject = Activator.CreateInstance(typeof(T), @params) as T; - - // Overwrite properties from the previous/source object var destWriteProperties = cachedTypeInfo.Properties.Except(cachedTypeInfo.ReadOnlyProperties); + var propsToSetFromUpdateData = GetPropertiesToSetFromUpdateData(updateProperties, + constructorParameters, + sourceReadProperties); - var propertiesToOverwrite = sourceReadProperties - .Select(sourceProperty => - destWriteProperties.TryFirst( - destProperty => sourceProperty.Name == destProperty.Name)) - .Where(x => x.HasValue) - .Select(x => x.Value); + var propsToSetFromSourceObject = GetPropertiesToSetFromSourceObject(sourceReadProperties, + constructorParameters, + propsToSetFromUpdateData, + destWriteProperties); - foreach (var propertyToOverwrite in propertiesToOverwrite) + try { - CopyPropertyValue(@object, propertyToOverwrite, newObject); + return Option.Some(CreateNewObjectApplyingUpdates(itemToCopy, + propertiesToUpdate, + constructorParameterValues, + propsToSetFromSourceObject, + propsToSetFromUpdateData)); + } + catch (Exception) + { + return Option.None(); + } + } + + public static T With(this T itemToCopy, TProps propertiesToUpdate) + where T : class where TProps : class + { + if (propertiesToUpdate == null) throw new ArgumentNullException(nameof(propertiesToUpdate)); + + var cachedTypeInfo = GetCachedTypeInfo(typeof(T)); + var sourceReadProperties = cachedTypeInfo.Properties.Except(cachedTypeInfo.WriteOnlyProperties).ToList(); + var updateProperties = typeof(TProps).GetRuntimeProperties().Where(x => x.CanRead).ToList(); + var constructorToUse = ConstructorToUseForWith(cachedTypeInfo, updateProperties, sourceReadProperties); + + if (!constructorToUse.HasValue) throw new CopyException( + $"Type {typeof(T).Name} does not supply a suitable constructor for use with With, which allows all " + + "non-writable properties to be set via that constructor."); + + var constructorParameters = constructorToUse.Value.Parameters; + var constructorParameterValues = MapUpdateValuesToConstructorParameters(itemToCopy, + propertiesToUpdate, + constructorParameters, + updateProperties, + sourceReadProperties); + + var destWriteProperties = cachedTypeInfo.Properties.Except(cachedTypeInfo.ReadOnlyProperties); + var propsToSetFromUpdateData = GetPropertiesToSetFromUpdateData(updateProperties, + constructorParameters, + sourceReadProperties); + + var propsToSetFromSourceObject = GetPropertiesToSetFromSourceObject(sourceReadProperties, + constructorParameters, + propsToSetFromUpdateData, + destWriteProperties); + + try + { + return CreateNewObjectApplyingUpdates(itemToCopy, + propertiesToUpdate, + constructorParameterValues, + propsToSetFromSourceObject, + propsToSetFromUpdateData); + } + catch (Exception ex) + { + throw new CopyException($"A problem occurred creating a new instance of {typeof(T).Name} using With." + + "See the inner exception for details of the problem.", + ex); + } + } + + private static T CreateNewObjectApplyingUpdates(T itemToCopy, + TProps propertiesToUpdate, + object[] constructorParameterValues, + IEnumerable propsToSetFromSourceObject, + IEnumerable<(PropertyInfo Value, PropertyInfo PropToUpdate)> propsToSetFromUpdateData) + where T : class where TProps : class + { + var newObject = Activator.CreateInstance(typeof(T), constructorParameterValues) as T; + + foreach (var propertyToOverwrite in propsToSetFromSourceObject) + { + CopyPropertyValue(itemToCopy, propertyToOverwrite, newObject); } - // Overwrite properties from the `propertiesToUpdate` - var tuplePropertiesToUpdate = updateProperties - .Select(propToUpdate => - ( - SourceProp: sourceReadProperties.TryFirst( - sp => sp.Name == propToUpdate.Name), - PropToUpdate: propToUpdate - ) - ) - .Where(x => x.SourceProp.HasValue && x.SourceProp.Value.CanWrite) - .Select(x => (x.SourceProp.Value, x.PropToUpdate)); - - foreach (var (sourceProp, propToUpdate) in tuplePropertiesToUpdate) + foreach (var (sourceProp, propToUpdate) in propsToSetFromUpdateData) { CopyPropertyValue(propertiesToUpdate, propToUpdate, newObject, sourceProp); } - return Option.Some(newObject); + return newObject; + } + + private static List GetPropertiesToSetFromSourceObject( + IEnumerable sourceReadProperties, + List constructorParameters, + List<(PropertyInfo Value, PropertyInfo PropToUpdate)> propsToSetFromUpdateData, + IEnumerable destWriteProperties) + { + return sourceReadProperties + .Where(p => !constructorParameters.Any(cp => AreLinked(cp, p))) + .Where(p => !propsToSetFromUpdateData.Any(tp => AreLinked(p, tp.PropToUpdate))) + .Select(sourceProperty => + destWriteProperties.TryFirst( + destProperty => AreLinked(sourceProperty, destProperty))) + .Where(x => x.HasValue) + .Select(x => x.Value).ToList(); + } + + private static List<(PropertyInfo Value, PropertyInfo PropToUpdate)> GetPropertiesToSetFromUpdateData( + IEnumerable updateProperties, + List constructorParameters, + List sourceReadProperties) + { + return updateProperties + .Where(p => !constructorParameters.Any(cp => AreLinked(cp, p))) + .Select(propToUpdate => + ( + SourceProp: sourceReadProperties.TryFirst( + sp => AreLinked(sp, propToUpdate)), + PropToUpdate: propToUpdate + ) + ) + .Where(x => x.SourceProp.HasValue && x.SourceProp.Value.CanWrite) + .Select(x => (x.SourceProp.Value, x.PropToUpdate)).ToList(); + } + + private static object[] MapUpdateValuesToConstructorParameters( + T @object, + TProps propertiesToUpdate, + List constructorParameters, + List updateProperties, + List sourceReadProperties) where T : class where TProps : class + { + return constructorParameters + .Select(p => + { + return updateProperties + .TryFirst(ptu => AreLinked(ptu, p)) + .Match>() + .Some().Do(ptu => Option.Some(ptu.GetValue(propertiesToUpdate, null))) + .None().Do(() => + { + return sourceReadProperties + .TryFirst(sp => AreLinked(sp, p)) + .Match>() + .Some().Do(sp => Option.Some(sp.GetValue(@object, null))) + .None().Do(Option.None) + .Result(); + }) + .Result(); + }) + .Where(x => x.HasValue) + .Select(x => x.Value) + .ToArray(); + } + + private static Option ConstructorToUseForWith(CachedTypeInfo cachedTypeInfo, + IEnumerable updateProperties, + IEnumerable readProperties) + { + return (from constructor in cachedTypeInfo.CachedPublicConstructors + let paramsNotCoveredByUpdates = + constructor.Parameters.Where(p => !updateProperties.Any(ptu => AreLinked(ptu, p))) + let remainingParamsNotCoveredByProperties = + paramsNotCoveredByUpdates.Where(p => !readProperties.Any(rp => AreLinked(rp, p))).ToList() + where !remainingParamsNotCoveredByProperties.Any() + orderby constructor.Parameters.Count descending + select constructor).TryFirst(); } private static CachedTypeInfo GetCachedTypeInfo(Type type) @@ -155,10 +267,14 @@ private static T GetOrAddValue(this Dictionary dictionary, string .Result(); } - private static bool AreLinked(MemberInfo memberInfo, ParameterInfo parameterInfo) - { - return string.Equals(memberInfo.Name, parameterInfo.Name, StringComparison.CurrentCultureIgnoreCase); - } + private static bool AreLinked(MemberInfo memberInfo, ParameterInfo parameterInfo) => + string.Equals(memberInfo.Name, parameterInfo.Name, StringComparison.CurrentCultureIgnoreCase); + + private static bool AreLinked(MemberInfo memberInfo, PropertyInfo propertyInfo) => + string.Equals(memberInfo.Name, propertyInfo.Name, StringComparison.CurrentCultureIgnoreCase); + + private static bool AreLinked(ParameterInfo parameterInfo, PropertyInfo propertyInfo) => + string.Equals(parameterInfo.Name, propertyInfo.Name, StringComparison.CurrentCultureIgnoreCase); private static void CopyPropertyValue(T from, PropertyInfo property, T to) where T : class { @@ -172,38 +288,5 @@ private static void CopyPropertyValue(T1 from, { toProperty.SetValue(to, fromProperty.GetValue(from, null)); } - - private class CachedTypeInfo - { - public List CachedPublicConstructors { get; } - public List Properties { get; } - public List ReadOnlyProperties { get; } - public List WriteOnlyProperties { get; } - - public CachedTypeInfo(Type type) - { - var typeInfo = type.GetTypeInfo(); - - var cachedConstructors = typeInfo.DeclaredConstructors.Select(c => new CachedConstructorInfo(c)).ToList(); - CachedPublicConstructors = cachedConstructors - .Where(cc => cc.Constructor.IsPublic && !cc.Constructor.IsStatic).ToList(); - - Properties = type.GetRuntimeProperties().ToList(); - ReadOnlyProperties = Properties.Where(p => p.CanRead && !p.CanWrite).ToList(); - WriteOnlyProperties = Properties.Where(p => !p.CanRead && p.CanWrite).ToList(); - } - } - - private class CachedConstructorInfo - { - public ConstructorInfo Constructor { get; } - public List Parameters { get; } - - public CachedConstructorInfo(ConstructorInfo constructorInfo) - { - Constructor = constructorInfo; - Parameters = constructorInfo.GetParameters().ToList(); - } - } } } diff --git a/src/SuccincT/SuccincT.csproj b/src/SuccincT/SuccincT.csproj index b73b744..d8c0941 100644 --- a/src/SuccincT/SuccincT.csproj +++ b/src/SuccincT/SuccincT.csproj @@ -30,6 +30,7 @@ + \ No newline at end of file diff --git a/tests/SuccincT.Tests/SuccincT/Functional/WithExtensionsAdverseConditionsTests.cs b/tests/SuccincT.Tests/SuccincT/Functional/WithExtensionsAdverseConditionsTests.cs new file mode 100644 index 0000000..e4efaad --- /dev/null +++ b/tests/SuccincT.Tests/SuccincT/Functional/WithExtensionsAdverseConditionsTests.cs @@ -0,0 +1,206 @@ +using NUnit.Framework; +using SuccincT.Functional; +using static NUnit.Framework.Assert; + +namespace SuccincTTests.SuccincT.Functional +{ + [TestFixture] + public class WithExtensionsAdverseConditionsTests + { + [Test] + public void TryWithOfTypeWithConstructorAndNoGetters_FailsCleanly() + { + var x = new TypeWithConstructorAndNoGetters(1); + var y = x.TryWith(new { }); + + IsFalse(y.HasValue); + } + + [Test] + public void WithOfTypeWithConstructorAndNoGetters_ThrowsCopyException() + { + var x = new TypeWithConstructorAndNoGetters(1); + Throws(() => x.With(new { })); + } + + [Test] + public void TryWithOfTypeWithConstructorAndNoGettersSuppliedWithWrongParam_FailsCleanly() + { + var x = new TypeWithConstructorAndNoGetters(1); + var y = x.TryWith(new { x = "2" }); + + IsFalse(y.HasValue); + } + + [Test] + public void WithOfTypeWithConstructorAndNoGettersSuppliedWithWrongParam_ThrowsCopyException() + { + var x = new TypeWithConstructorAndNoGetters(1); + Throws(() => x.With(new { x = "2" })); + } + + [Test] + public void TryWithOfTypeWithConstructorAndNoGettersSuppliedWithWrongParamName_FailsCleanly() + { + var x = new TypeWithConstructorAndNoGetters(1); + var y = x.TryWith(new { y = 1 }); + + IsFalse(y.HasValue); + } + + [Test] + public void WithOfTypeWithConstructorAndNoGettersSuppliedWithWrongParamName_ThrowsCopyException() + { + var x = new TypeWithConstructorAndNoGetters(1); + Throws(() => x.With(new { y = 1 })); + } + + [Test] + public void TryWithOfTypeWithConstructorAndNoMatchingGetter_CanHavePropSetViaConstructor() + { + var x = new TypeWithConstructorAndNoMatchingGetter(1); + var y = x.TryWith(new { a = 2 }); + + AreEqual(2, y.Value.B); + } + + [Test] + public void WithOfTypeWithConstructorAndNoMatchingGetter_CanHavePropSetViaConstructor() + { + var x = new TypeWithConstructorAndNoMatchingGetter(1); + var y = x.With(new { a = 2 }); + + AreEqual(2, y.B); + } + + [Test] + public void TryWithOfTypeWithConstructorAndNoMatchingGetter_FailsCleanlyWhenPropertyNameRatherThanParamNameIsSupplied() + { + var x = new TypeWithConstructorAndNoMatchingGetter(1); + var y = x.TryWith(new { b = 2 }); + + IsFalse(y.HasValue); + } + + [Test] + public void WithOfTypeWithConstructorAndNoMatchingGetter_ThrowsCopyExceptionWhenPropertyNameRatherThanParamNameIsSupplied() + { + var x = new TypeWithConstructorAndNoMatchingGetter(1); + Throws(() => x.With(new { b = 2 })); + } + + [Test] + public void TryWithOfTypeWithMultipleConstructors_ChoosesCorrectConstructor() + { + var x = new TypeWithMultipleConstructors(1); + + var y = x.TryWith(new { B = 3 }); + + AreEqual(1, y.Value.A); + AreEqual(3, y.Value.B); + } + + [Test] + public void WithOfTypeWithMultipleConstructors_ChoosesCorrectConstructor() + { + var x = new TypeWithMultipleConstructors(1); + + var y = x.With(new { B = 3 }); + + AreEqual(1, y.A); + AreEqual(3, y.B); + } + + [Test] + public void TryWithOfTypeToTestPropertiesSetViaConstructorArentSetAfterConstruction_OnlySetsPropertyViaConstructor() + { + var x = new TypeToTestPropertiesSetViaConstructorArentSetAfterConstruction(2); + + var y = x.TryWith(new { a = 3 }); + + AreEqual(3, y.Value.A); + } + + [Test] + public void WithOfTypeToTestPropertiesSetViaConstructorArentSetAfterConstruction_OnlySetsPropertyViaConstructor() + { + var x = new TypeToTestPropertiesSetViaConstructorArentSetAfterConstruction(2); + + var y = x.With(new { a = 3 }); + + AreEqual(3, y.A); + } + + [Test] + public void TryWithOfTypeToTestPropertiesAreOnlySetOnceWhenNoConstructor_OnlySetsPropertyOnce() + { + var x = new TypeToTestPropertiesAreOnlySetOnceWhenNoConstructor { A = 2 }; + + var y = x.TryWith(new { a = 3 }); + + AreEqual(3, y.Value.A); + } + + [Test] + public void WithOfTypeToTestPropertiesAreOnlySetOnceWhenNoConstructor_OnlySetsPropertyOnce() + { + var x = new TypeToTestPropertiesAreOnlySetOnceWhenNoConstructor { A = 2 }; + + var y = x.With(new { a = 3 }); + + AreEqual(3, y.A); + } + + private class TypeWithConstructorAndNoGetters + { + // ReSharper disable once UnusedParameter.Local + public TypeWithConstructorAndNoGetters(int x) { } + } + + private class TypeWithConstructorAndNoMatchingGetter + { + // ReSharper disable once UnusedMember.Local + // ReSharper disable UnusedParameter.Local + public TypeWithConstructorAndNoMatchingGetter(int a, int c) { } + public TypeWithConstructorAndNoMatchingGetter(int a) => B = a; + public int B { get; } + } + + private class TypeWithMultipleConstructors + { + public TypeWithMultipleConstructors(int a) => (A, B) = (a, 1); + // ReSharper disable once UnusedMember.Local + public TypeWithMultipleConstructors(int a, int b) => (A, B) = (a, b); + // ReSharper disable once UnusedMember.Local + // ReSharper disable once UnusedParameter.Local + public TypeWithMultipleConstructors(int a, int c, int b) => (A, B) = (a, b); + public int A { get; } + public int B { get; } + } + + private class TypeToTestPropertiesSetViaConstructorArentSetAfterConstruction + { + private int _a; + + public TypeToTestPropertiesSetViaConstructorArentSetAfterConstruction(int a) => A = a; + + public int A + { + get => _a; + // ReSharper disable once MemberCanBePrivate.Local + set => _a = _a + value; + } + } + + private class TypeToTestPropertiesAreOnlySetOnceWhenNoConstructor + { + private int _a; + + public int A + { + get => _a; + set => _a = _a + value; + } + } + } +} diff --git a/tests/SuccincT.Tests/SuccincT/Functional/WithExtensionsTests.cs b/tests/SuccincT.Tests/SuccincT/Functional/WithExtensionsTests.cs index 98f9ace..3bdafdb 100644 --- a/tests/SuccincT.Tests/SuccincT/Functional/WithExtensionsTests.cs +++ b/tests/SuccincT.Tests/SuccincT/Functional/WithExtensionsTests.cs @@ -9,20 +9,27 @@ namespace SuccincTTests.SuccincT.Functional public class WithExtensionsTests { [Test] - public void UpdatingWithNull_ShouldReturnSameObject() + public void TryWithWithNullUpdateParameters_ShouldFail() { var car = new Car(); - var optionResult = car.With(null); + var optionResult = car.TryWith(null); IsFalse(optionResult.HasValue); } [Test] - public void UpdatingWithoutProperty_ShouldReturnImmutableObject() + public void WithWithNullUpdateParameters_ShouldFail() { var car = new Car(); + Throws(() => car.With(null)); + } - var newCar = car.With(new { }).Value; + [Test] + public void TryWithWithoutProperties_ShouldReturnCopyOfOriginalObject() + { + var car = new Car(); + + var newCar = car.TryWith(new { }).Value; AreNotSame(car, newCar); AreEqual(car.Constructor, newCar.Constructor); @@ -31,10 +38,23 @@ public void UpdatingWithoutProperty_ShouldReturnImmutableObject() } [Test] - public void UpdatingWithOneProperty_ShouldReturnImmutableObjectWithUpdatedProperties() + public void WithWithoutProperties_ShouldReturnCopyOfOriginalObject() { var car = new Car(); - var newCar = car.With(new { Color = "Red" }).Value; + + var newCar = car.With(new { }); + + AreNotSame(car, newCar); + AreEqual(car.Constructor, newCar.Constructor); + AreEqual(car.Color, newCar.Color); + AreEqual(car.CreationDate, newCar.CreationDate); + } + + [Test] + public void TryWithWithOneProperty_ShouldCopyOtherPropertiesFromOriginalObject() + { + var car = new Car(); + var newCar = car.TryWith(new { Color = "Red" }).Value; AreNotSame(car, newCar); AreEqual(car.Constructor, newCar.Constructor); @@ -43,13 +63,25 @@ public void UpdatingWithOneProperty_ShouldReturnImmutableObjectWithUpdatedProper } [Test] - public void UpdatingWithOnePropertyMultipleTimes_ShouldReturnImmutableObjectWithLastUpdatedProperties() + public void WithWithOneProperty_ShouldCopyOtherPropertiesFromOriginalObject() { var car = new Car(); + var newCar = car.With(new { Color = "Red" }); + + AreNotSame(car, newCar); + AreEqual(car.Constructor, newCar.Constructor); + AreEqual("Red", newCar.Color); + AreEqual(car.CreationDate, newCar.CreationDate); + } - var newCar1 = car.With(new { Color = "Red" }).Value; - var newCar2 = newCar1.With(new { Color = "Blue" }).Value; - var newCar3 = newCar2.With(new { Color = "Green" }).Value; + [Test] + public void TryWithUsedMultipleTimes_ShouldUseLastUpdatedPropertyForFinalObject() + { + var car = new Car(); + + var newCar1 = car.TryWith(new { Color = "Red" }).Value; + var newCar2 = newCar1.TryWith(new { Color = "Blue" }).Value; + var newCar3 = newCar2.TryWith(new { Color = "Green" }).Value; AreNotSame(car, newCar3); AreEqual(car.Constructor, newCar3.Constructor); @@ -58,11 +90,26 @@ public void UpdatingWithOnePropertyMultipleTimes_ShouldReturnImmutableObjectWith } [Test] - public void UpdatingWithAllProperties_ShouldReturnImmutableObjectWithUpdatedProperties() + public void WithUsedMultipleTimes_ShouldUseLastUpdatedPropertyForFinalObject() { - var car = new Car {Constructor = "Ford", Color = "Black", CreationDate = new DateTime(1908, 10, 1)}; + var car = new Car(); - var newCar = car.With(new + var newCar1 = car.With(new { Color = "Red" }); + var newCar2 = newCar1.With(new { Color = "Blue" }); + var newCar3 = newCar2.With(new { Color = "Green" }); + + AreNotSame(car, newCar3); + AreEqual(car.Constructor, newCar3.Constructor); + AreEqual("Green", newCar3.Color); + AreEqual(car.CreationDate, newCar3.CreationDate); + } + + [Test] + public void TryWithWithAllProperties_ShouldReturnObjectWithAllPropertiesUpdated() + { + var car = new Car { Constructor = "Ford", Color = "Black", CreationDate = new DateTime(1908, 10, 1) }; + + var newCar = car.TryWith(new { Constructor = "BMW", Color = "Red", @@ -76,10 +123,28 @@ public void UpdatingWithAllProperties_ShouldReturnImmutableObjectWithUpdatedProp } [Test] - public void UpdatingWithNoMatchingProperty_ShouldReturnImmutableObject() + public void WithWithAllProperties_ShouldReturnObjectWithAllPropertiesUpdated() + { + var car = new Car { Constructor = "Ford", Color = "Black", CreationDate = new DateTime(1908, 10, 1) }; + + var newCar = car.With(new + { + Constructor = "BMW", + Color = "Red", + CreationDate = new DateTime(2017, 01, 01) + }); + + AreNotSame(car, newCar); + AreEqual("BMW", newCar.Constructor); + AreEqual("Red", newCar.Color); + AreEqual(new DateTime(2017, 01, 01), newCar.CreationDate); + } + + [Test] + public void TryWithWithNoMatchingProperty_ShouldReturnCopyOfOriginalObject() { var car = new Car { Color = "Blue" }; - var newCar = car.With(new { Cost = "1,000$" }).Value; + var newCar = car.TryWith(new { Cost = "1,000$" }).Value; AreNotSame(car, newCar); AreEqual(car.Constructor, newCar.Constructor); @@ -88,10 +153,34 @@ public void UpdatingWithNoMatchingProperty_ShouldReturnImmutableObject() } [Test] - public void UpdatingNotConstructorParameter_ShouldReturnImmutableObjectWithUpdatedProperties() + public void WithWithNoMatchingProperty_ShouldReturnCopyOfOriginalObject() + { + var car = new Car { Color = "Blue" }; + var newCar = car.With(new { Cost = "1,000$" }); + + AreNotSame(car, newCar); + AreEqual(car.Constructor, newCar.Constructor); + AreEqual("Blue", newCar.Color); + AreEqual(car.CreationDate, newCar.CreationDate); + } + + [Test] + public void TryWithUpdatingPropertyThatsNotHandledByConstructor_ShouldReturnNewObjectWithUpdatedProperty() + { + var book = new Book("Lewis Caroll", "Alice's Adventures In Wonderland"); + var publishedBook = book.TryWith(new { PublishDate = new DateTime(1865, 11, 26) }).Value; + + AreNotSame(book, publishedBook); + AreEqual(book.Author, publishedBook.Author); + AreEqual(book.Name, publishedBook.Name); + AreEqual(new DateTime(1865, 11, 26), publishedBook.PublishDate); + } + + [Test] + public void WithUpdatingPropertyThatsNotHandledByConstructor_ShouldReturnNewObjectWithUpdatedProperty() { var book = new Book("Lewis Caroll", "Alice's Adventures In Wonderland"); - var publishedBook = book.With(new { PublishDate = new DateTime(1865, 11, 26) }).Value; + var publishedBook = book.With(new { PublishDate = new DateTime(1865, 11, 26) }); AreNotSame(book, publishedBook); AreEqual(book.Author, publishedBook.Author); @@ -100,10 +189,10 @@ public void UpdatingNotConstructorParameter_ShouldReturnImmutableObjectWithUpdat } [Test] - public void UpdatingConstructorParameter_ShouldReturnImmutableObjectWithUpdatedProperties() + public void TryWithUpdatingPropertyViaConstructorParameter_ShouldReturnNewObjectWithUpdatedProperty() { var book = new Book("J.R.R. Tolkien", "The Lord Of The Rings"); - var newBook = book.With(new { Name = "The Hobbit" }).Value; + var newBook = book.TryWith(new { Name = "The Hobbit" }).Value; AreNotSame(book, newBook); AreEqual(book.Author, newBook.Author); @@ -112,7 +201,19 @@ public void UpdatingConstructorParameter_ShouldReturnImmutableObjectWithUpdatedP } [Test] - public void ResetValues_ShouldReturnImmutableObjectWithResetValues() + public void WithUpdatingPropertyViaConstructorParameter_ShouldReturnNewObjectWithUpdatedProperty() + { + var book = new Book("J.R.R. Tolkien", "The Lord Of The Rings"); + var newBook = book.With(new { Name = "The Hobbit" }); + + AreNotSame(book, newBook); + AreEqual(book.Author, newBook.Author); + AreEqual("The Hobbit", newBook.Name); + AreEqual(book.PublishDate, newBook.PublishDate); + } + + [Test] + public void TryWithThatResetsAllValues_ShouldReturnNewObjectWithResetValues() { var book = new Book("Lewis Caroll", "Alice's Adventures In Wonderland") { @@ -120,7 +221,7 @@ public void ResetValues_ShouldReturnImmutableObjectWithResetValues() }; var emptyBook = book - .With(new {Author = string.Empty, Name = string.Empty, PublishDate = default(DateTime?)}) + .TryWith(new { Author = string.Empty, Name = string.Empty, PublishDate = default(DateTime?) }) .Value; AreNotSame(book, emptyBook); @@ -129,6 +230,27 @@ public void ResetValues_ShouldReturnImmutableObjectWithResetValues() IsNull(emptyBook.PublishDate); } + [Test] + public void WithThatResetsAllValues_ShouldReturnNewObjectWithResetValues() + { + var book = new Book("Lewis Caroll", "Alice's Adventures In Wonderland") + { + PublishDate = new DateTime(1865, 11, 26) + }; + + var emptyBook = book.With(new + { + Author = string.Empty, + Name = string.Empty, + PublishDate = default(DateTime?) + }); + + AreNotSame(book, emptyBook); + IsEmpty(emptyBook.Author); + IsEmpty(emptyBook.Name); + IsNull(emptyBook.PublishDate); + } + private class Car { public string Constructor { get; set; } @@ -145,4 +267,4 @@ private class Book public DateTime? PublishDate { get; set; } } } -} +} \ No newline at end of file diff --git a/tests/SuccincT.Tests/SuccincT/PatternMatchers/TypeMatcherTests.cs b/tests/SuccincT.Tests/SuccincT/PatternMatchers/TypeMatcherTests.cs index b107272..f53619f 100644 --- a/tests/SuccincT.Tests/SuccincT/PatternMatchers/TypeMatcherTests.cs +++ b/tests/SuccincT.Tests/SuccincT/PatternMatchers/TypeMatcherTests.cs @@ -231,16 +231,19 @@ private interface ITest {} private class Test1 : ITest { + // ReSharper disable once MemberCanBeMadeStatic.Local public int F1() => 1; } private class Test2 : ITest { + // ReSharper disable once MemberCanBeMadeStatic.Local public int F2() => 2; } private class Test3 : ITest { + // ReSharper disable once MemberCanBeMadeStatic.Local public int F2() => 3; } }