LSTANCZYK
10/17/2014 - 3:15 PM

SpecFlow.ObjectConverter.cs

using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Linq;
using System.Linq.Expressions;

namespace TechTalk.SpecFlow.ObjectConversion
{
    public static class ObjectConverterExtensions
    {
        /// <summary>
        /// Creates a conversion object for the given set of properties
        /// </summary>
        /// <typeparam name="TEntity">the type of the object to create</typeparam>
        public static IObjectConverter<TEntity> AsObjectConverter<TEntity>(this IDictionary<string, string> objectData)
        {
            return new ObjectConverter<TEntity>(new[] { objectData });
        }

        /// <summary>
        /// Creates a conversion object for the given set of properties
        /// </summary>
        /// <typeparam name="TEntity">the type of the object to create</typeparam>
        public static IObjectConverter<TEntity> AsObjectConverter<TEntity>(this IEnumerable<IDictionary<string, string>> objectDataCollection)
        {
            return new ObjectConverter<TEntity>(objectDataCollection);
        }

        /// <summary>
        /// Creates the objects from the given <paramref name="objectDataCollection"/> argument
        /// </summary>
        public static IEnumerable<TEntity> CreateObjects<TEntity>(this IObjectConverter<TEntity> objectConverter, IEnumerable<IDictionary<string, string>> objectDataCollection)
        {
            return objectDataCollection.Select(objectConverter.CreateObject);
        }

        /// <summary>
        /// Specifies the aliases in the import data for the given property of the entity. There can be more than one alias for any property, but not vice versa.
        /// </summary>
        public static IConfiguredObjectConverter<TEntity> WithPropertyAlias<TEntity, TProperty>(this IConfiguredObjectConverter<TEntity> objectConverter, Expression<Func<TEntity, TProperty>> selector, params string[] aliases)
        {
            foreach (var alias in aliases)
                objectConverter.WithPropertyAlias(selector, alias);
            return objectConverter;
        }

        /// <summary>
        /// Shorthand to specify required fields that the import data is verified to contain in a type safe way
        /// </summary>
        public static IConfiguredObjectConverter<TEntity> WithRequiredField<TEntity, TProperty>(this IConfiguredObjectConverter<TEntity> objectConverter, params Expression<Func<TEntity, TProperty>>[] propertySelectors)
        {
            return objectConverter.WithRequiredField(propertySelectors.Select(ps => ps.GetPropertyName()).ToArray());
        }

        /// <summary>
        /// Specifies the default value to use for a property if the import data wouldn't specify otherwise
        /// </summary>
        public static IConfiguredObjectConverter<TEntity> WithDefaultValue<TEntity, TProperty>(this IConfiguredObjectConverter<TEntity> objectConverter, Expression<Func<TEntity, TProperty>> selector, TProperty defaultValue)
        {
            return objectConverter.WithDefaultValue(selector, () => defaultValue);
        }

        /// <summary>
        /// Returns a converter function for the given type. This method is the default converter provider.
        /// </summary>
        public static Func<string, object> GetDefaultConverter(Type type)
        {
            if (type == typeof(string))
                return s => s;

            if (type.IsGenericType && type.Name == "Nullable`1")
                return val =>
                {
                    if (string.IsNullOrEmpty(val)) return null;
                    else return GetDefaultConverter(type.GetGenericArguments()[0])(val);
                };

            if (type.IsEnum)
                return value => Enum.Parse(type, value);

            return value => TypeDescriptor.GetConverter(type).ConvertFromString(value);
        }
    }

    public static class ExpressionExtensions
    {
        /// <summary>
        /// Returns the property name from a property selector expression
        /// </summary>
        public static string GetPropertyName<TEntity, TValue>(this Expression<Func<TEntity, TValue>> property)
        {
            var exp = (LambdaExpression)property;

            if (exp.Body.NodeType == ExpressionType.Parameter)
                return "item";

            var mExp = (exp.Body.NodeType == ExpressionType.MemberAccess) ?
                (MemberExpression)exp.Body :
                (MemberExpression)((UnaryExpression)exp.Body).Operand;
            return mExp.Member.Name;
        }
    }

