-
-
Notifications
You must be signed in to change notification settings - Fork 1.1k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge branch 'StefanBertels-fix-2153-TypeConverterFactory'
- Loading branch information
Showing
6 changed files
with
223 additions
and
35 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -5,6 +5,7 @@ obj/ | |
artifacts/ | ||
.tmp/ | ||
cache/ | ||
.idea/ | ||
|
||
*.user | ||
*.psess | ||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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); | ||
} | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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(); | ||
} | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
140 changes: 140 additions & 0 deletions
140
tests/CsvHelper.Tests/TypeConversion/TypeConverterFactoryTests.cs
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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); | ||
} | ||
} |