Skip to content

Commit

Permalink
#420 ObjectCreator
Browse files Browse the repository at this point in the history
  • Loading branch information
YevgeniyShunevych committed Oct 30, 2020
1 parent f30a240 commit 6da9689
Show file tree
Hide file tree
Showing 5 changed files with 87 additions and 18 deletions.
32 changes: 31 additions & 1 deletion src/Atata.Tests/ObjectCreatorTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -92,14 +92,19 @@ public void ObjectCreator_Create_WithConstructorParametersAndPropertyValues()
}

[Test]
public void ObjectCreator_Create_WithAlternativeConstructorParameterName()
public void ObjectCreator_Create_WithAlternativeConstructorParameterName_Value()
{
object result = sut.Create(
typeof(FindByIdAttribute),
new Dictionary<string, object>
{
["match"] = TermMatch.EndsWith,
["value"] = "val1"
},
new Dictionary<string, string>
{
["value"] = "values",
["case"] = "termCase"
});

var castedResult = result.Should().BeOfType<FindByIdAttribute>().Subject;
Expand All @@ -110,5 +115,30 @@ public void ObjectCreator_Create_WithAlternativeConstructorParameterName()
castedResult.Values.Should().Equal(new[] { "val1" });
}
}

[Test]
public void ObjectCreator_Create_WithAlternativeConstructorParameterName_Case()
{
object result = sut.Create(
typeof(FindByIdAttribute),
new Dictionary<string, object>
{
["case"] = TermCase.LowerMerged,
["match"] = TermMatch.EndsWith
},
new Dictionary<string, string>
{
["value"] = "values",
["case"] = "termCase"
});

var castedResult = result.Should().BeOfType<FindByIdAttribute>().Subject;

using (new AssertionScope())
{
castedResult.Match.Should().Be(TermMatch.EndsWith);
castedResult.Case.Should().Be(TermCase.LowerMerged);
}
}
}
}
4 changes: 3 additions & 1 deletion src/Atata/GlobalSuppressions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,8 @@
[assembly: SuppressMessage("Minor Code Smell", "S4261:Methods should be named according to their synchronicities", Justification = "<Pending>", Scope = "member", Target = "~M:Atata.UIComponentScriptExecutor`1.ExecuteAsync``1(System.String,System.Object[])~Atata.DataProvider{``0,`0}")]
[assembly: SuppressMessage("Major Code Smell", "S1172:Unused method parameters should be removed", Justification = "<Pending>", Scope = "member", Target = "~M:Atata.TypeFinder.FilterByDeclaringTypeNames(System.Collections.Generic.IEnumerable{System.Type},System.Collections.Generic.IEnumerable{System.String})~System.Collections.Generic.IEnumerable{System.Type}")]
[assembly: SuppressMessage("Minor Code Smell", "S1125:Boolean literals should not be redundant", Justification = "<Pending>", Scope = "member", Target = "~M:Atata.TypeFinder.FilterByDeclaringTypeNames(System.Collections.Generic.IEnumerable{System.Type},System.Collections.Generic.IEnumerable{System.String})~System.Collections.Generic.IEnumerable{System.Type}")]
[assembly: SuppressMessage("Critical Code Smell", "S2302:\"nameof\" should be used", Justification = "<Pending>", Scope = "member", Target = "~M:Atata.ObjectCreator.RetrievePairByName(System.Collections.Generic.Dictionary{System.String,System.Object},System.Reflection.ParameterInfo)~System.Collections.Generic.KeyValuePair{System.String,System.Object}")]
[assembly: SuppressMessage("StyleCop.CSharp.OrderingRules", "SA1204:Static elements should appear before instance elements", Justification = "<Pending>", Scope = "member", Target = "~M:Atata.ObjectMapper.BuildMappingExceptionMessage(System.Type,System.String,System.String)~System.String")]
[assembly: SuppressMessage("Critical Code Smell", "S2302:\"nameof\" should be used", Justification = "<Pending>", Scope = "member", Target = "~M:Atata.ObjectCreator.RetrievePairByName(System.Collections.Generic.Dictionary{System.String,System.Object},System.Collections.Generic.Dictionary{System.String,System.String},System.Reflection.ParameterInfo)~System.Collections.Generic.KeyValuePair{System.String,System.Object}")]
[assembly: SuppressMessage("StyleCop.CSharp.OrderingRules", "SA1204:Static elements should appear before instance elements", Justification = "<Pending>", Scope = "member", Target = "~M:Atata.ObjectCreator.RetrievePairByName(System.Collections.Generic.Dictionary{System.String,System.Object},System.Collections.Generic.Dictionary{System.String,System.String},System.Reflection.ParameterInfo)~System.Collections.Generic.KeyValuePair{System.String,System.Object}")]

