Skip to content

Commit

Permalink
Merge branch 'StefanBertels-fix-2153-TypeConverterFactory'
Browse files Browse the repository at this point in the history
  • Loading branch information
JoshClose committed Feb 9, 2024
2 parents a9d9f7b + 253c785 commit 1b585eb
Show file tree
Hide file tree
Showing 6 changed files with 223 additions and 35 deletions.
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ obj/
artifacts/
.tmp/
cache/
.idea/

*.user
*.psess
Expand Down
2 changes: 1 addition & 1 deletion src/CsvHelper/CsvHelper.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
<!-- Assembly -->
<AssemblyTitle>CsvHelper</AssemblyTitle>
<Description>A library for reading and writing CSV files. Extremely fast, flexible, and easy to use. Supports reading and writing of custom class objects.</Description>
<Copyright>Copyright © 2009-2022 Josh Close</Copyright>
<Copyright>Copyright © 2009-2024 Josh Close</Copyright>
<Authors>Josh Close</Authors>

<!-- Build -->
Expand Down
50 changes: 50 additions & 0 deletions src/CsvHelper/TypeConversion/NotSupportedTypeConverter.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
// Copyright 2009-2024 Josh Close
// This file is a part of CsvHelper and is dual licensed under MS-PL and Apache 2.0.
// See LICENSE.txt for details or visit http://www.opensource.org/licenses/ms-pl.html for MS-PL and http://opensource.org/licenses/Apache-2.0 for Apache 2.0.
// https://github.com/JoshClose/CsvHelper
using CsvHelper.Configuration;
using System;

namespace CsvHelper.TypeConversion
{
/// <summary>
/// Throws an exception when used. This is here so that it's apparent
/// that there is no support for <see cref="Type"/> type conversion. A custom
/// converter will need to be created to have a field convert to and
/// from <see cref="Type"/>.
/// </summary>
public class NotSupportedTypeConverter<T> : TypeConverter<T>
{
/// <summary>
/// Throws an exception.
/// </summary>
/// <param name="text">The string to convert to an object.</param>
/// <param name="row">The <see cref="IReaderRow"/> for the current record.</param>
/// <param name="memberMapData">The <see cref="MemberMapData"/> for the member being created.</param>
/// <returns>The object created from the string.</returns>
public override T ConvertFromString(string text, IReaderRow row, MemberMapData memberMapData)
{
var message =
$"Converting {typeof(T).FullName} is not supported. " +
"If you want to do this, create your own ITypeConverter and register " +
"it in the TypeConverterFactory by calling AddConverter.";
throw new TypeConverterException(this, memberMapData, text ?? string.Empty, row.Context, message);
}

/// <summary>
/// Throws an exception.
/// </summary>
/// <param name="value">The object to convert to a string.</param>
/// <param name="row">The <see cref="IWriterRow"/> for the current record.</param>
/// <param name="memberMapData">The <see cref="MemberMapData"/> for the member being written.</param>
/// <returns>The string representation of the object.</returns>
public override string ConvertToString(T value, IWriterRow row, MemberMapData memberMapData)
{
var message =
$"Converting {typeof(T).FullName} is not supported. " +
"If you want to do this, create your own ITypeConverter and register " +
"it in the TypeConverterFactory by calling AddConverter.";
throw new TypeConverterException(this, memberMapData, value, row.Context, message);
}
}
}
44 changes: 17 additions & 27 deletions src/CsvHelper/TypeConversion/TypeConverter.cs
Original file line number Diff line number Diff line change
@@ -1,52 +1,42 @@
// Copyright 2009-2024 Josh Close
// Copyright 2009-2024 Josh Close
// This file is a part of CsvHelper and is dual licensed under MS-PL and Apache 2.0.
// See LICENSE.txt for details or visit http://www.opensource.org/licenses/ms-pl.html for MS-PL and http://opensource.org/licenses/Apache-2.0 for Apache 2.0.
// https://github.com/JoshClose/CsvHelper
using CsvHelper.Configuration;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace CsvHelper.TypeConversion
{
/// <summary>
/// Throws an exception when used. This is here so that it's apparent
/// that there is no support for <see cref="Type"/> type conversion. A custom
/// converter will need to be created to have a field convert to and
/// from <see cref="Type"/>.
/// Converts values to and from strings.
/// </summary>
public class TypeConverter : DefaultTypeConverter
public abstract class TypeConverter<T> : ITypeConverter
{
/// <summary>
/// Throws an exception.
/// Converts the string to a (T) value.
/// </summary>
/// <param name="text">The string to convert to an object.</param>
/// <param name="row">The <see cref="IReaderRow"/> for the current record.</param>
/// <param name="memberMapData">The <see cref="MemberMapData"/> for the member being created.</param>
/// <returns>The object created from the string.</returns>
public override object ConvertFromString(string text, IReaderRow row, MemberMapData memberMapData)
{
var message = "Converting System.Type is not supported. " +
"If you want to do this, create your own ITypeConverter and register " +
"it in the TypeConverterFactory by calling AddConverter.";
throw new TypeConverterException(this, memberMapData, text ?? string.Empty, row.Context, message);
}
/// <returns>The value created from the string.</returns>
public abstract T ConvertFromString(string text, IReaderRow row, MemberMapData memberMapData);

/// <summary>
/// Throws an exception.
/// Converts the value to a string.
/// </summary>
/// <param name="value">The object to convert to a string.</param>
/// <param name="value">The value to convert to a string.</param>
/// <param name="row">The <see cref="IWriterRow"/> for the current record.</param>
/// <param name="memberMapData">The <see cref="MemberMapData"/> for the member being written.</param>
/// <returns>The string representation of the object.</returns>
public override string ConvertToString(object value, IWriterRow row, MemberMapData memberMapData)
/// <returns>The string representation of the value.</returns>
public abstract string ConvertToString(T value, IWriterRow row, MemberMapData memberMapData);

object ITypeConverter.ConvertFromString(string text, IReaderRow row, MemberMapData memberMapData) => ConvertFromString(text, row, memberMapData);

string ITypeConverter.ConvertToString(object value, IWriterRow row, MemberMapData memberMapData)
{
var message = "Converting System.Type is not supported. " +
"If you want to do this, create your own ITypeConverter and register " +
"it in the TypeConverterFactory by calling AddConverter.";
throw new TypeConverterException(this, memberMapData, value, row.Context, message);
return value is T v
? ConvertToString(v, row, memberMapData)
: throw new InvalidCastException();
}
}
}
21 changes: 14 additions & 7 deletions src/CsvHelper/TypeConversion/TypeConverterCache.cs
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,7 @@
// https://github.com/JoshClose/CsvHelper
using CsvHelper.Configuration.Attributes;
using System;
using System.Collections;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Linq;
using System.Numerics;
using System.Reflection;
Expand All @@ -19,6 +17,7 @@ namespace CsvHelper.TypeConversion
public class TypeConverterCache
{
private readonly Dictionary<Type, ITypeConverter> typeConverters = new Dictionary<Type, ITypeConverter>();
private readonly List<ITypeConverterFactory> defaultTypeConverterFactories = new List<ITypeConverterFactory>();
private readonly List<ITypeConverterFactory> typeConverterFactories = new List<ITypeConverterFactory>();
private readonly Dictionary<Type, ITypeConverterFactory> typeConverterFactoryCache = new Dictionary<Type, ITypeConverterFactory>();

Expand Down Expand Up @@ -75,6 +74,14 @@ public void AddConverter(Type type, ITypeConverter typeConverter)
typeConverters[type] = typeConverter;
}