    public interface IObjectConverter<TEntity>
    {
        /// <summary>
        /// Allows customizing the object conversion
        /// </summary>
        IConfiguredObjectConverter<TEntity> WithConfiguration();
        /// <summary>
        /// Creates the objects from the argument provided at initialization
        /// </summary>
        IEnumerable<TEntity> CreateObjects();
        /// <summary>
        /// Creates the objects from the given <param name="collectionData"/> argument
        /// </summary>
        IEnumerable<TEntity> CreateObjects(IEnumerable<IDictionary<string, string>> collectionData);
        /// <summary>
        /// Creates the object from the given <paramref name="objectData"/> argument
        /// </summary>
        TEntity CreateObject(IDictionary<string, string> objectData);
    }

    public interface IConfiguredObjectConverter<TEntity> : IObjectConverter<TEntity>
    {
        /// <summary>
        /// Defines the object factory to use when creating a new entity
        /// </summary>
        IConfiguredObjectConverter<TEntity> WithObjectFactory(Func<TEntity> factory);
        /// <summary>
        /// Specifies that the import data should contain the specified fields
        /// </summary>
        IConfiguredObjectConverter<TEntity> WithRequiredField(params string[] fieldNames);
        /// <summary>
        /// Specifies the fields of the import data the algorithm should skip
        /// </summary>
        IConfiguredObjectConverter<TEntity> WithSkippedField(params string[] fieldNames);
        /// <summary>
        /// Specifies an alias in the import data for the given property of the entity. There can be more than one alias for any property, but not vice versa.
        /// </summary>
        IConfiguredObjectConverter<TEntity> WithPropertyAlias<TProperty>(Expression<Func<TEntity, TProperty>> selector, string alias);
        /// <summary>
        /// Specifies the converter to use for the given import data field (converter priority: table field, object property, target value, default)
        /// </summary>
        IConfiguredObjectConverter<TEntity> WithFieldValueConverter<TProperty>(string name, Func<string, TProperty> converter);
        /// <summary>
        /// Specifies the converter to use for the given property of the entity (converter priority: table field, object property, target value, default)
        /// </summary>
        IConfiguredObjectConverter<TEntity> WithPropertyValueConverter<TProperty>(Expression<Func<TEntity, TProperty>> selector, Func<string, TProperty> converter);
        /// <summary>
        /// Specifies a conversion method to use when converting to the given value (converter priority: table field, object property, target value, default)
        /// </summary>
        IConfiguredObjectConverter<TEntity> WithValueConverter<TValue>(Func<string, TValue> converter);
        /// <summary>
        /// Specifies the provider method to use to obtain the default value converter for any given type (converter priority: table field, object property, target value, default)
        /// </summary>
        IConfiguredObjectConverter<TEntity> WithDefaultConverter(Func<Type, Func<string, object>> converterProvider);
        /// <summary>
        /// Specifies the default value to use for a property if the import data wouldn't specify otherwise
        /// </summary>
        IConfiguredObjectConverter<TEntity> WithDefaultValue<TProperty>(Expression<Func<TEntity, TProperty>> selector, Func<TProperty> valueProvider);
    }

    public static class ObjectConverter
    {
        /// <summary>
        /// Creates an object converter for the given type
        /// </summary>
        public static IObjectConverter<TEntity> For<TEntity>()
        {
            return new ObjectConverter<TEntity>(null);
        }
    }

    internal sealed class ObjectConverter<TEntity> : IConfiguredObjectConverter<TEntity>
    {
        private readonly IEnumerable<IDictionary<string, string>> objectDataCollection;
        public ObjectConverter(IEnumerable<IDictionary<string, string>> objectDataCollection)
        {
            this.objectDataCollection = objectDataCollection;
        }

        public IConfiguredObjectConverter<TEntity> WithConfiguration()
        {
            return this;
        }

        public IEnumerable<TEntity> CreateObjects()
        {
            return CreateObjects(objectDataCollection);
        }

        public IEnumerable<TEntity> CreateObjects(IEnumerable<IDictionary<string, string>> collectionData)
        {
            if (collectionData == null) return Enumerable.Empty<TEntity>();
            return collectionData.Select(CreateObject).ToArray();
        }