#pragma warning restore S103 // Lines should not be too long
9 changes: 9 additions & 0 deletions src/Atata/Utils/Creation/IObjectCreator.cs
Original file line number Diff line number Diff line change
Expand Up @@ -15,5 +15,14 @@ public interface IObjectCreator
/// <param name="valuesMap">The values map.</param>
/// <returns>An instance of created object.</returns>
object Create(Type type, Dictionary<string, object> valuesMap);

/// <summary>
/// Creates an instance of specified type with a set of named values (constructor or property values).
/// </summary>
/// <param name="type">The type.</param>
/// <param name="valuesMap">The values map.</param>
/// <param name="alternativeParameterNamesMap">The map of alternative parameter names.</param>
/// <returns>An instance of created object.</returns>
object Create(Type type, Dictionary<string, object> valuesMap, Dictionary<string, string> alternativeParameterNamesMap);
}
}
44 changes: 30 additions & 14 deletions src/Atata/Utils/Creation/ObjectCreator.cs
Original file line number Diff line number Diff line change
Expand Up @@ -11,11 +11,6 @@ public class ObjectCreator : IObjectCreator

private readonly IObjectMapper objectMapper;

private readonly Dictionary<string, string> alternativeParameterNamesMap = new Dictionary<string, string>
{
["value"] = "values"
};

