From 82b7dc24fc93c3551092cc6932d82b31f23fbc09 Mon Sep 17 00:00:00 2001 From: Devin Duanne Date: Tue, 28 Oct 2025 12:57:21 -0500 Subject: [PATCH 01/16] Initial implementation --- src/serde/FixedWidth/FieldOverflowHandling.cs | 18 +++ .../FixedWidth/FixedFieldInfoAttribute.cs | 28 ++++ .../FixedWidth/FixedWidthDeserializer.cs | 111 ++++++++++++++ src/serde/FixedWidth/FixedWidthSerdeObject.cs | 139 ++++++++++++++++++ .../FixedWidthSerializationOptions.cs | 16 ++ .../FixedWidthSerializer.Serialize.cs | 69 +++++++++ src/serde/FixedWidth/FixedWidthSerializer.cs | 76 ++++++++++ .../FixedWidth/Reader/FixedWidthReader.cs | 103 +++++++++++++ 8 files changed, 560 insertions(+) create mode 100644 src/serde/FixedWidth/FieldOverflowHandling.cs create mode 100644 src/serde/FixedWidth/FixedFieldInfoAttribute.cs create mode 100644 src/serde/FixedWidth/FixedWidthDeserializer.cs create mode 100644 src/serde/FixedWidth/FixedWidthSerdeObject.cs create mode 100644 src/serde/FixedWidth/FixedWidthSerializationOptions.cs create mode 100644 src/serde/FixedWidth/FixedWidthSerializer.Serialize.cs create mode 100644 src/serde/FixedWidth/FixedWidthSerializer.cs create mode 100644 src/serde/FixedWidth/Reader/FixedWidthReader.cs diff --git a/src/serde/FixedWidth/FieldOverflowHandling.cs b/src/serde/FixedWidth/FieldOverflowHandling.cs new file mode 100644 index 00000000..17ac0538 --- /dev/null +++ b/src/serde/FixedWidth/FieldOverflowHandling.cs @@ -0,0 +1,18 @@ +namespace Serde.FixedWidth +{ + /// + /// Enumerates the supported options for field values that are too long. + /// + public enum FieldOverflowHandling + { + /// + /// Indicates that an exception should be thrown if the field value is longer than the field length. + /// + Throw = 0, + + /// + /// Indicates that the field value should be truncated if the value is longer than the field length. + /// + Truncate = 1, + } +} diff --git a/src/serde/FixedWidth/FixedFieldInfoAttribute.cs b/src/serde/FixedWidth/FixedFieldInfoAttribute.cs new file mode 100644 index 00000000..d3c9188b --- /dev/null +++ b/src/serde/FixedWidth/FixedFieldInfoAttribute.cs @@ -0,0 +1,28 @@ +using System; +using System.Collections.Generic; +using System.Text; + +namespace Serde.FixedWidth +{ + /// + /// Decorates a field in a fixed-width file with meta information about that field. + /// + [AttributeUsage(AttributeTargets.Property, AllowMultiple = false, Inherited = true)] + public sealed class FixedFieldInfoAttribute(int offset, int length, string format = "") : Attribute + { + /// + /// Gets the offset that indicates where the field begins. + /// + public int Offset => offset; + + /// + /// Gets the length of the field. + /// + public int Length => length; + + /// + /// Gets the format string for providing to TryParseExact. + /// + public string Format => format; + } +} diff --git a/src/serde/FixedWidth/FixedWidthDeserializer.cs b/src/serde/FixedWidth/FixedWidthDeserializer.cs new file mode 100644 index 00000000..d8a1702e --- /dev/null +++ b/src/serde/FixedWidth/FixedWidthDeserializer.cs @@ -0,0 +1,111 @@ +// Contains implementations of data interfaces for core types + +using System; +using System.Buffers; +using System.IO; +using System.Text; +using Serde.FixedWidth.Reader; +using Serde.IO; + +namespace Serde.FixedWidth +{ + internal sealed class FixedWidthDeserializer + { + public static FixedWidthDeserializer FromString(string s) + => FromUtf8_Unsafe(Encoding.UTF8.GetBytes(s)); + + internal static FixedWidthDeserializer FromUtf8_Unsafe(byte[] utf8Bytes) + { + var reader = new FixedWidthReader(); + return new FixedWidthDeserializer(reader); + } + } + + /// + /// Defines a type which handles deserializing a fixed-width file. + /// + internal sealed class FixedWidthDeserializer(TReader byteReader) : IDeserializer + where TReader : IByteReader + { + private readonly ScratchBuffer _scratch = new(); + + /// + public string ReadString() => Encoding.UTF8.GetString(ReadUtf8Span()); + + private Utf8Span ReadUtf8Span() + { + var peek = byteReader.Peek(); + if (peek == IByteReader.EndOfStream) + { + throw new EndOfStreamException(); + } + + byteReader.Advance(); + _scratch.Clear(); + return byteReader.LexUtf8Span(false, _scratch); + } + + public void EoF() + { + if (byteReader.Peek() != IByteReader.EndOfStream) + { + throw new InvalidOperationException("Expected end of stream."); + } + } + + /// + public bool ReadBool() => throw new NotImplementedException(); + + /// + public void ReadBytes(IBufferWriter writer) => throw new NotImplementedException(); + + /// + public char ReadChar() => throw new NotImplementedException(); + + /// + public DateTime ReadDateTime() => throw new NotImplementedException(); + + /// + public decimal ReadDecimal() => throw new NotImplementedException(); + + /// + public float ReadF32() => throw new NotImplementedException(); + + /// + public double ReadF64() => throw new NotImplementedException(); + + /// + public short ReadI16() => throw new NotImplementedException(); + + /// + public int ReadI32() => throw new NotImplementedException(); + + /// + public long ReadI64() => throw new NotImplementedException(); + + /// + public sbyte ReadI8() => throw new NotImplementedException(); + + /// + public T? ReadNullableRef(IDeserialize deserialize) + where T : class => throw new NotImplementedException(); + + /// + public ITypeDeserializer ReadType(ISerdeInfo typeInfo) => throw new NotImplementedException(); + + /// + public ushort ReadU16() => throw new NotImplementedException(); + + /// + public uint ReadU32() => throw new NotImplementedException(); + + /// + public ulong ReadU64() => throw new NotImplementedException(); + + /// + public byte ReadU8() => throw new NotImplementedException(); + + /// + public void Dispose() => throw new NotImplementedException(); + } +} diff --git a/src/serde/FixedWidth/FixedWidthSerdeObject.cs b/src/serde/FixedWidth/FixedWidthSerdeObject.cs new file mode 100644 index 00000000..5ced93e2 --- /dev/null +++ b/src/serde/FixedWidth/FixedWidthSerdeObject.cs @@ -0,0 +1,139 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; +using System.Globalization; +using System.Linq; +using System.Reflection; +using System.Text; + +namespace Serde.FixedWidth +{ + /// + /// Defines a type which handles (de)serialization of fixed-width text files. + /// + /// The underlying model for the file. + /// Options for configuring the serialization of the type. + public class FixedWidthSerdeObject<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicProperties)] T>(FixedWidthSerializationOptions options) : ISerde + { + /// + public ISerdeInfo SerdeInfo => StringProxy.SerdeInfo; + + /// + /// Initializes a new instance of the class. + /// + public FixedWidthSerdeObject() : this(FixedWidthSerializationOptions.Default) + { + } + + /// + /// Initializes a new instance of the class. + /// + /// Determines how to handle overflows, i.e. when the field value is longer than the field length. + public FixedWidthSerdeObject(FieldOverflowHandling overflowHandling) + : this(new FixedWidthSerializationOptions { FieldOverflowHandling = overflowHandling }) + { + } + + /// + public virtual void Serialize(T obj, ISerializer serializer) + { + var fieldInfo = FixedFieldInfo.FromProperties(typeof(T).GetProperties(BindingFlags.Instance | BindingFlags.Public)).OrderBy(it => it.Offset); + StringBuilder sb = new(); + + int index = 0; + const char padding = ' '; + foreach (var field in fieldInfo) + { + if (index <= field.Offset) + { + // Fill in any missing space with padding. + sb.Append(padding, field.Offset - index); + + object? value = field.Property.GetValue(obj); + + string valueText = value is IFormattable formattable + ? formattable.ToString(field.Format, CultureInfo.InvariantCulture) + : value?.ToString() ?? string.Empty; + + if (valueText.Length == 0) + { + // value is null or empty + continue; + } + + if (valueText.Length > field.Length) + { + if (options.FieldOverflowHandling is FieldOverflowHandling.Throw) + { + throw new InvalidOperationException($"Value '{field.Property.Name}' ({valueText}) is too long for field! Expected: {field.Length}; Actual: {valueText.Length}"); + } + + // Truncate value to the maximum field length. + valueText = valueText[..field.Length]; + } + + sb.Append(valueText.PadRight(field.Length)); + index += field.Length; + } + } + + serializer.WriteString(sb.ToString()); + } + + /// + public virtual T Deserialize(IDeserializer deserializer) + { + var fieldInfo = FixedFieldInfo.FromProperties(typeof(T).GetProperties(BindingFlags.Instance | BindingFlags.Public)).OrderBy(it => it.Offset); + string line = deserializer.ReadString(); + } + } + + /// + /// Gets information for the property. + /// + /// The property. + /// Its attribute. + file class FixedFieldInfo(PropertyInfo property, FixedFieldInfoAttribute attribute) + { + /// + /// Gets the offset for the start of the field. + /// + public int Offset => _attribute.Offset; + + /// + /// Gets the max length of the field. + /// + public int Length => _attribute.Length; + + /// + /// Gets the output format. + /// + public string Format => _attribute.Format; + + /// + /// Gets the property described by this fixed field info. + /// + public PropertyInfo Property => property; + + private readonly FixedFieldInfoAttribute _attribute = attribute; + + /// + /// Enumerates a set of FixedFieldInfos from a set of . + /// + /// A set of property infos. + /// An enumerable iterating over property infos. + public static IEnumerable FromProperties(PropertyInfo[] properties) + { + foreach (PropertyInfo property in properties) + { + if (property.GetCustomAttribute() is not { } attribute) + { + // If not decorated with the attribute, skip. + continue; + } + + yield return new(property, attribute); + } + } + } +} diff --git a/src/serde/FixedWidth/FixedWidthSerializationOptions.cs b/src/serde/FixedWidth/FixedWidthSerializationOptions.cs new file mode 100644 index 00000000..20576e1c --- /dev/null +++ b/src/serde/FixedWidth/FixedWidthSerializationOptions.cs @@ -0,0 +1,16 @@ +namespace Serde.FixedWidth +{ + /// + /// Gets options for configuring the Serde object. + /// + public sealed class FixedWidthSerializationOptions + { + public static FixedWidthSerializationOptions Default => new(); + + /// + /// Gets a value indicating how to handle field overflows, i.e. when the + /// field value is longer than the field length. + /// + public FieldOverflowHandling FieldOverflowHandling { get; init; } = FieldOverflowHandling.Throw; + } +} diff --git a/src/serde/FixedWidth/FixedWidthSerializer.Serialize.cs b/src/serde/FixedWidth/FixedWidthSerializer.Serialize.cs new file mode 100644 index 00000000..78d04158 --- /dev/null +++ b/src/serde/FixedWidth/FixedWidthSerializer.Serialize.cs @@ -0,0 +1,69 @@ +using System; +using System.Collections.Generic; +using System.Text; + +namespace Serde.FixedWidth +{ + public sealed partial class FixedWidthSerializer + { + /// + public void WriteString(string s) => writer.WriteLine(s); + + /// + public void WriteBool(bool b) => throw new NotImplementedException(); + + /// + public void WriteBytes(ReadOnlyMemory bytes) => throw new NotImplementedException(); + + /// + public void WriteChar(char c) => throw new NotImplementedException(); + + /// + public ITypeSerializer WriteCollection(ISerdeInfo info, int? count) => throw new NotImplementedException(); + + /// + public void WriteDateTime(DateTime dt) => throw new NotImplementedException(); + + /// + public void WriteDateTimeOffset(DateTimeOffset dt) => throw new NotImplementedException(); + + /// + public void WriteDecimal(decimal d) => throw new NotImplementedException(); + + /// + public void WriteF32(float f) => throw new NotImplementedException(); + + /// + public void WriteF64(double d) => throw new NotImplementedException(); + + /// + public void WriteI16(short i16) => throw new NotImplementedException(); + + /// + public void WriteI32(int i32) => throw new NotImplementedException(); + + /// + public void WriteI64(long i64) => throw new NotImplementedException(); + + /// + public void WriteI8(sbyte b) => throw new NotImplementedException(); + + /// + public void WriteNull() => throw new NotImplementedException(); + + /// + public ITypeSerializer WriteType(ISerdeInfo info) => throw new NotImplementedException(); + + /// + public void WriteU16(ushort u16) => throw new NotImplementedException(); + + /// + public void WriteU32(uint u32) => throw new NotImplementedException(); + + /// + public void WriteU64(ulong u64) => throw new NotImplementedException(); + + /// + public void WriteU8(byte b) => throw new NotImplementedException(); + } +} diff --git a/src/serde/FixedWidth/FixedWidthSerializer.cs b/src/serde/FixedWidth/FixedWidthSerializer.cs new file mode 100644 index 00000000..8730cb83 --- /dev/null +++ b/src/serde/FixedWidth/FixedWidthSerializer.cs @@ -0,0 +1,76 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Text; +using System.Text.Json; + +namespace Serde.FixedWidth +{ + /// + /// Defines a serializer for Fixed Width files. + /// + public sealed partial class FixedWidthSerializer(StringWriter writer) : ISerializer + { + public static string Serialize(T provider, ISerialize serde) + { + using var writer = new StringWriter(); + var serializer = new FixedWidthSerializer(writer); + serde.Serialize(provider, serializer); + writer.Flush(); + return writer.GetStringBuilder().ToString(); + } + + public static string Serialize(T serde) + where TProvider : ISerializeProvider + => Serialize(serde, TProvider.Instance); + + public static string Serialize(T serde) + where T : ISerializeProvider + => Serialize(serde, T.Instance); + + public static T Deserialize(string source) + where T : IDeserializeProvider + => Deserialize(source); + + public static List DeserializeList(string source) + where T : IDeserializeProvider +#if NET10_0_OR_GREATER + => Deserialize(source, List.Deserialize); +#else + => Deserialize(source, ListProxy.De.Instance); +#endif + + public static T Deserialize(string source) + where TProvider : IDeserializeProvider + => Deserialize(source, TProvider.Instance); + + public static T Deserialize(string source, IDeserialize d) + { + var bytes = Encoding.UTF8.GetBytes(source); + return Deserialize_Unsafe(bytes, d); + } + + public static T Deserialize(byte[] utf8Bytes, IDeserialize d) + { + try + { + // Checks for invalid utf8 as a side effect. + _ = Encoding.UTF8.GetCharCount(utf8Bytes); + } + catch (Exception ex) + { + throw new ArgumentException("Array is not valid UTF-8", nameof(utf8Bytes), ex); + } + return Deserialize_Unsafe(utf8Bytes, d); + } + + private static T Deserialize_Unsafe(byte[] utf8Bytes, IDeserialize d) + { + T result; + using var deserializer = FixedWidthDeserializer.FromUtf8_Unsafe(utf8Bytes); + result = d.Deserialize(deserializer); + deserializer.EoF(); + return result; + } + } +} diff --git a/src/serde/FixedWidth/Reader/FixedWidthReader.cs b/src/serde/FixedWidth/Reader/FixedWidthReader.cs new file mode 100644 index 00000000..64bf02e1 --- /dev/null +++ b/src/serde/FixedWidth/Reader/FixedWidthReader.cs @@ -0,0 +1,103 @@ +using System; +using System.Diagnostics; +using System.IO; +using Serde.IO; + +namespace Serde.FixedWidth.Reader +{ + internal struct FixedWidthReader(byte[] bytes) : IByteReader + { + private readonly byte[] _bytes = bytes; + private int _pos = 0; + + /// + public short Next() + { + var b = Peek(); + if (b != IByteReader.EndOfStream) + { + _pos++; + } + + return b; + } + + /// + public void Advance(int count = 1) + { + _pos += count; + } + + /// + public readonly short Peek() + { + if (_pos >= _bytes.Length) + { + return IByteReader.EndOfStream; + } + return _bytes[_pos]; + } + + /// + public readonly bool StartsWith(Utf8Span span) + { + if (span.Length > _bytes.Length - _pos) + { + return false; + } + + return span.SequenceEqual(_bytes.AsSpan(_pos, span.Length)); + } + + /// + public Utf8Span LexUtf8Span(bool skipOnly, ScratchBuffer? scratch) + { + var span = _bytes.AsSpan(); + int start = _pos; + + SkipToEndOfLine(); + if (_pos >= span.Length) + { + throw new EndOfStreamException(); + } + + if (skipOnly) + { + Advance(); + return Utf8Span.Empty; + } + + Debug.Assert(scratch is not null); + var curSpan = span[start.._pos]; + Utf8Span strSpan; + if (scratch.Count == 0) + { + strSpan = curSpan; + } + else + { + scratch.AddRange(curSpan); + strSpan = scratch.Span; + } + Advance(); + return strSpan; + } + + private void SkipToEndOfLine() + { + var span = _bytes.AsSpan(_pos); + var offset = 0; + while (offset < span.Length && !IsEndOfLine(span[offset])) + { + offset++; + } + + _pos += offset; + } + + private static bool IsEndOfLine(byte b) + { + return b == (byte)'\r' || b == (byte)'\n'; + } + } +} From cd89fd07d62d73d9b05968694188cae0ce37b0bb Mon Sep 17 00:00:00 2001 From: Devin Duanne Date: Wed, 29 Oct 2025 12:07:51 -0500 Subject: [PATCH 02/16] Move from serde object to (de)serializers --- Directory.Build.props | 4 +- .../FixedWidth/FixedFieldInfoAttribute.cs | 19 ++- .../FixedWidthDeserializer.Deserialize.cs | 68 +++++++++ .../FixedWidth/FixedWidthDeserializer.Type.cs | 112 ++++++++++++++ .../FixedWidth/FixedWidthDeserializer.cs | 102 +------------ src/serde/FixedWidth/FixedWidthSerdeObject.cs | 3 + .../FixedWidthSerializer.Serialize.cs | 89 ++++------- .../FixedWidth/FixedWidthSerializer.Type.cs | 37 +++++ src/serde/FixedWidth/FixedWidthSerializer.cs | 10 +- .../FixedWidth/Reader/FixedWidthReader.cs | 2 +- .../FixedWidth/Writer/FixedWidthWriter.cs | 138 ++++++++++++++++++ 11 files changed, 418 insertions(+), 166 deletions(-) create mode 100644 src/serde/FixedWidth/FixedWidthDeserializer.Deserialize.cs create mode 100644 src/serde/FixedWidth/FixedWidthDeserializer.Type.cs create mode 100644 src/serde/FixedWidth/FixedWidthSerializer.Type.cs create mode 100644 src/serde/FixedWidth/Writer/FixedWidthWriter.cs diff --git a/Directory.Build.props b/Directory.Build.props index 6dfe6eff..602e3725 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -4,7 +4,7 @@ enable true net10.0 - 0.9.1 - 0.9.1 + 0.10.0-fixedWidth.1 + 0.10.0-fixedWidth.1 diff --git a/src/serde/FixedWidth/FixedFieldInfoAttribute.cs b/src/serde/FixedWidth/FixedFieldInfoAttribute.cs index d3c9188b..9077fc7b 100644 --- a/src/serde/FixedWidth/FixedFieldInfoAttribute.cs +++ b/src/serde/FixedWidth/FixedFieldInfoAttribute.cs @@ -1,6 +1,4 @@ using System; -using System.Collections.Generic; -using System.Text; namespace Serde.FixedWidth { @@ -8,21 +6,30 @@ namespace Serde.FixedWidth /// Decorates a field in a fixed-width file with meta information about that field. /// [AttributeUsage(AttributeTargets.Property, AllowMultiple = false, Inherited = true)] - public sealed class FixedFieldInfoAttribute(int offset, int length, string format = "") : Attribute + public sealed class FixedFieldInfoAttribute : Attribute { + public FixedFieldInfoAttribute(int offset, int length, string format = "") + { + ArgumentOutOfRangeException.ThrowIfNegativeOrZero(offset); + ArgumentOutOfRangeException.ThrowIfNegativeOrZero(length); + Offset = offset; + Length = length; + Format = string.IsNullOrWhiteSpace(format) ? string.Empty : format; + } + /// /// Gets the offset that indicates where the field begins. /// - public int Offset => offset; + public int Offset { get; } /// /// Gets the length of the field. /// - public int Length => length; + public int Length { get; } /// /// Gets the format string for providing to TryParseExact. /// - public string Format => format; + public string Format { get; } } } diff --git a/src/serde/FixedWidth/FixedWidthDeserializer.Deserialize.cs b/src/serde/FixedWidth/FixedWidthDeserializer.Deserialize.cs new file mode 100644 index 00000000..95b175aa --- /dev/null +++ b/src/serde/FixedWidth/FixedWidthDeserializer.Deserialize.cs @@ -0,0 +1,68 @@ +using Serde.IO; +using System; +using System.Buffers; +using System.Collections.Generic; +using System.IO; +using System.Reflection.Metadata.Ecma335; +using System.Text; + +namespace Serde.FixedWidth +{ + internal sealed partial class FixedWidthDeserializer(TReader byteReader) : IDeserializer + where TReader : IByteReader + { + ITypeDeserializer IDeserializer.ReadType(ISerdeInfo typeInfo) + { + if (typeInfo.Kind is not InfoKind.CustomType) + { + throw new ArgumentException("Invalid type for ReadType: " + typeInfo.Kind); + } + + return this; + } + + private readonly ScratchBuffer _scratch = new(); + + /// + public string ReadString() => Encoding.UTF8.GetString(ReadUtf8Span()); + + private Utf8Span ReadUtf8Span() + { + var peek = _byteReader.Peek(); + if (peek == IByteReader.EndOfStream) + { + throw new EndOfStreamException(); + } + + byteReader.Advance(); + _scratch.Clear(); + return byteReader.LexUtf8Span(false, _scratch); + } + + public void EoF() + { + if (byteReader.Peek() != IByteReader.EndOfStream) + { + throw new InvalidOperationException("Expected end of stream."); + } + } + + T? IDeserializer.ReadNullableRef(IDeserialize deserialize) where T : class => throw new NotImplementedException(); + bool IDeserializer.ReadBool() => throw new NotImplementedException(); + char IDeserializer.ReadChar() => throw new NotImplementedException(); + byte IDeserializer.ReadU8() => throw new NotImplementedException(); + ushort IDeserializer.ReadU16() => throw new NotImplementedException(); + uint IDeserializer.ReadU32() => throw new NotImplementedException(); + ulong IDeserializer.ReadU64() => throw new NotImplementedException(); + sbyte IDeserializer.ReadI8() => throw new NotImplementedException(); + short IDeserializer.ReadI16() => throw new NotImplementedException(); + int IDeserializer.ReadI32() => throw new NotImplementedException(); + long IDeserializer.ReadI64() => throw new NotImplementedException(); + float IDeserializer.ReadF32() => throw new NotImplementedException(); + double IDeserializer.ReadF64() => throw new NotImplementedException(); + decimal IDeserializer.ReadDecimal() => throw new NotImplementedException(); + DateTime IDeserializer.ReadDateTime() => throw new NotImplementedException(); + void IDeserializer.ReadBytes(IBufferWriter writer) => throw new NotImplementedException(); + void IDisposable.Dispose() => throw new NotImplementedException(); + } +} diff --git a/src/serde/FixedWidth/FixedWidthDeserializer.Type.cs b/src/serde/FixedWidth/FixedWidthDeserializer.Type.cs new file mode 100644 index 00000000..dd6d94f9 --- /dev/null +++ b/src/serde/FixedWidth/FixedWidthDeserializer.Type.cs @@ -0,0 +1,112 @@ +using System; +using System.Buffers; +using System.Collections.Generic; +using System.Text; + +namespace Serde.FixedWidth +{ + internal sealed partial class FixedWidthDeserializer : ITypeDeserializer + { + int? ITypeDeserializer.SizeOpt => null; + + bool ITypeDeserializer.ReadBool(ISerdeInfo info, int index) + { + throw new NotImplementedException(); + } + + void ITypeDeserializer.ReadBytes(ISerdeInfo info, int index, IBufferWriter writer) + { + throw new NotImplementedException(); + } + + char ITypeDeserializer.ReadChar(ISerdeInfo info, int index) + { + throw new NotImplementedException(); + } + + DateTime ITypeDeserializer.ReadDateTime(ISerdeInfo info, int index) + { + throw new NotImplementedException(); + } + + decimal ITypeDeserializer.ReadDecimal(ISerdeInfo info, int index) + { + throw new NotImplementedException(); + } + + float ITypeDeserializer.ReadF32(ISerdeInfo info, int index) + { + throw new NotImplementedException(); + } + + double ITypeDeserializer.ReadF64(ISerdeInfo info, int index) + { + throw new NotImplementedException(); + } + + short ITypeDeserializer.ReadI16(ISerdeInfo info, int index) + { + throw new NotImplementedException(); + } + + int ITypeDeserializer.ReadI32(ISerdeInfo info, int index) + { + throw new NotImplementedException(); + } + + long ITypeDeserializer.ReadI64(ISerdeInfo info, int index) + { + throw new NotImplementedException(); + } + + sbyte ITypeDeserializer.ReadI8(ISerdeInfo info, int index) + { + throw new NotImplementedException(); + } + + string ITypeDeserializer.ReadString(ISerdeInfo info, int index) + { + throw new NotImplementedException(); + } + + ushort ITypeDeserializer.ReadU16(ISerdeInfo info, int index) + { + throw new NotImplementedException(); + } + + uint ITypeDeserializer.ReadU32(ISerdeInfo info, int index) + { + throw new NotImplementedException(); + } + + ulong ITypeDeserializer.ReadU64(ISerdeInfo info, int index) + { + throw new NotImplementedException(); + } + + byte ITypeDeserializer.ReadU8(ISerdeInfo info, int index) + { + throw new NotImplementedException(); + } + + T ITypeDeserializer.ReadValue(ISerdeInfo info, int index, IDeserialize deserialize) + { + throw new NotImplementedException(); + } + + void ITypeDeserializer.SkipValue(ISerdeInfo info, int index) + { + throw new NotImplementedException(); + } + + int ITypeDeserializer.TryReadIndex(ISerdeInfo info) + { + throw new NotImplementedException(); + } + + (int, string? errorName) ITypeDeserializer.TryReadIndexWithName(ISerdeInfo info) + { + throw new NotImplementedException(); + } + } +} diff --git a/src/serde/FixedWidth/FixedWidthDeserializer.cs b/src/serde/FixedWidth/FixedWidthDeserializer.cs index d8a1702e..692b30df 100644 --- a/src/serde/FixedWidth/FixedWidthDeserializer.cs +++ b/src/serde/FixedWidth/FixedWidthDeserializer.cs @@ -1,111 +1,25 @@ // Contains implementations of data interfaces for core types -using System; -using System.Buffers; -using System.IO; -using System.Text; using Serde.FixedWidth.Reader; using Serde.IO; +using System.Text; namespace Serde.FixedWidth { - internal sealed class FixedWidthDeserializer - { - public static FixedWidthDeserializer FromString(string s) - => FromUtf8_Unsafe(Encoding.UTF8.GetBytes(s)); - - internal static FixedWidthDeserializer FromUtf8_Unsafe(byte[] utf8Bytes) - { - var reader = new FixedWidthReader(); - return new FixedWidthDeserializer(reader); - } - } - /// /// Defines a type which handles deserializing a fixed-width file. /// - internal sealed class FixedWidthDeserializer(TReader byteReader) : IDeserializer - where TReader : IByteReader + internal sealed partial class FixedWidthDeserializer { - private readonly ScratchBuffer _scratch = new(); - - /// - public string ReadString() => Encoding.UTF8.GetString(ReadUtf8Span()); - - private Utf8Span ReadUtf8Span() - { - var peek = byteReader.Peek(); - if (peek == IByteReader.EndOfStream) - { - throw new EndOfStreamException(); - } + internal readonly TReader _byteReader = byteReader; - byteReader.Advance(); - _scratch.Clear(); - return byteReader.LexUtf8Span(false, _scratch); - } + public static FixedWidthDeserializer FromString(string s) + => FromUtf8_Unsafe(Encoding.UTF8.GetBytes(s)); - public void EoF() + public static FixedWidthDeserializer FromUtf8_Unsafe(byte[] utf8Bytes) { - if (byteReader.Peek() != IByteReader.EndOfStream) - { - throw new InvalidOperationException("Expected end of stream."); - } + var reader = new FixedWidthReader(utf8Bytes); + return new FixedWidthDeserializer(reader); } - - /// - public bool ReadBool() => throw new NotImplementedException(); - - /// - public void ReadBytes(IBufferWriter writer) => throw new NotImplementedException(); - - /// - public char ReadChar() => throw new NotImplementedException(); - - /// - public DateTime ReadDateTime() => throw new NotImplementedException(); - - /// - public decimal ReadDecimal() => throw new NotImplementedException(); - - /// - public float ReadF32() => throw new NotImplementedException(); - - /// - public double ReadF64() => throw new NotImplementedException(); - - /// - public short ReadI16() => throw new NotImplementedException(); - - /// - public int ReadI32() => throw new NotImplementedException(); - - /// - public long ReadI64() => throw new NotImplementedException(); - - /// - public sbyte ReadI8() => throw new NotImplementedException(); - - /// - public T? ReadNullableRef(IDeserialize deserialize) - where T : class => throw new NotImplementedException(); - - /// - public ITypeDeserializer ReadType(ISerdeInfo typeInfo) => throw new NotImplementedException(); - - /// - public ushort ReadU16() => throw new NotImplementedException(); - - /// - public uint ReadU32() => throw new NotImplementedException(); - - /// - public ulong ReadU64() => throw new NotImplementedException(); - - /// - public byte ReadU8() => throw new NotImplementedException(); - - /// - public void Dispose() => throw new NotImplementedException(); } } diff --git a/src/serde/FixedWidth/FixedWidthSerdeObject.cs b/src/serde/FixedWidth/FixedWidthSerdeObject.cs index 5ced93e2..99ec096c 100644 --- a/src/serde/FixedWidth/FixedWidthSerdeObject.cs +++ b/src/serde/FixedWidth/FixedWidthSerdeObject.cs @@ -13,6 +13,7 @@ namespace Serde.FixedWidth /// /// The underlying model for the file. /// Options for configuring the serialization of the type. + [Obsolete("This is now handled with the serializer/deserializer")] public class FixedWidthSerdeObject<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicProperties)] T>(FixedWidthSerializationOptions options) : ISerde { /// @@ -85,6 +86,8 @@ public virtual T Deserialize(IDeserializer deserializer) { var fieldInfo = FixedFieldInfo.FromProperties(typeof(T).GetProperties(BindingFlags.Instance | BindingFlags.Public)).OrderBy(it => it.Offset); string line = deserializer.ReadString(); + + throw new NotImplementedException(); } } diff --git a/src/serde/FixedWidth/FixedWidthSerializer.Serialize.cs b/src/serde/FixedWidth/FixedWidthSerializer.Serialize.cs index 78d04158..c4d3446e 100644 --- a/src/serde/FixedWidth/FixedWidthSerializer.Serialize.cs +++ b/src/serde/FixedWidth/FixedWidthSerializer.Serialize.cs @@ -4,66 +4,37 @@ namespace Serde.FixedWidth { - public sealed partial class FixedWidthSerializer + public sealed partial class FixedWidthSerializer : ISerializer { /// - public void WriteString(string s) => writer.WriteLine(s); - - /// - public void WriteBool(bool b) => throw new NotImplementedException(); - - /// - public void WriteBytes(ReadOnlyMemory bytes) => throw new NotImplementedException(); - - /// - public void WriteChar(char c) => throw new NotImplementedException(); - - /// - public ITypeSerializer WriteCollection(ISerdeInfo info, int? count) => throw new NotImplementedException(); - - /// - public void WriteDateTime(DateTime dt) => throw new NotImplementedException(); - - /// - public void WriteDateTimeOffset(DateTimeOffset dt) => throw new NotImplementedException(); - - /// - public void WriteDecimal(decimal d) => throw new NotImplementedException(); - - /// - public void WriteF32(float f) => throw new NotImplementedException(); - - /// - public void WriteF64(double d) => throw new NotImplementedException(); - - /// - public void WriteI16(short i16) => throw new NotImplementedException(); - - /// - public void WriteI32(int i32) => throw new NotImplementedException(); - - /// - public void WriteI64(long i64) => throw new NotImplementedException(); - - /// - public void WriteI8(sbyte b) => throw new NotImplementedException(); - - /// - public void WriteNull() => throw new NotImplementedException(); - - /// - public ITypeSerializer WriteType(ISerdeInfo info) => throw new NotImplementedException(); - - /// - public void WriteU16(ushort u16) => throw new NotImplementedException(); - - /// - public void WriteU32(uint u32) => throw new NotImplementedException(); - - /// - public void WriteU64(ulong u64) => throw new NotImplementedException(); - - /// - public void WriteU8(byte b) => throw new NotImplementedException(); + ITypeSerializer ISerializer.WriteType(ISerdeInfo info) + { + if (info.Kind is not InfoKind.CustomType) + { + throw new ArgumentException("Invalid type for WriteType: " + info.Kind); + } + + return this; + } + + void ISerializer.WriteString(string s) => throw new NotImplementedException(); + void ISerializer.WriteBool(bool b) => throw new NotImplementedException(); + void ISerializer.WriteBytes(ReadOnlyMemory bytes) => throw new NotImplementedException(); + void ISerializer.WriteChar(char c) => throw new NotImplementedException(); + ITypeSerializer ISerializer.WriteCollection(ISerdeInfo info, int? count) => throw new NotImplementedException(); + void ISerializer.WriteDateTime(DateTime dt) => throw new NotImplementedException(); + void ISerializer.WriteDateTimeOffset(DateTimeOffset dt) => throw new NotImplementedException(); + void ISerializer.WriteDecimal(decimal d) => throw new NotImplementedException(); + void ISerializer.WriteF32(float f) => throw new NotImplementedException(); + void ISerializer.WriteF64(double d) => throw new NotImplementedException(); + void ISerializer.WriteI16(short i16) => throw new NotImplementedException(); + void ISerializer.WriteI32(int i32) => throw new NotImplementedException(); + void ISerializer.WriteI64(long i64) => throw new NotImplementedException(); + void ISerializer.WriteI8(sbyte b) => throw new NotImplementedException(); + void ISerializer.WriteNull() => throw new NotImplementedException(); + void ISerializer.WriteU16(ushort u16) => throw new NotImplementedException(); + void ISerializer.WriteU32(uint u32) => throw new NotImplementedException(); + void ISerializer.WriteU64(ulong u64) => throw new NotImplementedException(); + void ISerializer.WriteU8(byte b) => throw new NotImplementedException(); } } diff --git a/src/serde/FixedWidth/FixedWidthSerializer.Type.cs b/src/serde/FixedWidth/FixedWidthSerializer.Type.cs new file mode 100644 index 00000000..6516485a --- /dev/null +++ b/src/serde/FixedWidth/FixedWidthSerializer.Type.cs @@ -0,0 +1,37 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Diagnostics.CodeAnalysis; +using System.IO; +using System.Linq; +using System.Reflection; +using System.Text; + +namespace Serde.FixedWidth +{ + public sealed partial class FixedWidthSerializer : ITypeSerializer + { + void ITypeSerializer.WriteValue(ISerdeInfo typeInfo, int index, T value, ISerialize serialize) + => serialize.Serialize(value, this); + + void ITypeSerializer.End(ISerdeInfo info) => writer.WriteLine(); + void ITypeSerializer.WriteBool(ISerdeInfo typeInfo, int index, bool b) => writer.WriteObject(typeInfo, index, b); + void ITypeSerializer.WriteChar(ISerdeInfo typeInfo, int index, char c) => writer.WriteObject(typeInfo, index, c); + void ITypeSerializer.WriteU8(ISerdeInfo typeInfo, int index, byte b) => writer.WriteObject(typeInfo, index, b); + void ITypeSerializer.WriteU16(ISerdeInfo typeInfo, int index, ushort u16) => writer.WriteObject(typeInfo, index, u16); + void ITypeSerializer.WriteU32(ISerdeInfo typeInfo, int index, uint u32) => writer.WriteObject(typeInfo, index, u32); + void ITypeSerializer.WriteU64(ISerdeInfo typeInfo, int index, ulong u64) => writer.WriteObject(typeInfo, index, u64); + void ITypeSerializer.WriteI8(ISerdeInfo typeInfo, int index, sbyte b) => writer.WriteObject(typeInfo, index, b); + void ITypeSerializer.WriteI16(ISerdeInfo typeInfo, int index, short i16) => writer.WriteObject(typeInfo, index, i16); + void ITypeSerializer.WriteI32(ISerdeInfo typeInfo, int index, int i32) => writer.WriteObject(typeInfo, index, i32); + void ITypeSerializer.WriteI64(ISerdeInfo typeInfo, int index, long i64) => writer.WriteObject(typeInfo, index, i64); + void ITypeSerializer.WriteF32(ISerdeInfo typeInfo, int index, float f) => writer.WriteObject(typeInfo, index, f); + void ITypeSerializer.WriteF64(ISerdeInfo typeInfo, int index, double d) => writer.WriteObject(typeInfo, index, d); + void ITypeSerializer.WriteDecimal(ISerdeInfo typeInfo, int index, decimal d) => writer.WriteObject(typeInfo, index, d); + void ITypeSerializer.WriteString(ISerdeInfo typeInfo, int index, string s) => writer.WriteObject(typeInfo, index, s); + void ITypeSerializer.WriteNull(ISerdeInfo typeInfo, int index) => writer.WriteObject(typeInfo, index, string.Empty); + void ITypeSerializer.WriteDateTime(ISerdeInfo typeInfo, int index, DateTime dt) => writer.WriteObject(typeInfo, index, dt); + void ITypeSerializer.WriteDateTimeOffset(ISerdeInfo typeInfo, int index, DateTimeOffset dt) => writer.WriteObject(typeInfo, index, dt); + void ITypeSerializer.WriteBytes(ISerdeInfo typeInfo, int index, ReadOnlyMemory bytes) => writer.WriteObject(typeInfo, index, bytes); + } +} diff --git a/src/serde/FixedWidth/FixedWidthSerializer.cs b/src/serde/FixedWidth/FixedWidthSerializer.cs index 8730cb83..0372bb3e 100644 --- a/src/serde/FixedWidth/FixedWidthSerializer.cs +++ b/src/serde/FixedWidth/FixedWidthSerializer.cs @@ -1,4 +1,6 @@ -using System; +using Serde.FixedWidth.Reader; +using Serde.FixedWidth.Writer; +using System; using System.Collections.Generic; using System.IO; using System.Text; @@ -9,11 +11,11 @@ namespace Serde.FixedWidth /// /// Defines a serializer for Fixed Width files. /// - public sealed partial class FixedWidthSerializer(StringWriter writer) : ISerializer + public sealed partial class FixedWidthSerializer(FixedWidthWriter writer) { public static string Serialize(T provider, ISerialize serde) { - using var writer = new StringWriter(); + using var writer = new FixedWidthWriter(); var serializer = new FixedWidthSerializer(writer); serde.Serialize(provider, serializer); writer.Flush(); @@ -67,7 +69,7 @@ public static T Deserialize(byte[] utf8Bytes, IDeserialize d) private static T Deserialize_Unsafe(byte[] utf8Bytes, IDeserialize d) { T result; - using var deserializer = FixedWidthDeserializer.FromUtf8_Unsafe(utf8Bytes); + var deserializer = FixedWidthDeserializer.FromUtf8_Unsafe(utf8Bytes); result = d.Deserialize(deserializer); deserializer.EoF(); return result; diff --git a/src/serde/FixedWidth/Reader/FixedWidthReader.cs b/src/serde/FixedWidth/Reader/FixedWidthReader.cs index 64bf02e1..fc59b676 100644 --- a/src/serde/FixedWidth/Reader/FixedWidthReader.cs +++ b/src/serde/FixedWidth/Reader/FixedWidthReader.cs @@ -56,7 +56,7 @@ public Utf8Span LexUtf8Span(bool skipOnly, ScratchBuffer? scratch) int start = _pos; SkipToEndOfLine(); - if (_pos >= span.Length) + if (_pos > span.Length) { throw new EndOfStreamException(); } diff --git a/src/serde/FixedWidth/Writer/FixedWidthWriter.cs b/src/serde/FixedWidth/Writer/FixedWidthWriter.cs new file mode 100644 index 00000000..112db6d8 --- /dev/null +++ b/src/serde/FixedWidth/Writer/FixedWidthWriter.cs @@ -0,0 +1,138 @@ +using System; +using System.Diagnostics.CodeAnalysis; +using System.Globalization; +using System.IO; +using System.Linq; +using System.Reflection; +using System.Text; + +namespace Serde.FixedWidth.Writer +{ + public sealed class FixedWidthWriter : IDisposable + { + private const char Padding = ' '; + private readonly StringWriter _writer = new(); + private int _pos = 0; + + public void Flush() + { + _writer.Flush(); + } + + public void Dispose() + { + Flush(); + } + + /// + public StringBuilder GetStringBuilder() => _writer.GetStringBuilder(); + + public void WriteObject(ISerdeInfo typeInfo, int index, T value) + { + var attributes = typeInfo.GetFieldAttributes(index); + var attribute = attributes.FirstOrDefault(it => it.AttributeType == typeof(FixedFieldInfoAttribute)) + ?? throw new InvalidOperationException($"Cannot write fixed field value without required '{nameof(FixedFieldInfoAttribute)}' annotation."); + + GetAttributeData(attribute, out int fieldOffset, out int fieldLength, out string format); + + WriteObject(value, fieldOffset, fieldLength, format); + } + + private void WriteObject(T value, int fieldOffset, int fieldLength, string format) + { + // We special case some types. + if (value is bool b) + { + WriteBool(b, fieldOffset, fieldLength, format); + return; + } + + if (value is ReadOnlyMemory byteMemory) + { + WriteUtf8Span(byteMemory.Span, fieldOffset, fieldLength); + return; + } + + if (value is IFormattable formattable) + { + WriteText(formattable.ToString(format, CultureInfo.CurrentCulture), fieldOffset, fieldLength); + } + + // Format string is either not used, null, or it's not a special case. + WriteText(value?.ToString() ?? string.Empty, fieldOffset, fieldLength); + } + + private void WriteUtf8Span(Utf8Span span, int fieldOffset, int fieldLength) + { + string text = Encoding.UTF8.GetString(span); + WriteText(text, fieldOffset, fieldLength); + } + + private void WriteBool(bool value, int fieldOffset, int fieldLength, string format) + { + // FixedFieldInfoAttribute ctor prevents format strings that are all white space. + if (string.IsNullOrEmpty(format)) + { + WriteText(value.ToString(), fieldOffset, fieldLength); + return; + } + + string[] splitFormat = format.Split('/', StringSplitOptions.TrimEntries); + if (splitFormat.Length != 2) + { + throw new InvalidOperationException("Split format must have true and false text separated by a forward slash ('/')"); + } + + WriteText(value ? splitFormat[0] : splitFormat[1], fieldOffset, fieldLength); + } + + private void WriteText(string value, int fieldOffset, int fieldLength) + { + if (_pos < fieldOffset) + { + // Fill in any missing space with padding. + _writer.Write(new string(Padding, fieldOffset - _pos)); + _pos = fieldOffset; + } + + if (value.Length > fieldLength) + { + throw new InvalidOperationException($"Cannot write {value} (length {value.Length}) to a field that is only {fieldLength} long."); + } + + _writer.Write(value.PadRight(fieldLength)); + } + + public void WriteLine() + => _writer.WriteLine(); + + private static void GetAttributeData(CustomAttributeData customAttribute, out int offset, out int length, out string format) + { + if (!TryGetNamedArgumentValue(customAttribute, nameof(FixedFieldInfoAttribute.Offset), out offset)) + { + offset = -1; + } + + if (!TryGetNamedArgumentValue(customAttribute, nameof(FixedFieldInfoAttribute.Length), out length)) + { + length = -1; + } + + if (!TryGetNamedArgumentValue(customAttribute, nameof(FixedFieldInfoAttribute.Format), out string? formatValue)) + { + format = string.Empty; + } + + format = formatValue ?? string.Empty; + + static bool TryGetNamedArgumentValue(CustomAttributeData customAttribute, string name, [NotNullWhen(true)] out T? value) + { + value = (T?)customAttribute.NamedArguments.FirstOrDefault(it => it.MemberInfo.Name == name).TypedValue.Value; + return value is { }; + } + } + + public override string ToString() + => GetStringBuilder().ToString(); + } +} From a3d602af3a73e3c8cadb883ba7970576160f86e5 Mon Sep 17 00:00:00 2001 From: Devin Duanne Date: Wed, 29 Oct 2025 12:08:15 -0500 Subject: [PATCH 03/16] Initial deserializer work: --- src/serde/FixedWidth/FixedWidthDeserializer.Deserialize.cs | 2 +- src/serde/FixedWidth/FixedWidthDeserializer.Type.cs | 2 +- src/serde/FixedWidth/FixedWidthDeserializer.cs | 4 +--- 3 files changed, 3 insertions(+), 5 deletions(-) diff --git a/src/serde/FixedWidth/FixedWidthDeserializer.Deserialize.cs b/src/serde/FixedWidth/FixedWidthDeserializer.Deserialize.cs index 95b175aa..027e69c4 100644 --- a/src/serde/FixedWidth/FixedWidthDeserializer.Deserialize.cs +++ b/src/serde/FixedWidth/FixedWidthDeserializer.Deserialize.cs @@ -28,7 +28,7 @@ ITypeDeserializer IDeserializer.ReadType(ISerdeInfo typeInfo) private Utf8Span ReadUtf8Span() { - var peek = _byteReader.Peek(); + var peek = byteReader.Peek(); if (peek == IByteReader.EndOfStream) { throw new EndOfStreamException(); diff --git a/src/serde/FixedWidth/FixedWidthDeserializer.Type.cs b/src/serde/FixedWidth/FixedWidthDeserializer.Type.cs index dd6d94f9..1987d11f 100644 --- a/src/serde/FixedWidth/FixedWidthDeserializer.Type.cs +++ b/src/serde/FixedWidth/FixedWidthDeserializer.Type.cs @@ -5,7 +5,7 @@ namespace Serde.FixedWidth { - internal sealed partial class FixedWidthDeserializer : ITypeDeserializer + internal sealed partial class FixedWidthDeserializer : ITypeDeserializer { int? ITypeDeserializer.SizeOpt => null; diff --git a/src/serde/FixedWidth/FixedWidthDeserializer.cs b/src/serde/FixedWidth/FixedWidthDeserializer.cs index 692b30df..0ddfea05 100644 --- a/src/serde/FixedWidth/FixedWidthDeserializer.cs +++ b/src/serde/FixedWidth/FixedWidthDeserializer.cs @@ -9,10 +9,8 @@ namespace Serde.FixedWidth /// /// Defines a type which handles deserializing a fixed-width file. /// - internal sealed partial class FixedWidthDeserializer + internal static class FixedWidthDeserializer { - internal readonly TReader _byteReader = byteReader; - public static FixedWidthDeserializer FromString(string s) => FromUtf8_Unsafe(Encoding.UTF8.GetBytes(s)); From 6259e6fff44ba2896606a8f7df629f27c9fae038 Mon Sep 17 00:00:00 2001 From: Devin Duanne Date: Thu, 30 Oct 2025 14:35:04 -0500 Subject: [PATCH 04/16] Update and reformat several types --- Directory.Build.props | 4 +- src/serde/FixedWidth/FieldOverflowHandling.cs | 5 +- .../FixedWidth/FixedFieldInfoAttribute.cs | 85 ++++++++++-- .../FixedWidthDeserializer.Deserialize.cs | 36 +---- .../FixedWidth/FixedWidthDeserializer.Type.cs | 118 ++++------------- .../FixedWidth/FixedWidthDeserializer.cs | 11 +- src/serde/FixedWidth/FixedWidthSerdeObject.cs | 2 +- src/serde/FixedWidth/FixedWidthSerializer.cs | 118 +++++++++++------ .../FixedWidth/Reader/FixedWidthReader.cs | 123 ++++++++---------- .../FixedWidth/Writer/FixedWidthWriter.cs | 68 +++------- src/serde/Text/TextSerializer.cs | 10 ++ 11 files changed, 277 insertions(+), 303 deletions(-) create mode 100644 src/serde/Text/TextSerializer.cs diff --git a/Directory.Build.props b/Directory.Build.props index 602e3725..a2498bd6 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -4,7 +4,7 @@ enable true net10.0 - 0.10.0-fixedWidth.1 - 0.10.0-fixedWidth.1 + 0.10.0-fixedWidth.2 + 0.10.0-fixedWidth.2 diff --git a/src/serde/FixedWidth/FieldOverflowHandling.cs b/src/serde/FixedWidth/FieldOverflowHandling.cs index 17ac0538..ad1f5653 100644 --- a/src/serde/FixedWidth/FieldOverflowHandling.cs +++ b/src/serde/FixedWidth/FieldOverflowHandling.cs @@ -1,8 +1,11 @@ -namespace Serde.FixedWidth +using StaticCs; + +namespace Serde.FixedWidth { /// /// Enumerates the supported options for field values that are too long. /// + [Closed] public enum FieldOverflowHandling { /// diff --git a/src/serde/FixedWidth/FixedFieldInfoAttribute.cs b/src/serde/FixedWidth/FixedFieldInfoAttribute.cs index 9077fc7b..df628401 100644 --- a/src/serde/FixedWidth/FixedFieldInfoAttribute.cs +++ b/src/serde/FixedWidth/FixedFieldInfoAttribute.cs @@ -1,4 +1,7 @@ using System; +using System.Diagnostics.CodeAnalysis; +using System.Linq; +using System.Reflection; namespace Serde.FixedWidth { @@ -8,15 +11,6 @@ namespace Serde.FixedWidth [AttributeUsage(AttributeTargets.Property, AllowMultiple = false, Inherited = true)] public sealed class FixedFieldInfoAttribute : Attribute { - public FixedFieldInfoAttribute(int offset, int length, string format = "") - { - ArgumentOutOfRangeException.ThrowIfNegativeOrZero(offset); - ArgumentOutOfRangeException.ThrowIfNegativeOrZero(length); - Offset = offset; - Length = length; - Format = string.IsNullOrWhiteSpace(format) ? string.Empty : format; - } - /// /// Gets the offset that indicates where the field begins. /// @@ -30,6 +24,79 @@ public FixedFieldInfoAttribute(int offset, int length, string format = "") /// /// Gets the format string for providing to TryParseExact. /// + /// + /// Defaults to . + /// public string Format { get; } + + /// + /// Gets a value indicating how to handle field overflows. + /// + /// + /// Defaults to . + /// + public FieldOverflowHandling OverflowHandling { get; } + + public FixedFieldInfoAttribute(int offset, int length, string format = "", FieldOverflowHandling overflowHandling = FieldOverflowHandling.Throw) + { + ArgumentOutOfRangeException.ThrowIfNegativeOrZero(offset); + ArgumentOutOfRangeException.ThrowIfNegativeOrZero(length); + Offset = offset; + Length = length; + Format = string.IsNullOrWhiteSpace(format) ? string.Empty : format; + OverflowHandling = overflowHandling; + } + + public static FixedFieldInfoAttribute FromCustomAttributeData(CustomAttributeData? customAttribute) + { + if (!TryGetFixedFieldInfoAttribute(customAttribute, out var attribute)) + { + throw new InvalidOperationException($"Cannot write fixed field value without required '{nameof(FixedFieldInfoAttribute)}' annotation."); + } + + return attribute; + } + + public static bool TryGetFixedFieldInfoAttribute(CustomAttributeData? customAttribute, [NotNullWhen(true)] out FixedFieldInfoAttribute? fixedFieldInfoAttribute) + { + fixedFieldInfoAttribute = null; + + if (customAttribute is null) + { + return false; + } + + string format; + + if (!TryGetNamedArgumentValue(customAttribute, nameof(Offset), out int offset)) + { + return false; + } + + if (!TryGetNamedArgumentValue(customAttribute, nameof(Length), out int length)) + { + return false; + } + + if (!TryGetNamedArgumentValue(customAttribute, nameof(Format), out string? formatValue)) + { + format = string.Empty; + } + format = formatValue ?? string.Empty; + + if (!TryGetNamedArgumentValue(customAttribute, nameof(FieldOverflowHandling), out FieldOverflowHandling fieldOverflowHandling)) + { + fieldOverflowHandling = FieldOverflowHandling.Throw; + } + + fixedFieldInfoAttribute = new(offset, length, format, fieldOverflowHandling); + return true; + + static bool TryGetNamedArgumentValue(CustomAttributeData customAttribute, string name, [NotNullWhen(true)] out T? value) + { + value = (T?)customAttribute.NamedArguments.FirstOrDefault(it => it.MemberInfo.Name == name).TypedValue.Value; + return value is { }; + } + } } } diff --git a/src/serde/FixedWidth/FixedWidthDeserializer.Deserialize.cs b/src/serde/FixedWidth/FixedWidthDeserializer.Deserialize.cs index 027e69c4..d1738d78 100644 --- a/src/serde/FixedWidth/FixedWidthDeserializer.Deserialize.cs +++ b/src/serde/FixedWidth/FixedWidthDeserializer.Deserialize.cs @@ -1,15 +1,10 @@ -using Serde.IO; +using Serde.FixedWidth.Reader; using System; using System.Buffers; -using System.Collections.Generic; -using System.IO; -using System.Reflection.Metadata.Ecma335; -using System.Text; namespace Serde.FixedWidth { - internal sealed partial class FixedWidthDeserializer(TReader byteReader) : IDeserializer - where TReader : IByteReader + internal sealed partial class FixedWidthDeserializer : IDeserializer { ITypeDeserializer IDeserializer.ReadType(ISerdeInfo typeInfo) { @@ -21,32 +16,7 @@ ITypeDeserializer IDeserializer.ReadType(ISerdeInfo typeInfo) return this; } - private readonly ScratchBuffer _scratch = new(); - - /// - public string ReadString() => Encoding.UTF8.GetString(ReadUtf8Span()); - - private Utf8Span ReadUtf8Span() - { - var peek = byteReader.Peek(); - if (peek == IByteReader.EndOfStream) - { - throw new EndOfStreamException(); - } - - byteReader.Advance(); - _scratch.Clear(); - return byteReader.LexUtf8Span(false, _scratch); - } - - public void EoF() - { - if (byteReader.Peek() != IByteReader.EndOfStream) - { - throw new InvalidOperationException("Expected end of stream."); - } - } - + string IDeserializer.ReadString() => throw new NotImplementedException(); T? IDeserializer.ReadNullableRef(IDeserialize deserialize) where T : class => throw new NotImplementedException(); bool IDeserializer.ReadBool() => throw new NotImplementedException(); char IDeserializer.ReadChar() => throw new NotImplementedException(); diff --git a/src/serde/FixedWidth/FixedWidthDeserializer.Type.cs b/src/serde/FixedWidth/FixedWidthDeserializer.Type.cs index 1987d11f..b4e4aac5 100644 --- a/src/serde/FixedWidth/FixedWidthDeserializer.Type.cs +++ b/src/serde/FixedWidth/FixedWidthDeserializer.Type.cs @@ -1,110 +1,46 @@ using System; using System.Buffers; -using System.Collections.Generic; -using System.Text; +using System.Globalization; namespace Serde.FixedWidth { - internal sealed partial class FixedWidthDeserializer : ITypeDeserializer + internal sealed partial class FixedWidthDeserializer : ITypeDeserializer { - int? ITypeDeserializer.SizeOpt => null; - - bool ITypeDeserializer.ReadBool(ISerdeInfo info, int index) - { - throw new NotImplementedException(); - } - - void ITypeDeserializer.ReadBytes(ISerdeInfo info, int index, IBufferWriter writer) - { - throw new NotImplementedException(); - } + private const NumberStyles Numeric = NumberStyles.Integer | NumberStyles.AllowThousands; - char ITypeDeserializer.ReadChar(ISerdeInfo info, int index) - { - throw new NotImplementedException(); - } - - DateTime ITypeDeserializer.ReadDateTime(ISerdeInfo info, int index) - { - throw new NotImplementedException(); - } - - decimal ITypeDeserializer.ReadDecimal(ISerdeInfo info, int index) - { - throw new NotImplementedException(); - } - - float ITypeDeserializer.ReadF32(ISerdeInfo info, int index) - { - throw new NotImplementedException(); - } - - double ITypeDeserializer.ReadF64(ISerdeInfo info, int index) - { - throw new NotImplementedException(); - } - - short ITypeDeserializer.ReadI16(ISerdeInfo info, int index) - { - throw new NotImplementedException(); - } - - int ITypeDeserializer.ReadI32(ISerdeInfo info, int index) - { - throw new NotImplementedException(); - } - - long ITypeDeserializer.ReadI64(ISerdeInfo info, int index) - { - throw new NotImplementedException(); - } - - sbyte ITypeDeserializer.ReadI8(ISerdeInfo info, int index) - { - throw new NotImplementedException(); - } - - string ITypeDeserializer.ReadString(ISerdeInfo info, int index) - { - throw new NotImplementedException(); - } - - ushort ITypeDeserializer.ReadU16(ISerdeInfo info, int index) - { - throw new NotImplementedException(); - } + int? ITypeDeserializer.SizeOpt => null; - uint ITypeDeserializer.ReadU32(ISerdeInfo info, int index) - { - throw new NotImplementedException(); - } + T ITypeDeserializer.ReadValue(ISerdeInfo info, int index, IDeserialize deserialize) + => deserialize.Deserialize(this); - ulong ITypeDeserializer.ReadU64(ISerdeInfo info, int index) - { - throw new NotImplementedException(); - } + string ITypeDeserializer.ReadString(ISerdeInfo info, int index) => _reader.ReadString(info, index); + bool ITypeDeserializer.ReadBool(ISerdeInfo info, int index) => _reader.ReadBool(info, index); + char ITypeDeserializer.ReadChar(ISerdeInfo info, int index) => _reader.ReadChar(info, index); + DateTime ITypeDeserializer.ReadDateTime(ISerdeInfo info, int index) => _reader.ReadDateTime(info, index); + decimal ITypeDeserializer.ReadDecimal(ISerdeInfo info, int index) => _reader.ReadNumber(info, index, NumberStyles.Currency | NumberStyles.AllowLeadingSign); + float ITypeDeserializer.ReadF32(ISerdeInfo info, int index) => _reader.ReadNumber(info, index, NumberStyles.Float); + double ITypeDeserializer.ReadF64(ISerdeInfo info, int index) => _reader.ReadNumber(info, index, NumberStyles.Float); + short ITypeDeserializer.ReadI16(ISerdeInfo info, int index) => _reader.ReadNumber(info, index, Numeric); + int ITypeDeserializer.ReadI32(ISerdeInfo info, int index) => _reader.ReadNumber(info, index, Numeric); + long ITypeDeserializer.ReadI64(ISerdeInfo info, int index) => _reader.ReadNumber(info, index, Numeric); + sbyte ITypeDeserializer.ReadI8(ISerdeInfo info, int index) => _reader.ReadNumber(info, index, Numeric); + ushort ITypeDeserializer.ReadU16(ISerdeInfo info, int index) => _reader.ReadNumber(info, index, Numeric); + uint ITypeDeserializer.ReadU32(ISerdeInfo info, int index) => _reader.ReadNumber(info, index, Numeric); + ulong ITypeDeserializer.ReadU64(ISerdeInfo info, int index) => _reader.ReadNumber(info, index, Numeric); + byte ITypeDeserializer.ReadU8(ISerdeInfo info, int index) => _reader.ReadNumber(info, index, Numeric); - byte ITypeDeserializer.ReadU8(ISerdeInfo info, int index) - { - throw new NotImplementedException(); - } + void ITypeDeserializer.SkipValue(ISerdeInfo info, int index) { } - T ITypeDeserializer.ReadValue(ISerdeInfo info, int index, IDeserialize deserialize) - { - throw new NotImplementedException(); - } + int ITypeDeserializer.TryReadIndex(ISerdeInfo info) => TryReadIndexWithName(info).Item1; - void ITypeDeserializer.SkipValue(ISerdeInfo info, int index) - { - throw new NotImplementedException(); - } + (int, string? errorName) ITypeDeserializer.TryReadIndexWithName(ISerdeInfo info) => TryReadIndexWithName(info); - int ITypeDeserializer.TryReadIndex(ISerdeInfo info) + private (int, string? errorName) TryReadIndexWithName(ISerdeInfo serdeInfo) { throw new NotImplementedException(); } - - (int, string? errorName) ITypeDeserializer.TryReadIndexWithName(ISerdeInfo info) + + void ITypeDeserializer.ReadBytes(ISerdeInfo info, int index, IBufferWriter writer) { throw new NotImplementedException(); } diff --git a/src/serde/FixedWidth/FixedWidthDeserializer.cs b/src/serde/FixedWidth/FixedWidthDeserializer.cs index 0ddfea05..0452525d 100644 --- a/src/serde/FixedWidth/FixedWidthDeserializer.cs +++ b/src/serde/FixedWidth/FixedWidthDeserializer.cs @@ -9,15 +9,8 @@ namespace Serde.FixedWidth /// /// Defines a type which handles deserializing a fixed-width file. /// - internal static class FixedWidthDeserializer + internal sealed partial class FixedWidthDeserializer(string document) { - public static FixedWidthDeserializer FromString(string s) - => FromUtf8_Unsafe(Encoding.UTF8.GetBytes(s)); - - public static FixedWidthDeserializer FromUtf8_Unsafe(byte[] utf8Bytes) - { - var reader = new FixedWidthReader(utf8Bytes); - return new FixedWidthDeserializer(reader); - } + private readonly FixedWidthReader _reader = new(document); } } diff --git a/src/serde/FixedWidth/FixedWidthSerdeObject.cs b/src/serde/FixedWidth/FixedWidthSerdeObject.cs index 99ec096c..581897f1 100644 --- a/src/serde/FixedWidth/FixedWidthSerdeObject.cs +++ b/src/serde/FixedWidth/FixedWidthSerdeObject.cs @@ -13,7 +13,7 @@ namespace Serde.FixedWidth /// /// The underlying model for the file. /// Options for configuring the serialization of the type. - [Obsolete("This is now handled with the serializer/deserializer")] + [Obsolete("This is now handled with the serializer/deserializer", error: true)] public class FixedWidthSerdeObject<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicProperties)] T>(FixedWidthSerializationOptions options) : ISerde { /// diff --git a/src/serde/FixedWidth/FixedWidthSerializer.cs b/src/serde/FixedWidth/FixedWidthSerializer.cs index 0372bb3e..ed777984 100644 --- a/src/serde/FixedWidth/FixedWidthSerializer.cs +++ b/src/serde/FixedWidth/FixedWidthSerializer.cs @@ -1,10 +1,7 @@ -using Serde.FixedWidth.Reader; -using Serde.FixedWidth.Writer; -using System; +using System; using System.Collections.Generic; -using System.IO; -using System.Text; -using System.Text.Json; +using System.Linq; +using Serde.FixedWidth.Writer; namespace Serde.FixedWidth { @@ -13,66 +10,101 @@ namespace Serde.FixedWidth /// public sealed partial class FixedWidthSerializer(FixedWidthWriter writer) { - public static string Serialize(T provider, ISerialize serde) + /// + /// Serializes a single line of a fixed-width text file. + /// + /// The type of the . + /// The object instance to serialize. + /// A type which can serialize. + /// A fixed-width string. + public static string Serialize(T value, ISerialize serde) { using var writer = new FixedWidthWriter(); var serializer = new FixedWidthSerializer(writer); - serde.Serialize(provider, serializer); + serde.Serialize(value, serializer); writer.Flush(); return writer.GetStringBuilder().ToString(); } - public static string Serialize(T serde) + /// + /// + /// The serialize provider.. + public static string Serialize(T value) where TProvider : ISerializeProvider - => Serialize(serde, TProvider.Instance); + => Serialize(value, TProvider.Instance); + + /// + public static string Serialize(T value) + where T : ISerializeProvider + => Serialize(value, T.Instance); - public static string Serialize(T serde) + /// + public static IEnumerable SerializeDocument(IEnumerable values) where T : ISerializeProvider - => Serialize(serde, T.Instance); + => SerializeDocument(values, T.Instance); + + /// + /// Serializes a collection of , returning an enumerable for writing to a stream. + /// + /// The type of the object to serialize. + /// A collection of the items to serialize. + /// The serialize provider. + /// An enumerable of the rows of the document. + public static IEnumerable SerializeDocument(IEnumerable values, ISerialize serde) + where T : ISerializeProvider + { + foreach (var provider in values) + { + yield return Serialize(provider, serde); + } + } + /// + /// Deserializes a single line of a fixed-width text file. + /// + /// The type of the value to return. + /// The line to deserialize. + /// A type which can deerialize. + /// A fixed-width string. + public static T Deserialize(string source, IDeserialize d) + { + T result; + var deserializer = new FixedWidthDeserializer(source); + result = d.Deserialize(deserializer); + return result; + } + + /// public static T Deserialize(string source) where T : IDeserializeProvider => Deserialize(source); - public static List DeserializeList(string source) - where T : IDeserializeProvider -#if NET10_0_OR_GREATER - => Deserialize(source, List.Deserialize); -#else - => Deserialize(source, ListProxy.De.Instance); -#endif - + /// + /// + /// The deserialize provider to use. public static T Deserialize(string source) where TProvider : IDeserializeProvider => Deserialize(source, TProvider.Instance); - public static T Deserialize(string source, IDeserialize d) - { - var bytes = Encoding.UTF8.GetBytes(source); - return Deserialize_Unsafe(bytes, d); - } - - public static T Deserialize(byte[] utf8Bytes, IDeserialize d) + /// + /// Deserializes the provided document. + /// + /// The type of the value to return. + /// The document to deserialize. + /// The deserializer. + /// The number of lines to skip. + /// An enumerable of deserialized rows. + public static IEnumerable DeserializeDocument(string document, IDeserialize d, int headerLines = 0) { - try + foreach (var line in document.Split(Environment.NewLine).Skip(headerLines)) { - // Checks for invalid utf8 as a side effect. - _ = Encoding.UTF8.GetCharCount(utf8Bytes); + yield return Deserialize(line, d); } - catch (Exception ex) - { - throw new ArgumentException("Array is not valid UTF-8", nameof(utf8Bytes), ex); - } - return Deserialize_Unsafe(utf8Bytes, d); } - private static T Deserialize_Unsafe(byte[] utf8Bytes, IDeserialize d) - { - T result; - var deserializer = FixedWidthDeserializer.FromUtf8_Unsafe(utf8Bytes); - result = d.Deserialize(deserializer); - deserializer.EoF(); - return result; - } + /// + public static IEnumerable DeserializeDocument(string document, int headerLines = 0) + where TProvider : IDeserializeProvider + => DeserializeDocument(document, TProvider.Instance, headerLines); } } diff --git a/src/serde/FixedWidth/Reader/FixedWidthReader.cs b/src/serde/FixedWidth/Reader/FixedWidthReader.cs index fc59b676..2d97cf34 100644 --- a/src/serde/FixedWidth/Reader/FixedWidthReader.cs +++ b/src/serde/FixedWidth/Reader/FixedWidthReader.cs @@ -1,103 +1,92 @@ -using System; +using Serde.IO; +using System; using System.Diagnostics; +using System.Globalization; using System.IO; -using Serde.IO; +using System.Linq; +using System.Numerics; +using System.Reflection; +using System.Text; namespace Serde.FixedWidth.Reader { - internal struct FixedWidthReader(byte[] bytes) : IByteReader + internal struct FixedWidthReader(string line) { - private readonly byte[] _bytes = bytes; + private const char padding = ' '; + private readonly string _line = line; private int _pos = 0; - /// - public short Next() - { - var b = Peek(); - if (b != IByteReader.EndOfStream) - { - _pos++; - } - - return b; - } + public string ReadString(ISerdeInfo typeInfo, int index) + => GetText(typeInfo, index, out _).ToString(); - /// - public void Advance(int count = 1) + public bool ReadBool(ISerdeInfo typeInfo, int index) { - _pos += count; - } + var span = GetText(typeInfo, index, out var attribute); - /// - public readonly short Peek() - { - if (_pos >= _bytes.Length) + if (string.IsNullOrEmpty(attribute.Format)) { - return IByteReader.EndOfStream; + return bool.Parse(span); } - return _bytes[_pos]; - } - - /// - public readonly bool StartsWith(Utf8Span span) - { - if (span.Length > _bytes.Length - _pos) - { - return false; - } - - return span.SequenceEqual(_bytes.AsSpan(_pos, span.Length)); - } - /// - public Utf8Span LexUtf8Span(bool skipOnly, ScratchBuffer? scratch) - { - var span = _bytes.AsSpan(); - int start = _pos; - - SkipToEndOfLine(); - if (_pos > span.Length) + string[] splitFormat = attribute.Format.Split('/', StringSplitOptions.TrimEntries); + if (splitFormat.Length != 2) { - throw new EndOfStreamException(); + throw new InvalidOperationException("Split format must be an empty string or have true and false text separated by a forward slash ('/')"); } - if (skipOnly) + if (span.Equals(splitFormat[0], StringComparison.OrdinalIgnoreCase)) { - Advance(); - return Utf8Span.Empty; + return true; } - - Debug.Assert(scratch is not null); - var curSpan = span[start.._pos]; - Utf8Span strSpan; - if (scratch.Count == 0) + else if (span.Equals(splitFormat[1], StringComparison.OrdinalIgnoreCase)) { - strSpan = curSpan; + return false; } else { - scratch.AddRange(curSpan); - strSpan = scratch.Span; + throw new InvalidOperationException($"Value '{span}' was neither '{splitFormat[0]}' nor '{splitFormat[1]}'."); } - Advance(); - return strSpan; } - private void SkipToEndOfLine() + public char ReadChar(ISerdeInfo typeInfo, int index) + { + var span = GetText(typeInfo, index, out var attribute); + + return span.Length == 1 ? span[0] : throw new InvalidOperationException("Char field comprised of multiple non-space characters."); + } + + public DateTime ReadDateTime(ISerdeInfo typeInfo, int index) { - var span = _bytes.AsSpan(_pos); - var offset = 0; - while (offset < span.Length && !IsEndOfLine(span[offset])) + var span = GetText(typeInfo, index, out var attribute); + + return string.IsNullOrEmpty(attribute.Format) + ? DateTime.Parse(span) + : DateTime.ParseExact(span, attribute.Format, CultureInfo.CurrentCulture); + } + + public TNumber ReadNumber(ISerdeInfo typeInfo, int index, NumberStyles numberStyles) + where TNumber : struct, INumber + { + var span = GetText(typeInfo, index, out _); + + var trimmedValue = span.Trim(); + + if (trimmedValue.IsEmpty) { - offset++; + return TNumber.Zero; } - _pos += offset; + return TNumber.Parse(trimmedValue, numberStyles, CultureInfo.InvariantCulture); } - private static bool IsEndOfLine(byte b) + private ReadOnlySpan GetText(ISerdeInfo typeInfo, int index, out FixedFieldInfoAttribute attribute) { - return b == (byte)'\r' || b == (byte)'\n'; + var customAttribute = typeInfo.GetFieldAttributes(index).FirstOrDefault(it => it.AttributeType == typeof(FixedFieldInfoAttribute)); + attribute = FixedFieldInfoAttribute.FromCustomAttributeData(customAttribute); + + _pos = attribute.Offset + attribute.Length; + + return _line.AsSpan(attribute.Offset, attribute.Length); } } } diff --git a/src/serde/FixedWidth/Writer/FixedWidthWriter.cs b/src/serde/FixedWidth/Writer/FixedWidthWriter.cs index 112db6d8..c1104758 100644 --- a/src/serde/FixedWidth/Writer/FixedWidthWriter.cs +++ b/src/serde/FixedWidth/Writer/FixedWidthWriter.cs @@ -1,9 +1,7 @@ using System; -using System.Diagnostics.CodeAnalysis; using System.Globalization; using System.IO; using System.Linq; -using System.Reflection; using System.Text; namespace Serde.FixedWidth.Writer @@ -29,64 +27,61 @@ public void Dispose() public void WriteObject(ISerdeInfo typeInfo, int index, T value) { - var attributes = typeInfo.GetFieldAttributes(index); - var attribute = attributes.FirstOrDefault(it => it.AttributeType == typeof(FixedFieldInfoAttribute)) - ?? throw new InvalidOperationException($"Cannot write fixed field value without required '{nameof(FixedFieldInfoAttribute)}' annotation."); + var customAttribute = typeInfo.GetFieldAttributes(index).FirstOrDefault(it => it.AttributeType == typeof(FixedFieldInfoAttribute)); + var attribute = FixedFieldInfoAttribute.FromCustomAttributeData(customAttribute); - GetAttributeData(attribute, out int fieldOffset, out int fieldLength, out string format); - - WriteObject(value, fieldOffset, fieldLength, format); + WriteObject(value, attribute.Offset, attribute.Length, attribute.Format, attribute.OverflowHandling); } - private void WriteObject(T value, int fieldOffset, int fieldLength, string format) + private void WriteObject(T value, int fieldOffset, int fieldLength, string format, FieldOverflowHandling overflowHandling) { // We special case some types. if (value is bool b) { - WriteBool(b, fieldOffset, fieldLength, format); + WriteBool(b, fieldOffset, fieldLength, format, overflowHandling); return; } if (value is ReadOnlyMemory byteMemory) { - WriteUtf8Span(byteMemory.Span, fieldOffset, fieldLength); + WriteUtf8Span(byteMemory.Span, fieldOffset, fieldLength, overflowHandling); return; } if (value is IFormattable formattable) { - WriteText(formattable.ToString(format, CultureInfo.CurrentCulture), fieldOffset, fieldLength); + WriteText(formattable.ToString(format, CultureInfo.CurrentCulture), fieldOffset, fieldLength, overflowHandling); } // Format string is either not used, null, or it's not a special case. - WriteText(value?.ToString() ?? string.Empty, fieldOffset, fieldLength); + WriteText(value?.ToString() ?? string.Empty, fieldOffset, fieldLength, overflowHandling); } - private void WriteUtf8Span(Utf8Span span, int fieldOffset, int fieldLength) + private void WriteUtf8Span(Utf8Span span, int fieldOffset, int fieldLength, FieldOverflowHandling overflowHandling) { string text = Encoding.UTF8.GetString(span); - WriteText(text, fieldOffset, fieldLength); + WriteText(text, fieldOffset, fieldLength, overflowHandling); } - private void WriteBool(bool value, int fieldOffset, int fieldLength, string format) + private void WriteBool(bool value, int fieldOffset, int fieldLength, string format, FieldOverflowHandling overflowHandling) { // FixedFieldInfoAttribute ctor prevents format strings that are all white space. if (string.IsNullOrEmpty(format)) { - WriteText(value.ToString(), fieldOffset, fieldLength); + WriteText(value.ToString(), fieldOffset, fieldLength, overflowHandling); return; } string[] splitFormat = format.Split('/', StringSplitOptions.TrimEntries); if (splitFormat.Length != 2) { - throw new InvalidOperationException("Split format must have true and false text separated by a forward slash ('/')"); + throw new InvalidOperationException("Split format must be an empty string or have true and false text separated by a forward slash ('/')"); } - WriteText(value ? splitFormat[0] : splitFormat[1], fieldOffset, fieldLength); + WriteText(value ? splitFormat[0] : splitFormat[1], fieldOffset, fieldLength, overflowHandling); } - private void WriteText(string value, int fieldOffset, int fieldLength) + private void WriteText(string value, int fieldOffset, int fieldLength, FieldOverflowHandling overflowHandling) { if (_pos < fieldOffset) { @@ -97,7 +92,12 @@ private void WriteText(string value, int fieldOffset, int fieldLength) if (value.Length > fieldLength) { - throw new InvalidOperationException($"Cannot write {value} (length {value.Length}) to a field that is only {fieldLength} long."); + value = overflowHandling switch + { + FieldOverflowHandling.Throw => throw new InvalidOperationException($"Cannot write {value} (length {value.Length}) to a field that is only {fieldLength} long."), + FieldOverflowHandling.Truncate => value[..fieldLength], + _ => throw new ArgumentOutOfRangeException(nameof(overflowHandling), $"{overflowHandling} is not a valid value for {nameof(FieldOverflowHandling)}.") + }; } _writer.Write(value.PadRight(fieldLength)); @@ -106,32 +106,6 @@ private void WriteText(string value, int fieldOffset, int fieldLength) public void WriteLine() => _writer.WriteLine(); - private static void GetAttributeData(CustomAttributeData customAttribute, out int offset, out int length, out string format) - { - if (!TryGetNamedArgumentValue(customAttribute, nameof(FixedFieldInfoAttribute.Offset), out offset)) - { - offset = -1; - } - - if (!TryGetNamedArgumentValue(customAttribute, nameof(FixedFieldInfoAttribute.Length), out length)) - { - length = -1; - } - - if (!TryGetNamedArgumentValue(customAttribute, nameof(FixedFieldInfoAttribute.Format), out string? formatValue)) - { - format = string.Empty; - } - - format = formatValue ?? string.Empty; - - static bool TryGetNamedArgumentValue(CustomAttributeData customAttribute, string name, [NotNullWhen(true)] out T? value) - { - value = (T?)customAttribute.NamedArguments.FirstOrDefault(it => it.MemberInfo.Name == name).TypedValue.Value; - return value is { }; - } - } - public override string ToString() => GetStringBuilder().ToString(); } diff --git a/src/serde/Text/TextSerializer.cs b/src/serde/Text/TextSerializer.cs new file mode 100644 index 00000000..a60a1d31 --- /dev/null +++ b/src/serde/Text/TextSerializer.cs @@ -0,0 +1,10 @@ +using System; +using System.Collections.Generic; +using System.Text; + +namespace Serde.Text +{ + internal class TextSerializer + { + } +} From fc1d5e6245bc6580e36d83d8c71f980698c49671 Mon Sep 17 00:00:00 2001 From: Devin Duanne Date: Thu, 30 Oct 2025 14:45:41 -0500 Subject: [PATCH 05/16] Remove TextSerializer test --- src/serde/FixedWidth/FixedWidthSerializer.Type.cs | 7 ------- src/serde/Text/TextSerializer.cs | 10 ---------- 2 files changed, 17 deletions(-) delete mode 100644 src/serde/Text/TextSerializer.cs diff --git a/src/serde/FixedWidth/FixedWidthSerializer.Type.cs b/src/serde/FixedWidth/FixedWidthSerializer.Type.cs index 6516485a..f8b887d6 100644 --- a/src/serde/FixedWidth/FixedWidthSerializer.Type.cs +++ b/src/serde/FixedWidth/FixedWidthSerializer.Type.cs @@ -1,11 +1,4 @@ using System; -using System.Collections.Generic; -using System.Diagnostics; -using System.Diagnostics.CodeAnalysis; -using System.IO; -using System.Linq; -using System.Reflection; -using System.Text; namespace Serde.FixedWidth { diff --git a/src/serde/Text/TextSerializer.cs b/src/serde/Text/TextSerializer.cs deleted file mode 100644 index a60a1d31..00000000 --- a/src/serde/Text/TextSerializer.cs +++ /dev/null @@ -1,10 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Text; - -namespace Serde.Text -{ - internal class TextSerializer - { - } -} From 8d0fe22524975683ed10f9ce9a0ff642b3757fd0 Mon Sep 17 00:00:00 2001 From: Devin Duanne Date: Fri, 31 Oct 2025 08:30:13 -0500 Subject: [PATCH 06/16] Fix bug with attribute helper --- Directory.Build.props | 4 ++-- src/serde/FixedWidth/FixedFieldInfoAttribute.cs | 12 ++++++------ 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/Directory.Build.props b/Directory.Build.props index a2498bd6..7f75f825 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -4,7 +4,7 @@ enable true net10.0 - 0.10.0-fixedWidth.2 - 0.10.0-fixedWidth.2 + 0.10.0-fixedWidth.4 + 0.10.0-fixedWidth.4 diff --git a/src/serde/FixedWidth/FixedFieldInfoAttribute.cs b/src/serde/FixedWidth/FixedFieldInfoAttribute.cs index df628401..d3a75787 100644 --- a/src/serde/FixedWidth/FixedFieldInfoAttribute.cs +++ b/src/serde/FixedWidth/FixedFieldInfoAttribute.cs @@ -68,23 +68,23 @@ public static bool TryGetFixedFieldInfoAttribute(CustomAttributeData? customAttr string format; - if (!TryGetNamedArgumentValue(customAttribute, nameof(Offset), out int offset)) + if (!TryGetNamedArgumentValue(customAttribute, 0, out int offset)) { return false; } - if (!TryGetNamedArgumentValue(customAttribute, nameof(Length), out int length)) + if (!TryGetNamedArgumentValue(customAttribute, 1, out int length)) { return false; } - if (!TryGetNamedArgumentValue(customAttribute, nameof(Format), out string? formatValue)) + if (!TryGetNamedArgumentValue(customAttribute, 2, out string? formatValue)) { format = string.Empty; } format = formatValue ?? string.Empty; - if (!TryGetNamedArgumentValue(customAttribute, nameof(FieldOverflowHandling), out FieldOverflowHandling fieldOverflowHandling)) + if (!TryGetNamedArgumentValue(customAttribute, 3, out FieldOverflowHandling fieldOverflowHandling)) { fieldOverflowHandling = FieldOverflowHandling.Throw; } @@ -92,9 +92,9 @@ public static bool TryGetFixedFieldInfoAttribute(CustomAttributeData? customAttr fixedFieldInfoAttribute = new(offset, length, format, fieldOverflowHandling); return true; - static bool TryGetNamedArgumentValue(CustomAttributeData customAttribute, string name, [NotNullWhen(true)] out T? value) + static bool TryGetNamedArgumentValue(CustomAttributeData customAttribute, int argumentIndex, [NotNullWhen(true)] out T? value) { - value = (T?)customAttribute.NamedArguments.FirstOrDefault(it => it.MemberInfo.Name == name).TypedValue.Value; + value = (T?)customAttribute.ConstructorArguments[argumentIndex].Value; return value is { }; } } From c99e49d3c726b7a1da21de9565283917846596d9 Mon Sep 17 00:00:00 2001 From: Devin Duanne Date: Fri, 31 Oct 2025 08:34:13 -0500 Subject: [PATCH 07/16] Offset should be allowed to be zero --- src/serde/FixedWidth/FixedFieldInfoAttribute.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/serde/FixedWidth/FixedFieldInfoAttribute.cs b/src/serde/FixedWidth/FixedFieldInfoAttribute.cs index d3a75787..c1554dc8 100644 --- a/src/serde/FixedWidth/FixedFieldInfoAttribute.cs +++ b/src/serde/FixedWidth/FixedFieldInfoAttribute.cs @@ -39,7 +39,7 @@ public sealed class FixedFieldInfoAttribute : Attribute public FixedFieldInfoAttribute(int offset, int length, string format = "", FieldOverflowHandling overflowHandling = FieldOverflowHandling.Throw) { - ArgumentOutOfRangeException.ThrowIfNegativeOrZero(offset); + ArgumentOutOfRangeException.ThrowIfNegative(offset); ArgumentOutOfRangeException.ThrowIfNegativeOrZero(length); Offset = offset; Length = length; From 621f713bac43e8c5351da0d94ec6aa7829db4a75 Mon Sep 17 00:00:00 2001 From: Devin Duanne Date: Fri, 31 Oct 2025 09:07:33 -0500 Subject: [PATCH 08/16] Special case DateTime because it does not format IFormattable --- Directory.Build.props | 4 ++-- src/serde/FixedWidth/Writer/FixedWidthWriter.cs | 7 +++++++ 2 files changed, 9 insertions(+), 2 deletions(-) diff --git a/Directory.Build.props b/Directory.Build.props index 7f75f825..6190760c 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -4,7 +4,7 @@ enable true net10.0 - 0.10.0-fixedWidth.4 - 0.10.0-fixedWidth.4 + 0.10.0-fixedWidth.6 + 0.10.0-fixedWidth.6 diff --git a/src/serde/FixedWidth/Writer/FixedWidthWriter.cs b/src/serde/FixedWidth/Writer/FixedWidthWriter.cs index c1104758..6cd061d3 100644 --- a/src/serde/FixedWidth/Writer/FixedWidthWriter.cs +++ b/src/serde/FixedWidth/Writer/FixedWidthWriter.cs @@ -48,9 +48,16 @@ private void WriteObject(T value, int fieldOffset, int fieldLength, string fo return; } + if (value is DateTime dt) + { + WriteText(dt.ToString(format, CultureInfo.CurrentCulture), fieldOffset, fieldLength, overflowHandling); + return; + } + if (value is IFormattable formattable) { WriteText(formattable.ToString(format, CultureInfo.CurrentCulture), fieldOffset, fieldLength, overflowHandling); + return; } // Format string is either not used, null, or it's not a special case. From e46602611d83c8b5559dcdb6d4de74fe8b3d72af Mon Sep 17 00:00:00 2001 From: Devin Duanne Date: Fri, 31 Oct 2025 10:46:00 -0500 Subject: [PATCH 09/16] Special case DateTime --- Directory.Build.props | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Directory.Build.props b/Directory.Build.props index 6190760c..072613e0 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -4,7 +4,7 @@ enable true net10.0 - 0.10.0-fixedWidth.6 - 0.10.0-fixedWidth.6 + 0.10.0-fixedWidth.7 + 0.10.0-fixedWidth.7 From 4d233869ee8ddab15300f5c048a3536093ee09ad Mon Sep 17 00:00:00 2001 From: Devin Duanne Date: Fri, 31 Oct 2025 10:51:53 -0500 Subject: [PATCH 10/16] Fix serialization positioning. --- Directory.Build.props | 4 +- .../FixedWidth/Writer/FixedWidthWriter.cs | 47 +++++++++---------- 2 files changed, 24 insertions(+), 27 deletions(-) diff --git a/Directory.Build.props b/Directory.Build.props index 072613e0..e71d14a3 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -4,7 +4,7 @@ enable true net10.0 - 0.10.0-fixedWidth.7 - 0.10.0-fixedWidth.7 + 0.10.0-fixedWidth.8 + 0.10.0-fixedWidth.8 diff --git a/src/serde/FixedWidth/Writer/FixedWidthWriter.cs b/src/serde/FixedWidth/Writer/FixedWidthWriter.cs index 6cd061d3..cf00b6dd 100644 --- a/src/serde/FixedWidth/Writer/FixedWidthWriter.cs +++ b/src/serde/FixedWidth/Writer/FixedWidthWriter.cs @@ -35,30 +35,33 @@ public void WriteObject(ISerdeInfo typeInfo, int index, T value) private void WriteObject(T value, int fieldOffset, int fieldLength, string format, FieldOverflowHandling overflowHandling) { - // We special case some types. - if (value is bool b) + // attribute ctor sets white space strings to empty. + if (!string.IsNullOrEmpty(format)) { - WriteBool(b, fieldOffset, fieldLength, format, overflowHandling); - return; - } + if (value is bool b) + { + WriteBool(b, fieldOffset, fieldLength, format, overflowHandling); + return; + } - if (value is ReadOnlyMemory byteMemory) - { - WriteUtf8Span(byteMemory.Span, fieldOffset, fieldLength, overflowHandling); - return; - } + if (value is DateTime dt) + { + WriteText(dt.ToString(format, CultureInfo.CurrentCulture), fieldOffset, fieldLength, overflowHandling); + return; + } - if (value is DateTime dt) - { - WriteText(dt.ToString(format, CultureInfo.CurrentCulture), fieldOffset, fieldLength, overflowHandling); - return; - } + if (value is IFormattable formattable) + { + WriteText(formattable.ToString(format, CultureInfo.CurrentCulture), fieldOffset, fieldLength, overflowHandling); + return; + } + } - if (value is IFormattable formattable) + if (value is ReadOnlyMemory byteMemory) { - WriteText(formattable.ToString(format, CultureInfo.CurrentCulture), fieldOffset, fieldLength, overflowHandling); + WriteUtf8Span(byteMemory.Span, fieldOffset, fieldLength, overflowHandling); return; - } + } // Format string is either not used, null, or it's not a special case. WriteText(value?.ToString() ?? string.Empty, fieldOffset, fieldLength, overflowHandling); @@ -72,13 +75,6 @@ private void WriteUtf8Span(Utf8Span span, int fieldOffset, int fieldLength, Fiel private void WriteBool(bool value, int fieldOffset, int fieldLength, string format, FieldOverflowHandling overflowHandling) { - // FixedFieldInfoAttribute ctor prevents format strings that are all white space. - if (string.IsNullOrEmpty(format)) - { - WriteText(value.ToString(), fieldOffset, fieldLength, overflowHandling); - return; - } - string[] splitFormat = format.Split('/', StringSplitOptions.TrimEntries); if (splitFormat.Length != 2) { @@ -108,6 +104,7 @@ private void WriteText(string value, int fieldOffset, int fieldLength, FieldOver } _writer.Write(value.PadRight(fieldLength)); + _pos += fieldLength; } public void WriteLine() From 57882c3f5bef87f5dbdef8974d0b1306a455d6a9 Mon Sep 17 00:00:00 2001 From: Devin Duanne Date: Fri, 31 Oct 2025 11:00:22 -0500 Subject: [PATCH 11/16] Implement TryReadIndexWithName() --- Directory.Build.props | 4 ++-- src/serde/FixedWidth/FixedWidthDeserializer.Type.cs | 12 ++++++++++-- 2 files changed, 12 insertions(+), 4 deletions(-) diff --git a/Directory.Build.props b/Directory.Build.props index e71d14a3..732774a3 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -4,7 +4,7 @@ enable true net10.0 - 0.10.0-fixedWidth.8 - 0.10.0-fixedWidth.8 + 0.10.0-fixedWidth.9 + 0.10.0-fixedWidth.9 diff --git a/src/serde/FixedWidth/FixedWidthDeserializer.Type.cs b/src/serde/FixedWidth/FixedWidthDeserializer.Type.cs index b4e4aac5..5cdd72a5 100644 --- a/src/serde/FixedWidth/FixedWidthDeserializer.Type.cs +++ b/src/serde/FixedWidth/FixedWidthDeserializer.Type.cs @@ -5,11 +5,13 @@ namespace Serde.FixedWidth { internal sealed partial class FixedWidthDeserializer : ITypeDeserializer - { + { private const NumberStyles Numeric = NumberStyles.Integer | NumberStyles.AllowThousands; int? ITypeDeserializer.SizeOpt => null; + private int _fieldIndex = -1; + T ITypeDeserializer.ReadValue(ISerdeInfo info, int index, IDeserialize deserialize) => deserialize.Deserialize(this); @@ -37,7 +39,13 @@ void ITypeDeserializer.SkipValue(ISerdeInfo info, int index) { } private (int, string? errorName) TryReadIndexWithName(ISerdeInfo serdeInfo) { - throw new NotImplementedException(); + _fieldIndex++; + if (_fieldIndex == serdeInfo.FieldCount) + { + return (ITypeDeserializer.EndOfType, null); + } + + return (_fieldIndex, null); } void ITypeDeserializer.ReadBytes(ISerdeInfo info, int index, IBufferWriter writer) From c8c5bf60994560b9a39bf5db3da4ce15c6d7b888 Mon Sep 17 00:00:00 2001 From: Devin Duanne Date: Fri, 31 Oct 2025 11:38:53 -0500 Subject: [PATCH 12/16] Clean up text reader --- Directory.Build.props | 4 ++-- src/serde/FixedWidth/Reader/FixedWidthReader.cs | 12 ++++-------- 2 files changed, 6 insertions(+), 10 deletions(-) diff --git a/Directory.Build.props b/Directory.Build.props index 732774a3..e7764804 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -4,7 +4,7 @@ enable true net10.0 - 0.10.0-fixedWidth.9 - 0.10.0-fixedWidth.9 + 0.10.0-fixedWidth.10 + 0.10.0-fixedWidth.10 diff --git a/src/serde/FixedWidth/Reader/FixedWidthReader.cs b/src/serde/FixedWidth/Reader/FixedWidthReader.cs index 2d97cf34..4778d0ec 100644 --- a/src/serde/FixedWidth/Reader/FixedWidthReader.cs +++ b/src/serde/FixedWidth/Reader/FixedWidthReader.cs @@ -12,12 +12,10 @@ namespace Serde.FixedWidth.Reader { internal struct FixedWidthReader(string line) { - private const char padding = ' '; private readonly string _line = line; - private int _pos = 0; public string ReadString(ISerdeInfo typeInfo, int index) - => GetText(typeInfo, index, out _).ToString(); + => GetText(typeInfo, index, out _).Trim().ToString(); public bool ReadBool(ISerdeInfo typeInfo, int index) { @@ -50,7 +48,7 @@ public bool ReadBool(ISerdeInfo typeInfo, int index) public char ReadChar(ISerdeInfo typeInfo, int index) { - var span = GetText(typeInfo, index, out var attribute); + var span = GetText(typeInfo, index, out _); return span.Length == 1 ? span[0] : throw new InvalidOperationException("Char field comprised of multiple non-space characters."); } @@ -79,14 +77,12 @@ public TNumber ReadNumber(ISerdeInfo typeInfo, int index, NumberStyles return TNumber.Parse(trimmedValue, numberStyles, CultureInfo.InvariantCulture); } - private ReadOnlySpan GetText(ISerdeInfo typeInfo, int index, out FixedFieldInfoAttribute attribute) + private readonly ReadOnlySpan GetText(ISerdeInfo typeInfo, int index, out FixedFieldInfoAttribute attribute) { var customAttribute = typeInfo.GetFieldAttributes(index).FirstOrDefault(it => it.AttributeType == typeof(FixedFieldInfoAttribute)); attribute = FixedFieldInfoAttribute.FromCustomAttributeData(customAttribute); - _pos = attribute.Offset + attribute.Length; - - return _line.AsSpan(attribute.Offset, attribute.Length); + return _line.AsSpan(attribute.Offset, attribute.Length).Trim(); } } } From 8a263c80809a8e2b1ad5971b334473767bc73f5f Mon Sep 17 00:00:00 2001 From: Devin Duanne Date: Fri, 31 Oct 2025 11:40:45 -0500 Subject: [PATCH 13/16] Mark reader as readonly --- Directory.Build.props | 4 ++-- src/serde/FixedWidth/Reader/FixedWidthReader.cs | 11 +++-------- 2 files changed, 5 insertions(+), 10 deletions(-) diff --git a/Directory.Build.props b/Directory.Build.props index e7764804..346a4bd4 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -4,7 +4,7 @@ enable true net10.0 - 0.10.0-fixedWidth.10 - 0.10.0-fixedWidth.10 + 0.10.0-fixedWidth.11 + 0.10.0-fixedWidth.11 diff --git a/src/serde/FixedWidth/Reader/FixedWidthReader.cs b/src/serde/FixedWidth/Reader/FixedWidthReader.cs index 4778d0ec..b6978c39 100644 --- a/src/serde/FixedWidth/Reader/FixedWidthReader.cs +++ b/src/serde/FixedWidth/Reader/FixedWidthReader.cs @@ -1,16 +1,11 @@ -using Serde.IO; -using System; -using System.Diagnostics; +using System; using System.Globalization; -using System.IO; using System.Linq; using System.Numerics; -using System.Reflection; -using System.Text; namespace Serde.FixedWidth.Reader { - internal struct FixedWidthReader(string line) + internal readonly struct FixedWidthReader(string line) { private readonly string _line = line; @@ -77,7 +72,7 @@ public TNumber ReadNumber(ISerdeInfo typeInfo, int index, NumberStyles return TNumber.Parse(trimmedValue, numberStyles, CultureInfo.InvariantCulture); } - private readonly ReadOnlySpan GetText(ISerdeInfo typeInfo, int index, out FixedFieldInfoAttribute attribute) + private ReadOnlySpan GetText(ISerdeInfo typeInfo, int index, out FixedFieldInfoAttribute attribute) { var customAttribute = typeInfo.GetFieldAttributes(index).FirstOrDefault(it => it.AttributeType == typeof(FixedFieldInfoAttribute)); attribute = FixedFieldInfoAttribute.FromCustomAttributeData(customAttribute); From f7654fa201bc22c112302f04b92fefea07f267e8 Mon Sep 17 00:00:00 2001 From: Devin Duanne Date: Fri, 31 Oct 2025 14:03:26 -0500 Subject: [PATCH 14/16] Remove FixedWidthSerdeObject --- src/serde/FixedWidth/FixedWidthSerdeObject.cs | 142 ------------------ 1 file changed, 142 deletions(-) delete mode 100644 src/serde/FixedWidth/FixedWidthSerdeObject.cs diff --git a/src/serde/FixedWidth/FixedWidthSerdeObject.cs b/src/serde/FixedWidth/FixedWidthSerdeObject.cs deleted file mode 100644 index 581897f1..00000000 --- a/src/serde/FixedWidth/FixedWidthSerdeObject.cs +++ /dev/null @@ -1,142 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Diagnostics.CodeAnalysis; -using System.Globalization; -using System.Linq; -using System.Reflection; -using System.Text; - -namespace Serde.FixedWidth -{ - /// - /// Defines a type which handles (de)serialization of fixed-width text files. - /// - /// The underlying model for the file. - /// Options for configuring the serialization of the type. - [Obsolete("This is now handled with the serializer/deserializer", error: true)] - public class FixedWidthSerdeObject<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicProperties)] T>(FixedWidthSerializationOptions options) : ISerde - { - /// - public ISerdeInfo SerdeInfo => StringProxy.SerdeInfo; - - /// - /// Initializes a new instance of the class. - /// - public FixedWidthSerdeObject() : this(FixedWidthSerializationOptions.Default) - { - } - - /// - /// Initializes a new instance of the class. - /// - /// Determines how to handle overflows, i.e. when the field value is longer than the field length. - public FixedWidthSerdeObject(FieldOverflowHandling overflowHandling) - : this(new FixedWidthSerializationOptions { FieldOverflowHandling = overflowHandling }) - { - } - - /// - public virtual void Serialize(T obj, ISerializer serializer) - { - var fieldInfo = FixedFieldInfo.FromProperties(typeof(T).GetProperties(BindingFlags.Instance | BindingFlags.Public)).OrderBy(it => it.Offset); - StringBuilder sb = new(); - - int index = 0; - const char padding = ' '; - foreach (var field in fieldInfo) - { - if (index <= field.Offset) - { - // Fill in any missing space with padding. - sb.Append(padding, field.Offset - index); - - object? value = field.Property.GetValue(obj); - - string valueText = value is IFormattable formattable - ? formattable.ToString(field.Format, CultureInfo.InvariantCulture) - : value?.ToString() ?? string.Empty; - - if (valueText.Length == 0) - { - // value is null or empty - continue; - } - - if (valueText.Length > field.Length) - { - if (options.FieldOverflowHandling is FieldOverflowHandling.Throw) - { - throw new InvalidOperationException($"Value '{field.Property.Name}' ({valueText}) is too long for field! Expected: {field.Length}; Actual: {valueText.Length}"); - } - - // Truncate value to the maximum field length. - valueText = valueText[..field.Length]; - } - - sb.Append(valueText.PadRight(field.Length)); - index += field.Length; - } - } - - serializer.WriteString(sb.ToString()); - } - - /// - public virtual T Deserialize(IDeserializer deserializer) - { - var fieldInfo = FixedFieldInfo.FromProperties(typeof(T).GetProperties(BindingFlags.Instance | BindingFlags.Public)).OrderBy(it => it.Offset); - string line = deserializer.ReadString(); - - throw new NotImplementedException(); - } - } - - /// - /// Gets information for the property. - /// - /// The property. - /// Its attribute. - file class FixedFieldInfo(PropertyInfo property, FixedFieldInfoAttribute attribute) - { - /// - /// Gets the offset for the start of the field. - /// - public int Offset => _attribute.Offset; - - /// - /// Gets the max length of the field. - /// - public int Length => _attribute.Length; - - /// - /// Gets the output format. - /// - public string Format => _attribute.Format; - - /// - /// Gets the property described by this fixed field info. - /// - public PropertyInfo Property => property; - - private readonly FixedFieldInfoAttribute _attribute = attribute; - - /// - /// Enumerates a set of FixedFieldInfos from a set of . - /// - /// A set of property infos. - /// An enumerable iterating over property infos. - public static IEnumerable FromProperties(PropertyInfo[] properties) - { - foreach (PropertyInfo property in properties) - { - if (property.GetCustomAttribute() is not { } attribute) - { - // If not decorated with the attribute, skip. - continue; - } - - yield return new(property, attribute); - } - } - } -} From 51840061f4ea3455eb20127f87dd3306fa45ddb2 Mon Sep 17 00:00:00 2001 From: TheBrambleShark Date: Mon, 3 Nov 2025 10:56:50 -0600 Subject: [PATCH 15/16] Remove options type, fix bug with DeserializeDocument causing it to try to deserialize empty lines. --- .../FixedWidth/FixedWidthSerializationOptions.cs | 16 ---------------- src/serde/FixedWidth/FixedWidthSerializer.cs | 15 ++++++++++----- 2 files changed, 10 insertions(+), 21 deletions(-) delete mode 100644 src/serde/FixedWidth/FixedWidthSerializationOptions.cs diff --git a/src/serde/FixedWidth/FixedWidthSerializationOptions.cs b/src/serde/FixedWidth/FixedWidthSerializationOptions.cs deleted file mode 100644 index 20576e1c..00000000 --- a/src/serde/FixedWidth/FixedWidthSerializationOptions.cs +++ /dev/null @@ -1,16 +0,0 @@ -namespace Serde.FixedWidth -{ - /// - /// Gets options for configuring the Serde object. - /// - public sealed class FixedWidthSerializationOptions - { - public static FixedWidthSerializationOptions Default => new(); - - /// - /// Gets a value indicating how to handle field overflows, i.e. when the - /// field value is longer than the field length. - /// - public FieldOverflowHandling FieldOverflowHandling { get; init; } = FieldOverflowHandling.Throw; - } -} diff --git a/src/serde/FixedWidth/FixedWidthSerializer.cs b/src/serde/FixedWidth/FixedWidthSerializer.cs index ed777984..c09fe93d 100644 --- a/src/serde/FixedWidth/FixedWidthSerializer.cs +++ b/src/serde/FixedWidth/FixedWidthSerializer.cs @@ -86,6 +86,16 @@ public static T Deserialize(string source) where TProvider : IDeserializeProvider => Deserialize(source, TProvider.Instance); + /// + public static IEnumerable DeserializeDocument(string document, int headerLines = 0) + where T : IDeserializeProvider + => DeserializeDocument(document, T.Instance, headerLines); + + /// + public static IEnumerable DeserializeDocument(string document, int headerLines = 0) + where TProvider : IDeserializeProvider + => DeserializeDocument(document, TProvider.Instance, headerLines); + /// /// Deserializes the provided document. /// @@ -101,10 +111,5 @@ public static IEnumerable DeserializeDocument(string document, IDeserializ yield return Deserialize(line, d); } } - - /// - public static IEnumerable DeserializeDocument(string document, int headerLines = 0) - where TProvider : IDeserializeProvider - => DeserializeDocument(document, TProvider.Instance, headerLines); } } From 1b12972df3b094404184097d6840267e75b9d15d Mon Sep 17 00:00:00 2001 From: TheBrambleShark <7003081+TheBrambleShark@users.noreply.github.com> Date: Mon, 3 Nov 2025 15:00:30 -0600 Subject: [PATCH 16/16] Implement ISerializer --- .../FixedWidthSerializer.Serialize.cs | 34 +++++++++---------- .../FixedWidth/FixedWidthSerializer.Type.cs | 7 ++-- .../FixedWidth/Writer/FixedWidthWriter.cs | 9 ++++- 3 files changed, 30 insertions(+), 20 deletions(-) diff --git a/src/serde/FixedWidth/FixedWidthSerializer.Serialize.cs b/src/serde/FixedWidth/FixedWidthSerializer.Serialize.cs index c4d3446e..33627a3a 100644 --- a/src/serde/FixedWidth/FixedWidthSerializer.Serialize.cs +++ b/src/serde/FixedWidth/FixedWidthSerializer.Serialize.cs @@ -17,24 +17,24 @@ ITypeSerializer ISerializer.WriteType(ISerdeInfo info) return this; } - void ISerializer.WriteString(string s) => throw new NotImplementedException(); - void ISerializer.WriteBool(bool b) => throw new NotImplementedException(); + void ISerializer.WriteString(string s) => writer.WriteRaw(s); + void ISerializer.WriteBool(bool b) => writer.WriteRaw(b); void ISerializer.WriteBytes(ReadOnlyMemory bytes) => throw new NotImplementedException(); - void ISerializer.WriteChar(char c) => throw new NotImplementedException(); + void ISerializer.WriteChar(char c) => writer.WriteRaw(c); ITypeSerializer ISerializer.WriteCollection(ISerdeInfo info, int? count) => throw new NotImplementedException(); - void ISerializer.WriteDateTime(DateTime dt) => throw new NotImplementedException(); - void ISerializer.WriteDateTimeOffset(DateTimeOffset dt) => throw new NotImplementedException(); - void ISerializer.WriteDecimal(decimal d) => throw new NotImplementedException(); - void ISerializer.WriteF32(float f) => throw new NotImplementedException(); - void ISerializer.WriteF64(double d) => throw new NotImplementedException(); - void ISerializer.WriteI16(short i16) => throw new NotImplementedException(); - void ISerializer.WriteI32(int i32) => throw new NotImplementedException(); - void ISerializer.WriteI64(long i64) => throw new NotImplementedException(); - void ISerializer.WriteI8(sbyte b) => throw new NotImplementedException(); - void ISerializer.WriteNull() => throw new NotImplementedException(); - void ISerializer.WriteU16(ushort u16) => throw new NotImplementedException(); - void ISerializer.WriteU32(uint u32) => throw new NotImplementedException(); - void ISerializer.WriteU64(ulong u64) => throw new NotImplementedException(); - void ISerializer.WriteU8(byte b) => throw new NotImplementedException(); + void ISerializer.WriteDateTime(DateTime dt) => writer.WriteRaw(dt); + void ISerializer.WriteDateTimeOffset(DateTimeOffset dto) => writer.WriteRaw(dto); + void ISerializer.WriteDecimal(decimal d) => writer.WriteRaw(d); + void ISerializer.WriteF32(float f) => writer.WriteRaw(f); + void ISerializer.WriteF64(double d) => writer.WriteRaw(d); + void ISerializer.WriteI16(short i16) => writer.WriteRaw(i16); + void ISerializer.WriteI32(int i32) => writer.WriteRaw(i32); + void ISerializer.WriteI64(long i64) => writer.WriteRaw(i64); + void ISerializer.WriteI8(sbyte b) => writer.WriteRaw(b); + void ISerializer.WriteNull() { } + void ISerializer.WriteU16(ushort u16) => writer.WriteRaw(u16); + void ISerializer.WriteU32(uint u32) => writer.WriteRaw(u32); + void ISerializer.WriteU64(ulong u64) => writer.WriteRaw(u64); + void ISerializer.WriteU8(byte b) => writer.WriteRaw(b); } } diff --git a/src/serde/FixedWidth/FixedWidthSerializer.Type.cs b/src/serde/FixedWidth/FixedWidthSerializer.Type.cs index f8b887d6..e7d3d6e4 100644 --- a/src/serde/FixedWidth/FixedWidthSerializer.Type.cs +++ b/src/serde/FixedWidth/FixedWidthSerializer.Type.cs @@ -7,8 +7,11 @@ public sealed partial class FixedWidthSerializer : ITypeSerializer void ITypeSerializer.WriteValue(ISerdeInfo typeInfo, int index, T value, ISerialize serialize) => serialize.Serialize(value, this); - void ITypeSerializer.End(ISerdeInfo info) => writer.WriteLine(); - void ITypeSerializer.WriteBool(ISerdeInfo typeInfo, int index, bool b) => writer.WriteObject(typeInfo, index, b); + void ITypeSerializer.End(ISerdeInfo info) => writer.WriteRaw(); + void ITypeSerializer.WriteBool(ISerdeInfo typeInfo, int index, bool b) + { + writer.WriteObject(typeInfo, index, b); + } void ITypeSerializer.WriteChar(ISerdeInfo typeInfo, int index, char c) => writer.WriteObject(typeInfo, index, c); void ITypeSerializer.WriteU8(ISerdeInfo typeInfo, int index, byte b) => writer.WriteObject(typeInfo, index, b); void ITypeSerializer.WriteU16(ISerdeInfo typeInfo, int index, ushort u16) => writer.WriteObject(typeInfo, index, u16); diff --git a/src/serde/FixedWidth/Writer/FixedWidthWriter.cs b/src/serde/FixedWidth/Writer/FixedWidthWriter.cs index cf00b6dd..4243f82a 100644 --- a/src/serde/FixedWidth/Writer/FixedWidthWriter.cs +++ b/src/serde/FixedWidth/Writer/FixedWidthWriter.cs @@ -107,9 +107,16 @@ private void WriteText(string value, int fieldOffset, int fieldLength, FieldOver _pos += fieldLength; } - public void WriteLine() + internal void WriteRaw() => _writer.WriteLine(); + internal void WriteRaw(string value) + => _writer.WriteLine(value); + + internal void WriteRaw(T value) + where T : notnull + => _writer.WriteLine(value.ToString()); + public override string ToString() => GetStringBuilder().ToString(); }