Skip to content

Commit

Permalink
feat: add a descriptor for the runtime type parser (#113)
Browse files Browse the repository at this point in the history
* feat: add a descriptor for the runtime type parser
* docs: review for all fluent APIs
---------

Co-authored-by: codefactor-io <[email protected]>
  • Loading branch information
Seddryck and code-factor authored Feb 9, 2025
1 parent 59697bc commit f2fcf59
Show file tree
Hide file tree
Showing 21 changed files with 446 additions and 68 deletions.
13 changes: 13 additions & 0 deletions PocketCsvReader.Testing/Configuration/CsvReaderBuilderTest.cs
Original file line number Diff line number Diff line change
Expand Up @@ -66,4 +66,17 @@ public void WithSchema_ShouldSetSchema()
Assert.That(reader.Profile.Schema, Is.Not.Null);
Assert.That(reader.Profile.Schema!.Fields, Is.Not.Null.And.Not.Empty);
}

[Test]
public void WithParsers_ShouldSetParsers()
{
var builder = new CsvReaderBuilder().WithParsers
(
new RuntimeParsersDescriptorBuilder()
.WithParser((string s) => s.Equals(s, StringComparison.InvariantCultureIgnoreCase))
);
var reader = builder.Build();
Assert.That(reader.Profile.Parsers, Is.Not.Null);
Assert.That(reader.Profile.Parsers!.Count, Is.EqualTo(1));
}
}
6 changes: 1 addition & 5 deletions PocketCsvReader.Testing/CsvDataReaderTest.cs
Original file line number Diff line number Diff line change
@@ -1,16 +1,12 @@
using PocketCsvReader;
using NUnit.Framework;
using NUnit.Framework;
using System;
using System.Collections.Generic;
using System.Data;
using System.IO;
using System.Linq;
using System.Text;
using System.Reflection;
using System.Buffers;
using Microsoft.VisualStudio.TestPlatform.PlatformAbstractions.Interfaces;
using PocketCsvReader.Configuration;
using System.Globalization;

namespace PocketCsvReader.Testing;

Expand Down
92 changes: 88 additions & 4 deletions PocketCsvReader.Testing/CsvDataRecordTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -64,24 +64,108 @@ YearMonth parse(string input)
[TestCase("yyyy.MM", "2025.01")]
[TestCase("MM.yyyy", "01.2025")]
public void GetValue_RegisteredGlobally_NotParsable_Correct(string format, string input)
{
YearMonth parse(string input)
{
(int year, int month) = new YearMonthParser().Parse(input, format, CultureInfo.InvariantCulture);
return new YearMonth(year, month);
}

var profile = new CsvProfile(
new DialectDescriptorBuilder().Build()
, new SchemaDescriptorBuilder()
.Indexed()
.WithField<YearMonth>()
.Build()
, null
, new RuntimeParsersDescriptorBuilder()
.WithParser(parse)
.Build()
);

var record = new CsvDataRecord(new RecordMemory(input, [new FieldSpan(0, input.Length)]), profile);
var value = record.GetValue(0);
Assert.That(value, Is.EqualTo(new YearMonth(2025, 1)));
}


[Test]
[TestCase("yyyy-MM", "2025-01")]
[TestCase("yyyy-M", "2025-1")]
[TestCase("yyyy.MM", "2025.01")]
[TestCase("MM.yyyy", "01.2025")]
public void GetValue_AutoDiscoveryParsable_Correct(string format, string input)
{
var culture = new CultureInfo("en-US");
culture.DateTimeFormat.YearMonthPattern = format;

var profile = new CsvProfile(
new DialectDescriptorBuilder().Build()
, new SchemaDescriptorBuilder().Indexed().WithTemporalField<YearMonth>(
tf => tf.WithFormat(format)
).Build()
);

var record = new CsvDataRecord(new RecordMemory(input, [new FieldSpan(0, input.Length)]), profile);
var value = record.GetValue(0);
Assert.That(value, Is.EqualTo(new YearMonth(2025, 1)));
}

[Test]
[TestCase("J25")]
public void GetValue_RegisteredWithBuilderNotParsable_Correct(string input)
{
YearMonth parse(string input)
{
(int year, int month) = new YearMonthParser().Parse(input, format, culture);
return new YearMonth(year, month);
if (input[0] == 'J' && input.EndsWith("25"))
return new YearMonth(2025, 1);
throw new ArgumentException();
}

var profile = new CsvProfile(
new DialectDescriptorBuilder().Build()
, new SchemaDescriptorBuilder().Indexed().WithField<YearMonth>().Build()
, new SchemaDescriptorBuilder().Indexed().WithTemporalField<YearMonth>(
tf => tf.WithParser((str) => parse(str))
).Build()
);

var record = new CsvDataRecord(new RecordMemory(input, [new FieldSpan(0, input.Length)]), profile);
record.Register(parse);
var value = record.GetValue(0);
Assert.That(value, Is.EqualTo(new YearMonth(2025, 1)));
}


