Skip to content

Commit 5c754d2

Browse files
authored
feature/parse new overload with exception handling (#105)
* done adds impl * adds tests * minor visual refactor * change index to be startIndex * adds documentation to methods interface * improve doc new method
1 parent 6c10eb9 commit 5c754d2

10 files changed

Lines changed: 359 additions & 17 deletions

‎RecordParser.Test/FixedLengthReaderBuilderTest.cs‎

Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
using RecordParser.Builders.Writer;
55
using RecordParser.Parsers;
66
using System;
7+
using System.Collections.Generic;
78
using System.Diagnostics;
89
using System.Globalization;
910
using System.Linq;
@@ -134,6 +135,51 @@ public void Given_trim_is_enabled_should_remove_whitespace_from_both_sides_of_st
134135
Baz: "baz"));
135136
}
136137

138+
private class MyCustomException : Exception
139+
{
140+
public MyCustomException(string msg) : base(msg) { }
141+
}
142+
143+
[Fact]
144+
public void Given_partial_invalid_record_called_with_parse_that_handle_exception_should_not_throw_and_parse_valid_values()
145+
{
146+
var reader = new FixedLengthReaderBuilder<(string Name, DateTime Birthday, decimal Money, Color Color1, Color Color2, bool? IsValid)>()
147+
.Map(x => x.Name, 0, 5)
148+
.Map(x => x.Birthday, 5, 10)
149+
.Map(x => x.Money, 15, 7)
150+
.Map(x => x.Color1, 22, 15)
151+
.Map(x => x.Color2, 37, 15)
152+
.Map(x => x.IsValid, 52, 5, text => bool.TryParse(text, out bool result) ? result : throw new MyCustomException($"Invalid boolean value: {text}"))
153+
.Build();
154+
155+
var erros = new List<(Exception ex, int index)>();
156+
var result = reader.Parse(" foo _datehere_ 123.45_invalid_color_ LightBlue _FOO_", (ex, index) => erros.Add((ex, index)));
157+
158+
result.Should().BeEquivalentTo((Name: "foo",
159+
Birthday: default(DateTime),
160+
Money: 123.45M,
161+
Color1: default(Color),
162+
Color2: Color.LightBlue,
163+
IsValid: default(bool?)));
164+
165+
erros.Should().SatisfyRespectively(
166+
e1 =>
167+
{
168+
e1.ex.Should().Match<FormatException>(ex => ex.Message.StartsWith("String '_datehere_' was not recognized as a valid DateTime."));
169+
e1.index.Should().Be(5);
170+
},
171+
e2 =>
172+
{
173+
e2.ex.Should().Match<ArgumentException>(ex => ex.Message.StartsWith("Requested value '_invalid_color_' was not found."));
174+
e2.index.Should().Be(22);
175+
},
176+
e3 =>
177+
{
178+
e3.ex.Should().Match<MyCustomException>(ex => ex.Message.StartsWith($"Invalid boolean value: _FOO_"));
179+
e3.index.Should().Be(52);
180+
});
181+
}
182+
137183
[Fact]
138184
public void Given_invalid_record_called_with_try_parse_should_not_throw()
139185
{
@@ -148,6 +194,31 @@ public void Given_invalid_record_called_with_try_parse_should_not_throw()
148194
result.Should().Be(default);
149195
}
150196

197+
[Fact]
198+
public void Given_valid_record_called_with_parse_that_handle_exception_should_not_throw_and_parse_valid_values()
199+
{
200+
var reader = new FixedLengthReaderBuilder<(string Name, DateTime Birthday, decimal Money, Color Color1, Color Color2, bool? IsValid)>()
201+
.Map(x => x.Name, 0, 5)
202+
.Map(x => x.Birthday, 5, 10)
203+
.Map(x => x.Money, 15, 7)
204+
.Map(x => x.Color1, 22, 15)
205+
.Map(x => x.Color2, 37, 15)
206+
.Map(x => x.IsValid, 52, 5, text => bool.TryParse(text, out bool result) ? result : throw new MyCustomException($"Invalid boolean value: {text}"))
207+
.Build();
208+
209+
var erros = new List<(Exception ex, int index)>();
210+
var result = reader.Parse(" foo 2026.03.19 123.45 White LightBlue TRUE ", (ex, index) => erros.Add((ex, index)));
211+
212+
result.Should().BeEquivalentTo((Name: "foo",
213+
Birthday: new DateTime(2026, 03, 19),
214+
Money: 123.45M,
215+
Color1: Color.White,
216+
Color2: Color.LightBlue,
217+
IsValid: true));
218+
219+
erros.Should().BeEmpty();
220+
}
221+
151222
[Fact]
152223
public void Given_valid_record_called_with_try_parse_should_set_out_parameter_with_result()
153224
{

‎RecordParser.Test/VariableLengthReaderBuilderTest.cs‎

Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
using RecordParser.Builders.Writer;
55
using RecordParser.Parsers;
66
using System;
7+
using System.Collections.Generic;
78
using System.Diagnostics;
89
using System.Globalization;
910
using System.Linq;
@@ -161,6 +162,51 @@ public void Given_trim_is_enabled_should_remove_whitespace_from_both_sides_of_st
161162
Baz: "baz"));
162163
}
163164

