Skip to content

Commit 4bb46f4

Browse files
committed
Merge branch 'twinee-patch-1'
2 parents 34edcc6 + 2c95a02 commit 4bb46f4

File tree

3 files changed

+185
-16
lines changed

3 files changed

+185
-16
lines changed

src/CsvHelper/TypeConversion/DefaultTypeConverter.cs

Lines changed: 42 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -14,25 +14,30 @@ public class DefaultTypeConverter : ITypeConverter
1414
/// <inheritdoc/>
1515
public virtual object? ConvertFromString(string? text, IReaderRow row, MemberMapData memberMapData)
1616
{
17-
if (memberMapData.UseDefaultOnConversionFailure && memberMapData.IsDefaultSet && memberMapData.Member!.MemberType() == memberMapData.Default?.GetType())
17+
// Conversion has failed
18+
// Check if a default value should be returned
19+
if (!memberMapData.UseDefaultOnConversionFailure || !memberMapData.IsDefaultSet)
1820
{
19-
return memberMapData.Default;
21+
throw CreateTypeConverterException(text, row, memberMapData);
2022
}
2123

22-
if (!row.Configuration.ExceptionMessagesContainRawData)
24+
// Try to get a valid default value from the memberMapData
25+
var memberType = memberMapData.Member!.MemberType();
26+
27+
if (memberMapData.Default is null)
2328
{
24-
text = $"Hidden because {nameof(IParserConfiguration.ExceptionMessagesContainRawData)} is false.";
29+
if (TypeAllowsNull(memberType))
30+
{
31+
return null;
32+
}
33+
}
34+
else if (memberType.IsAssignableFrom(memberMapData.Default.GetType()))
35+
{
36+
return memberMapData.Default;
2537
}
2638

27-
text ??= string.Empty;
28-
29-
var message =
30-
$"The conversion cannot be performed.{Environment.NewLine}" +
31-
$" Text: '{text}'{Environment.NewLine}" +
32-
$" MemberName: {memberMapData.Member?.Name}{Environment.NewLine}" +
33-
$" MemberType: {memberMapData.Member?.MemberType().FullName}{Environment.NewLine}" +
34-
$" TypeConverter: '{memberMapData.TypeConverter?.GetType().FullName}'";
35-
throw new TypeConverterException(this, memberMapData, text, row.Context, message);
39+
// No valid default value was configured, throw a TypeConverterException
40+
throw CreateTypeConverterException(text, row, memberMapData);
3641
}
3742

3843
/// <inheritdoc/>
@@ -56,4 +61,28 @@ public class DefaultTypeConverter : ITypeConverter
5661

5762
return value?.ToString() ?? string.Empty;
5863
}
64+
65+
private TypeConverterException CreateTypeConverterException(string? text, IReaderRow row, MemberMapData memberMapData)
66+
{
67+
if (!row.Configuration.ExceptionMessagesContainRawData)
68+
{
69+
text = $"Hidden because {nameof(IParserConfiguration.ExceptionMessagesContainRawData)} is false.";
70+
}
71+
72+
text ??= string.Empty;
73+
74+
var message =
75+
$"The conversion cannot be performed.{Environment.NewLine}" +
76+
$" Text: '{text}'{Environment.NewLine}" +
77+
$" MemberName: {memberMapData.Member?.Name}{Environment.NewLine}" +
78+
$" MemberType: {memberMapData.Member?.MemberType().FullName}{Environment.NewLine}" +
79+
$" TypeConverter: '{memberMapData.TypeConverter?.GetType().FullName}'";
80+
81+
return new TypeConverterException(this, memberMapData, text, row.Context, message);
82+
}
83+
84+
private static bool TypeAllowsNull(Type type)
85+
{
86+
return !type.IsValueType || Nullable.GetUnderlyingType(type) is not null;
87+
}
5988
}

tests/CsvHelper.Tests/Mocks/ReaderRowMock.cs

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -27,19 +27,28 @@ public class ReaderRowMock : IReaderRow
2727

2828
public string[] HeaderRecord => throw new NotImplementedException();
2929

30-
public IParser Parser => throw new NotImplementedException();
30+
public IParser Parser { get; }
3131

32-
public CsvContext Context => throw new NotImplementedException();
32+
public CsvContext Context { get; }
3333

3434
public IReaderConfiguration Configuration { get; private set; }
3535

3636
public ReaderRowMock()
37+
: this(new CsvConfiguration(CultureInfo.InvariantCulture))
3738
{
38-
Configuration = new CsvConfiguration(CultureInfo.InvariantCulture);
39+
}
40+
41+
public ReaderRowMock(IParser parser)
42+
{
43+
Parser = parser;
44+
Context = Parser.Context;
45+
Configuration = Context.Configuration;
3946
}
4047

4148
public ReaderRowMock(CsvConfiguration configuration)
4249
{
50+
Parser = new ParserMock(configuration);
51+
Context = Parser.Context;
4352
Configuration = configuration;
4453
}
4554