[Test]
[TestCase("J25;F25")]
public void GetValue_RegisteredWithManyBuilderNotParsable_Correct(string input)
{
YearMonth parseAlpha(string input)
{
if (input[0] == 'J' && input.EndsWith("25"))
return new YearMonth(2025, 1);
throw new ArgumentException();
}

YearMonth parseBeta(string input)
{
if (input[0] == 'F' && input.EndsWith("25"))
return new YearMonth(2025, 2);
throw new ArgumentException();
}

var profile = new CsvProfile(
new DialectDescriptorBuilder().WithDelimiter(';').Build()
, new SchemaDescriptorBuilder().Indexed()
.WithTemporalField<YearMonth>(
tf => tf.WithParser((str) => parseAlpha(str))
)
.WithTemporalField<YearMonth>(
tf => tf.WithParser((str) => parseBeta(str))
).Build()
);

var record = new CsvDataRecord(new RecordMemory(input, [new FieldSpan(0, 3), new FieldSpan(4,3)]), profile);
Assert.That(record.GetValue(0), Is.EqualTo(new YearMonth(2025, 1)));
Assert.That(record.GetValue(1), Is.EqualTo(new YearMonth(2025, 2)));
}
}
16 changes: 14 additions & 2 deletions PocketCsvReader/Configuration/CsvReaderBuilder.cs
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ public class CsvReaderBuilder
private DialectDescriptorBuilder _dialectBuilder = new();
private ISchemaDescriptorBuilder? _schemaBuilder;
private ResourceDescriptorBuilder? _resourceBuilder;
private RuntimeParsersDescriptorBuilder? _parserBuilder;

public CsvReaderBuilder WithDialect(Func<DialectDescriptorBuilder, DialectDescriptorBuilder> func)
{
Expand All @@ -34,7 +35,6 @@ public CsvReaderBuilder WithSchema(ISchemaDescriptorBuilder schemaBuilder)
return this;
}


public CsvReaderBuilder WithResource(Func<ResourceDescriptorBuilder, ResourceDescriptorBuilder> func)
{
_resourceBuilder = func(new());
Expand All @@ -47,6 +47,18 @@ public CsvReaderBuilder WithResource(ResourceDescriptorBuilder resourceBuilder)
return this;
}

public CsvReaderBuilder WithParsers(Func<RuntimeParsersDescriptorBuilder, RuntimeParsersDescriptorBuilder> func)
{
_parserBuilder = func(new());
return this;
}

public CsvReaderBuilder WithParsers(RuntimeParsersDescriptorBuilder parserBuilder)
{
_parserBuilder = parserBuilder;
return this;
}

public CsvReader Build()
=> new (new CsvProfile(_dialectBuilder.Build(), _schemaBuilder?.Build(), _resourceBuilder?.Build()));
=> new (new CsvProfile(_dialectBuilder.Build(), _schemaBuilder?.Build(), _resourceBuilder?.Build(), _parserBuilder?.Build()));
}
2 changes: 2 additions & 0 deletions PocketCsvReader/Configuration/CustomFieldDescriptorBuilder.cs
Original file line number Diff line number Diff line change
Expand Up @@ -25,4 +25,6 @@ public CustomFieldDescriptorBuilder WithFormat(string pattern, IFormatProvider?

public new CustomFieldDescriptorBuilder WithDataSourceTypeName(string typeName)
=> (CustomFieldDescriptorBuilder)base.WithDataSourceTypeName(typeName);
public new CustomFieldDescriptorBuilder WithParser(ParseFunction parse)
=> (CustomFieldDescriptorBuilder)base.WithParser(parse);
}
1 change: 1 addition & 0 deletions PocketCsvReader/Configuration/FieldDescriptor.cs
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ public record FieldDescriptor
Type RuntimeType
, string? Name = null
, IFormatDescriptor? Format = null
, ParseFunction? Parse = null
, ImmutableSequenceCollection? Sequences = null
, string DataSourceTypeName = ""
)
Expand Down
8 changes: 7 additions & 1 deletion PocketCsvReader/Configuration/FieldDescriptorBuilder.cs
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ public class FieldDescriptorBuilder
private Dictionary<Type, FormatDescriptorBuilder> DefaultFormatBuilders = new();
protected Type _runtimeType;
protected FormatDescriptorBuilder? _format;
protected ParseFunction? _parse;
protected string? _name;
protected SequenceCollection? _sequences;
protected string? _dataSourceTypeName;
Expand Down Expand Up @@ -57,6 +58,11 @@ public FieldDescriptorBuilder WithDataSourceTypeName(string typeName)
return this;
}

public FieldDescriptorBuilder WithParser(ParseFunction parse)
{
_parse = parse;
return this;
}