165+
private class MyCustomException : Exception
166+
{
167+
public MyCustomException(string msg) : base(msg) { }
168+
}
169+
170+
[Fact]
171+
public void Given_partial_invalid_record_called_with_parse_that_handle_exception_should_not_throw_and_parse_valid_values()
172+
{
173+
var reader = new VariableLengthReaderBuilder<(string Name, DateTime Birthday, decimal Money, Color Color1, Color Color2, bool? IsValid)>()
174+
.Map(x => x.Name, 0)
175+
.Map(x => x.Birthday, 1)
176+
.Map(x => x.Money, 2)
177+
.Map(x => x.Color1, 3)
178+
.Map(x => x.Color2, 4)
179+
.Map(x => x.IsValid, 5, text => bool.TryParse(text, out bool result) ? result : throw new MyCustomException($"Invalid boolean value: {text}"))
180+
.Build(";");
181+
182+
var erros = new List<(Exception ex, int index)>();
183+
var result = reader.Parse("foo;_datehere_;123.45;_invalid_color_;LightBlue;_FOO_", (ex, index) => erros.Add((ex, index)));
184+
185+
result.Should().BeEquivalentTo((Name: "foo",
186+
Birthday: default(DateTime),
187+
Money: 123.45M,
188+
Color1: default(Color),
189+
Color2: Color.LightBlue,
190+
IsValid: default(bool?)));
191+
192+
erros.Should().SatisfyRespectively(
193+
e1 =>
194+
{
195+
e1.ex.Should().Match<FormatException>(ex => ex.Message.StartsWith("String '_datehere_' was not recognized as a valid DateTime."));
196+
e1.index.Should().Be(1);
197+
},
198+
e2 =>
199+
{
200+
e2.ex.Should().Match<ArgumentException>(ex => ex.Message.StartsWith("Requested value '_invalid_color_' was not found."));
201+
e2.index.Should().Be(3);
202+
},
203+
e3 =>
204+
{
205+
e3.ex.Should().Match<MyCustomException>(ex => ex.Message.StartsWith($"Invalid boolean value: _FOO_"));
206+
e3.index.Should().Be(5);
207+
});
208+
}
209+
164210
[Fact]
165211
public void Given_invalid_record_called_with_try_parse_should_not_throw()
166212
{
@@ -175,6 +221,31 @@ public void Given_invalid_record_called_with_try_parse_should_not_throw()
175221
result.Should().Be(default);
176222
}
177223

224+
[Fact]
225+
public void Given_valid_record_called_with_parse_that_handle_exception_should_not_throw_and_parse_valid_values()
226+
{
227+
var reader = new VariableLengthReaderBuilder<(string Name, DateTime Birthday, decimal Money, Color Color1, Color Color2, bool? IsValid)>()
228+
.Map(x => x.Name, 0)
229+
.Map(x => x.Birthday, 1)
230+
.Map(x => x.Money, 2)
231+
.Map(x => x.Color1, 3)
232+
.Map(x => x.Color2, 4)
233+
.Map(x => x.IsValid, 5, text => bool.TryParse(text, out bool result) ? result : throw new MyCustomException($"Invalid boolean value: {text}"))
234+
.Build(";");
235+
236+
var erros = new List<(Exception ex, int index)>();
237+
var result = reader.Parse("foo bar baz;2026.03.19;123.45;White;LightBlue;TRUE", (ex, index) => erros.Add((ex, index)));
238+
239+
result.Should().BeEquivalentTo((Name: "foo bar baz",
240+
Birthday: new DateTime(2026, 03, 19),
241+
Money: 123.45M,
242+
Color1: Color.White,
243+
Color2: Color.LightBlue,
244+
IsValid: true));
245+
246+
erros.Should().BeEmpty();
247+
}
248+
178249
[Fact]
179250
public void Given_valid_record_called_with_try_parse_should_set_out_parameter_with_result()
180251
{

‎RecordParser/Builders/Reader/FixedLengthReaderBuilder.cs‎

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -95,10 +95,12 @@ public IFixedLengthReader<T> Build(CultureInfo cultureInfo = null, Func<T> facto
9595
{
9696
var map = MappingReadConfiguration.Merge(list, dic);
9797
var func = ReaderEngine.RecordParserSpanFlat(map, factory);
98-
9998
func = CultureInfoVisitor.ReplaceCulture(func, cultureInfo);
10099

101-
return new FixedLengthReader<T>(func.Compile());
100+
var func2 = ReaderEngine.RecordParserSpanFlatSafe(map, factory);
101+
func2 = CultureInfoVisitor.ReplaceCulture(func2, cultureInfo);
102+
103+
return new FixedLengthReader<T>(func.Compile(), func2.Compile());
102104
}
103105
}
104106
}

