diff --git a/.gitignore b/.gitignore index 6feb516..b36df8f 100644 --- a/.gitignore +++ b/.gitignore @@ -157,3 +157,5 @@ $RECYCLE.BIN/ assets/FlatFile.Core.Compiled.nuspec assets/FlatFile.Delimited.Compiled.nuspec assets/FlatFile.FixedLength.Compiled.nuspec + +.vs/ \ No newline at end of file diff --git a/LICENSE b/LICENSE index 057e902..4369777 100644 --- a/LICENSE +++ b/LICENSE @@ -1,5 +1,6 @@ The MIT License (MIT) +Copyright (c) 2018 Matt Hamilton Copyright (c) 2014 Pavel Nosovich Permission is hereby granted, free of charge, to any person obtaining a copy diff --git a/appveyor.yml b/appveyor.yml index 2eb7710..24774ec 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -1,5 +1,5 @@ - - version: 0.2.{build} + version: 0.3.{build} environment: BuildEnvironment: appveyor diff --git a/src/FlatFile.Benchmark/Converters/CsvHelperTypeConverterForCustomType.cs b/src/FlatFile.Benchmark/Converters/CsvHelperTypeConverterForCustomType.cs index 8f82fab..75ac847 100644 --- a/src/FlatFile.Benchmark/Converters/CsvHelperTypeConverterForCustomType.cs +++ b/src/FlatFile.Benchmark/Converters/CsvHelperTypeConverterForCustomType.cs @@ -14,12 +14,12 @@ public CsvHelperTypeConverterForCustomType() public string ConvertToString(CsvHelperTypeConversion.TypeConverterOptions options, object value) { - return converter.ConvertToString(value); + return converter.ConvertToString(value, null); } public object ConvertFromString(CsvHelperTypeConversion.TypeConverterOptions options, string text) { - return converter.ConvertFromString(text); + return converter.ConvertFromString(text, null); } public bool CanConvertFrom(Type type) diff --git a/src/FlatFile.Benchmark/Converters/FlatFileTypeConverterForCustomType.cs b/src/FlatFile.Benchmark/Converters/FlatFileTypeConverterForCustomType.cs index 148e297..3c6abcb 100644 --- a/src/FlatFile.Benchmark/Converters/FlatFileTypeConverterForCustomType.cs +++ b/src/FlatFile.Benchmark/Converters/FlatFileTypeConverterForCustomType.cs @@ -1,6 +1,7 @@ namespace FlatFile.Benchmark.Converters { using System; + using System.Reflection; using FlatFile.Benchmark.Entities; using FlatFile.Core; @@ -16,13 +17,13 @@ public bool CanConvertTo(Type type) return type == typeof (CustomType); } - public string ConvertToString(object source) + public string ConvertToString(object source, PropertyInfo sourceProperty) { var obj = (CustomType)source; return string.Format("{0}|{1}|{2}", obj.First, obj.Second, obj.Third); } - public object ConvertFromString(string source) + public object ConvertFromString(string source, PropertyInfo targetProperty) { var values = source.Split('|'); diff --git a/src/FlatFile.Core/Base/FlatFileEngine.cs b/src/FlatFile.Core/Base/FlatFileEngine.cs index 22d18a3..80e9aa2 100644 --- a/src/FlatFile.Core/Base/FlatFileEngine.cs +++ b/src/FlatFile.Core/Base/FlatFileEngine.cs @@ -18,13 +18,13 @@ public abstract class FlatFileEngine : IFlatF /// /// The handle entry read error func /// - private readonly Func _handleEntryReadError; + private readonly Func _handleEntryReadError; /// /// Gets the line builder. /// /// The line builder. - protected abstract ILineBulder LineBuilder { get; } + protected abstract ILineBuilder LineBuilder { get; } /// /// Gets the line parser. @@ -42,7 +42,7 @@ public abstract class FlatFileEngine : IFlatF /// Initializes a new instance of the class. /// /// The handle entry read error. - protected FlatFileEngine(Func handleEntryReadError = null) + protected FlatFileEngine(Func handleEntryReadError = null) { _handleEntryReadError = handleEntryReadError; } @@ -57,6 +57,18 @@ protected FlatFileEngine(Func handleEntryReadError = nu public virtual IEnumerable Read(Stream stream) where TEntity : class, new() { var reader = new StreamReader(stream); + return Read(reader); + } + + /// + /// Reads the specified text reader. + /// + /// The type of the t entity. + /// The reader. + /// IEnumerable<TEntity>. + /// Impossible to parse line + public virtual IEnumerable Read(TextReader reader) where TEntity : class, new() + { string line; int lineNumber = 0; @@ -68,7 +80,7 @@ protected FlatFileEngine(Func handleEntryReadError = nu while ((line = reader.ReadLine()) != null) { if (string.IsNullOrEmpty(line) || string.IsNullOrEmpty(line.Trim())) continue; - + bool ignoreEntry = false; var entry = new TEntity(); try @@ -85,7 +97,7 @@ protected FlatFileEngine(Func handleEntryReadError = nu throw; } - if (!_handleEntryReadError(line, ex)) + if (!_handleEntryReadError(new FlatFileErrorContext(line, lineNumber, ex))) { throw; } @@ -104,7 +116,7 @@ protected FlatFileEngine(Func handleEntryReadError = nu /// Processes the header. /// /// The reader. - protected virtual void ProcessHeader(StreamReader reader) + protected virtual void ProcessHeader(TextReader reader) { reader.ReadLine(); } @@ -147,7 +159,17 @@ protected virtual void WriteEntry(TextWriter writer, int lineNumber, TE public virtual void Write(Stream stream, IEnumerable entries) where TEntity : class, new() { TextWriter writer = new StreamWriter(stream); + Write(writer, entries); + } + /// + /// Writes to the specified text writer. + /// + /// The type of the t entity. + /// The text writer. + /// The entries. + public void Write(TextWriter writer, IEnumerable entries) where TEntity : class, new() + { this.WriteHeader(writer); int lineNumber = 0; diff --git a/src/FlatFile.Core/Base/FlatFileErrorContext.cs b/src/FlatFile.Core/Base/FlatFileErrorContext.cs new file mode 100644 index 0000000..eb48237 --- /dev/null +++ b/src/FlatFile.Core/Base/FlatFileErrorContext.cs @@ -0,0 +1,51 @@ +namespace FlatFile.Core.Base +{ + using System; + + /// + /// Provides information about a file parsing error. + /// + public struct FlatFileErrorContext + { + private readonly string line; + private readonly int lineNumber; + private readonly Exception exception; + + /// + /// Initializes a new instance of . + /// + /// The content of the line on which the error occurred. + /// The line numer at which the error occurred. + /// The error that occurred. + public FlatFileErrorContext(string line, int lineNumber, Exception exception) + { + this.line = line; + this.lineNumber = lineNumber; + this.exception = exception; + } + + /// + /// The content of the line on which the error occurred. + /// + public string Line + { + get { return line; } + } + + /// + /// The line numer at which the error occurred. + /// + public int LineNumber + { + get { return lineNumber; } + } + + /// + /// The error that occurred. + /// + public Exception Exception + { + get { return exception; } + } + } +} \ No newline at end of file diff --git a/src/FlatFile.Core/Base/LineBulderBase.cs b/src/FlatFile.Core/Base/LineBuilderBase.cs similarity index 58% rename from src/FlatFile.Core/Base/LineBulderBase.cs rename to src/FlatFile.Core/Base/LineBuilderBase.cs index 1bbfa2d..234b12e 100644 --- a/src/FlatFile.Core/Base/LineBulderBase.cs +++ b/src/FlatFile.Core/Base/LineBuilderBase.cs @@ -1,12 +1,12 @@ namespace FlatFile.Core.Base { - public abstract class LineBulderBase : ILineBulder + public abstract class LineBuilderBase : ILineBuilder where TLayoutDescriptor : ILayoutDescriptor where TFieldSettings : IFieldSettingsContainer { private readonly TLayoutDescriptor _descriptor; - protected LineBulderBase(TLayoutDescriptor descriptor) + protected LineBuilderBase(TLayoutDescriptor descriptor) { this._descriptor = descriptor; } @@ -21,7 +21,7 @@ protected TLayoutDescriptor Descriptor protected virtual string GetStringValueFromField(TFieldSettings field, object fieldValue) { string lineValue = fieldValue != null - ? fieldValue.ToString() + ? ConvertToString(field, fieldValue) : field.NullValue ?? string.Empty; lineValue = TransformFieldValue(field, lineValue); @@ -33,5 +33,14 @@ protected virtual string TransformFieldValue(TFieldSettings field, string lineVa { return lineValue; } + + private static string ConvertToString(TFieldSettings field, object fieldValue) + { + var converter = field.TypeConverter; + if (converter != null && converter.CanConvertTo(typeof(string)) && converter.CanConvertFrom(field.PropertyInfo.PropertyType)) + return field.TypeConverter.ConvertToString(fieldValue, field.PropertyInfo); + + return fieldValue.ToString(); + } } } \ No newline at end of file diff --git a/src/FlatFile.Core/Base/LineParserBase.cs b/src/FlatFile.Core/Base/LineParserBase.cs index 9a802fc..78f2d16 100644 --- a/src/FlatFile.Core/Base/LineParserBase.cs +++ b/src/FlatFile.Core/Base/LineParserBase.cs @@ -1,6 +1,7 @@ namespace FlatFile.Core.Base { using System; + using System.Reflection; using FlatFile.Core.Extensions; public abstract class LineParserBase : ILineParser @@ -39,7 +40,7 @@ protected virtual object GetFieldValueFromString(TFieldSettings fieldSettings, s object obj; - if (!fieldSettings.TypeConverter.ConvertFromStringTo(memberValue, type, out obj)) + if (!fieldSettings.TypeConverter.ConvertFromStringTo(memberValue, type, fieldSettings.PropertyInfo, out obj)) { obj = memberValue.Convert(type); } @@ -56,11 +57,11 @@ protected virtual string TransformStringValue(TFieldSettings fieldSettingsBuilde public static class TypeConverterExtensions { - public static bool ConvertFromStringTo(this ITypeConverter converter, string source, Type targetType, out object obj) + public static bool ConvertFromStringTo(this ITypeConverter converter, string source, Type targetType, PropertyInfo targetProperty, out object obj) { if (converter != null && converter.CanConvertFrom(typeof(string)) && converter.CanConvertTo(targetType)) { - obj = converter.ConvertFromString(source); + obj = converter.ConvertFromString(source, targetProperty); return true; } diff --git a/src/FlatFile.Core/Base/MasterDetailTrackerBase.cs b/src/FlatFile.Core/Base/MasterDetailTrackerBase.cs new file mode 100644 index 0000000..51bd660 --- /dev/null +++ b/src/FlatFile.Core/Base/MasterDetailTrackerBase.cs @@ -0,0 +1,70 @@ +using System; + +namespace FlatFile.Core.Base +{ + /// + /// Uses records that implement and to handle + /// master-detail record relationships. + /// + public class MasterDetailTrackerBase : IMasterDetailTracker + { + /// + /// Determines whether a record is a master record. + /// + readonly Func checkIsMasterRecord; + /// + /// Determines whether a record is a detail record. + /// + readonly Func checkIsDetailRecord; + /// + /// Handles confirmed detail records. + /// + readonly Action handleDetailRecord; + /// + /// The last record parsed that implements + /// + object lastMasterRecord; + + /// + /// Initializes a new instance of the class. + /// + /// Determines whether a record is a master record. + /// Determines whether a record is a detail record. + /// Handles confirmed detail records. + public MasterDetailTrackerBase( + Func checkIsMasterRecord, + Func checkIsDetailRecord, + Action handleDetailRecord) + { + this.checkIsMasterRecord = checkIsMasterRecord; + this.checkIsDetailRecord = checkIsDetailRecord; + this.handleDetailRecord = handleDetailRecord; + } + + public void HandleMasterDetail(object entry, out bool isDetailRecord) + { + isDetailRecord = false; + + if (checkIsMasterRecord(entry)) + { + // Found new master record + lastMasterRecord = entry; + return; + } + + // Record is standalone or unassociated detail record + if (lastMasterRecord == null) return; + + if (!checkIsDetailRecord(entry)) + { + // Record is standalone, reset master + lastMasterRecord = null; + return; + } + + // Add detail record and indicate that it should not be added to the results dictionary + handleDetailRecord(lastMasterRecord, entry); + isDetailRecord = true; + } + } +} \ No newline at end of file diff --git a/src/FlatFile.Core/Base/TypeConverterBase.cs b/src/FlatFile.Core/Base/TypeConverterBase.cs new file mode 100644 index 0000000..9bf82bb --- /dev/null +++ b/src/FlatFile.Core/Base/TypeConverterBase.cs @@ -0,0 +1,36 @@ +using System; +using System.Reflection; + +namespace FlatFile.Core.Base +{ + /// + /// A generic base class for converting between strings and a given type. + /// + /// The type to convert to and from a string. + public abstract class TypeConverterBase : ITypeConverter + { + public virtual bool CanConvertFrom(Type type) + { + return type == typeof(string) || type == typeof(TValue); + } + + public virtual bool CanConvertTo(Type type) + { + return type == typeof(string) || type == typeof(TValue); + } + + public object ConvertFromString(string source, PropertyInfo targetProperty) + { + return ConvertFrom(source, targetProperty); + } + + protected abstract TValue ConvertFrom(string source, PropertyInfo targetProperty); + + public string ConvertToString(object source, PropertyInfo sourceProperty) + { + return ConvertTo((TValue)source, sourceProperty); + } + + protected abstract string ConvertTo(TValue source, PropertyInfo sourceProperty); + } +} diff --git a/src/FlatFile.Core/DelegatingTypeConverter.cs b/src/FlatFile.Core/DelegatingTypeConverter.cs new file mode 100644 index 0000000..f991a77 --- /dev/null +++ b/src/FlatFile.Core/DelegatingTypeConverter.cs @@ -0,0 +1,37 @@ +using System; +using System.Reflection; + +namespace FlatFile.Core +{ + /// + /// An implementation of that uses delegates for conversion. + /// + class DelegatingTypeConverter : ITypeConverter + { + internal Func ConversionFromString { get; set; } + + internal Func ConversionToString { get; set; } + + public bool CanConvertFrom(Type type) + { + return (type == typeof(string) && ConversionFromString != null) || + (type == typeof(TProperty) && ConversionToString != null); + } + + public bool CanConvertTo(Type type) + { + return (type == typeof(string) && ConversionToString != null) || + (type == typeof(TProperty) && ConversionFromString != null); + } + + public object ConvertFromString(string source, PropertyInfo targetProperty) + { + return ConversionFromString(source); + } + + public string ConvertToString(object source, PropertyInfo sourceProperty) + { + return ConversionToString((TProperty)source); + } + } +} diff --git a/src/FlatFile.Core/FlatFile.Core.csproj b/src/FlatFile.Core/FlatFile.Core.csproj index 08ab631..08403b2 100644 --- a/src/FlatFile.Core/FlatFile.Core.csproj +++ b/src/FlatFile.Core/FlatFile.Core.csproj @@ -95,13 +95,17 @@ + + - + + + @@ -117,9 +121,10 @@ - + + diff --git a/src/FlatFile.Core/IFieldSettingsConstructor.cs b/src/FlatFile.Core/IFieldSettingsConstructor.cs index 4acca7e..45e63c7 100644 --- a/src/FlatFile.Core/IFieldSettingsConstructor.cs +++ b/src/FlatFile.Core/IFieldSettingsConstructor.cs @@ -1,11 +1,14 @@ namespace FlatFile.Core { using FlatFile.Core.Base; + using System; public interface IFieldSettingsConstructor : IFieldSettingsContainer where TConstructor : IFieldSettingsConstructor { TConstructor AllowNull(string nullValue); TConstructor WithTypeConverter() where TConverter : ITypeConverter; + TConstructor WithConversionFromString(Func conversion); + TConstructor WithConversionToString(Func conversion); } } \ No newline at end of file diff --git a/src/FlatFile.Core/IFlatFileEngine.cs b/src/FlatFile.Core/IFlatFileEngine.cs index 05a6d0f..b0b8a48 100644 --- a/src/FlatFile.Core/IFlatFileEngine.cs +++ b/src/FlatFile.Core/IFlatFileEngine.cs @@ -16,6 +16,14 @@ public interface IFlatFileEngine /// IEnumerable<TEntity>. IEnumerable Read(Stream stream) where TEntity : class, new(); + /// + /// Reads from the specified text reader. + /// + /// The type of the t entity. + /// The text reader. + /// IEnumerable<TEntity>. + IEnumerable Read(TextReader reader) where TEntity : class, new(); + /// /// Writes to the specified stream. /// @@ -23,5 +31,13 @@ public interface IFlatFileEngine /// The stream. /// The entries. void Write(Stream stream, IEnumerable entries) where TEntity : class, new(); + + /// + /// Writes to the specified text writer. + /// + /// The type of the t entity. + /// The text writer. + /// The entries. + void Write(TextWriter writer, IEnumerable entries) where TEntity : class, new(); } } \ No newline at end of file diff --git a/src/FlatFile.Core/IFlatFileMultiEngine.cs b/src/FlatFile.Core/IFlatFileMultiEngine.cs index a47b36d..c33299d 100644 --- a/src/FlatFile.Core/IFlatFileMultiEngine.cs +++ b/src/FlatFile.Core/IFlatFileMultiEngine.cs @@ -13,12 +13,20 @@ public interface IFlatFileMultiEngine : IFlatFileEngine /// /// The stream. void Read(Stream stream); + + /// + /// Reads the specified text reader. + /// + /// The text reader. + void Read(TextReader reader); + /// /// Gets any records of type read by . /// /// /// IEnumerable<T>. IEnumerable GetRecords() where T : class, new(); + /// /// Gets or sets a value indicating whether this instance has a file header. /// diff --git a/src/FlatFile.Core/ILineBulder.cs b/src/FlatFile.Core/ILineBuilder.cs similarity index 69% rename from src/FlatFile.Core/ILineBulder.cs rename to src/FlatFile.Core/ILineBuilder.cs index 7d3e923..de3ca92 100644 --- a/src/FlatFile.Core/ILineBulder.cs +++ b/src/FlatFile.Core/ILineBuilder.cs @@ -1,6 +1,6 @@ namespace FlatFile.Core { - public interface ILineBulder + public interface ILineBuilder { string BuildLine(T entry); } diff --git a/src/FlatFile.Core/ILineBuilderFactory.cs b/src/FlatFile.Core/ILineBuilderFactory.cs index f2c561b..c649c0d 100644 --- a/src/FlatFile.Core/ILineBuilderFactory.cs +++ b/src/FlatFile.Core/ILineBuilderFactory.cs @@ -5,7 +5,7 @@ namespace FlatFile.Core public interface ILineBuilderFactory where TFieldSettings : IFieldSettings where TLayout : ILayoutDescriptor - where TBuilder : ILineBulder + where TBuilder : ILineBuilder { TBuilder GetBuilder(TLayout layout); } diff --git a/src/FlatFile.Core/IMasterDetailTracker.cs b/src/FlatFile.Core/IMasterDetailTracker.cs new file mode 100644 index 0000000..49755df --- /dev/null +++ b/src/FlatFile.Core/IMasterDetailTracker.cs @@ -0,0 +1,10 @@ +namespace FlatFile.Core +{ + /// + /// Determines how master-detail record relationships are handled. + /// + public interface IMasterDetailTracker + { + void HandleMasterDetail(object entry, out bool isDetailRecord); + } +} \ No newline at end of file diff --git a/src/FlatFile.Core/ITypeConverter.cs b/src/FlatFile.Core/ITypeConverter.cs index f199ea8..06b7143 100644 --- a/src/FlatFile.Core/ITypeConverter.cs +++ b/src/FlatFile.Core/ITypeConverter.cs @@ -1,6 +1,7 @@ namespace FlatFile.Core { using System; + using System.Reflection; public interface ITypeConverter { @@ -8,8 +9,8 @@ public interface ITypeConverter bool CanConvertTo(Type type); - string ConvertToString(object source); + string ConvertToString(object source, PropertyInfo sourceProperty); - object ConvertFromString(string source); + object ConvertFromString(string source, PropertyInfo targetProperty); } } \ No newline at end of file diff --git a/src/FlatFile.Delimited.Attributes/FlatFileEngineFactoryExtensions.cs b/src/FlatFile.Delimited.Attributes/FlatFileEngineFactoryExtensions.cs index df16159..23a1e86 100644 --- a/src/FlatFile.Delimited.Attributes/FlatFileEngineFactoryExtensions.cs +++ b/src/FlatFile.Delimited.Attributes/FlatFileEngineFactoryExtensions.cs @@ -37,16 +37,18 @@ public static IFlatFileEngine GetEngine( /// The record types. /// The type selector function. /// The handle entry read error. + /// Determines how master-detail record relationships are handled. /// IFlatFileMultiEngine. public static IFlatFileMultiEngine GetEngine( this DelimitedFileEngineFactory engineFactory, IEnumerable recordTypes, Func typeSelectorFunc, - Func handleEntryReadError = null) + Func handleEntryReadError = null, + IMasterDetailTracker masterDetailTracker = null) { var descriptorProvider = new DelimitedLayoutDescriptorProvider(); var descriptors = recordTypes.Select(type => descriptorProvider.GetDescriptor(type)).ToList(); - return engineFactory.GetEngine(descriptors, typeSelectorFunc, handleEntryReadError); + return engineFactory.GetEngine(descriptors, typeSelectorFunc, handleEntryReadError, masterDetailTracker); } } } \ No newline at end of file diff --git a/src/FlatFile.Delimited/FlatFile.Delimited.csproj b/src/FlatFile.Delimited/FlatFile.Delimited.csproj index 1523482..b7a87a8 100644 --- a/src/FlatFile.Delimited/FlatFile.Delimited.csproj +++ b/src/FlatFile.Delimited/FlatFile.Delimited.csproj @@ -113,6 +113,7 @@ + diff --git a/src/FlatFile.Delimited/IDelimitedLineBuilder.cs b/src/FlatFile.Delimited/IDelimitedLineBuilder.cs index 3d2cf7f..3c822d9 100644 --- a/src/FlatFile.Delimited/IDelimitedLineBuilder.cs +++ b/src/FlatFile.Delimited/IDelimitedLineBuilder.cs @@ -2,7 +2,7 @@ { using FlatFile.Core; - public interface IDelimitedLineBuilder : ILineBulder + public interface IDelimitedLineBuilder : ILineBuilder { } } \ No newline at end of file diff --git a/src/FlatFile.Delimited/Implementation/DelimetedFileMultiEngine.cs b/src/FlatFile.Delimited/Implementation/DelimetedFileMultiEngine.cs index 215d974..6fc1e69 100644 --- a/src/FlatFile.Delimited/Implementation/DelimetedFileMultiEngine.cs +++ b/src/FlatFile.Delimited/Implementation/DelimetedFileMultiEngine.cs @@ -18,7 +18,7 @@ public class DelimitedFileMultiEngine : FlatFileEngine /// The handle entry read error func /// - readonly Func handleEntryReadError; + readonly Func handleEntryReadError; /// /// The layout descriptors for this engine /// @@ -40,9 +40,9 @@ public class DelimitedFileMultiEngine : FlatFileEngine readonly Dictionary results; /// - /// The last record parsed that implements + /// Determines how master-detail record relationships are handled. /// - IMasterRecord lastMasterRecord; + readonly IMasterDetailTracker masterDetailTracker; /// /// Initializes a new instance of the class. @@ -58,7 +58,8 @@ internal DelimitedFileMultiEngine( Func typeSelectorFunc, IDelimitedLineBuilderFactory lineBuilderFactory, IDelimitedLineParserFactory lineParserFactory, - Func handleEntryReadError = null) + IMasterDetailTracker masterDetailTracker, + Func handleEntryReadError = null) { if (typeSelectorFunc == null) throw new ArgumentNullException("typeSelectorFunc"); this.layoutDescriptors = layoutDescriptors.ToList(); @@ -70,6 +71,7 @@ internal DelimitedFileMultiEngine( this.typeSelectorFunc = typeSelectorFunc; this.lineBuilderFactory = lineBuilderFactory; this.lineParserFactory = lineParserFactory; + this.masterDetailTracker = masterDetailTracker; this.handleEntryReadError = handleEntryReadError; } @@ -79,7 +81,7 @@ internal DelimitedFileMultiEngine( /// The line builder. /// The does not contain just a single line builder. /// - protected override ILineBulder LineBuilder { get { throw new NotImplementedException(); } } + protected override ILineBuilder LineBuilder { get { throw new NotImplementedException(); } } /// /// Gets the line parser. @@ -137,7 +139,16 @@ protected override bool TryParseLine(string line, int lineNumber, ref T /// Impossible to parse line public void Read(Stream stream) { - var reader = new StreamReader(stream); + Read(new StreamReader(stream)); + } + + /// + /// Reads from the specified text reader. + /// + /// The text reader. + /// Impossible to parse line + public void Read(TextReader reader) + { string line; var lineNumber = 0; @@ -170,7 +181,7 @@ public void Read(Stream stream) throw; } - if (!handleEntryReadError(line, ex)) + if (!handleEntryReadError(new FlatFileErrorContext(line, lineNumber, ex))) { throw; } @@ -181,48 +192,12 @@ public void Read(Stream stream) if (ignoreEntry) continue; bool isDetailRecord; - HandleMasterDetail(entry, out isDetailRecord); + masterDetailTracker.HandleMasterDetail(entry, out isDetailRecord); if (isDetailRecord) continue; results[type].Add(entry); } } - - - /// - /// Handles any master/detail relationships for this . - /// - /// - /// The entry. - /// if set to true [is detail record] and should not be added to the results dictionary. - void HandleMasterDetail(T entry, out bool isDetailRecord) - { - isDetailRecord = false; - - var masterRecord = entry as IMasterRecord; - if (masterRecord != null) - { - // Found new master record - lastMasterRecord = masterRecord; - return; - } - - // Record is standalone or unassociated detail record - if (lastMasterRecord == null) return; - - var detailRecord = entry as IDetailRecord; - if (detailRecord == null) - { - // Record is standalone, reset master - lastMasterRecord = null; - return; - } - - // Add detail record and indicate that it should not be added to the results dictionary - lastMasterRecord.DetailRecords.Add(detailRecord); - isDetailRecord = true; - } - } } diff --git a/src/FlatFile.Delimited/Implementation/DelimitedFieldSettingsConstructor.cs b/src/FlatFile.Delimited/Implementation/DelimitedFieldSettingsConstructor.cs index 2124a0f..8f3cdbc 100644 --- a/src/FlatFile.Delimited/Implementation/DelimitedFieldSettingsConstructor.cs +++ b/src/FlatFile.Delimited/Implementation/DelimitedFieldSettingsConstructor.cs @@ -1,5 +1,6 @@ namespace FlatFile.Delimited.Implementation { + using System; using System.Reflection; using FlatFile.Core; using FlatFile.Core.Extensions; @@ -25,6 +26,32 @@ public IDelimitedFieldSettingsConstructor WithTypeConverter() where return this; } + public IDelimitedFieldSettingsConstructor WithConversionFromString(Func conversion) + { + if (TypeConverter == null) + TypeConverter = new DelegatingTypeConverter(); + + if (TypeConverter is DelegatingTypeConverter) + ((DelegatingTypeConverter)TypeConverter).ConversionFromString = conversion; + else + throw new InvalidOperationException("A type converter has already been explicitly set."); + + return this; + } + + public IDelimitedFieldSettingsConstructor WithConversionToString(Func conversion) + { + if (TypeConverter == null) + TypeConverter = new DelegatingTypeConverter(); + + if (TypeConverter is DelegatingTypeConverter) + ((DelegatingTypeConverter)TypeConverter).ConversionToString = conversion; + else + throw new InvalidOperationException("A type converter has already been explicitly set."); + + return this; + } + public IDelimitedFieldSettingsConstructor WithName(string name) { Name = name; diff --git a/src/FlatFile.Delimited/Implementation/DelimitedFileEngine.cs b/src/FlatFile.Delimited/Implementation/DelimitedFileEngine.cs index cccbd39..292118f 100644 --- a/src/FlatFile.Delimited/Implementation/DelimitedFileEngine.cs +++ b/src/FlatFile.Delimited/Implementation/DelimitedFileEngine.cs @@ -38,7 +38,7 @@ internal DelimitedFileEngine( IDelimitedLayoutDescriptor layoutDescriptor, IDelimitedLineBuilderFactory builderFactory, IDelimitedLineParserFactory parserFactory, - Func handleEntryReadError = null) + Func handleEntryReadError = null) : base(handleEntryReadError) { _builderFactory = builderFactory; @@ -50,7 +50,7 @@ internal DelimitedFileEngine( /// Gets the line builder. /// /// The line builder. - protected override ILineBulder LineBuilder + protected override ILineBuilder LineBuilder { get { return _builderFactory.GetBuilder(LayoutDescriptor); } } diff --git a/src/FlatFile.Delimited/Implementation/DelimitedFileEngineFactory.cs b/src/FlatFile.Delimited/Implementation/DelimitedFileEngineFactory.cs index a91ad3a..39ec8d2 100644 --- a/src/FlatFile.Delimited/Implementation/DelimitedFileEngineFactory.cs +++ b/src/FlatFile.Delimited/Implementation/DelimitedFileEngineFactory.cs @@ -51,10 +51,48 @@ public IFlatFileEngine GetEngine( descriptor, new DelimitedLineBuilderFactory(), new DelimitedLineParserFactory(), - handleEntryReadError); + ctx => handleEntryReadError(ctx.Line, ctx.Exception)); } + /// + /// Gets the . + /// + /// The descriptor. + /// The handle entry read error func. + /// IFlatFileEngine. + public IFlatFileEngine GetEngine( + IDelimitedLayoutDescriptor descriptor, + Func handleEntryReadError) + { + return new DelimitedFileEngine( + descriptor, + new DelimitedLineBuilderFactory(), + new DelimitedLineParserFactory(), + handleEntryReadError); + } + /// + /// Gets the . + /// + /// The layout descriptors. + /// The type selector function. + /// The handle entry read error func. + /// Determines how master-detail record relationships are handled. + /// IFlatFileMultiEngine. + public IFlatFileMultiEngine GetEngine( + IEnumerable layoutDescriptors, + Func typeSelectorFunc, + Func handleEntryReadError = null, + IMasterDetailTracker masterDetailTracker = null) + { + return new DelimitedFileMultiEngine( + layoutDescriptors, + typeSelectorFunc, + new DelimitedLineBuilderFactory(), + lineParserFactory, + masterDetailTracker ?? new DelimitedMasterDetailTracker(), + ctx => handleEntryReadError(ctx.Line, ctx.Exception)); + } /// /// Gets the . @@ -62,17 +100,20 @@ public IFlatFileEngine GetEngine( /// The layout descriptors. /// The type selector function. /// The handle entry read error func. + /// Determines how master-detail record relationships are handled. /// IFlatFileMultiEngine. public IFlatFileMultiEngine GetEngine( IEnumerable layoutDescriptors, Func typeSelectorFunc, - Func handleEntryReadError = null) + Func handleEntryReadError, + IMasterDetailTracker masterDetailTracker = null) { return new DelimitedFileMultiEngine( layoutDescriptors, typeSelectorFunc, new DelimitedLineBuilderFactory(), lineParserFactory, + masterDetailTracker ?? new DelimitedMasterDetailTracker(), handleEntryReadError); } } diff --git a/src/FlatFile.Delimited/Implementation/DelimitedLineBuilder.cs b/src/FlatFile.Delimited/Implementation/DelimitedLineBuilder.cs index c7aaf02..e479c04 100644 --- a/src/FlatFile.Delimited/Implementation/DelimitedLineBuilder.cs +++ b/src/FlatFile.Delimited/Implementation/DelimitedLineBuilder.cs @@ -4,7 +4,7 @@ using FlatFile.Core.Base; public class DelimitedLineBuilder : - LineBulderBase, + LineBuilderBase, IDelimitedLineBuilder { public DelimitedLineBuilder(IDelimitedLayoutDescriptor descriptor) diff --git a/src/FlatFile.Delimited/Implementation/DelimitedMasterDetailTracker.cs b/src/FlatFile.Delimited/Implementation/DelimitedMasterDetailTracker.cs new file mode 100644 index 0000000..be46a0d --- /dev/null +++ b/src/FlatFile.Delimited/Implementation/DelimitedMasterDetailTracker.cs @@ -0,0 +1,21 @@ +using FlatFile.Core.Base; + +namespace FlatFile.Delimited.Implementation +{ + /// + /// Uses records that implement and to handle + /// master-detail record relationships. + /// + public class DelimitedMasterDetailTracker : MasterDetailTrackerBase + { + /// + /// Initializes a new instance of the class. + /// + public DelimitedMasterDetailTracker() + : base(entry => entry is IMasterRecord, + entry => entry is IDetailRecord, + (master, detail) => ((IMasterRecord)master).DetailRecords.Add((IDetailRecord)detail)) + { + } + } +} \ No newline at end of file diff --git a/src/FlatFile.FixedLength/FlatFile.FixedLength.csproj b/src/FlatFile.FixedLength/FlatFile.FixedLength.csproj index f0090b9..9e17617 100644 --- a/src/FlatFile.FixedLength/FlatFile.FixedLength.csproj +++ b/src/FlatFile.FixedLength/FlatFile.FixedLength.csproj @@ -112,6 +112,7 @@ + diff --git a/src/FlatFile.FixedLength/IFixedLengthLineBuilder.cs b/src/FlatFile.FixedLength/IFixedLengthLineBuilder.cs index 55927a3..d4e0fb4 100644 --- a/src/FlatFile.FixedLength/IFixedLengthLineBuilder.cs +++ b/src/FlatFile.FixedLength/IFixedLengthLineBuilder.cs @@ -2,7 +2,7 @@ namespace FlatFile.FixedLength { using FlatFile.Core; - public interface IFixedLengthLineBuilder : ILineBulder + public interface IFixedLengthLineBuilder : ILineBuilder { } } \ No newline at end of file diff --git a/src/FlatFile.FixedLength/Implementation/FixedFieldSettingsConstructor.cs b/src/FlatFile.FixedLength/Implementation/FixedFieldSettingsConstructor.cs index e8683be..4ea46fb 100644 --- a/src/FlatFile.FixedLength/Implementation/FixedFieldSettingsConstructor.cs +++ b/src/FlatFile.FixedLength/Implementation/FixedFieldSettingsConstructor.cs @@ -57,5 +57,31 @@ public IFixedFieldSettingsConstructor WithTypeConverter() where TCon this.TypeConverter = ReflectionHelper.CreateInstance(true); return this; } + + public IFixedFieldSettingsConstructor WithConversionFromString(Func conversion) + { + if (TypeConverter == null) + TypeConverter = new DelegatingTypeConverter(); + + if (TypeConverter is DelegatingTypeConverter) + ((DelegatingTypeConverter)TypeConverter).ConversionFromString = conversion; + else + throw new InvalidOperationException("A type converter has already been explicitly set."); + + return this; + } + + public IFixedFieldSettingsConstructor WithConversionToString(Func conversion) + { + if (TypeConverter == null) + TypeConverter = new DelegatingTypeConverter(); + + if (TypeConverter is DelegatingTypeConverter) + ((DelegatingTypeConverter)TypeConverter).ConversionToString = conversion; + else + throw new InvalidOperationException("A type converter has already been explicitly set."); + + return this; + } } } \ No newline at end of file diff --git a/src/FlatFile.FixedLength/Implementation/FixedLengthFileEngine.cs b/src/FlatFile.FixedLength/Implementation/FixedLengthFileEngine.cs index 22686c9..20eee89 100644 --- a/src/FlatFile.FixedLength/Implementation/FixedLengthFileEngine.cs +++ b/src/FlatFile.FixedLength/Implementation/FixedLengthFileEngine.cs @@ -33,7 +33,7 @@ internal FixedLengthFileEngine( ILayoutDescriptor layoutDescriptor, IFixedLengthLineBuilderFactory lineBuilderFactory, IFixedLengthLineParserFactory lineParserFactory, - Func handleEntryReadError = null) : base(handleEntryReadError) + Func handleEntryReadError = null) : base(handleEntryReadError) { this.lineBuilderFactory = lineBuilderFactory; this.lineParserFactory = lineParserFactory; @@ -44,7 +44,7 @@ internal FixedLengthFileEngine( /// Gets the line builder. /// /// The line builder. - protected override ILineBulder LineBuilder + protected override ILineBuilder LineBuilder { get { return lineBuilderFactory.GetBuilder(LayoutDescriptor); } } diff --git a/src/FlatFile.FixedLength/Implementation/FixedLengthFileEngineFactory.cs b/src/FlatFile.FixedLength/Implementation/FixedLengthFileEngineFactory.cs index 132ee69..8b28f14 100644 --- a/src/FlatFile.FixedLength/Implementation/FixedLengthFileEngineFactory.cs +++ b/src/FlatFile.FixedLength/Implementation/FixedLengthFileEngineFactory.cs @@ -48,7 +48,24 @@ public IFlatFileEngine GetEngine( return new FixedLengthFileEngine( descriptor, new FixedLengthLineBuilderFactory(), - lineParserFactory, + lineParserFactory, + ctx => handleEntryReadError(ctx.Line, ctx.Exception)); + } + + /// + /// Gets the . + /// + /// The descriptor. + /// The handle entry read error func. + /// IFlatFileEngine. + public IFlatFileEngine GetEngine( + ILayoutDescriptor descriptor, + Func handleEntryReadError) + { + return new FixedLengthFileEngine( + descriptor, + new FixedLengthLineBuilderFactory(), + lineParserFactory, handleEntryReadError); } @@ -58,17 +75,43 @@ public IFlatFileEngine GetEngine( /// The layout descriptors. /// The type selector function. /// The handle entry read error func. + /// Determines how master-detail record relationships are handled. /// IFlatFileMultiEngine. public IFlatFileMultiEngine GetEngine( IEnumerable> layoutDescriptors, Func typeSelectorFunc, - Func handleEntryReadError = null) + Func handleEntryReadError = null, + IMasterDetailTracker masterDetailTracker = null) + { + return new FixedLengthFileMultiEngine( + layoutDescriptors, + typeSelectorFunc, + new FixedLengthLineBuilderFactory(), + lineParserFactory, + masterDetailTracker ?? new FixedLengthMasterDetailTracker(), + ctx => handleEntryReadError(ctx.Line, ctx.Exception)); + } + + /// + /// Gets the . + /// + /// The layout descriptors. + /// The type selector function. + /// The handle entry read error func. + /// Determines how master-detail record relationships are handled. + /// IFlatFileMultiEngine. + public IFlatFileMultiEngine GetEngine( + IEnumerable> layoutDescriptors, + Func typeSelectorFunc, + Func handleEntryReadError, + IMasterDetailTracker masterDetailTracker = null) { return new FixedLengthFileMultiEngine( layoutDescriptors, typeSelectorFunc, new FixedLengthLineBuilderFactory(), lineParserFactory, + masterDetailTracker ?? new FixedLengthMasterDetailTracker(), handleEntryReadError); } } diff --git a/src/FlatFile.FixedLength/Implementation/FixedLengthFileMultiEngine.cs b/src/FlatFile.FixedLength/Implementation/FixedLengthFileMultiEngine.cs index 9226d09..b4df606 100644 --- a/src/FlatFile.FixedLength/Implementation/FixedLengthFileMultiEngine.cs +++ b/src/FlatFile.FixedLength/Implementation/FixedLengthFileMultiEngine.cs @@ -18,7 +18,7 @@ public class FixedLengthFileMultiEngine : FlatFileEngine /// The handle entry read error func /// - readonly Func handleEntryReadError; + readonly Func handleEntryReadError; /// /// The layout descriptors for this engine /// @@ -40,9 +40,9 @@ public class FixedLengthFileMultiEngine : FlatFileEngine readonly Dictionary results; /// - /// The last record parsed that implements + /// Determines how master-detail record relationships are handled. /// - IMasterRecord lastMasterRecord; + readonly IMasterDetailTracker masterDetailTracker; /// /// Initializes a new instance of the class. @@ -51,6 +51,7 @@ public class FixedLengthFileMultiEngine : FlatFileEngineThe type selector function. /// The line builder factory. /// The line parser factory. + /// Determines how master-detail record relationships are handled. /// The handle entry read error. /// typeSelectorFunc internal FixedLengthFileMultiEngine( @@ -58,7 +59,8 @@ internal FixedLengthFileMultiEngine( Func typeSelectorFunc, IFixedLengthLineBuilderFactory lineBuilderFactory, IFixedLengthLineParserFactory lineParserFactory, - Func handleEntryReadError = null) + IMasterDetailTracker masterDetailTracker, + Func handleEntryReadError = null) { if (typeSelectorFunc == null) throw new ArgumentNullException("typeSelectorFunc"); this.layoutDescriptors = layoutDescriptors.ToList(); @@ -70,6 +72,7 @@ internal FixedLengthFileMultiEngine( this.typeSelectorFunc = typeSelectorFunc; this.lineBuilderFactory = lineBuilderFactory; this.lineParserFactory = lineParserFactory; + this.masterDetailTracker = masterDetailTracker; this.handleEntryReadError = handleEntryReadError; } @@ -79,7 +82,7 @@ internal FixedLengthFileMultiEngine( /// The line builder. /// The does not contain just a single line builder. /// - protected override ILineBulder LineBuilder { get { throw new NotImplementedException(); } } + protected override ILineBuilder LineBuilder { get { throw new NotImplementedException(); } } /// /// Gets the line parser. @@ -142,22 +145,22 @@ public void Read(Stream stream) } /// - /// Reads the specified streamReader. + /// Reads from the specified text reader. /// - /// The stream reader configured as the user wants. + /// The text reader configured as the user wants. /// Impossible to parse line - public void Read(StreamReader reader) + public void Read(TextReader reader) { ReadInternal(reader); } /// - /// Internal method (private) to read from streamreader instead of stream + /// Internal method (private) to read from a text reader instead of stream /// This way the client code have a way to specify encoding. /// - /// The stream reader to read. + /// The text reader to read. /// Impossible to parse line - private void ReadInternal(StreamReader reader) + private void ReadInternal(TextReader reader) { string line; var lineNumber = 0; @@ -191,7 +194,7 @@ private void ReadInternal(StreamReader reader) throw; } - if (!handleEntryReadError(line, ex)) + if (!handleEntryReadError(new FlatFileErrorContext(line, lineNumber, ex))) { throw; } @@ -202,47 +205,12 @@ private void ReadInternal(StreamReader reader) if (ignoreEntry) continue; bool isDetailRecord; - HandleMasterDetail(entry, out isDetailRecord); + masterDetailTracker.HandleMasterDetail(entry, out isDetailRecord); if (isDetailRecord) continue; results[type].Add(entry); } } - - - /// - /// Handles any master/detail relationships for this . - /// - /// - /// The entry. - /// if set to true [is detail record] and should not be added to the results dictionary. - void HandleMasterDetail(T entry, out bool isDetailRecord) - { - isDetailRecord = false; - - var masterRecord = entry as IMasterRecord; - if (masterRecord != null) - { - // Found new master record - lastMasterRecord = masterRecord; - return; - } - - // Record is standalone or unassociated detail record - if (lastMasterRecord == null) return; - - var detailRecord = entry as IDetailRecord; - if (detailRecord == null) - { - // Record is standalone, reset master - lastMasterRecord = null; - return; - } - - // Add detail record and indicate that it should not be added to the results dictionary - lastMasterRecord.DetailRecords.Add(detailRecord); - isDetailRecord = true; - } } } \ No newline at end of file diff --git a/src/FlatFile.FixedLength/Implementation/FixedLengthLineBuilder.cs b/src/FlatFile.FixedLength/Implementation/FixedLengthLineBuilder.cs index 088ccc6..e49466d 100644 --- a/src/FlatFile.FixedLength/Implementation/FixedLengthLineBuilder.cs +++ b/src/FlatFile.FixedLength/Implementation/FixedLengthLineBuilder.cs @@ -5,7 +5,7 @@ namespace FlatFile.FixedLength.Implementation using FlatFile.Core.Base; public class FixedLengthLineBuilder : - LineBulderBase, IFixedFieldSettingsContainer>, + LineBuilderBase, IFixedFieldSettingsContainer>, IFixedLengthLineBuilder { public FixedLengthLineBuilder(ILayoutDescriptor descriptor) diff --git a/src/FlatFile.FixedLength/Implementation/FixedLengthMasterDetailTracker.cs b/src/FlatFile.FixedLength/Implementation/FixedLengthMasterDetailTracker.cs new file mode 100644 index 0000000..62bb7bc --- /dev/null +++ b/src/FlatFile.FixedLength/Implementation/FixedLengthMasterDetailTracker.cs @@ -0,0 +1,21 @@ +using FlatFile.Core.Base; + +namespace FlatFile.FixedLength.Implementation +{ + /// + /// Uses records that implement and to handle + /// master-detail record relationships. + /// + public class FixedLengthMasterDetailTracker : MasterDetailTrackerBase + { + /// + /// Initializes a new instance of the class. + /// + public FixedLengthMasterDetailTracker() + : base(entry => entry is IMasterRecord, + entry => entry is IDetailRecord, + (master, detail) => ((IMasterRecord)master).DetailRecords.Add((IDetailRecord)detail)) + { + } + } +} \ No newline at end of file diff --git a/src/FlatFile.Tests/Delimited/DelimitedAttributeMappingIntegrationTests.cs b/src/FlatFile.Tests/Delimited/DelimitedAttributeMappingIntegrationTests.cs index 03b2626..70376e9 100644 --- a/src/FlatFile.Tests/Delimited/DelimitedAttributeMappingIntegrationTests.cs +++ b/src/FlatFile.Tests/Delimited/DelimitedAttributeMappingIntegrationTests.cs @@ -36,7 +36,7 @@ public void EngineShouldCallTypeConverterWhenConverterAttributeIsPresent() { // a converter to convert "A" to "foo" var converter = A.Fake(); - A.CallTo(() => converter.ConvertFromString("A")).Returns("foo"); + A.CallTo(() => converter.ConvertFromString("A", A.Ignored)).Returns("foo"); A.CallTo(() => converter.CanConvertFrom(typeof(string))).Returns(true); A.CallTo(() => converter.CanConvertTo(typeof(string))).Returns(true); diff --git a/src/FlatFile.Tests/Delimited/DelimitedErrorHandlingTests.cs b/src/FlatFile.Tests/Delimited/DelimitedErrorHandlingTests.cs new file mode 100644 index 0000000..612be95 --- /dev/null +++ b/src/FlatFile.Tests/Delimited/DelimitedErrorHandlingTests.cs @@ -0,0 +1,94 @@ +using FakeItEasy; +using FlatFile.Core.Base; +using FlatFile.Delimited; +using FlatFile.Delimited.Implementation; +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Text; +using Xunit; + +namespace FlatFile.Tests.Delimited +{ + public class DelimitedErrorHandlingTests + { + private IDelimitedLayoutDescriptor layout; + readonly IDelimitedLineParserFactory lineParserFactory; + readonly IList errorContexts = new List(); + + const string TestData = +@"S,Test Description,00042 +S,Test Description,00043 +S,Test Description,00044"; + + public DelimitedErrorHandlingTests() + { + layout = A.Fake(); + A.CallTo(() => layout.TargetType).Returns(typeof(Record)); + + lineParserFactory = A.Fake(); + A.CallTo(() => lineParserFactory.GetParser(A.Ignored)) + .Returns(new FakeLineParser()); + + new DelimitedLineParserFactory(new Dictionary + { + { typeof(Record), typeof(FakeLineParser) } + }); + } + + [Fact] + public void ErrorContextShouldProvideAccurateInformation() + { + var engine = new DelimitedFileEngine( + layout, + A.Fake(), + lineParserFactory, + HandleError); + + using (var stream = new MemoryStream(Encoding.Default.GetBytes(TestData))) + engine.Read(stream).ToList(); + + Assert.Equal(3, errorContexts.Count); + Assert.Equal(new[] { 1, 2, 3 }, errorContexts.Select(ctx => ctx.LineNumber)); + Assert.Equal(TestData.Split(new[] { '\r', '\n' }, StringSplitOptions.RemoveEmptyEntries), errorContexts.Select(ctx => ctx.Line)); + Assert.All(errorContexts, ctx => Assert.Equal("Parsing failed!", ctx.Exception.Message)); + } + + [Fact] + public void MultiEngineErrorContextShouldProvideAccurateInformation() + { + var engine = new DelimitedFileMultiEngine( + new[] { layout }, + l => typeof(Record), + A.Fake(), + lineParserFactory, + new DelimitedMasterDetailTracker(), + HandleError); + + using (var stream = new MemoryStream(Encoding.Default.GetBytes(TestData))) + engine.Read(stream); + + Assert.Equal(3, errorContexts.Count); + Assert.Equal(new[] { 1, 2, 3 }, errorContexts.Select(ctx => ctx.LineNumber)); + Assert.Equal(TestData.Split(new[] { '\r', '\n' }, StringSplitOptions.RemoveEmptyEntries), errorContexts.Select(ctx => ctx.Line)); + Assert.All(errorContexts, ctx => Assert.Equal("Parsing failed!", ctx.Exception.Message)); + } + + private bool HandleError(FlatFileErrorContext context) + { + errorContexts.Add(context); + return true; + } + + private class FakeLineParser : IDelimitedLineParser + { + public TEntity ParseLine(string line, TEntity entity) where TEntity : new() + { + throw new Exception("Parsing failed!"); + } + } + + private class Record { } + } +} diff --git a/src/FlatFile.Tests/Delimited/DelimitedLineBuilderTests.cs b/src/FlatFile.Tests/Delimited/DelimitedLineBuilderTests.cs new file mode 100644 index 0000000..6e8ddc0 --- /dev/null +++ b/src/FlatFile.Tests/Delimited/DelimitedLineBuilderTests.cs @@ -0,0 +1,68 @@ +namespace FlatFile.Tests.Delimited +{ + using FlatFile.Core.Base; + using FlatFile.Delimited; + using FlatFile.Delimited.Implementation; + using FlatFile.Tests.Base.Entities; + using FluentAssertions; + using System; + using System.Globalization; + using System.Reflection; + using Xunit; + + public class DelimitedLineBuilderTests + { + private readonly DelimitedLineBuilder builder; + private readonly IDelimitedLayout layout; + + public DelimitedLineBuilderTests() + { + layout = new DelimitedLayout().WithDelimiter(","); + + builder = new DelimitedLineBuilder(layout); + } + + [Fact] + public void BuilderShouldUseTypeConverter() + { + layout.WithMember(o => o.Id, set => set.WithTypeConverter()); + + var entry = new TestObject + { + Id = 48879 + }; + + var line = builder.BuildLine(entry); + + line.Should().Be("BEEF"); + } + + [Fact] + public void BuilderShouldUseConversionFunction() + { + layout.WithMember(o => o.Id, set => set.WithConversionToString((int id) => id.ToString("X"))); + + var entry = new TestObject + { + Id = 48879 + }; + + var line = builder.BuildLine(entry); + + line.Should().Be("BEEF"); + } + + class IdHexConverter : TypeConverterBase + { + protected override int ConvertFrom(string source, PropertyInfo targetProperty) + { + return Int32.Parse(source, NumberStyles.AllowHexSpecifier); + } + + protected override string ConvertTo(int source, PropertyInfo sourceProperty) + { + return source.ToString("X"); + } + } + } +} diff --git a/src/FlatFile.Tests/Delimited/DelimitedLineParserTests.cs b/src/FlatFile.Tests/Delimited/DelimitedLineParserTests.cs new file mode 100644 index 0000000..04f7669 --- /dev/null +++ b/src/FlatFile.Tests/Delimited/DelimitedLineParserTests.cs @@ -0,0 +1,60 @@ +namespace FlatFile.Tests.Delimited +{ + using FlatFile.Core.Base; + using FlatFile.Delimited; + using FlatFile.Delimited.Implementation; + using FlatFile.Tests.Base.Entities; + using FluentAssertions; + using System; + using System.Globalization; + using System.Reflection; + using Xunit; + + public class DelimitedLineParserTests + { + private readonly DelimitedLineParser parser; + private readonly IDelimitedLayout layout; + + public DelimitedLineParserTests() + { + layout = new DelimitedLayout().WithDelimiter(","); + + parser = new DelimitedLineParser(layout); + } + + [Fact] + public void ParserShouldUseTypeConverter() + { + layout.WithMember(o => o.Id, set => set.WithTypeConverter()); + + var entry = new TestObject(); + var parsedEntity = parser.ParseLine("BEEF", entry); + + parsedEntity.Id.Should().Be(48879); + } + + [Fact] + public void ParserShouldUseConversionFunction() + { + layout.WithMember(o => o.Id, set => set.WithConversionFromString(s => Int32.Parse(s, NumberStyles.AllowHexSpecifier))); + + var entry = new TestObject(); + var parsedEntity = parser.ParseLine("BEEF", entry); + + parsedEntity.Id.Should().Be(48879); + } + + class IdHexConverter : TypeConverterBase + { + protected override int ConvertFrom(string source, PropertyInfo targetProperty) + { + return Int32.Parse(source, NumberStyles.AllowHexSpecifier); + } + + protected override string ConvertTo(int source, PropertyInfo sourceProperty) + { + return source.ToString("X"); + } + } + } +} diff --git a/src/FlatFile.Tests/FixedLength/FixedLengthErrorHandlingTests.cs b/src/FlatFile.Tests/FixedLength/FixedLengthErrorHandlingTests.cs new file mode 100644 index 0000000..0895ebf --- /dev/null +++ b/src/FlatFile.Tests/FixedLength/FixedLengthErrorHandlingTests.cs @@ -0,0 +1,96 @@ +using FakeItEasy; +using FlatFile.Core; +using FlatFile.Core.Base; +using FlatFile.FixedLength; +using FlatFile.FixedLength.Implementation; +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Text; +using Xunit; + +namespace FlatFile.Tests.FixedLength +{ + public class FixedLengthErrorHandlingTests + { + private ILayoutDescriptor layout; + readonly IFixedLengthLineParserFactory lineParserFactory; + readonly IList errorContexts = new List(); + + const string TestData = +@"STest Description 00042 +STest Description 00043 +STest Description 00044"; + + public FixedLengthErrorHandlingTests() + { + layout = A.Fake>(); + A.CallTo(() => layout.TargetType).Returns(typeof(Record)); + + lineParserFactory = new FixedLengthLineParserFactory(new Dictionary + { + { typeof(Record), typeof(FakeLineParser) } + }); + } + + [Fact] + public void ErrorContextShouldProvideAccurateInformation() + { + var engine = new FixedLengthFileEngine( + layout, + A.Fake(), + lineParserFactory, + HandleError); + + using (var stream = new MemoryStream(Encoding.Default.GetBytes(TestData))) + engine.Read(stream).ToList(); + + Assert.Equal(3, errorContexts.Count); + Assert.Equal(new[] { 1, 2, 3 }, errorContexts.Select(ctx => ctx.LineNumber)); + Assert.Equal(TestData.Split(new[] { '\r', '\n' }, StringSplitOptions.RemoveEmptyEntries), errorContexts.Select(ctx => ctx.Line)); + Assert.All(errorContexts, ctx => Assert.Equal("Parsing failed!", ctx.Exception.Message)); + } + + [Fact] + public void MultiEngineErrorContextShouldProvideAccurateInformation() + { + var engine = new FixedLengthFileMultiEngine( + new[] { layout }, + (l, i) => typeof(Record), + A.Fake(), + lineParserFactory, + new FixedLengthMasterDetailTracker(), + HandleError); + + using (var stream = new MemoryStream(Encoding.Default.GetBytes(TestData))) + engine.Read(stream); + + Assert.Equal(3, errorContexts.Count); + Assert.Equal(new[] { 1, 2, 3 }, errorContexts.Select(ctx => ctx.LineNumber)); + Assert.Equal(TestData.Split(new[] { '\r', '\n' }, StringSplitOptions.RemoveEmptyEntries), errorContexts.Select(ctx => ctx.Line)); + Assert.All(errorContexts, ctx => Assert.Equal("Parsing failed!", ctx.Exception.Message)); + } + + private bool HandleError(FlatFileErrorContext context) + { + errorContexts.Add(context); + return true; + } + + private class FakeLineParser : IFixedLengthLineParser + { + public FakeLineParser(ILayoutDescriptor descriptor) + { + + } + + public TEntity ParseLine(string line, TEntity entity) where TEntity : new() + { + throw new Exception("Parsing failed!"); + } + } + + private class Record { } + } +} diff --git a/src/FlatFile.Tests/FixedLength/FixedLengthLineBuilderTests.cs b/src/FlatFile.Tests/FixedLength/FixedLengthLineBuilderTests.cs new file mode 100644 index 0000000..6609035 --- /dev/null +++ b/src/FlatFile.Tests/FixedLength/FixedLengthLineBuilderTests.cs @@ -0,0 +1,68 @@ +namespace FlatFile.Tests.FixedLength +{ + using FlatFile.Core.Base; + using FlatFile.FixedLength; + using FlatFile.FixedLength.Implementation; + using FlatFile.Tests.Base.Entities; + using FluentAssertions; + using System; + using System.Globalization; + using System.Reflection; + using Xunit; + + public class FixedLengthLineBuilderTests + { + private readonly FixedLengthLineBuilder builder; + private readonly IFixedLayout layout; + + public FixedLengthLineBuilderTests() + { + layout = new FixedLayout(); + + builder = new FixedLengthLineBuilder(layout); + } + + [Fact] + public void BuilderShouldUseTypeConverter() + { + layout.WithMember(o => o.Id, set => set.WithLength(4).WithTypeConverter()); + + var entry = new TestObject + { + Id = 48879 + }; + + var line = builder.BuildLine(entry); + + line.Should().Be("BEEF"); + } + + [Fact] + public void BuilderShouldUseConversionFunction() + { + layout.WithMember(o => o.Id, set => set.WithLength(4).WithConversionToString((int id) => id.ToString("X"))); + + var entry = new TestObject + { + Id = 48879 + }; + + var line = builder.BuildLine(entry); + + line.Should().Be("BEEF"); + } + + class IdHexConverter : TypeConverterBase + { + protected override int ConvertFrom(string source, PropertyInfo targetProperty) + { + return Int32.Parse(source, NumberStyles.AllowHexSpecifier); + } + + protected override string ConvertTo(int source, PropertyInfo sourceProperty) + { + return source.ToString("X"); + } + } + } +} diff --git a/src/FlatFile.Tests/FixedLength/FixedLengthLineParserTests.cs b/src/FlatFile.Tests/FixedLength/FixedLengthLineParserTests.cs index a047bb7..bea64aa 100644 --- a/src/FlatFile.Tests/FixedLength/FixedLengthLineParserTests.cs +++ b/src/FlatFile.Tests/FixedLength/FixedLengthLineParserTests.cs @@ -2,10 +2,14 @@ namespace FlatFile.Tests.FixedLength { + using FlatFile.Core.Base; using FlatFile.FixedLength; using FlatFile.FixedLength.Implementation; using FlatFile.Tests.Base.Entities; using FluentAssertions; + using System; + using System.Globalization; + using System.Reflection; public class FixedLengthLineParserTests { @@ -50,5 +54,44 @@ public void ParserShouldSetValueNullValueIfStringIsToShort(string inputString, i parsedEntity.Description.Should().Be(description); parsedEntity.NullableInt.Should().Be(nullableInt); } + + [Fact] + public void ParserShouldUseTypeConverter() + { + layout.WithMember(o => o.Id, set => set.WithLength(4).WithTypeConverter()) + .WithMember(o => o.Description, set => set.WithLength(25).AllowNull(string.Empty)) + .WithMember(o => o.NullableInt, set => set.WithLength(5).AllowNull(string.Empty)); + + var entry = new TestObject(); + var parsedEntity = parser.ParseLine("BEEF", entry); + + parsedEntity.Id.Should().Be(48879); + } + + [Fact] + public void ParserShouldUseConversionFunction() + { + layout.WithMember(o => o.Id, set => set.WithLength(4).WithConversionFromString(s => Int32.Parse(s, NumberStyles.AllowHexSpecifier))) + .WithMember(o => o.Description, set => set.WithLength(25).AllowNull(string.Empty)) + .WithMember(o => o.NullableInt, set => set.WithLength(5).AllowNull(string.Empty)); + + var entry = new TestObject(); + var parsedEntity = parser.ParseLine("BEEF", entry); + + parsedEntity.Id.Should().Be(48879); + } + + class IdHexConverter : TypeConverterBase + { + protected override int ConvertFrom(string source, PropertyInfo targetProperty) + { + return Int32.Parse(source, NumberStyles.AllowHexSpecifier); + } + + protected override string ConvertTo(int source, PropertyInfo sourceProperty) + { + return source.ToString("X"); + } + } } } diff --git a/src/FlatFile.Tests/FixedLength/FixedLengthMasterDetailCustomTests.cs b/src/FlatFile.Tests/FixedLength/FixedLengthMasterDetailCustomTests.cs new file mode 100644 index 0000000..c8335ec --- /dev/null +++ b/src/FlatFile.Tests/FixedLength/FixedLengthMasterDetailCustomTests.cs @@ -0,0 +1,192 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; +using System.IO; +using System.Linq; +using System.Reflection; +using FlatFile.Core; +using FlatFile.Core.Base; +using FlatFile.FixedLength; +using FlatFile.FixedLength.Implementation; +using FluentAssertions; +using Xunit; + +namespace FlatFile.Tests.FixedLength +{ + public class FixedLengthMasterDetailCustomTests + { + readonly IFlatFileMultiEngine engine; + + const string TestData = @"HHeader +MHeaderLine2 +MHeaderLine3 +D20150323Some Data +D20150512More Data +HAnotherHeader +D20150511FooBarBaz +SNonHeaderRecord +D20150512Standalone "; + + [AttributeUsage(AttributeTargets.Class, AllowMultiple = false, Inherited = true)] + public sealed class MasterAttribute : Attribute { } + + [AttributeUsage(AttributeTargets.Class, AllowMultiple = false, Inherited = true)] + public sealed class DetailAttribute : Attribute { } + + abstract class RecordBase + { + public char Type { get; set; } + public string Data { get; set; } + + bool Equals(RecordBase other) { return Type == other.Type && string.Equals(Data, other.Data); } + + public override int GetHashCode() + { + unchecked + { + var hashCode = Type.GetHashCode(); + hashCode = (hashCode * 397) ^ (Data != null ? Data.GetHashCode() : 0); + return hashCode; + } + } + + public override bool Equals(object obj) + { + if (ReferenceEquals(null, obj)) return false; + if (ReferenceEquals(this, obj)) return true; + return obj.GetType() == GetType() && Equals((RecordBase)obj); + } + } + + [Master] + class HeaderRecord : RecordBase + { + public IList DetailRecords { get; protected set; } + public HeaderRecord() + { + Type = 'H'; + DetailRecords = new List(); + } + } + + class HeaderRecordContinuation : HeaderRecord + { + public HeaderRecordContinuation() + { + Type = 'M'; + DetailRecords = new List(); + } + } + + [Detail] + class DetailRecord : RecordBase + { + public DetailRecord() { Type = 'D'; } + } + + class StandaloneRecord : RecordBase + { + public StandaloneRecord() { Type = 'S'; } + } + + abstract class RecordBaseLayout : FixedLayout where T : RecordBase + { + [SuppressMessage("ReSharper", "DoNotCallOverridableMethodsInConstructor")] + protected RecordBaseLayout() + { + WithMember(x => x.Type, c => c.WithLength(1)) + .WithMember(x => x.Data, c => c.WithLength(20).WithRightPadding(' ')); + } + } + + class HeaderLayout : RecordBaseLayout { } + + class HeaderContinuationLayout : RecordBaseLayout { } + + class DetailLayout : RecordBaseLayout { } + + class StandaloneLayout : RecordBaseLayout { } + + public FixedLengthMasterDetailCustomTests() + { + var layouts = new List> + { + new HeaderLayout(), + new HeaderContinuationLayout(), + new DetailLayout(), + new StandaloneLayout() + }; + engine = new FixedLengthFileMultiEngine(layouts, + (s, i) => + { + if (String.IsNullOrEmpty(s) || s.Length < 1) return null; + + switch (s[0]) + { + case 'H': + return typeof (HeaderRecord); + case 'M': + return typeof (HeaderRecordContinuation); + case 'S': + return typeof (StandaloneRecord); + case 'D': + return typeof (DetailRecord); + default: + return null; + } + }, + new FixedLengthLineBuilderFactory(), + new FixedLengthLineParserFactory(), + new MasterDetailTrackerBase( + x => x.GetType().GetCustomAttribute(true) != null, + x => x.GetType().GetCustomAttribute(true) != null, + (master, detail) => ((HeaderRecord)master).DetailRecords.Add((DetailRecord)detail))); + } + + [Fact] + [SuppressMessage("ReSharper", "PossibleNullReferenceException")] + void EngineShouldAssociateDetailRecordsWithPreceedingMasterRecord() + { + using (var stream = GetStringStream(TestData)) + engine.Read(stream); + + var headers = engine.GetRecords().ToList(); + var continuations = engine.GetRecords().ToList(); + + var header1 = headers.FirstOrDefault(r => r.Data == "Header"); + header1.Should().NotBeNull("The first header record should exist"); + var header2 = continuations.FirstOrDefault(r => r.Data == "HeaderLine2"); + header2.Should().NotBeNull("The second header continuation record should exist"); + var header3 = continuations.FirstOrDefault(r => r.Data == "HeaderLine3"); + header3.Should().NotBeNull("The third header continuation record should exist"); + + header1.DetailRecords.Should().BeNullOrEmpty("It does not have any detail records"); + header2.DetailRecords.Should().BeNullOrEmpty("It does not have any detail records"); + header3.DetailRecords.Should().HaveCount(2, "Two detail records exist"); + + var detail = header3.DetailRecords[1] as DetailRecord; + detail.Should().NotBeNull("It should be parsed correctly"); + detail.Data.Should().Be("20150512More Data", "It should preserve ordering"); + + var anotherHeader = headers.FirstOrDefault(r => r.Data == "AnotherHeader"); + anotherHeader.Should().NotBeNull("The other header record should exist"); + anotherHeader.DetailRecords.Should().HaveCount(1, "One detail record exists"); + + var anotherDetail = anotherHeader.DetailRecords[0] as DetailRecord; + anotherDetail.Should().NotBeNull("It should be parsed correctly"); + anotherDetail.Data.Should().Be("20150511FooBarBaz", "It should associate the correct record"); + + engine.GetRecords().Should().HaveCount(1, "Only unassociated detail records should be available when calling GetRecords"); + } + + static Stream GetStringStream(string s) + { + var memoryStream = new MemoryStream(); + var writer = new StreamWriter(memoryStream); + writer.Write(s); + writer.Flush(); + memoryStream.Position = 0; + return memoryStream; + } + } +} \ No newline at end of file diff --git a/src/FlatFile.Tests/FixedLength/FixedLengthMasterDetailTests.cs b/src/FlatFile.Tests/FixedLength/FixedLengthMasterDetailTests.cs index f80164c..2ebcfaf 100644 --- a/src/FlatFile.Tests/FixedLength/FixedLengthMasterDetailTests.cs +++ b/src/FlatFile.Tests/FixedLength/FixedLengthMasterDetailTests.cs @@ -126,7 +126,8 @@ public FixedLengthMasterDetailTests() } }, new FixedLengthLineBuilderFactory(), - new FixedLengthLineParserFactory()); + new FixedLengthLineParserFactory(), + new FixedLengthMasterDetailTracker()); } [Fact] diff --git a/src/FlatFile.Tests/FixedLength/FixedLengthMultiEngineTests.cs b/src/FlatFile.Tests/FixedLength/FixedLengthMultiEngineTests.cs index c94d6a5..bfe8074 100644 --- a/src/FlatFile.Tests/FixedLength/FixedLengthMultiEngineTests.cs +++ b/src/FlatFile.Tests/FixedLength/FixedLengthMultiEngineTests.cs @@ -115,7 +115,8 @@ public FixedLengthMultiEngineTests() } }, new FixedLengthLineBuilderFactory(), - new FixedLengthLineParserFactory()) {HasHeader = true}; + new FixedLengthLineParserFactory(), + new FixedLengthMasterDetailTracker()) {HasHeader = true}; } [Fact] diff --git a/src/FlatFile.Tests/FlatFile.Tests.csproj b/src/FlatFile.Tests/FlatFile.Tests.csproj index 57f98f9..b24d043 100644 --- a/src/FlatFile.Tests/FlatFile.Tests.csproj +++ b/src/FlatFile.Tests/FlatFile.Tests.csproj @@ -1,5 +1,6 @@  + Debug @@ -13,6 +14,8 @@ 512 ..\ true + + true @@ -79,11 +82,17 @@ + + + + + + @@ -138,6 +147,12 @@ + + + This project references NuGet package(s) that are missing on this computer. Use NuGet Package Restore to download them. For more information, see http://go.microsoft.com/fwlink/?LinkID=322105. The missing file is {0}. + + +