public ObjectCreator(IObjectConverter objectConverter, IObjectMapper objectMapper)
{
this.objectConverter = objectConverter;
Expand All @@ -24,22 +19,32 @@ public ObjectCreator(IObjectConverter objectConverter, IObjectMapper objectMappe

/// <inheritdoc/>
public object Create(Type type, Dictionary<string, object> valuesMap)
{
return Create(type, valuesMap, new Dictionary<string, string>());
}

/// <inheritdoc/>
public object Create(Type type, Dictionary<string, object> valuesMap, Dictionary<string, string> alternativeParameterNamesMap)
{
type.CheckNotNull(nameof(type));
valuesMap.CheckNotNull(nameof(valuesMap));
alternativeParameterNamesMap.CheckNotNull(nameof(alternativeParameterNamesMap));

if (!valuesMap.Any())
return ActivatorEx.CreateInstance(type);

string[] parameterNamesWithAlternatives = valuesMap.Keys
.Concat(GetAlternativeParameterNames(valuesMap.Keys))
.Concat(GetAlternativeParameterNames(valuesMap.Keys, alternativeParameterNamesMap))
.ToArray();

ConstructorInfo constructor = FindMostAppropriateConstructor(type, parameterNamesWithAlternatives);

var workingValuesMap = new Dictionary<string, object>(valuesMap);

object instance = CreateInstanceViaConstructorAndRemoveUsedValues(constructor, workingValuesMap);
object instance = CreateInstanceViaConstructorAndRemoveUsedValues(
constructor,
workingValuesMap,
alternativeParameterNamesMap);

objectMapper.Map(workingValuesMap, instance);

Expand All @@ -65,16 +70,21 @@ private static ConstructorInfo FindMostAppropriateConstructor(Type type, IEnumer
$"No appropriate constructor found for {type.FullName} type.");
}

private IEnumerable<string> GetAlternativeParameterNames(IEnumerable<string> parameterNames)
private static IEnumerable<string> GetAlternativeParameterNames(
IEnumerable<string> parameterNames,
Dictionary<string, string> alternativeParameterNamesMap)
{
foreach (string parameterName in parameterNames)
{
if (TryGetAlternativeParameterName(parameterName, out string alternativeParameterName))
if (TryGetAlternativeParameterName(alternativeParameterNamesMap, parameterName, out string alternativeParameterName))
yield return alternativeParameterName;
}
}

private bool TryGetAlternativeParameterName(string parameterName, out string alternativeParameterName)
private static bool TryGetAlternativeParameterName(
Dictionary<string, string> alternativeParameterNamesMap,
string parameterName,
out string alternativeParameterName)
{
KeyValuePair<string, string> alternativePair = alternativeParameterNamesMap.FirstOrDefault(x => x.Key.Equals(parameterName, StringComparison.OrdinalIgnoreCase));

Expand All @@ -90,12 +100,15 @@ private bool TryGetAlternativeParameterName(string parameterName, out string alt
}
}

private object CreateInstanceViaConstructorAndRemoveUsedValues(ConstructorInfo constructor, Dictionary<string, object> valuesMap)
private object CreateInstanceViaConstructorAndRemoveUsedValues(
ConstructorInfo constructor,
Dictionary<string, object> valuesMap,
Dictionary<string, string> alternativeParameterNamesMap)
{
object[] arguments = constructor.GetParameters()
.Select(parameter =>
{
KeyValuePair<string, object> valuePair = RetrievePairByName(valuesMap, parameter);
KeyValuePair<string, object> valuePair = RetrievePairByName(valuesMap, alternativeParameterNamesMap, parameter);
valuesMap.Remove(valuePair.Key);
Expand All @@ -106,13 +119,16 @@ private object CreateInstanceViaConstructorAndRemoveUsedValues(ConstructorInfo c
return constructor.Invoke(arguments);
}

private KeyValuePair<string, object> RetrievePairByName(Dictionary<string, object> valuesMap, ParameterInfo parameter)
private static KeyValuePair<string, object> RetrievePairByName(
Dictionary<string, object> valuesMap,
Dictionary<string, string> alternativeParameterNamesMap,
ParameterInfo parameter)
{
KeyValuePair<string, object> valuePair = valuesMap.FirstOrDefault(pair =>
{
if (pair.Key.Equals(parameter.Name, StringComparison.OrdinalIgnoreCase))
return true;
else if (TryGetAlternativeParameterName(pair.Key, out string alternativeParameterName))
else if (TryGetAlternativeParameterName(alternativeParameterNamesMap, pair.Key, out string alternativeParameterName))
return alternativeParameterName.Equals(parameter.Name, StringComparison.OrdinalIgnoreCase);
else
return false;
Expand Down
16 changes: 14 additions & 2 deletions src/Atata/Utils/Mapping/ObjectMapper.cs
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,12 @@ private void Map(string propertyName, object propertyValue, object destination,
BindingFlags.SetProperty | BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.IgnoreCase);

if (property == null)
throw new MappingException($"Failed to map \"{propertyName}\" property for {destinationType.FullName} type. Property is not found.");
throw new MappingException(
BuildMappingExceptionMessage(destinationType, propertyName, "Property is not found."));

if (!property.CanWrite)
throw new MappingException(
BuildMappingExceptionMessage(destinationType, property.Name, "Property cannot be set."));

Type propertyValueType = propertyValue?.GetType();
Type propertyType = property.PropertyType;
Expand All @@ -53,8 +58,15 @@ private void Map(string propertyName, object propertyValue, object destination,
? $"Property null value cannot be converted to {propertyType} type."
: $"Property \"{propertyValue}\" value of {propertyValueType} type cannot be converted to {propertyType} type.";

throw new MappingException($"Failed to map \"{propertyName}\" property for {destinationType.FullName} type. {additionalMessage}", exception);
throw new MappingException(
BuildMappingExceptionMessage(destinationType, property.Name, additionalMessage),
exception);
}
}

private static string BuildMappingExceptionMessage(Type type, string propertyName, string additionalMessage)
{
return $"Failed to map \"{propertyName}\" property for {type.FullName} type. {additionalMessage}";
}
}
}

0 comments on commit 6da9689

Please sign in to comment.