/// <summary>
/// Adds the <see cref="TypeConverter{T}"/> for the given <see cref="System.Type"/>.
/// </summary>
/// <typeparam name="T">The type the converter converts.</typeparam>
/// <param name="typeConverter">The type converter that converts the type.</param>
public void AddConverter<T>(TypeConverter<T> typeConverter) =>
AddConverter<T>(typeConverter as ITypeConverter);

/// <summary>
/// Adds the <see cref="ITypeConverter"/> for the given <see cref="System.Type"/>.
/// </summary>
Expand Down Expand Up @@ -158,7 +165,7 @@ public ITypeConverter GetConverter(Type type)

if (!typeConverterFactoryCache.TryGetValue(type, out var factory))
{
factory = typeConverterFactories.FirstOrDefault(f => f.CanCreate(type));
factory = typeConverterFactories.Concat(defaultTypeConverterFactories).FirstOrDefault(f => f.CanCreate(type));
if (factory != null)
{
typeConverterFactoryCache[type] = factory;
Expand Down Expand Up @@ -224,7 +231,7 @@ private void CreateDefaultConverters()
AddConverter(typeof(sbyte), new SByteConverter());
AddConverter(typeof(string), new StringConverter());
AddConverter(typeof(TimeSpan), new TimeSpanConverter());
AddConverter(typeof(Type), new TypeConverter());
AddConverter(new NotSupportedTypeConverter<Type>());
AddConverter(typeof(ushort), new UInt16Converter());
AddConverter(typeof(uint), new UInt32Converter());
AddConverter(typeof(ulong), new UInt64Converter());
Expand All @@ -234,9 +241,9 @@ private void CreateDefaultConverters()
AddConverter(typeof(TimeOnly), new TimeOnlyConverter());
#endif

AddConverterFactory(new EnumConverterFactory());
AddConverterFactory(new NullableConverterFactory());
AddConverterFactory(new CollectionConverterFactory());
defaultTypeConverterFactories.Add(new EnumConverterFactory());
defaultTypeConverterFactories.Add(new NullableConverterFactory());
defaultTypeConverterFactories.Add(new CollectionConverterFactory());
}
}
}
140 changes: 140 additions & 0 deletions tests/CsvHelper.Tests/TypeConversion/TypeConverterFactoryTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,140 @@
// Copyright 2009-2024 Josh Close
// This file is a part of CsvHelper and is dual licensed under MS-PL and Apache 2.0.
// See LICENSE.txt for details or visit http://www.opensource.org/licenses/ms-pl.html for MS-PL and http://opensource.org/licenses/Apache-2.0 for Apache 2.0.
// https://github.com/JoshClose/CsvHelper
using CsvHelper.Configuration;
using CsvHelper.TypeConversion;
using System;
using System.Collections;
using System.Collections.Generic;
using System.Globalization;
using System.IO;
using System.Linq;
using Xunit;