Lines changed: 131 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,131 @@
1+
// Copyright 2009-2024 Josh Close
2+
// This file is a part of CsvHelper and is dual licensed under MS-PL and Apache 2.0.
3+
// 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.
4+
// https://github.com/JoshClose/CsvHelper
5+
using System.Globalization;
6+
using System.Reflection;
7+
using CsvHelper.Configuration;
8+
using CsvHelper.Tests.Mocks;
9+
using CsvHelper.TypeConversion;
10+
using Xunit;
11+
12+
namespace CsvHelper.Tests.TypeConversion
13+
{
14+
public class NullableConverterTests
15+
{
16+
private const string InvalidIntString = "abc";
17+
18+
private readonly MemberInfo _nullableIntMemberInfo = typeof(NullableConverterTests)
19+
.GetProperty(nameof(NullableInt), BindingFlags.Instance | BindingFlags.NonPublic)!;
20+
21+
// this property is used to create a MemberMapData instance for a nullable int member
22+
private int? NullableInt { get; set; }
23+
24+
[Fact]
25+
public void ConvertNullableTypeFromStringThrowsWithUseDefaultOnConversionFailureFalse()
26+
{
27+
// setup
28+
var converter = new NullableConverter(typeof(int?), new TypeConverterCache());
29+
var propertyMapData = new MemberMapData(_nullableIntMemberInfo)
30+
{
31+
TypeConverter = converter,
32+
TypeConverterOptions = {
33+
CultureInfo = CultureInfo.InvariantCulture,
34+
},
35+
Default = null,
36+
IsDefaultSet = true,
37+
UseDefaultOnConversionFailure = false,
38+
};
39+
var csvConfiguration = new CsvConfiguration(CultureInfo.InvariantCulture)
40+
{
41+
ExceptionMessagesContainRawData = false,
42+
};
43+
var row = new ReaderRowMock(new ParserMock(csvConfiguration));
44+
45+
// act
46+
var exception = Record.Exception(() => converter.ConvertFromString(InvalidIntString, row, propertyMapData));
47+
48+
// assert
49+
Assert.IsType<TypeConverterException>(exception);
50+
}
51+
52+
[Fact]
53+
public void ConvertNullableTypeFromStringWithConfiguredNullValueDoesNotThrow()
54+
{
55+
// setup
56+
var converter = new NullableConverter(typeof(int?), new TypeConverterCache());
57+
var configuredNullValue = InvalidIntString;
58+
var propertyMapData = new MemberMapData(_nullableIntMemberInfo)
59+
{
60+
TypeConverter = converter,
61+
TypeConverterOptions = {
62+
// configure InvalidIntString as an expected null value for this member.
63+
NullValues = { configuredNullValue },
64+
CultureInfo = CultureInfo.InvariantCulture,
65+
},
66+
Default = null,
67+
IsDefaultSet = true,
68+
UseDefaultOnConversionFailure = false,
69+
};
70+
71+
// act
72+
object? result = null;
73+
var exception = Record.Exception(() => result = converter.ConvertFromString(configuredNullValue, null!, propertyMapData));
74+
75+
// assert
76+
Assert.Null(exception);
77+
Assert.Null(result);
78+
}
79+
80+
[Fact]
81+
public void ConvertNullableTypeFromStringWithUseDefaultOnConversionFailureTrueDoesNotThrow()
82+
{
83+
// setup
84+
var converter = new NullableConverter(typeof(int?), new TypeConverterCache());
85+
var propertyMapData = new MemberMapData(_nullableIntMemberInfo)
86+
{
87+
TypeConverter = converter,
88+
TypeConverterOptions = {
89+
CultureInfo = CultureInfo.InvariantCulture,
90+
},
91+
Default = null,
92+
IsDefaultSet = true,
93+
UseDefaultOnConversionFailure = true,
94+
};
95+
96+
// act
97+
var exception = Record.Exception(() => converter.ConvertFromString(InvalidIntString, null!, propertyMapData));
98+
99+
// assert
100+
Assert.Null(exception);
101+
}
102+
103+
[Theory]
104+
[InlineData(null)]
105+
[InlineData(-1)]
106+
[InlineData(99)]
107+
public void ConvertNullableTypeFromStringWithUseDefaultOnConversionFailureTrueReturnsDefaultValue(object? defaultValue)
108+
{
109+
// setup
110+
var converter = new NullableConverter(typeof(int?), new TypeConverterCache());
111+
var propertyMapData = new MemberMapData(_nullableIntMemberInfo)
112+
{
113+
TypeConverter = converter,
114+
TypeConverterOptions = {
115+
CultureInfo = CultureInfo.InvariantCulture,
116+
},
117+
Default = defaultValue,
118+
IsDefaultSet = true,
119+
UseDefaultOnConversionFailure = true,
120+
};
121+
122+
// act
123+
object? result = null;
124+
var exception = Record.Exception(() => result = converter.ConvertFromString(InvalidIntString, null!, propertyMapData));
125+
126+
// assert
127+
Assert.Null(exception);
128+
Assert.StrictEqual(defaultValue, result);
129+
}
130+
}
131+
}

0 commit comments

Comments
 (0)