‎RecordParser/Builders/Reader/MappingReadConfiguration.cs‎

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,10 @@ public static IEnumerable<MappingReadConfiguration> Merge(
4242
});
4343

4444
result = result
45-
.OrderBy(x => x.start)
45+
.Select((x, i) => (item: x, index: i))
46+
.OrderBy(x => x.item.start)
47+
.ThenBy(x => x.index)
48+
.Select(x => x.item)
4649
.ToList();
4750

4851
return result;

‎RecordParser/Builders/Reader/VariableLengthReaderBuilder.cs‎

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -101,11 +101,14 @@ public IVariableLengthReader<T> Build(string separator, CultureInfo cultureInfo
101101
var quote = QuoteHelper.Quote.Char;
102102

103103
var map = MappingReadConfiguration.Merge(list.Select(x => x.Value), dic);
104-
var func = ReaderEngine.RecordParserSpanCSV(map, factory);
105104

105+
var func = ReaderEngine.RecordParserSpanCSV(map, factory);
106106
func = CultureInfoVisitor.ReplaceCulture(func, cultureInfo);
107107

108-
return new VariableLengthReader<T>(func.Compile(), separator, quote);
108+
var func2 = ReaderEngine.RecordParserSpanCSVSafe(map, factory);
109+
func2 = CultureInfoVisitor.ReplaceCulture(func2, cultureInfo);
110+
111+
return new VariableLengthReader<T>(func.Compile(), func2.Compile(), separator, quote);
109112
}
110113
}
111114
}

‎RecordParser/Engines/Reader/ReaderEngine.cs‎

Lines changed: 61 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -7,9 +7,11 @@
77
using System.Linq.Expressions;
88
using static RecordParser.Engines.ExpressionHelper;
99

10-
internal delegate T FuncSpanIntT<T>(ReadOnlySpan<T> span, int index);
1110
public delegate T FuncSpanT<T>(ReadOnlySpan<char> text);
11+
internal delegate T FuncSpanTSafe<T>(ReadOnlySpan<char> text, Action<Exception, int> exceptionHandler);
12+
1213
internal delegate T FuncSpanArrayT<T>(in TextFindHelper finder);
14+
internal delegate T FuncSpanArrayTSafe<T>(in TextFindHelper finder, Action<Exception, int> exceptionHandler);
1315

