Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 9 additions & 9 deletions RecordParser.Test/FileReaderTest.cs
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@
// construct a CSV with a header row
// and a *single* data row, where the 4th column contains a large inlined, CSV file enclosed in quotes.
// this is an extreme case, but is a valid CSV according to the spec.
var tw = new StringWriter();
using var tw = new StringWriter();
tw.WriteLine("A,B,C,D");
tw.Write("1,2,3,\"");

Expand All @@ -51,7 +51,7 @@
tw.WriteLine("\"");

var fileContent = tw.ToString();
var reader = new StringReader(fileContent);
using var reader = new StringReader(fileContent);

// Act

Expand Down Expand Up @@ -112,7 +112,7 @@
[InlineData(13, 5)]
[InlineData(14, 7)]
[InlineData(15, 7)]
public void Given_record_is_too_large_for_custom_buffer_size_then_exception_should_be_throw(int bufferSize, int canRead)

Check warning on line 115 in RecordParser.Test/FileReaderTest.cs

View workflow job for this annotation

GitHub Actions / dotnet test

Theory method 'Given_record_is_too_large_for_custom_buffer_size_then_exception_should_be_throw' on test class 'FileReaderTest' does not use parameter 'bufferSize'. (https://xunit.github.io/xunit.analyzers/rules/xUnit1026)

Check warning on line 115 in RecordParser.Test/FileReaderTest.cs

View workflow job for this annotation

GitHub Actions / dotnet test

Theory method 'Given_record_is_too_large_for_custom_buffer_size_then_exception_should_be_throw' on test class 'FileReaderTest' does not use parameter 'bufferSize'. (https://xunit.github.io/xunit.analyzers/rules/xUnit1026)
{
// Arrange

Expand All @@ -138,7 +138,7 @@
(88,89,90,91),
};

var reader = new StringReader(fileContent);
using var reader = new StringReader(fileContent);

var parser = new VariableLengthReaderSequentialBuilder<(int A, int B, int C, int D)>()
.Map(x => x.A)
Expand Down Expand Up @@ -224,7 +224,7 @@

[Theory]
[MemberData(nameof(Given_quoted_csv_file_should_read_quoted_properly_theory), new object[] { "AllFieldsQuotedCsv.csv" })]
public void Read_csv_file_all_fields_quoted(string fileContent, bool hasHeader, bool parallelProcessing, bool blankLineAtEnd, int repeat)

Check warning on line 227 in RecordParser.Test/FileReaderTest.cs

View workflow job for this annotation

GitHub Actions / dotnet test

Theory method 'Read_csv_file_all_fields_quoted' on test class 'FileReaderTest' does not use parameter 'blankLineAtEnd'. (https://xunit.github.io/xunit.analyzers/rules/xUnit1026)

Check warning on line 227 in RecordParser.Test/FileReaderTest.cs

View workflow job for this annotation

GitHub Actions / dotnet test

Theory method 'Read_csv_file_all_fields_quoted' on test class 'FileReaderTest' does not use parameter 'blankLineAtEnd'. (https://xunit.github.io/xunit.analyzers/rules/xUnit1026)
{
// Arrange

Expand Down Expand Up @@ -274,7 +274,7 @@

[Theory]
[MemberData(nameof(Given_quoted_csv_file_should_read_quoted_properly_theory), new object[] { "QuotedCsv.csv" })]
public void Read_quoted_csv_file(string fileContent, bool hasHeader, bool parallelProcessing, bool blankLineAtEnd, int repeat)

Check warning on line 277 in RecordParser.Test/FileReaderTest.cs

View workflow job for this annotation

GitHub Actions / dotnet test

Theory method 'Read_quoted_csv_file' on test class 'FileReaderTest' does not use parameter 'blankLineAtEnd'. (https://xunit.github.io/xunit.analyzers/rules/xUnit1026)

Check warning on line 277 in RecordParser.Test/FileReaderTest.cs

View workflow job for this annotation

GitHub Actions / dotnet test

Theory method 'Read_quoted_csv_file' on test class 'FileReaderTest' does not use parameter 'blankLineAtEnd'. (https://xunit.github.io/xunit.analyzers/rules/xUnit1026)
{
// Arrange

Expand Down Expand Up @@ -364,7 +364,7 @@

[Theory]
[MemberData(nameof(Given_not_quoted_csv_file_should_read_quoted_properly_theory))]
public void Read_not_quoted_csv_file(string fileContent, bool hasHeader, bool parallelProcessing, bool blankLineAtEnd, bool containgQuote, int repeat)

Check warning on line 367 in RecordParser.Test/FileReaderTest.cs

View workflow job for this annotation

GitHub Actions / dotnet test

Theory method 'Read_not_quoted_csv_file' on test class 'FileReaderTest' does not use parameter 'blankLineAtEnd'. (https://xunit.github.io/xunit.analyzers/rules/xUnit1026)

Check warning on line 367 in RecordParser.Test/FileReaderTest.cs

View workflow job for this annotation

GitHub Actions / dotnet test

Theory method 'Read_not_quoted_csv_file' on test class 'FileReaderTest' does not use parameter 'blankLineAtEnd'. (https://xunit.github.io/xunit.analyzers/rules/xUnit1026)
{
// Arrange

Expand Down Expand Up @@ -453,7 +453,7 @@

[Theory]
[MemberData(nameof(Given_fixed_length_file_should_read_quoted_properly_theory))]
public void Read_fixed_length_file(string fileContent, bool parallelProcessing, bool blankLineAtEnd, int repeat)

Check warning on line 456 in RecordParser.Test/FileReaderTest.cs

View workflow job for this annotation

GitHub Actions / dotnet test

Theory method 'Read_fixed_length_file' on test class 'FileReaderTest' does not use parameter 'blankLineAtEnd'. (https://xunit.github.io/xunit.analyzers/rules/xUnit1026)

Check warning on line 456 in RecordParser.Test/FileReaderTest.cs

View workflow job for this annotation

GitHub Actions / dotnet test

Theory method 'Read_fixed_length_file' on test class 'FileReaderTest' does not use parameter 'blankLineAtEnd'. (https://xunit.github.io/xunit.analyzers/rules/xUnit1026)
{
// Arrange

Expand Down Expand Up @@ -538,7 +538,7 @@

[Theory]
[MemberData(nameof(Given_fixed_length_file_should_read_quoted_properly_theory))]
public void Read_plain_text_of_fixed_length_file(string fileContent, bool parallelProcessing, bool blankLineAtEnd, int repeat)

Check warning on line 541 in RecordParser.Test/FileReaderTest.cs

View workflow job for this annotation

GitHub Actions / dotnet test

Theory method 'Read_plain_text_of_fixed_length_file' on test class 'FileReaderTest' does not use parameter 'repeat'. (https://xunit.github.io/xunit.analyzers/rules/xUnit1026)

Check warning on line 541 in RecordParser.Test/FileReaderTest.cs

View workflow job for this annotation

GitHub Actions / dotnet test

Theory method 'Read_plain_text_of_fixed_length_file' on test class 'FileReaderTest' does not use parameter 'blankLineAtEnd'. (https://xunit.github.io/xunit.analyzers/rules/xUnit1026)

Check warning on line 541 in RecordParser.Test/FileReaderTest.cs

View workflow job for this annotation

GitHub Actions / dotnet test

Theory method 'Read_plain_text_of_fixed_length_file' on test class 'FileReaderTest' does not use parameter 'parallelProcessing'. (https://xunit.github.io/xunit.analyzers/rules/xUnit1026)

Check warning on line 541 in RecordParser.Test/FileReaderTest.cs

View workflow job for this annotation

GitHub Actions / dotnet test

Theory method 'Read_plain_text_of_fixed_length_file' on test class 'FileReaderTest' does not use parameter 'repeat'. (https://xunit.github.io/xunit.analyzers/rules/xUnit1026)

Check warning on line 541 in RecordParser.Test/FileReaderTest.cs

View workflow job for this annotation

GitHub Actions / dotnet test

Theory method 'Read_plain_text_of_fixed_length_file' on test class 'FileReaderTest' does not use parameter 'blankLineAtEnd'. (https://xunit.github.io/xunit.analyzers/rules/xUnit1026)

Check warning on line 541 in RecordParser.Test/FileReaderTest.cs

View workflow job for this annotation

GitHub Actions / dotnet test

Theory method 'Read_plain_text_of_fixed_length_file' on test class 'FileReaderTest' does not use parameter 'parallelProcessing'. (https://xunit.github.io/xunit.analyzers/rules/xUnit1026)
{
// Arrange

Expand Down Expand Up @@ -639,7 +639,7 @@
88,89,90,91
""";

var reader = new StringReader(fileContent);
using var reader = new StringReader(fileContent);
var expected = new RegularCaseRecord[]
{
new(10,20,30,40),
Expand Down Expand Up @@ -679,7 +679,7 @@
2020.05.23 ; son name ; 1980.01.15 ; mother name
""";

var reader = new StringReader(fileContent);
using var reader = new StringReader(fileContent);
var expected = new Person[]
{
new Person
Expand Down Expand Up @@ -721,7 +721,7 @@
99 ; 2020.05.23 ; son name ; 100 ; 1980.01.15 ; mother name
""";

var reader = new StringReader(fileContent);
using var reader = new StringReader(fileContent);
var expected = new PersonDerivated[]
{
new ()
Expand Down Expand Up @@ -769,7 +769,7 @@
99 ; 2020.05.23 ; son name ; 100 ; 1980.01.15 ; mother name
""";

var reader = new StringReader(fileContent);
using var reader = new StringReader(fileContent);
var expected = new Person[]
{
new ()
Expand Down Expand Up @@ -824,7 +824,7 @@
"""
.Replace(",", separator);

var reader = new StringReader(fileContent);
using var reader = new StringReader(fileContent);
var expected = new RegularCaseRecord[]
{
new(1,2,3,4),
Expand Down Expand Up @@ -888,7 +888,7 @@
"""
.Replace(",", fileSeparator);

var reader = new StringReader(fileContent);
using var reader = new StringReader(fileContent);
var expected = new RegularCaseRecord[]
{
new(1,2,3,4),
Expand Down
128 changes: 127 additions & 1 deletion RecordParser.Test/FileWriterTest.cs
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
using System.IO;
using System.Linq;
using Xunit;
using static RecordParser.Test.FileReaderTest;

namespace RecordParser.Test
{
Expand Down Expand Up @@ -95,6 +96,131 @@ public void Write_csv_file(int repeat, bool parallel, bool ordered)
items.Should().BeEquivalentTo(expectedItems, cfg => cfg.WithStrictOrdering());
else
items.Should().BeEquivalentTo(expectedItems);
}
}

[Fact]
public void Write_csv_file_with_autobind_should_support_regular_properties()
{
// Arrange

var items = new RegularCaseRecord[]
{
new(1,2,3,4),
new(5,6,7,8),
new(9,10,11,12),
new(13,14,15,16),
new(87,88,89,100),
new(89,99,100,101),
new(88,89,90,91),
};

var expected = $"""
Aaa;Bbb;Ccc;Ddd
1;2;3;4
5;6;7;8
9;10;11;12
13;14;15;16
87;88;89;100
89;99;100;101
88;89;90;91

""";

// Act

using var memory = new MemoryStream();
using var textWriter = new StreamWriter(memory);

textWriter.WriteRecords(items);
textWriter.Flush();

// Assert

memory.Seek(0, SeekOrigin.Begin);
using var textReader = new StreamReader(memory);
var content = textReader.ReadToEnd();
content.Should<string>().Be(expected);
}

[Fact]
public void Write_csv_file_with_autobind_should_respect_max_depth_configuration()
{
// Arrange

var items = new Person[]
{
new("Bob",22,new("CPF", "123")),
new("Carla",26, new("Other", "ABC")),
};

var expected = $"""
Name;Age
Bob;22
Carla;26

""";

// Act

using var memory = new MemoryStream();
using var textWriter = new StreamWriter(memory);
var options = new VariableLengthWriterAutoBindOptions
{
MaxDepth = 1
};

textWriter.WriteRecords(items, options);
textWriter.Flush();

// Assert

memory.Seek(0, SeekOrigin.Begin);
using var textReader = new StreamReader(memory);
var content = textReader.ReadToEnd();
content.Should<string>().Be(expected);
}


[Fact]
public void Write_csv_file_with_autobind_should_support_nested_properties()
{
// Arrange

var items = new Person[]
{
new("Bob",22,new("CPF", "123")),
new("Carla",26, new("Other", "ABC")),
};

var expected = $"""
Name;Age;Document.Type;Document.Value
Bob;22;CPF;123
Carla;26;Other;ABC

""";

// Act

using var memory = new MemoryStream();
using var textWriter = new StreamWriter(memory);

textWriter.WriteRecords(items);
textWriter.Flush();

// Assert

memory.Seek(0, SeekOrigin.Begin);
using var textReader = new StreamReader(memory);
var content = textReader.ReadToEnd();
content.Should<string>().Be(expected);

memory.Seek(0, SeekOrigin.Begin);
var opt = new VariableLengthReaderAutoBindOptions() { HasHeader = true };
var readItems = textReader.ReadRecords<Person>(opt);
readItems.Should().BeEquivalentTo(items);
}

public record class Person(string Name, int Age, Documento Document);
public record class Documento(string Type, string Value);
}
}
3 changes: 3 additions & 0 deletions RecordParser/Engines/Reader/PrimitiveTypeReaderEngine.cs
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,9 @@ static PrimitiveTypeReaderEngine()
dic = mapping;
}

public static bool IsPrimitiveType(Type type) =>
type.IsPrimitive || type.IsEnum || dic.Any(kvp => kvp.Key.to == type);

private static char ToChar(ReadOnlySpan<char> span) => span[0];

private static void AddMapForReadOnlySpan<T>(
Expand Down
123 changes: 122 additions & 1 deletion RecordParser/Extensions/FileWriter/WriterExtensions.cs
Original file line number Diff line number Diff line change
@@ -1,8 +1,13 @@
using System;
using RecordParser.Builders.Writer;
using RecordParser.Engines.Reader;
using RecordParser.Parsers;
using System;
using System.Buffers;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Linq.Expressions;
using System.Reflection;

namespace RecordParser.Extensions
{
Expand All @@ -18,6 +23,19 @@ namespace RecordParser.Extensions
/// </returns>
public delegate bool TryFormat<T>(T instance, Span<char> destination, out int charsWritten);

public record class VariableLengthWriterAutoBindOptions
{
/// <summary>
/// Maximum depth to search for nested properties.
/// </summary>
public int MaxDepth { get; set; } = 3;

/// <summary>
/// Options to configure parallel processing
/// </summary>
public ParallelismOptions ParallelismOptions { get; set; }
}

public static class WriterExtensions
{
private const int initialPow = 10;
Expand Down Expand Up @@ -118,5 +136,108 @@ private static void WriteSequential<T>(TextWriter textWriter, IEnumerable<T> ite
ArrayPool<char>.Shared.Return(buffer);
}
}

private class NodeState
{
public Type Type { get; set; }
public Expression CurrentExpression { get; set; }
public int Depth { get; set; }
}

/// <summary>
/// Writes each item of the sequence as a csv record into the <paramref name="textWriter"/> as well the header.
/// </summary>
/// <typeparam name="T">Type of items in the sequence.</typeparam>
/// <param name="textWriter">The TextWriter where the items will be written into.</param>
/// <param name="items">Sequence of the elements.</param>
public static void WriteRecords<T>(this TextWriter textWriter, IEnumerable<T> items) =>
WriteRecords(textWriter, items, default(VariableLengthWriterAutoBindOptions));

/// <summary>
/// Writes each item of the sequence as a csv record into the <paramref name="textWriter"/> as well the header.
/// </summary>
/// <typeparam name="T">Type of items in the sequence.</typeparam>
/// <param name="textWriter">The TextWriter where the items will be written into.</param>
/// <param name="items">Sequence of the elements.</param>
/// <param name="options">Options to configure autobind processing.</param>
public static void WriteRecords<T>(this TextWriter textWriter, IEnumerable<T> items, VariableLengthWriterAutoBindOptions options)
{
options ??= new();
var parallel = options.ParallelismOptions ?? new();

const string separator = ";";
var members = GetPropertyExpressions(typeof(T), options.MaxDepth);
var builder = new VariableLengthWriterSequentialBuilder<T>();

foreach (var item in members.Select(x => x.exp))
if (item.ReturnType == typeof(string))
builder.Map((dynamic)item, converter: default(FuncSpanTIntBool));
else
builder.Map((dynamic)item);

var parser = builder.Build(separator);
var header = string.Join(separator, members.Select(x => x.column));

textWriter.WriteLine(header);

WriteRecords(textWriter, items, parser.TryFormat, parallel);
}

private static IReadOnlyList<(LambdaExpression exp, string column)> GetPropertyExpressions(Type type, int maxDepth)
{
var expressions = new List<(LambdaExpression, string)>();

if (maxDepth < 1)
return expressions;

var paramText = Guid.NewGuid().ToString();
var rootParameter = Expression.Parameter(type, paramText);

var queue = new Queue<NodeState>();

queue.Enqueue(new NodeState
{
Type = type,
CurrentExpression = rootParameter,
Depth = 1
});

// loop BFS
while (queue.Count > 0)
{
var currentState = queue.Dequeue();

if (currentState.Depth > maxDepth)
continue;

var properties = currentState.Type.GetProperties(BindingFlags.Public | BindingFlags.Instance);

foreach (var prop in properties)
{
if (prop.CanRead == false)
continue;

var propertyAccess = Expression.Property(currentState.CurrentExpression, prop);

if (PrimitiveTypeReaderEngine.IsPrimitiveType(prop.PropertyType))
{
var lambda = Expression.Lambda(propertyAccess, rootParameter);
var column = propertyAccess.ToString().Replace(paramText + ".", string.Empty);
expressions.Add((lambda, column));
}
else
{
queue.Enqueue(new NodeState
{
Type = prop.PropertyType,
CurrentExpression = propertyAccess,
Depth = currentState.Depth + 1
});
}
}
}

return expressions;
}
}
}
Loading