namespace CsvHelper.Tests.TypeConversion
{
public class TypeConverterFactoryTests
{
[Fact]
public void ReadTypeConverterGenericInt()
{
var input = """
MaybeNumber
23
""";

using var cr = new CsvReader(new StringReader(input), CultureInfo.InvariantCulture);
cr.Context.TypeConverterCache.AddConverter(new MyOptionTypeFactory.OptionConverter<int>());
var firstRow = cr.GetRecords<RecordWithGenerics>().First();
Assert.Equal(new Option<int>(23), firstRow.MaybeNumber);
}

[Fact]
public void WriteTypeConverterGenericInt()
{
var expected = """
MaybeNumber
42
""";

var stringWriter = new StringWriter();
using var cw = new CsvWriter(stringWriter, CultureInfo.InvariantCulture);
cw.Context.TypeConverterCache.AddConverter(new MyOptionTypeFactory.OptionConverter<int>());
cw.WriteRecords(new[]
{
new RecordWithGenerics(new Option<int>(42))
});
Assert.Equal(expected, stringWriter.ToString());
}

[Fact]
public void ReadTypeConverterFactory()
{
var input = """
MaybeNumber
23
""";

using var cr = new CsvReader(new StringReader(input), CultureInfo.InvariantCulture);
cr.Context.TypeConverterCache.AddConverterFactory(new MyOptionTypeFactory());
var firstRow = cr.GetRecords<RecordWithGenerics>().First();
Assert.Equal(new Option<int>(23), firstRow.MaybeNumber);
}

[Fact]
public void WriteTypeConverterFactory()
{
var expected = """
MaybeNumber
42
""";

var stringWriter = new StringWriter();
using var cw = new CsvWriter(stringWriter, CultureInfo.InvariantCulture);
cw.Context.TypeConverterCache.AddConverterFactory(new MyOptionTypeFactory());
cw.WriteRecords(new[]
{
new RecordWithGenerics(new Option<int>(42))
});
Assert.Equal(expected, stringWriter.ToString());
}

public readonly record struct Option<T> : IEnumerable<T>
{
public bool IsPresent { get; }
private readonly T value;

internal Option(T value)
{
IsPresent = true;
this.value = value;
}

public IEnumerator<T> GetEnumerator()
{
if (IsPresent) yield return value;
}

IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();
}

private class MyOptionTypeFactory : ITypeConverterFactory
{
public bool CanCreate(Type type) => type.IsGenericType && type.GetGenericTypeDefinition() == typeof(Option<>);

public bool Create(Type type, TypeConverterCache cache, out ITypeConverter typeConverter)
{
var wrappedType = type.GetGenericArguments().Single();
typeConverter = Activator.CreateInstance(typeof(OptionConverter<>).MakeGenericType(wrappedType)) as ITypeConverter ?? throw new NullReferenceException();

return true;
}

internal class OptionConverter<T> : TypeConverter<Option<T>>
{
public override string ConvertToString(Option<T> value, IWriterRow row, MemberMapData memberMapData)
{
var wrappedTypeConverter = row.Context.TypeConverterCache.GetConverter<T>();

return value.IsPresent ? wrappedTypeConverter.ConvertToString(value.Single(), row, memberMapData) : "";
}

public override Option<T> ConvertFromString(string text, IReaderRow row, MemberMapData memberMapData)
{
var wrappedTypeConverter = row.Context.TypeConverterCache.GetConverter<T>();

return text == ""
? new Option<T>()
: new Option<T>((T)wrappedTypeConverter.ConvertFromString(text, row, memberMapData));
}
}
}

private record RecordWithGenerics(Option<int> MaybeNumber);
}
}

0 comments on commit 1b585eb

Please sign in to comment.