1416
namespace RecordParser.Engines.Reader
1517
{
@@ -23,7 +25,7 @@ public static Expression<FuncSpanArrayT<T>> RecordParserSpanCSV<T>(IEnumerable<M
2325
// variables
2426
var instanceVariable = Expression.Variable(typeof(T), "inst");
2527

26-
var blockThatSetProperties = MountSetProperties(instanceVariable, mappedColumns, (i, mapConfig) =>
28+
var blockThatSetProperties = MountSetProperties(instanceVariable, mappedColumns, mapConfig =>
2729
{
2830
return Expression.Call(configParameter, nameof(TextFindHelper.GetValue), Type.EmptyTypes, Expression.Constant(mapConfig.start));
2931
});
@@ -34,6 +36,32 @@ public static Expression<FuncSpanArrayT<T>> RecordParserSpanCSV<T>(IEnumerable<M
3436
return result;
3537
}
3638

39+
public static Expression<FuncSpanArrayTSafe<T>> RecordParserSpanCSVSafe<T>(IEnumerable<MappingReadConfiguration> mappedColumns, Func<T> factory)
40+
{
41+
// parameters
42+
var configParameter = Expression.Parameter(typeof(TextFindHelper).MakeByRefType(), "config");
43+
var exceptionHandler = Expression.Parameter(typeof(Action<Exception, int>), "exceptionHandler");
44+
45+
// variables
46+
var instanceVariable = Expression.Variable(typeof(T), "inst");
47+
48+
var blockThatSetProperties = MountSetProperties(instanceVariable, mappedColumns, mapConfig =>
49+
{
50+
return Expression.Call(configParameter, nameof(TextFindHelper.GetValue), Type.EmptyTypes, Expression.Constant(mapConfig.start));
51+
},
52+
(mapConfig, assign) =>
53+
{
54+
var visitor = new TryCatchVisitor(exceptionHandler, mapConfig.start);
55+
var result = visitor.Visit(assign);
56+
return result;
57+
});
58+
59+
var body = MountBody(instanceVariable, blockThatSetProperties, mappedColumns, factory);
60+
var result = Expression.Lambda<FuncSpanArrayTSafe<T>>(body, configParameter, exceptionHandler);
61+
62+
return result;
63+
}
64+
3765
public static Expression<FuncSpanT<T>> RecordParserSpanFlat<T>(IEnumerable<MappingReadConfiguration> mappedColumns, Func<T> factory)
3866
{
3967
// parameters
@@ -42,7 +70,7 @@ public static Expression<FuncSpanT<T>> RecordParserSpanFlat<T>(IEnumerable<Mappi
4270
// variables
4371
var instanceVariable = Expression.Variable(typeof(T), "inst");
4472

45-
var blockThatSetProperties = MountSetProperties(instanceVariable, mappedColumns, (i, mapConfig) =>
73+
var blockThatSetProperties = MountSetProperties(instanceVariable, mappedColumns, mapConfig =>
4674
{
4775
return Slice(line, Expression.Constant(mapConfig.start), Expression.Constant(mapConfig.length.Value));
4876
});
@@ -53,6 +81,32 @@ public static Expression<FuncSpanT<T>> RecordParserSpanFlat<T>(IEnumerable<Mappi
5381
return result;
5482
}
5583

84+
public static Expression<FuncSpanTSafe<T>> RecordParserSpanFlatSafe<T>(IEnumerable<MappingReadConfiguration> mappedColumns, Func<T> factory)
85+
{
86+
// parameters
87+
var line = Expression.Parameter(typeof(ReadOnlySpan<char>), "span");
88+
var exceptionHandler = Expression.Parameter(typeof(Action<Exception, int>), "exceptionHandler");
89+
90+
// variables
91+
var instanceVariable = Expression.Variable(typeof(T), "inst");
92+
93+
var blockThatSetProperties = MountSetProperties(instanceVariable, mappedColumns, mapConfig =>
94+
{
95+
return Slice(line, Expression.Constant(mapConfig.start), Expression.Constant(mapConfig.length.Value));
96+
},
97+
(mapConfig, assign) =>
98+
{
99+
var visitor = new TryCatchVisitor(exceptionHandler, mapConfig.start);
100+
var result = visitor.Visit(assign);
101+
return result;
102+
});
103+
104+
var body = MountBody(instanceVariable, blockThatSetProperties, mappedColumns, factory);
105+
var result = Expression.Lambda<FuncSpanTSafe<T>>(body, line, exceptionHandler);
106+
107+
return result;
108+
}
109+
56110
private static BlockExpression MountBody<T>(
57111
ParameterExpression instanceVariable,
58112
BlockExpression blockThatSetProperties,
@@ -75,17 +129,15 @@ private static BlockExpression MountBody<T>(
75129
private static BlockExpression MountSetProperties(
76130
ParameterExpression objectParameter,
77131
IEnumerable<MappingReadConfiguration> mappedColumns,
78-
Func<int, MappingReadConfiguration, Expression> getTextValue)
132+
Func<MappingReadConfiguration, Expression> getTextValue,
133+
Func<MappingReadConfiguration, BinaryExpression, Expression> assignHandler = null)
79134
{
80135
var replacer = new ParameterReplacerVisitor(objectParameter);
81136
var assignsExpressions = new List<Expression>();
82-
var i = -1;
83137

84138
foreach (var x in mappedColumns)
85139
{
86-
i++;
87-
88-
Expression textValue = getTextValue(i, x);
140+
Expression textValue = getTextValue(x);
89141

90142
if (x.ShouldTrim)
91143
textValue = Trim(textValue);
@@ -115,7 +167,7 @@ private static BlockExpression MountSetProperties(
115167

116168
var assign = Expression.Assign(replacer.Visit(x.prop), valueToBeSetExpression);
117169

118-
assignsExpressions.Add(assign);
170+
assignsExpressions.Add(assignHandler?.Invoke(x, assign) ?? assign);
119171
}
120172

121173
assignsExpressions.Add(objectParameter);

0 commit comments

Comments
 (0)