        public TEntity CreateObject(IDictionary<string, string> objectData)
        {
            var missingFields = requiredFields.Except(objectData.Keys).ToArray();
            if (missingFields.Length > 0)
                new InvalidOperationException("Required field(s) missing: " + string.Join(", ", missingFields));

            var entity = objectFactory();
            foreach (var name in objectData.Keys.Except(skippedFields))
            {
                string propertyName;
                if (!propertyAliases.TryGetValue(name, out propertyName))
                    propertyName = name;

                var property = typeof(TEntity).GetProperty(propertyName);
                if (property == null)
                    throw new InvalidOperationException(string.Format("Invalid property name '{0}'", propertyName));

                Func<string, object> converter = null;
                if (converter == null) fieldValueConverters.TryGetValue(name, out converter);
                if (converter == null) propertyValueConverters.TryGetValue(propertyName, out converter);
                if (converter == null) valueConverters.TryGetValue(property.PropertyType, out converter);
                if (converter == null) converter = defaultConverterProvider(property.PropertyType);

                property.SetValue(entity, converter(objectData[name]), null);
            }

            foreach (var entry in defaultValueProviders)
            {
                var fieldNames = propertyAliases.Where(pa => pa.Value == entry.Key).Select(pa => pa.Key).Concat(new[] { entry.Key });
                if (!objectData.Keys.Any(k => fieldNames.Contains(k)))
                {
                    var property = typeof(TEntity).GetProperty(entry.Key);
                    property.SetValue(entity, entry.Value(), null);
                }
            }

            return entity;
        }

        private readonly List<string> requiredFields = new List<string>();
        public IConfiguredObjectConverter<TEntity> WithRequiredField(params string[] fieldNames)
        {
            requiredFields.AddRange(fieldNames);
            return this;
        }

        private Func<TEntity> objectFactory = () => Activator.CreateInstance<TEntity>();
        public IConfiguredObjectConverter<TEntity> WithObjectFactory(Func<TEntity> factory)
        {
            objectFactory = factory;
            return this;
        }

        private readonly List<string> skippedFields = new List<string>();
        public IConfiguredObjectConverter<TEntity> WithSkippedField(params string[] fieldNames)
        {
            skippedFields.AddRange(fieldNames);
            return this;
        }

        private readonly Dictionary<string, string> propertyAliases = new Dictionary<string, string>();
        public IConfiguredObjectConverter<TEntity> WithPropertyAlias<TProperty>(Expression<Func<TEntity, TProperty>> selector, string alias)
        {
            propertyAliases.Add(alias, selector.GetPropertyName());
            return this;
        }

        private readonly Dictionary<string, Func<string, object>> propertyValueConverters = new Dictionary<string, Func<string, object>>();
        public IConfiguredObjectConverter<TEntity> WithPropertyValueConverter<TProperty>(Expression<Func<TEntity, TProperty>> selector, Func<string, TProperty> converter)
        {
            propertyValueConverters.Add(selector.GetPropertyName(), v => converter(v));
            return this;
        }

        private readonly Dictionary<string, Func<string, object>> fieldValueConverters = new Dictionary<string, Func<string, object>>();
        public IConfiguredObjectConverter<TEntity> WithFieldValueConverter<TProperty>(string name, Func<string, TProperty> converter)
        {
            fieldValueConverters.Add(name, v => converter(v));
            return this;
        }

        private readonly Dictionary<Type, Func<string, object>> valueConverters = new Dictionary<Type, Func<string, object>>();
        public IConfiguredObjectConverter<TEntity> WithValueConverter<TValue>(Func<string, TValue> converter)
        {
            valueConverters.Add(typeof(TValue), v => converter(v));
            return this;
        }

        private Func<Type, Func<string, object>> defaultConverterProvider = ObjectConverterExtensions.GetDefaultConverter;
        public IConfiguredObjectConverter<TEntity> WithDefaultConverter(Func<Type, Func<string, object>> converterProvider)
        {
            defaultConverterProvider = converterProvider;
            return this;
        }

        private readonly Dictionary<string, Func<object>> defaultValueProviders = new Dictionary<string, Func<object>>();
        public IConfiguredObjectConverter<TEntity> WithDefaultValue<TProperty>(Expression<Func<TEntity, TProperty>> selector, Func<TProperty> valueProvider)
        {
            defaultValueProviders.Add(selector.GetPropertyName(), () => valueProvider());
            return this;
        }
    }
}