diff --git a/SaneTsv/SaneTsv.cs b/SaneTsv/SaneTsv.cs index f2fc0dc..5e7e074 100644 --- a/SaneTsv/SaneTsv.cs +++ b/SaneTsv/SaneTsv.cs @@ -3,9 +3,14 @@ using System.Text; namespace NathanMcRae; -public class Tsv where T : SaneTsv.SaneTsvRecord +public class Tsv where T : SaneTsv.TsvRecord { - public List Records { get; set; } + public virtual List Records { get; set; } +} + +public class CommentedTsv: Tsv where T : SaneTsv.TsvRecord +{ + public override List Records { get; set; } public string FileComment { get; set; } = null; } @@ -54,25 +59,34 @@ public class SaneTsv } } - public static Tsv ParseSimpleTsv(byte[] inputBuffer) where T : SaneTsvRecord, new() + public static Tsv ParseSimpleTsv(byte[] inputBuffer) where T : TsvRecord, new() { return Parse(inputBuffer, FormatType.SIMPLE_TSV); } - public static Tsv ParseTypedTsv(byte[] inputBuffer) where T : SaneTsvRecord, new() + public static Tsv ParseTypedTsv(byte[] inputBuffer) where T : TsvRecord, new() { return Parse(inputBuffer, FormatType.TYPED_TSV); } - public static Tsv ParseCommentedTsv(byte[] inputBuffer) where T : SaneTsvRecord, new() + public static CommentedTsv ParseCommentedTsv(byte[] inputBuffer) where T : CommentedTsvRecord, new() { - return Parse(inputBuffer, FormatType.COMMENTED_TSV); + // TODO: add the file comment? + return (CommentedTsv)Parse(inputBuffer, FormatType.COMMENTED_TSV); } // TODO: Have parsing errors include line / column # - protected static Tsv Parse(byte[] inputBuffer, FormatType format) where T : SaneTsvRecord, new() + protected static Tsv Parse(byte[] inputBuffer, FormatType format) where T : TsvRecord, new() { - var parsed = new Tsv(); + Tsv parsed; + if (format == FormatType.COMMENTED_TSV) + { + parsed = new CommentedTsv(); + } + else + { + parsed = new Tsv(); + } parsed.Records = new List(); var headerTypes = new List(); @@ -82,7 +96,7 @@ public class SaneTsv foreach (PropertyInfo property in typeof(T).GetProperties()) { - SaneTsvColumnAttribute attribute = (SaneTsvColumnAttribute)Attribute.GetCustomAttribute(property, typeof(SaneTsvColumnAttribute)); + TypedTsvColumnAttribute attribute = (TypedTsvColumnAttribute)Attribute.GetCustomAttribute(property, typeof(TypedTsvColumnAttribute)); if (attribute == null) { continue; @@ -248,9 +262,17 @@ public class SaneTsv if (currentComment.Length > 0) { - parsed.FileComment = currentComment.ToString(); - currentComment.Clear(); + if (parsed is CommentedTsv commentedParsed) + { + commentedParsed.FileComment = currentComment.ToString(); + currentComment.Clear(); + } + else + { + throw new Exception("Found a file comment, but parser wasn't expecting a comment"); + } } + fields.Clear(); } @@ -333,11 +355,24 @@ public class SaneTsv return parsed; } - protected static T ParseCurrentRecord(Type[] columnTypes, PropertyInfo[] properties, List fields, string comment, int line) where T : SaneTsvRecord, new() + protected static T ParseCurrentCommentedRecord (Type[] columnTypes, PropertyInfo[] properties, List fields, string comment, int line) where T : CommentedTsvRecord, new() + { + return (T)ParseCurrentRecord(columnTypes, properties, fields, comment, line); + } + + protected static T ParseCurrentRecord(Type[] columnTypes, PropertyInfo[] properties, List fields, string comment, int line) where T : TsvRecord, new() { T record = new T(); - record.Comment = comment; + if (record is CommentedTsvRecord commentedRecord) + { + commentedRecord.Comment = comment; + } + else if (comment != null) + { + throw new Exception($"Found comment for line {line}, but format does not support comments"); + } + record.Line = line; for (int j = 0; j < fields.Count; j++) @@ -705,7 +740,22 @@ public class SaneTsv } } - public static byte[] SerializeTypedTsv(IList data) where T : SaneTsvRecord + public static byte[] SerializeSimpleTsv(IList data) where T : TsvRecord + { + return SerializeTsv(data, FormatType.TYPED_TSV); + } + + public static byte[] SerializeTypedTsv(IList data) where T : TsvRecord + { + return SerializeTsv(data, FormatType.TYPED_TSV); + } + + public static byte[] SerializeCommentedTsv(IList data, string fileComment) where T : CommentedTsvRecord + { + return SerializeTsv(data, FormatType.COMMENTED_TSV); + } + + protected static byte[] SerializeTsv(IList data, FormatType tsvFormat) where T : TsvRecord { var bytes = new List(); @@ -716,14 +766,20 @@ public class SaneTsv foreach (PropertyInfo property in typeof(T).GetProperties()) { - SaneTsvColumnAttribute attribute = (SaneTsvColumnAttribute)Attribute.GetCustomAttribute(property, typeof(SaneTsvColumnAttribute)); + TsvColumnAttribute attribute = (TsvColumnAttribute)Attribute.GetCustomAttribute(property, typeof(TsvColumnAttribute)); if (attribute == null) { continue; } - headerNames.Add(attribute.ColumnName ?? property.Name); - headerTypes.Add(attribute.ColumnType ?? GetColumnFromType(property.PropertyType)); + string headerName = attribute.ColumnName ?? property.Name; + headerNames.Add(headerName); + Type headerType = attribute.ColumnType ?? GetColumnFromType(property.PropertyType); + if (tsvFormat == FormatType.SIMPLE_TSV && headerType != typeof(StringType)) + { + throw new Exception($"Serializing Simple TSV requires all columns be of type string, but column '{headerName}' has type '{headerType}'"); + } + headerTypes.Add(headerType); headerPropertyInfos.Add(property); // TODO: Check that the property type and given column type are compatible columnCount++; @@ -990,42 +1046,64 @@ public class SaneTsv } } - public class SaneTsvRecord + public class TsvRecord + { + public int? Line { get; set; } + public TsvRecord(int? line) + { + Line = line; + } + + public TsvRecord() { } + } + + public class CommentedTsvRecord : TsvRecord { public string Comment { get; set; } - public int? Line { get; set; } - public SaneTsvRecord(string comment, int? line) + public CommentedTsvRecord(string comment, int? line) { Comment = comment; Line = line; } - public SaneTsvRecord() { } + public CommentedTsvRecord() { } } - public class TestRecord : SaneTsvRecord + public class TestRecord : CommentedTsvRecord { - [SaneTsvColumn("my-column")] + [TypedTsvColumn("my-column")] public string MyColumn { get; set; } } // TODO: Add column ordering - public class SaneTsvColumnAttribute : Attribute + public class TsvColumnAttribute : Attribute { public string ColumnName { get; } - public Type ColumnType { get; } + public virtual Type ColumnType { get; } - public SaneTsvColumnAttribute() { } - - public SaneTsvColumnAttribute(string columnName) + public TsvColumnAttribute() { + ColumnType = typeof(StringType); + } + public TsvColumnAttribute(string columnName) + { + ColumnType = typeof(StringType); ColumnName = columnName; } + } - public SaneTsvColumnAttribute(string columnName, Type columnType) + // TODO: Add column ordering + public class TypedTsvColumnAttribute : TsvColumnAttribute + { + public override Type ColumnType { get; } + + public TypedTsvColumnAttribute() { } + + public TypedTsvColumnAttribute(string columnName) : base(columnName) { } + + public TypedTsvColumnAttribute(string columnName, Type columnType) : base(columnName) { - ColumnName = columnName; if (columnType.BaseType != typeof(ColumnType)) { throw new Exception("Column type must inherit from SaneTsv.ColumnType"); @@ -1033,7 +1111,7 @@ public class SaneTsv ColumnType = columnType; } - public SaneTsvColumnAttribute(Type columnType) + public TypedTsvColumnAttribute(Type columnType) { if (columnType.BaseType != typeof(ColumnType)) { diff --git a/SaneTsv/SaneTsvTest/Program.cs b/SaneTsv/SaneTsvTest/Program.cs index fd0b7e1..d117329 100644 --- a/SaneTsv/SaneTsvTest/Program.cs +++ b/SaneTsv/SaneTsvTest/Program.cs @@ -3,39 +3,39 @@ using System.Text; internal class Program { - public class TestRecord : SaneTsv.SaneTsvRecord + public class TestRecord : SaneTsv.TsvRecord { - [SaneTsv.SaneTsvColumn("string-test")] + [SaneTsv.TypedTsvColumn("string-test")] public string StringTest { get; set; } - [SaneTsv.SaneTsvColumn("bool-test")] + [SaneTsv.TypedTsvColumn("bool-test")] public bool BoolTest { get; set; } - [SaneTsv.SaneTsvColumn("float32-test")] + [SaneTsv.TypedTsvColumn("float32-test")] public float Float32Test { get; set; } - [SaneTsv.SaneTsvColumn("float32-le-test", typeof(SaneTsv.Float32LEType))] + [SaneTsv.TypedTsvColumn("float32-le-test", typeof(SaneTsv.Float32LEType))] public float Float32LETest { get; set; } - [SaneTsv.SaneTsvColumn("float64-test")] + [SaneTsv.TypedTsvColumn("float64-test")] public double Float64Test { get; set; } - [SaneTsv.SaneTsvColumn("float64-le-test", typeof(SaneTsv.Float64LEType))] + [SaneTsv.TypedTsvColumn("float64-le-test", typeof(SaneTsv.Float64LEType))] public double Float64LETest { get; set; } - [SaneTsv.SaneTsvColumn("uint32-test")] + [SaneTsv.TypedTsvColumn("uint32-test")] public UInt32 UInt32Test { get; set; } - [SaneTsv.SaneTsvColumn("uint64-test")] + [SaneTsv.TypedTsvColumn("uint64-test")] public UInt64 UInt64Test { get; set; } - [SaneTsv.SaneTsvColumn("int32-test")] + [SaneTsv.TypedTsvColumn("int32-test")] public Int32 Int32Test { get; set; } - [SaneTsv.SaneTsvColumn("int64-test")] + [SaneTsv.TypedTsvColumn("int64-test")] public Int64 Int64Test { get; set; } - [SaneTsv.SaneTsvColumn("binary-test")] + [SaneTsv.TypedTsvColumn("binary-test")] public byte[] BinaryTest { get; set; } public TestRecord(string stringTest, bool boolTest, float float32Test, float float32LETest, double float64Test, double float64LETest, UInt32 uInt32Test, UInt64 uInt64Test, Int32 int32Test, Int64 int64Test, byte[] binaryTest) @@ -56,48 +56,48 @@ internal class Program public TestRecord() { } } - public class BoolTestRecord : SaneTsv.SaneTsvRecord + public class BoolTestRecord : SaneTsv.CommentedTsvRecord { - [SaneTsv.SaneTsvColumn("column1:ty#pe")] + [SaneTsv.TypedTsvColumn("column1:ty#pe")] public bool Column1 { get; set; } - [SaneTsv.SaneTsvColumn] + [SaneTsv.TypedTsvColumn] public byte[] column2 { get; set; } - [SaneTsv.SaneTsvColumn("columnthree\nyep")] + [SaneTsv.TypedTsvColumn("columnthree\nyep")] public string Column3 { get; set; } } - public class BoolTestRecord2 : SaneTsv.SaneTsvRecord + public class BoolTestRecord2 : SaneTsv.CommentedTsvRecord { - [SaneTsv.SaneTsvColumn("column1:type")] + [SaneTsv.TypedTsvColumn("column1:type")] public bool Column1 { get; set; } - [SaneTsv.SaneTsvColumn] + [SaneTsv.TypedTsvColumn] public byte[] column2 { get; set; } - [SaneTsv.SaneTsvColumn("columnthree\nyep")] + [SaneTsv.TypedTsvColumn("columnthree\nyep")] public string Column3 { get; set; } } - public class SerdeTestRecord : SaneTsv.SaneTsvRecord + public class SerdeTestRecord : SaneTsv.CommentedTsvRecord { - [SaneTsv.SaneTsvColumn("column1")] + [SaneTsv.TypedTsvColumn("column1")] public bool Column1 { get; set; } - [SaneTsv.SaneTsvColumn] + [SaneTsv.TypedTsvColumn] public byte[] column2 { get; set; } - [SaneTsv.SaneTsvColumn("columnthree\nyep")] + [SaneTsv.TypedTsvColumn("columnthree\nyep")] public string Column3 { get; set; } } - public class FloatTestRecord : SaneTsv.SaneTsvRecord + public class FloatTestRecord : SaneTsv.CommentedTsvRecord { - [SaneTsv.SaneTsvColumn("somefloat")] + [SaneTsv.TypedTsvColumn("somefloat")] public double SomeFloat { get; set; } - [SaneTsv.SaneTsvColumn("binfloat", typeof(SaneTsv.Float64LEType))] + [SaneTsv.TypedTsvColumn("binfloat", typeof(SaneTsv.Float64LEType))] public double BinFloat { get; set; } } @@ -148,7 +148,7 @@ internal class Program "\nTRUE\tvalue\\\\t\0woo\tvaluetrhee" + "\nFALSE\tnother\tno\\ther"; - Tsv parsed = SaneTsv.ParseCommentedTsv(Encoding.UTF8.GetBytes(testString1)); + CommentedTsv parsed = SaneTsv.ParseCommentedTsv(Encoding.UTF8.GetBytes(testString1)); } //{ @@ -188,8 +188,6 @@ internal class Program } } - Console.WriteLine("Done with tests"); - { string testName = "Serde test"; @@ -215,5 +213,87 @@ internal class Program Console.WriteLine($"Failed {testName}"); } } + + { + string testName = "Trying to parse a not commented record as a Commented TSV test"; + + // These should not compile: + + //byte[] serialized = SaneTsv.SerializeCommentedTsv(data); + // Gives this error: error CS7036: There is no argument given that corresponds to the required parameter 'fileComment' of 'SaneTsv.SerializeCommentedTsv(IList, string)' + + //Tsv parsed = SaneTsv.ParseCommentedTsv(serialized); + // Gives this error: error CS0311: The type 'Program.TestRecord' cannot be used as type parameter 'T' in the generic type or method 'SaneTsv.ParseCommentedTsv(byte[])'. There is no implicit reference conversion from 'Program.TestRecord' to 'NathanMcRae.SaneTsv.CommentedTsvRecord'. + } + + { + string testName = "Try to parsed a Commented TSV as a Simple TSV"; + + string testString1 = "#This is a file comment\n" + + "#One more file comment line\n" + + "column1:type:boolean\tcolumn2:binary\tcolumnthree\\nyep:string" + + "\n#This is a comment" + + "\n#Another comment line" + + "\nTRUE\tvalue\\\\t\0woo\tvaluetrhee" + + "\nFALSE\tnother\tno\\ther"; + + try + { + Tsv parsed = SaneTsv.ParseSimpleTsv(Encoding.UTF8.GetBytes(testString1)); + + Console.WriteLine($"Failed {testName}"); + } + catch (Exception e) + { + Console.WriteLine($"Passed {testName}"); + } + } + + { + string testName = "Try to parsed a Commented TSV as a Typed TSV"; + + string testString1 = "#This is a file comment\n" + + "#One more file comment line\n" + + "column1:type:boolean\tcolumn2:binary\tcolumnthree\\nyep:string" + + "\n#This is a comment" + + "\n#Another comment line" + + "\nTRUE\tvalue\\\\t\0woo\tvaluetrhee" + + "\nFALSE\tnother\tno\\ther"; + + try + { + Tsv parsed = SaneTsv.ParseTypedTsv(Encoding.UTF8.GetBytes(testString1)); + + Console.WriteLine($"Failed {testName}"); + } + catch (Exception e) + { + Console.WriteLine($"Passed {testName}"); + } + } + + { + string testName = "Try to parsed a Typed TSV as a Simple TSV"; + + string testString1 = + "column1:type:boolean\tcolumn2:binary\tcolumnthree\\nyep:string" + + "\nTRUE\tvalue\\\\t\0woo\tvaluetrhee" + + "\nFALSE\tnother\tno\\ther"; + + try + { + Tsv parsed = SaneTsv.ParseSimpleTsv(Encoding.UTF8.GetBytes(testString1)); + + Console.WriteLine($"Failed {testName}"); + } + catch (Exception e) + { + Console.WriteLine($"Passed {testName}"); + } + } + + Console.WriteLine("Done with tests"); } } + +