private FormatDescriptorBuilder GetDefaultFormat()
{
Expand All @@ -67,6 +73,6 @@ private FormatDescriptorBuilder GetDefaultFormat()

public virtual FieldDescriptor Build()
{
return new FieldDescriptor(_runtimeType, _name, (_format ?? GetDefaultFormat()).Build(), _sequences?.ToImmutable(), _dataSourceTypeName ?? string.Empty);
return new FieldDescriptor(_runtimeType, _name, (_format ?? GetDefaultFormat()).Build(), _parse, _sequences?.ToImmutable(), _dataSourceTypeName ?? string.Empty);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -24,4 +24,6 @@ public IntegerFieldDescriptorBuilder WithFormat(Func<IntegerFormatDescriptorBuil

public new IntegerFieldDescriptorBuilder WithDataSourceTypeName(string typeName)
=> (IntegerFieldDescriptorBuilder)base.WithDataSourceTypeName(typeName);
public new IntegerFieldDescriptorBuilder WithParser(ParseFunction parse)
=> (IntegerFieldDescriptorBuilder)base.WithParser(parse);
}
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,9 @@ public NumberFieldDescriptorBuilder WithFormat(Func<NumberFormatDescriptorBuilde

public new NumberFieldDescriptorBuilder WithDataSourceTypeName(string typeName)
=> (NumberFieldDescriptorBuilder)base.WithDataSourceTypeName(typeName);
public new NumberFieldDescriptorBuilder WithParser(ParseFunction parse)
=> (NumberFieldDescriptorBuilder)base.WithParser(parse);

public override FieldDescriptor Build()
=> new FieldDescriptor(_runtimeType, _name, _format?.Build(), _sequences?.ToImmutable(), _dataSourceTypeName ?? string.Empty);
=> new FieldDescriptor(_runtimeType, _name, _format?.Build(), _parse, _sequences?.ToImmutable(), _dataSourceTypeName ?? string.Empty);
}
9 changes: 9 additions & 0 deletions PocketCsvReader/Configuration/ParseFunction.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace PocketCsvReader.Configuration;
public delegate object ParseFunction(string input);
public delegate T ParseFunction<T>(string input);
31 changes: 31 additions & 0 deletions PocketCsvReader/Configuration/RuntimeParsersDescriptor.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
using System;
using System.Collections;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace PocketCsvReader.Configuration;
public class RuntimeParsersDescriptor : IEnumerable<KeyValuePair<Type, ParseFunction>>
{
private Dictionary<Type, ParseFunction> Parsers { get; init; } = [];

public void AddParser<T>(ParseFunction<T> parse)
=> AddParser(typeof(T), (string str) => parse.Invoke(str)!);

public void AddParser(Type type, ParseFunction parse)
{
var returnType = parse.Method.ReturnType;
if (!type.IsAssignableTo(returnType))
throw new ArgumentException($"The provided parser returns {returnType}, which is not assignable from {type}.");

Parsers.Add(type, parse);
}

public int Count => Parsers.Count;

public IEnumerator<KeyValuePair<Type, ParseFunction>> GetEnumerator()
=> Parsers.GetEnumerator();
IEnumerator IEnumerable.GetEnumerator()
=> GetEnumerator();
}
37 changes: 37 additions & 0 deletions PocketCsvReader/Configuration/RuntimeParsersDescriptorBuilder.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
using System;
using System.Collections.Generic;
using System.ComponentModel.Design;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Xml.Linq;

namespace PocketCsvReader.Configuration;

public class RuntimeParsersDescriptorBuilder
{
private Dictionary<Type, ParseFunction> _types = new();
private RuntimeParsersDescriptor? Descriptor { get; set; }

public RuntimeParsersDescriptorBuilder WithParser<T>(ParseFunction<T> parse)
=> WithParser(typeof(T), (string str) => parse.Invoke(str)!);

public RuntimeParsersDescriptorBuilder WithParser(Type type, ParseFunction parse)
{
var returnType = parse.Method.ReturnType;
if (!type.IsAssignableTo(returnType))
throw new ArgumentException($"The provided parser returns {returnType}, which is not assignable from {type}.");

if (!_types.TryAdd(type, parse))
_types[type] = parse;
return this;
}

public RuntimeParsersDescriptor? Build()
{
Descriptor = new RuntimeParsersDescriptor();
foreach (var type in _types)
Descriptor.AddParser(type.Key, type.Value);
return Descriptor.Count > 0 ? Descriptor : null;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -26,4 +26,7 @@ public TemporalFieldDescriptorBuilder WithFormat(string pattern, Func<TemporalFo

public new TemporalFieldDescriptorBuilder WithDataSourceTypeName(string typeName)
=> (TemporalFieldDescriptorBuilder)base.WithDataSourceTypeName(typeName);

public new TemporalFieldDescriptorBuilder WithParser(ParseFunction parse)
=> (TemporalFieldDescriptorBuilder)base.WithParser(parse);
}
Loading

0 comments on commit f2fcf59

Please sign in to comment.