~ruther/NosSmooth

e0d7901d99513b4a92a5152ccb1eb68ca6cc02a5 — František Boháček 3 years ago 21a2c9b
feat: add base packet types converting
42 files changed, 2229 insertions(+), 0 deletions(-)

A Core/NosSmooth.Packets/Attributes/GenerateSerializerAttribute.cs
A Core/NosSmooth.Packets/Attributes/PacketContextListAttribute.cs
A Core/NosSmooth.Packets/Attributes/PacketHeaderAttribute.cs
A Core/NosSmooth.Packets/Attributes/PacketIndexAttribute.cs
A Core/NosSmooth.Packets/Attributes/PacketListIndexAttribute.cs
A Core/NosSmooth.Packets/Attributes/PacketSource.cs
A Core/NosSmooth.Packets/Converters/BaseTypeConverter.cs
A Core/NosSmooth.Packets/Converters/Basic/BasicTypeConverter.cs
A Core/NosSmooth.Packets/Converters/Basic/ByteTypeConverter.cs
A Core/NosSmooth.Packets/Converters/Basic/IntTypeConverter.cs
A Core/NosSmooth.Packets/Converters/Basic/LongTypeConverter.cs
A Core/NosSmooth.Packets/Converters/Basic/ShortTypeConverter.cs
A Core/NosSmooth.Packets/Converters/Basic/UIntTypeConverter.cs
A Core/NosSmooth.Packets/Converters/Basic/ULongTypeConverter.cs
A Core/NosSmooth.Packets/Converters/Basic/UShortTypeConverter.cs
A Core/NosSmooth.Packets/Converters/ITypeConverter.cs
A Core/NosSmooth.Packets/Converters/Packets/InPacketConverter.cs
A Core/NosSmooth.Packets/Converters/Special/EnumTypeConverter.cs
A Core/NosSmooth.Packets/Converters/Special/ISpecialTypeConverter.cs
A Core/NosSmooth.Packets/Converters/Special/ListTypeConverter.cs
A Core/NosSmooth.Packets/Converters/TypeConverterRepository.cs
A Core/NosSmooth.Packets/Errors/AmbiguousHeaderError.cs
A Core/NosSmooth.Packets/Errors/CouldNotConvertError.cs
A Core/NosSmooth.Packets/Errors/DeserializedValueNullError.cs
A Core/NosSmooth.Packets/Errors/ListSerializerError.cs
A Core/NosSmooth.Packets/Errors/PacketConverterNotFoundError.cs
A Core/NosSmooth.Packets/Errors/PacketEndReachedError.cs
A Core/NosSmooth.Packets/Errors/TypeConverterNotFoundError.cs
A Core/NosSmooth.Packets/Errors/WrongTypeError.cs
A Core/NosSmooth.Packets/Extensions/ServiceCollectionExtensions.cs
A Core/NosSmooth.Packets/IsExternalInit.cs
A Core/NosSmooth.Packets/NosCore.Packets.csproj.DotSettings
A Core/NosSmooth.Packets/PacketSerializer.cs
A Core/NosSmooth.Packets/PacketStringBuilder.cs
A Core/NosSmooth.Packets/PacketStringEnumerator.cs
A Core/NosSmooth.Packets/PacketToken.cs
A Core/NosSmooth.Packets/Packets/IPacket.cs
A Core/NosSmooth.Packets/Packets/PacketInfo.cs
A Core/NosSmooth.Packets/Packets/PacketTypesRepository.cs
A Core/NosSmooth.Packets/Packets/UnresolvedPacket.cs
A Tests/NosSmooth.Packets.Tests/PacketStringBuilderTests.cs
A Tests/NosSmooth.Packets.Tests/PacketStringEnumeratorTests.cs
A Core/NosSmooth.Packets/Attributes/GenerateSerializerAttribute.cs => Core/NosSmooth.Packets/Attributes/GenerateSerializerAttribute.cs +17 -0
@@ 0,0 1,17 @@
//
//  GenerateSerializerAttribute.cs
//
//  Copyright (c) František Boháček. All rights reserved.
//  Licensed under the MIT license. See LICENSE file in the project root for full license information.

using System;

namespace NosSmooth.Packets.Attributes;

/// <summary>
/// Attribute for marking packets that should have their generator generated using Roslyn code generator.
/// </summary>
[AttributeUsage(AttributeTargets.Class)]
public class GenerateSerializerAttribute : Attribute
{
}
\ No newline at end of file

A Core/NosSmooth.Packets/Attributes/PacketContextListAttribute.cs => Core/NosSmooth.Packets/Attributes/PacketContextListAttribute.cs +35 -0
@@ 0,0 1,35 @@
//
//  PacketContextListAttribute.cs
//
//  Copyright (c) František Boháček. All rights reserved.
//  Licensed under the MIT license. See LICENSE file in the project root for full license information.

using System;

namespace NosSmooth.Packets.Attributes;

/// <summary>
/// Attribute for marking properties as a contextual list.
/// </summary>
/// <remarks>
/// Contextual list gets its length from another property that was already set.
/// </remarks>
[AttributeUsage(AttributeTargets.Property | AttributeTargets.Parameter)]
public class PacketContextListAttribute : PacketListIndexAttribute
{
    /// <summary>
    /// Initializes a new instance of the <see cref="PacketContextListAttribute"/> class.
    /// </summary>
    /// <param name="index">The position in the packet.</param>
    /// <param name="lengthStoredIn">The name of the property the length is stored in.</param>
    public PacketContextListAttribute(ushort index, string lengthStoredIn)
        : base(index)
    {
        LengthStoredIn = lengthStoredIn;
    }

    /// <summary>
    /// Gets or sets the attribute name that stores the length of this list.
    /// </summary>
    public string LengthStoredIn { get; set; }
}
\ No newline at end of file

A Core/NosSmooth.Packets/Attributes/PacketHeaderAttribute.cs => Core/NosSmooth.Packets/Attributes/PacketHeaderAttribute.cs +37 -0
@@ 0,0 1,37 @@
//
//  PacketHeaderAttribute.cs
//
//  Copyright (c) František Boháček. All rights reserved.
//  Licensed under the MIT license. See LICENSE file in the project root for full license information.

using System;

namespace NosSmooth.Packets.Attributes;

/// <summary>
/// Attribute for specifying the header identifier of the packet.
/// </summary>
[AttributeUsage(AttributeTargets.Class)]
public class PacketHeaderAttribute : Attribute
{
    /// <summary>
    /// Initializes a new instance of the <see cref="PacketHeaderAttribute"/> class.
    /// </summary>
    /// <param name="identifier">The packet identifier (the first entry).</param>
    /// <param name="source">The source of the packet.</param>
    public PacketHeaderAttribute(string? identifier, PacketSource source)
    {
        Identifier = identifier;
        Source = source;
    }

    /// <summary>
    /// Gets the identifier of the packet (the first entry in the packet).
    /// </summary>
    public string? Identifier { get; }

    /// <summary>
    /// Gets the source of the packet used for determining where the packet is from.
    /// </summary>
    public PacketSource Source { get; }
}
\ No newline at end of file

A Core/NosSmooth.Packets/Attributes/PacketIndexAttribute.cs => Core/NosSmooth.Packets/Attributes/PacketIndexAttribute.cs +40 -0
@@ 0,0 1,40 @@
//
//  PacketIndexAttribute.cs
//
//  Copyright (c) František Boháček. All rights reserved.
//  Licensed under the MIT license. See LICENSE file in the project root for full license information.

using System;

namespace NosSmooth.Packets.Attributes;

/// <summary>
/// Attribute for marking properties in packets with their position in the packet.
/// </summary>
[AttributeUsage(AttributeTargets.Property | AttributeTargets.Parameter)]
public class PacketIndexAttribute : Attribute
{
    /// <summary>
    /// Initializes a new instance of the <see cref="PacketIndexAttribute"/> class.
    /// </summary>
    /// <param name="index">The position of the property.</param>
    public PacketIndexAttribute(ushort index)
    {
        Index = index;
    }

    /// <summary>
    /// Gets the index of the current property.
    /// </summary>
    public ushort Index { get; }

    /// <summary>
    /// Gets the inner separator used for complex types such as sub packets.
    /// </summary>
    public char? InnerSeparator { get; set; }

    /// <summary>
    /// Gets the separator after this field.
    /// </summary>
    public char? AfterSeparator { get; set; }
}
\ No newline at end of file

A Core/NosSmooth.Packets/Attributes/PacketListIndexAttribute.cs => Core/NosSmooth.Packets/Attributes/PacketListIndexAttribute.cs +27 -0
@@ 0,0 1,27 @@
//
//  PacketListIndexAttribute.cs
//
//  Copyright (c) František Boháček. All rights reserved.
//  Licensed under the MIT license. See LICENSE file in the project root for full license information.

namespace NosSmooth.Packets.Attributes;

/// <summary>
/// Attribute for marking property as a packet list.
/// </summary>
public class PacketListIndexAttribute : PacketIndexAttribute
{
    /// <summary>
    /// Initializes a new instance of the <see cref="PacketListIndexAttribute"/> class.
    /// </summary>
    /// <param name="index">The position in the packet.</param>
    public PacketListIndexAttribute(ushort index)
        : base(index)
    {
    }

    /// <summary>
    /// Gets or sets the separator of the items in the array.
    /// </summary>
    public string ListSeparator { get; set; } = "|";
}
\ No newline at end of file

A Core/NosSmooth.Packets/Attributes/PacketSource.cs => Core/NosSmooth.Packets/Attributes/PacketSource.cs +23 -0
@@ 0,0 1,23 @@
//
//  PacketSource.cs
//
//  Copyright (c) František Boháček. All rights reserved.
//  Licensed under the MIT license. See LICENSE file in the project root for full license information.

namespace NosSmooth.Packets.Attributes;

/// <summary>
/// Specifies the source of the packet.
/// </summary>
public enum PacketSource
{
    /// <summary>
    /// The packet is from the server.
    /// </summary>
    Server,

    /// <summary>
    /// The packet is from the client.
    /// </summary>
    Client
}
\ No newline at end of file

A Core/NosSmooth.Packets/Converters/BaseTypeConverter.cs => Core/NosSmooth.Packets/Converters/BaseTypeConverter.cs +38 -0
@@ 0,0 1,38 @@
//
//  BaseTypeConverter.cs
//
//  Copyright (c) František Boháček. All rights reserved.
//  Licensed under the MIT license. See LICENSE file in the project root for full license information.

using NosSmooth.Packets.Errors;
using Remora.Results;

namespace NosSmooth.Packets.Converters;

/// <summary>
/// Base type for converting objects that maps object converting methods to the actual type.
/// </summary>
/// <typeparam name="TParseType">The type of the object that this converts.</typeparam>
public abstract class BaseTypeConverter<TParseType> : ITypeConverter<TParseType>
{
    /// <inheritdoc />
    public abstract Result Serialize(TParseType obj, PacketStringBuilder builder);

    /// <inheritdoc />
    public abstract Result<TParseType> Deserialize(PacketStringEnumerator stringEnumerator);

    /// <inheritdoc/>
    Result<object> ITypeConverter.Deserialize(PacketStringEnumerator stringEnumerator)
        => Deserialize(stringEnumerator);

    /// <inheritdoc/>
    Result ITypeConverter.Serialize(object obj, PacketStringBuilder builder)
    {
        if (!(obj is TParseType parseType))
        {
            return new WrongTypeError(this, typeof(TParseType), obj);
        }

        return Serialize(parseType, builder);
    }
}
\ No newline at end of file

A Core/NosSmooth.Packets/Converters/Basic/BasicTypeConverter.cs => Core/NosSmooth.Packets/Converters/Basic/BasicTypeConverter.cs +43 -0
@@ 0,0 1,43 @@
//
//  BasicTypeConverter.cs
//
//  Copyright (c) František Boháček. All rights reserved.
//  Licensed under the MIT license. See LICENSE file in the project root for full license information.

using System;
using Remora.Results;

namespace NosSmooth.Packets.Converters.Basic;

/// <summary>
/// Basic type converter for converting using <see cref="Convert"/>.
/// </summary>
/// <typeparam name="TBasicType">The basic type, that contains correct to string.</typeparam>
public abstract class BasicTypeConverter<TBasicType> : BaseTypeConverter<TBasicType>
{
    /// <inheritdoc />
    public override Result Serialize(TBasicType obj, PacketStringBuilder builder)
    {
        builder.Append(obj?.ToString() ?? "-");
        return Result.FromSuccess();
    }

    /// <inheritdoc />
    public override Result<TBasicType> Deserialize(PacketStringEnumerator stringEnumerator)
    {
        var nextTokenResult = stringEnumerator.GetNextToken();
        if (!nextTokenResult.IsSuccess)
        {
            return Result<TBasicType>.FromError(nextTokenResult);
        }

        return Deserialize(nextTokenResult.Entity.Token);
    }

    /// <summary>
    /// Deserialize the given string value.
    /// </summary>
    /// <param name="value">The value to deserialize.</param>
    /// <returns>The deserialized value or an error.</returns>
    protected abstract Result<TBasicType> Deserialize(string value);
}
\ No newline at end of file

A Core/NosSmooth.Packets/Converters/Basic/ByteTypeConverter.cs => Core/NosSmooth.Packets/Converters/Basic/ByteTypeConverter.cs +27 -0
@@ 0,0 1,27 @@
//
//  ByteTypeConverter.cs
//
//  Copyright (c) František Boháček. All rights reserved.
//  Licensed under the MIT license. See LICENSE file in the project root for full license information.

using NosSmooth.Packets.Errors;
using Remora.Results;

namespace NosSmooth.Packets.Converters.Basic;

/// <summary>
/// Converter of <see cref="byte"/>.
/// </summary>
public class ByteTypeConverter : BasicTypeConverter<byte>
{
    /// <inheritdoc />
    protected override Result<byte> Deserialize(string value)
    {
        if (!byte.TryParse(value, out var parsed))
        {
            return new CouldNotConvertError(this, value, "Could not parse as an byte.");
        }

        return parsed;
    }
}
\ No newline at end of file

A Core/NosSmooth.Packets/Converters/Basic/IntTypeConverter.cs => Core/NosSmooth.Packets/Converters/Basic/IntTypeConverter.cs +27 -0
@@ 0,0 1,27 @@
//
//  IntTypeConverter.cs
//
//  Copyright (c) František Boháček. All rights reserved.
//  Licensed under the MIT license. See LICENSE file in the project root for full license information.

using NosSmooth.Packets.Errors;
using Remora.Results;

namespace NosSmooth.Packets.Converters.Basic;

/// <summary>
/// Converter of <see cref="int"/>.
/// </summary>
public class IntTypeConverter : BasicTypeConverter<int>
{
    /// <inheritdoc />
    protected override Result<int> Deserialize(string value)
    {
        if (!int.TryParse(value, out var parsed))
        {
            return new CouldNotConvertError(this, value, "Could not parse as int.");
        }

        return parsed;
    }
}
\ No newline at end of file

A Core/NosSmooth.Packets/Converters/Basic/LongTypeConverter.cs => Core/NosSmooth.Packets/Converters/Basic/LongTypeConverter.cs +27 -0
@@ 0,0 1,27 @@
//
//  LongTypeConverter.cs
//
//  Copyright (c) František Boháček. All rights reserved.
//  Licensed under the MIT license. See LICENSE file in the project root for full license information.

using NosSmooth.Packets.Errors;
using Remora.Results;

namespace NosSmooth.Packets.Converters.Basic;

/// <summary>
/// Converter of <see cref="long"/>.
/// </summary>
public class LongTypeConverter : BasicTypeConverter<long>
{
    /// <inheritdoc />
    protected override Result<long> Deserialize(string value)
    {
        if (!long.TryParse(value, out var parsed))
        {
            return new CouldNotConvertError(this, value, "Could not parse as an long.");
        }

        return parsed;
    }
}
\ No newline at end of file

A Core/NosSmooth.Packets/Converters/Basic/ShortTypeConverter.cs => Core/NosSmooth.Packets/Converters/Basic/ShortTypeConverter.cs +27 -0
@@ 0,0 1,27 @@
//
//  ShortTypeConverter.cs
//
//  Copyright (c) František Boháček. All rights reserved.
//  Licensed under the MIT license. See LICENSE file in the project root for full license information.

using NosSmooth.Packets.Errors;
using Remora.Results;

namespace NosSmooth.Packets.Converters.Basic;

/// <summary>
/// Converter of <see cref="short"/>.
/// </summary>
public class ShortTypeConverter : BasicTypeConverter<short>
{
    /// <inheritdoc />
    protected override Result<short> Deserialize(string value)
    {
        if (!short.TryParse(value, out var parsed))
        {
            return new CouldNotConvertError(this, value, "Could not parse as short.");
        }

        return parsed;
    }
}
\ No newline at end of file

A Core/NosSmooth.Packets/Converters/Basic/UIntTypeConverter.cs => Core/NosSmooth.Packets/Converters/Basic/UIntTypeConverter.cs +27 -0
@@ 0,0 1,27 @@
//
//  UIntTypeConverter.cs
//
//  Copyright (c) František Boháček. All rights reserved.
//  Licensed under the MIT license. See LICENSE file in the project root for full license information.

using NosSmooth.Packets.Errors;
using Remora.Results;

namespace NosSmooth.Packets.Converters.Basic;

/// <summary>
/// Converter of <see cref="uint"/>.
/// </summary>
public class UIntTypeConverter : BasicTypeConverter<uint>
{
    /// <inheritdoc />
    protected override Result<uint> Deserialize(string value)
    {
        if (!uint.TryParse(value, out var parsed))
        {
            return new CouldNotConvertError(this, value, "Could not parse as uint.");
        }

        return parsed;
    }
}
\ No newline at end of file

A Core/NosSmooth.Packets/Converters/Basic/ULongTypeConverter.cs => Core/NosSmooth.Packets/Converters/Basic/ULongTypeConverter.cs +27 -0
@@ 0,0 1,27 @@
//
//  ULongTypeConverter.cs
//
//  Copyright (c) František Boháček. All rights reserved.
//  Licensed under the MIT license. See LICENSE file in the project root for full license information.

using NosSmooth.Packets.Errors;
using Remora.Results;

namespace NosSmooth.Packets.Converters.Basic;

/// <summary>
/// Converter of <see cref="ulong"/>.
/// </summary>
public class ULongTypeConverter : BasicTypeConverter<ulong>
{
    /// <inheritdoc />
    protected override Result<ulong> Deserialize(string value)
    {
        if (!ulong.TryParse(value, out var parsed))
        {
            return new CouldNotConvertError(this, value, "Could not parse as an ulong.");
        }

        return parsed;
    }
}
\ No newline at end of file

A Core/NosSmooth.Packets/Converters/Basic/UShortTypeConverter.cs => Core/NosSmooth.Packets/Converters/Basic/UShortTypeConverter.cs +27 -0
@@ 0,0 1,27 @@
//
//  UShortTypeConverter.cs
//
//  Copyright (c) František Boháček. All rights reserved.
//  Licensed under the MIT license. See LICENSE file in the project root for full license information.

using NosSmooth.Packets.Errors;
using Remora.Results;

namespace NosSmooth.Packets.Converters.Basic;

/// <summary>
/// Converter of <see cref="ushort"/>.
/// </summary>
public class UShortTypeConverter : BasicTypeConverter<ushort>
{
    /// <inheritdoc />
    protected override Result<ushort> Deserialize(string value)
    {
        if (!ushort.TryParse(value, out var parsed))
        {
            return new CouldNotConvertError(this, value, "Could not parse as an ushort.");
        }

        return parsed;
    }
}
\ No newline at end of file

A Core/NosSmooth.Packets/Converters/ITypeConverter.cs => Core/NosSmooth.Packets/Converters/ITypeConverter.cs +55 -0
@@ 0,0 1,55 @@
//
//  ITypeConverter.cs
//
//  Copyright (c) František Boháček. All rights reserved.
//  Licensed under the MIT license. See LICENSE file in the project root for full license information.

using Remora.Results;

namespace NosSmooth.Packets.Converters;

/// <summary>
/// Base type for converting types.
/// </summary>
public interface ITypeConverter
{
    /// <summary>
    /// Convert the data from the enumerator to the given type.
    /// </summary>
    /// <param name="stringEnumerator">The packet string enumerator with the current position.</param>
    /// <returns>The parsed object or an error.</returns>
    public Result<object> Deserialize(PacketStringEnumerator stringEnumerator);

    /// <summary>
    /// Serializes the given object to string by appending to the packet string builder.
    /// </summary>
    /// <param name="obj">The object to serialize.</param>
    /// <param name="builder">The string builder to append to.</param>
    /// <returns>A result that may or may not have succeeded.</returns>
    public Result Serialize(object obj, PacketStringBuilder builder);
}

/// <summary>
/// Converts string to an object.
/// </summary>
/// <remarks>
/// Used for converting packets.
/// </remarks>
/// <typeparam name="TParseType">The type that can be parsed.</typeparam>
public interface ITypeConverter<TParseType> : ITypeConverter
{
    /// <summary>
    /// Convert the data from the enumerator to the given type.
    /// </summary>
    /// <param name="stringEnumerator">The packet string enumerator with the current position.</param>
    /// <returns>The parsed object or an error.</returns>
    public new Result<TParseType> Deserialize(PacketStringEnumerator stringEnumerator);

    /// <summary>
    /// Serializes the given object to string by appending to the packet string builder.
    /// </summary>
    /// <param name="obj">The object to serialize.</param>
    /// <param name="builder">The string builder to append to.</param>
    /// <returns>A result that may or may not have succeeded.</returns>
    public Result Serialize(TParseType obj, PacketStringBuilder builder);
}
\ No newline at end of file

A Core/NosSmooth.Packets/Converters/Packets/InPacketConverter.cs => Core/NosSmooth.Packets/Converters/Packets/InPacketConverter.cs +28 -0
@@ 0,0 1,28 @@
//
//  InPacketConverter.cs
//
//  Copyright (c) František Boháček. All rights reserved.
//  Licensed under the MIT license. See LICENSE file in the project root for full license information.

using NosSmooth.Packets.Packets.Server.Map;
using Remora.Results;

namespace NosSmooth.Packets.Converters.Packets;

/// <summary>
/// Converter for "in" packet. <see cref="InPacket"/>.
/// </summary>
public class InPacketConverter : BaseTypeConverter<InPacket>
{
    /// <inheritdoc />
    public override Result Serialize(InPacket obj, PacketStringBuilder builder)
    {
        throw new System.NotImplementedException();
    }

    /// <inheritdoc />
    public override Result<InPacket> Deserialize(PacketStringEnumerator stringEnumerator)
    {
        throw new System.NotImplementedException();
    }
}
\ No newline at end of file

A Core/NosSmooth.Packets/Converters/Special/EnumTypeConverter.cs => Core/NosSmooth.Packets/Converters/Special/EnumTypeConverter.cs +45 -0
@@ 0,0 1,45 @@
//
//  EnumTypeConverter.cs
//
//  Copyright (c) František Boháček. All rights reserved.
//  Licensed under the MIT license. See LICENSE file in the project root for full license information.

using System;
using Remora.Results;

namespace NosSmooth.Packets.Converters.Special;

/// <summary>
/// Converts all enums.
/// </summary>
public class EnumTypeConverter : ISpecialTypeConverter
{
    /// <inheritdoc />
    public bool ShouldHandle(Type type)
        => type.IsEnum;

    /// <inheritdoc />
    public Result<object?> Deserialize(Type type, PacketStringEnumerator stringEnumerator)
    {
        var tokenResult = stringEnumerator.GetNextToken();
        if (!tokenResult.IsSuccess)
        {
            return Result.FromError(tokenResult);
        }

        return Enum.Parse(type, tokenResult.Entity.Token);
    }

    /// <inheritdoc />
    public Result Serialize(Type type, object? obj, PacketStringBuilder builder)
    {
        if (obj is null)
        {
            builder.Append("-");
            return Result.FromSuccess();
        }

        builder.Append(((int)obj).ToString());
        return Result.FromSuccess();
    }
}
\ No newline at end of file

A Core/NosSmooth.Packets/Converters/Special/ISpecialTypeConverter.cs => Core/NosSmooth.Packets/Converters/Special/ISpecialTypeConverter.cs +41 -0
@@ 0,0 1,41 @@
//
//  ISpecialTypeConverter.cs
//
//  Copyright (c) František Boháček. All rights reserved.
//  Licensed under the MIT license. See LICENSE file in the project root for full license information.

using System;
using NosSmooth.Packets.Errors;
using Remora.Results;

namespace NosSmooth.Packets.Converters.Special;

/// <summary>
/// Converts special types such as enums or lists.
/// </summary>
public interface ISpecialTypeConverter
{
    /// <summary>
    /// Whether this type converter should handle the given type.
    /// </summary>
    /// <param name="type">The type to handle.</param>
    /// <returns>Whether the type should be handled.</returns>
    public bool ShouldHandle(Type type);

    /// <summary>
    /// Deserialize the given string to the object.
    /// </summary>
    /// <param name="type">The type to deserialize.</param>
    /// <param name="stringEnumerator">The packets string enumerator.</param>
    /// <returns>A result that may or may not have succeeded.</returns>
    public Result<object?> Deserialize(Type type, PacketStringEnumerator stringEnumerator);

    /// <summary>
    /// Deserialize the given object into string.
    /// </summary>
    /// <param name="type">The type to serialize.</param>
    /// <param name="obj">The object to serialize.</param>
    /// <param name="builder">The packet string builder.</param>
    /// <returns>A result that may or may not have succeeded.</returns>
    public Result Serialize(Type type, object? obj, PacketStringBuilder builder);
}
\ No newline at end of file

A Core/NosSmooth.Packets/Converters/Special/ListTypeConverter.cs => Core/NosSmooth.Packets/Converters/Special/ListTypeConverter.cs +138 -0
@@ 0,0 1,138 @@
//
//  ListTypeConverter.cs
//
//  Copyright (c) František Boháček. All rights reserved.
//  Licensed under the MIT license. See LICENSE file in the project root for full license information.

using System;
using System.Collections;
using System.Collections.Generic;
using System.Linq;
using System.Linq.Expressions;
using System.Reflection;
using System.Text;
using NosSmooth.Packets.Errors;
using Remora.Results;

namespace NosSmooth.Packets.Converters.Special;

/// <summary>
/// Converts lists.
/// </summary>
public class ListTypeConverter : ISpecialTypeConverter
{
    private readonly TypeConverterRepository _typeConverterRepository;

    /// <summary>
    /// Initializes a new instance of the <see cref="ListTypeConverter"/> class.
    /// </summary>
    /// <param name="typeConverterRepository">The type converter repository.</param>
    public ListTypeConverter(TypeConverterRepository typeConverterRepository)
    {
        _typeConverterRepository = typeConverterRepository;
    }

    /// <inheritdoc />
    public bool ShouldHandle(Type type)
        => typeof(IEnumerable).IsAssignableFrom(type);

    /// <inheritdoc />
    public Result<object?> Deserialize(Type type, PacketStringEnumerator stringEnumerator)
    {
        var data = new List<object?>();
        var genericType = type.GetElementType() ?? type.GetGenericArguments()[0];

        do
        {
            if (stringEnumerator.PushPreparedLevel())
            {
                return new ArgumentInvalidError(nameof(stringEnumerator), "The string enumerator has to have a prepared level for all lists.");
            }

            var result = _typeConverterRepository.Deserialize(genericType, stringEnumerator);
            stringEnumerator.PopLevel();
            if (!result.IsSuccess)
            {
                return Result<object?>.FromError(new ListSerializerError(result, data.Count));
            }

            data.Add(result.Entity);
        }
        while (!(stringEnumerator.IsOnLastToken() ?? false));

        return GetAndFillListMethod(genericType)(data);
    }

    /// <inheritdoc />
    public Result Serialize(Type type, object? obj, PacketStringBuilder builder)
    {
        if (obj is null)
        {
            builder.Append("-");
            return Result.FromSuccess();
        }

        var items = (IEnumerable)obj;
        var genericType = type.GetElementType() ?? type.GetGenericArguments()[0];

        foreach (var item in items)
        {
            if (!builder.PushPreparedLevel())
            {
                return new ArgumentInvalidError(nameof(builder), "The string builder has to have a prepared level for all lists.");
            }

            var serializeResult = _typeConverterRepository.Serialize(genericType, item, builder);
            builder.ReplaceWithParentSeparator();
            builder.PopLevel();
            if (!serializeResult.IsSuccess)
            {
                return serializeResult;
            }
        }
        builder.ReplaceWithParentSeparator();

        return Result.FromSuccess();
    }

    // TODO: cache the functions?

    /// <summary>
    /// From https://stackoverflow.com/questions/35913495/moving-from-reflection-to-expression-tree.
    /// </summary>
    /// <param name="genericType">The generic type.</param>
    /// <returns>The function.</returns>
    private Func<IEnumerable<object?>, object> GetAndFillListMethod(Type genericType)
    {
        var listType = typeof(List<>);
        var listGenericType = listType.MakeGenericType(genericType);

        var values = Expression.Parameter(typeof(IEnumerable<object?>), "values");

        var ctor = listGenericType.GetConstructor(BindingFlags.Instance | BindingFlags.Public, null, new Type[0], null);

        // I prefer using Expression.Variable to Expression.Parameter
        // for internal variables
        var instance = Expression.Variable(listGenericType, "list");

        var assign = Expression.Assign(instance, Expression.New(ctor!));

        var addMethod = listGenericType.GetMethod("AddRange", new[] { typeof(IEnumerable<>).MakeGenericType(genericType) });

        // Enumerable.Cast<T>
        var castMethod = typeof(Enumerable).GetMethod("Cast", new[] { typeof(IEnumerable) })!.MakeGenericMethod(genericType);

        // For the parameters there is a params Expression[], so no explicit array necessary
        var castCall = Expression.Call(castMethod, values);
        var addCall = Expression.Call(instance, addMethod!, castCall);

        var block = Expression.Block(
            new[] { instance },
            assign,
            addCall,
            Expression.Convert(instance, typeof(object))
        );

        return (Func<IEnumerable<object?>, object>)Expression.Lambda(block, values).Compile();
    }
}
\ No newline at end of file

A Core/NosSmooth.Packets/Converters/TypeConverterRepository.cs => Core/NosSmooth.Packets/Converters/TypeConverterRepository.cs +217 -0
@@ 0,0 1,217 @@
//
//  TypeConverterRepository.cs
//
//  Copyright (c) František Boháček. All rights reserved.
//  Licensed under the MIT license. See LICENSE file in the project root for full license information.

using System;
using Microsoft.Extensions.DependencyInjection;
using NosSmooth.Packets.Converters.Special;
using NosSmooth.Packets.Errors;
using Remora.Results;

namespace NosSmooth.Packets.Converters;

/// <summary>
/// Repository for <see cref="ITypeConverter"/>.
/// </summary>
public class TypeConverterRepository
{
    private readonly IServiceProvider _serviceProvider;

    /// <summary>
    /// Initializes a new instance of the <see cref="TypeConverterRepository"/> class.
    /// </summary>
    /// <param name="serviceProvider">The dependency injection service provider.</param>
    public TypeConverterRepository(IServiceProvider serviceProvider)
    {
        _serviceProvider = serviceProvider;
    }

    /// <summary>
    /// Gets the type converter for the given type.
    /// </summary>
    /// <param name="type">The type to find converter for.</param>
    /// <returns>The type converter or an error.</returns>
    public Result<ITypeConverter> GetTypeConverter(Type type)
    {
        var converterType = typeof(ITypeConverter<>).MakeGenericType(type);
        var typeConverter = (ITypeConverter?)_serviceProvider.GetService(converterType);

        if (typeConverter is null)
        {
            return new TypeConverterNotFoundError(type);
        }

        return Result<ITypeConverter>.FromSuccess(typeConverter);
    }

    /// <summary>
    /// Gets the type converter for the given type.
    /// </summary>
    /// <typeparam name="TParseType">The type to find converter for.</typeparam>
    /// <returns>The type converter or an error.</returns>
    public Result<ITypeConverter<TParseType>> GetTypeConverter<TParseType>()
    {
        var typeConverter = _serviceProvider.GetService<ITypeConverter<TParseType>>();

        if (typeConverter is null)
        {
            return new TypeConverterNotFoundError(typeof(TParseType));
        }

        return Result<ITypeConverter<TParseType>>.FromSuccess(typeConverter);
    }

    /// <summary>
    /// Convert the data from the enumerator to the given type.
    /// </summary>
    /// <param name="parseType">The type of the object to serialize.</param>
    /// <param name="stringEnumerator">The packet string enumerator with the current position.</param>
    /// <returns>The parsed object or an error.</returns>
    public Result<object?> Deserialize(Type parseType, PacketStringEnumerator stringEnumerator)
    {
        var specialConverter = GetSpecialConverter(parseType);
        if (specialConverter is not null)
        {
            var deserializeResult = specialConverter.Deserialize(parseType, stringEnumerator);
            if (!deserializeResult.IsSuccess)
            {
                return Result<object?>.FromError(deserializeResult);
            }

            if (deserializeResult.Entity is null)
            {
                if (parseType.DeclaringType == typeof(Nullable<>))
                {
                    return default;
                }

                return Result<object?>.FromError(new DeserializedValueNullError(parseType));
            }

            return deserializeResult.Entity;
        }

        var converterResult = GetTypeConverter(parseType);
        if (!converterResult.IsSuccess)
        {
            return Result<object?>.FromError(converterResult);
        }

        var deserializedResult = converterResult.Entity.Deserialize(stringEnumerator);
        if (!deserializedResult.IsSuccess)
        {
            return Result<object?>.FromError(deserializedResult);
        }

        return Result<object?>.FromSuccess(deserializedResult);
    }

    /// <summary>
    /// Serializes the given object to string by appending to the packet string builder.
    /// </summary>
    /// <param name="parseType">The type of the object to serialize.</param>
    /// <param name="obj">The object to serialize.</param>
    /// <param name="builder">The string builder to append to.</param>
    /// <returns>A result that may or may not have succeeded.</returns>
    public Result Serialize(Type parseType, object obj, PacketStringBuilder builder)
    {
        var specialConverter = GetSpecialConverter(parseType);
        if (specialConverter is not null)
        {
            return specialConverter.Serialize(parseType, obj, builder);
        }

        var converterResult = GetTypeConverter(parseType);
        if (!converterResult.IsSuccess)
        {
            return Result.FromError(converterResult);
        }

        return converterResult.Entity.Serialize(obj, builder);
    }

    /// <summary>
    /// Convert the data from the enumerator to the given type.
    /// </summary>
    /// <param name="stringEnumerator">The packet string enumerator with the current position.</param>
    /// <typeparam name="TParseType">The type of the object to serialize.</typeparam>
    /// <returns>The parsed object or an error.</returns>
    public Result<TParseType> Deserialize<TParseType>(PacketStringEnumerator stringEnumerator)
    {
        var specialConverter = GetSpecialConverter(typeof(TParseType));
        if (specialConverter is not null)
        {
            var deserializeResult = specialConverter.Deserialize(typeof(TParseType), stringEnumerator);
            if (!deserializeResult.IsSuccess)
            {
                return Result<TParseType>.FromError(deserializeResult);
            }

            if (deserializeResult.Entity is null)
            {
                if (typeof(TParseType).DeclaringType == typeof(Nullable<>))
                {
                    return default;
                }

                return Result<TParseType>.FromError(new DeserializedValueNullError(typeof(TParseType)));
            }

            return (TParseType)deserializeResult.Entity;
        }

        var converterResult = GetTypeConverter<TParseType>();
        if (!converterResult.IsSuccess)
        {
            return Result<TParseType>.FromError(converterResult);
        }

        return converterResult.Entity.Deserialize(stringEnumerator);
    }

    /// <summary>
    /// Serializes the given object to string by appending to the packet string builder.
    /// </summary>
    /// <param name="obj">The object to serialize.</param>
    /// <param name="builder">The string builder to append to.</param>
    /// <typeparam name="TParseType">The type of the object to deserialize.</typeparam>
    /// <returns>A result that may or may not have succeeded.</returns>
    public Result Serialize<TParseType>(TParseType obj, PacketStringBuilder builder)
    {
        if (obj is null)
        {
            builder.Append("-");
            return Result.FromSuccess();
        }

        var specialConverter = GetSpecialConverter(typeof(TParseType));
        if (specialConverter is not null)
        {
            return specialConverter.Serialize(typeof(TParseType), obj, builder);
        }

        var converterResult = GetTypeConverter<TParseType>();
        if (!converterResult.IsSuccess)
        {
            return Result.FromError(converterResult);
        }

        return converterResult.Entity.Serialize(obj, builder);
    }

    private ISpecialTypeConverter? GetSpecialConverter(Type type)
    {
        var specialConverters = _serviceProvider.GetServices<ISpecialTypeConverter>();
        foreach (var specialConverter in specialConverters)
        {
            if (specialConverter.ShouldHandle(type))
            {
                return specialConverter;
            }
        }

        return null;
    }
}
\ No newline at end of file

A Core/NosSmooth.Packets/Errors/AmbiguousHeaderError.cs => Core/NosSmooth.Packets/Errors/AmbiguousHeaderError.cs +23 -0
@@ 0,0 1,23 @@
//
//  AmbiguousHeaderError.cs
//
//  Copyright (c) František Boháček. All rights reserved.
//  Licensed under the MIT license. See LICENSE file in the project root for full license information.

using System;
using System.Collections.Generic;
using System.Linq;
using NosSmooth.Packets.Attributes;
using NosSmooth.Packets.Packets;
using Remora.Results;

namespace NosSmooth.Packets.Errors;

/// <summary>
/// The header was ambiguous, there were at least two packets with the same header and source.
/// </summary>
/// <param name="Header">The packet's header.</param>
/// <param name="Source">The packet's source.</param>
/// <param name="PacketTypes">The types that were ambiguous.</param>
public record AmbiguousHeaderError(string Header, PacketSource? Source, IReadOnlyList<PacketInfo> PacketTypes)
    : ResultError($"There was more than one packet with the header {Header} in the {Source.ToString() ?? "Unknown"} source. ({string.Join(", ", PacketTypes.Select(x => x.PacketType.FullName))})");
\ No newline at end of file

A Core/NosSmooth.Packets/Errors/CouldNotConvertError.cs => Core/NosSmooth.Packets/Errors/CouldNotConvertError.cs +21 -0
@@ 0,0 1,21 @@
//
//  CouldNotConvertError.cs
//
//  Copyright (c) František Boháček. All rights reserved.
//  Licensed under the MIT license. See LICENSE file in the project root for full license information.

using System;
using NosSmooth.Packets.Converters;
using Remora.Results;

namespace NosSmooth.Packets.Errors;

/// <summary>
/// The value could not be converted.
/// </summary>
/// <param name="Converter">The converter that failed the parsing.</param>
/// <param name="Value">The value that failed to parse.</param>
/// <param name="Reason">The reason for the error.</param>
/// <param name="Exception">The underlying exception, if any.</param>
public record CouldNotConvertError(ITypeConverter Converter, string Value, string Reason, Exception? Exception = default)
    : ResultError($"Converter {Converter.GetType().FullName} could not convert {Value} due to {Reason}.");
\ No newline at end of file

A Core/NosSmooth.Packets/Errors/DeserializedValueNullError.cs => Core/NosSmooth.Packets/Errors/DeserializedValueNullError.cs +15 -0
@@ 0,0 1,15 @@
//
//  DeserializedValueNullError.cs
//
//  Copyright (c) František Boháček. All rights reserved.
//  Licensed under the MIT license. See LICENSE file in the project root for full license information.

using System;
using Remora.Results;

namespace NosSmooth.Packets.Errors;

/// <summary>
/// Deserialized value is null, but it cannot be.
/// </summary>
public record DeserializedValueNullError(Type ParseType) : ResultError($"Got a value of type {ParseType.FullName} as null even though it's non-nullable");
\ No newline at end of file

A Core/NosSmooth.Packets/Errors/ListSerializerError.cs => Core/NosSmooth.Packets/Errors/ListSerializerError.cs +16 -0
@@ 0,0 1,16 @@
//
//  ListSerializerError.cs
//
//  Copyright (c) František Boháček. All rights reserved.
//  Licensed under the MIT license. See LICENSE file in the project root for full license information.

using Remora.Results;

namespace NosSmooth.Packets.Errors;

/// <summary>
/// Could not parse an item in the list.
/// </summary>
/// <param name="Result">The errorful result.</param>
/// <param name="Index">The index in the list.</param>
public record ListSerializerError(IResult Result, int Index) : ResultError($"Could not parse an item (index {Index}) from the list. {Result.Error!.Message}");
\ No newline at end of file

A Core/NosSmooth.Packets/Errors/PacketConverterNotFoundError.cs => Core/NosSmooth.Packets/Errors/PacketConverterNotFoundError.cs +16 -0
@@ 0,0 1,16 @@
//
//  PacketConverterNotFoundError.cs
//
//  Copyright (c) František Boháček. All rights reserved.
//  Licensed under the MIT license. See LICENSE file in the project root for full license information.

using Remora.Results;

namespace NosSmooth.Packets.Errors;

/// <summary>
/// The converter for the given packet was not found.
/// </summary>
/// <param name="Header">The header of the packet.</param>
public record PacketConverterNotFoundError(string Header)
    : ResultError($"Could not find converter for packet with header {Header}.");
\ No newline at end of file

A Core/NosSmooth.Packets/Errors/PacketEndReachedError.cs => Core/NosSmooth.Packets/Errors/PacketEndReachedError.cs +17 -0
@@ 0,0 1,17 @@
//
//  PacketEndReachedError.cs
//
//  Copyright (c) František Boháček. All rights reserved.
//  Licensed under the MIT license. See LICENSE file in the project root for full license information.

using Remora.Results;

namespace NosSmooth.Packets.Errors;

/// <summary>
/// The end of a packet was reached already.
/// </summary>
/// <param name="Packet">The packet string.</param>
/// <param name="LevelEnd">Whether this indicates end of a level instead of the whole packet.</param>
public record PacketEndReachedError(string Packet, bool LevelEnd = false)
    : ResultError($"Reached and end of the packet (or it's level) {Packet}, cannot read any more tokens in the current level.");
\ No newline at end of file

A Core/NosSmooth.Packets/Errors/TypeConverterNotFoundError.cs => Core/NosSmooth.Packets/Errors/TypeConverterNotFoundError.cs +16 -0
@@ 0,0 1,16 @@
//
//  TypeConverterNotFoundError.cs
//
//  Copyright (c) František Boháček. All rights reserved.
//  Licensed under the MIT license. See LICENSE file in the project root for full license information.

using System;
using Remora.Results;

namespace NosSmooth.Packets.Errors;

/// <summary>
/// Could not find type converter for the given type.
/// </summary>
/// <param name="Type">The type of the object.</param>
public record TypeConverterNotFoundError(Type Type) : ResultError($"Could not find converter for {Type.FullName}.");
\ No newline at end of file

A Core/NosSmooth.Packets/Errors/WrongTypeError.cs => Core/NosSmooth.Packets/Errors/WrongTypeError.cs +20 -0
@@ 0,0 1,20 @@
//
//  WrongTypeError.cs
//
//  Copyright (c) František Boháček. All rights reserved.
//  Licensed under the MIT license. See LICENSE file in the project root for full license information.

using System;
using NosSmooth.Packets.Converters;
using Remora.Results;

namespace NosSmooth.Packets.Errors;

/// <summary>
/// The wrong type was passed to a type converter.
/// </summary>
/// <param name="TypeConverter">The converter that failed to convert the object.</param>
/// <param name="ExpectedType">The expected type of the converting object.</param>
/// <param name="ActualObject">The actual object the converter got.</param>
public record WrongTypeError(ITypeConverter TypeConverter, Type ExpectedType, object ActualObject)
    : ResultError($"{TypeConverter.GetType().FullName} expected type {ExpectedType.FullName}, but got {ActualObject.GetType().FullName}");
\ No newline at end of file

A Core/NosSmooth.Packets/Extensions/ServiceCollectionExtensions.cs => Core/NosSmooth.Packets/Extensions/ServiceCollectionExtensions.cs +16 -0
@@ 0,0 1,16 @@
//
//  ServiceCollectionExtensions.cs
//
//  Copyright (c) František Boháček. All rights reserved.
//  Licensed under the MIT license. See LICENSE file in the project root for full license information.

using Microsoft.Extensions.DependencyInjection;

namespace NosSmooth.Packets.Extensions;

/// <summary>
/// Extensions for <see cref="IServiceCollection"/>.
/// </summary>
public static class ServiceCollectionExtensions
{
}
\ No newline at end of file

A Core/NosSmooth.Packets/IsExternalInit.cs => Core/NosSmooth.Packets/IsExternalInit.cs +15 -0
@@ 0,0 1,15 @@
//
//  IsExternalInit.cs
//
//  Copyright (c) František Boháček. All rights reserved.
//  Licensed under the MIT license. See LICENSE file in the project root for full license information.

namespace System.Runtime.CompilerServices
{
    /// <summary>
    /// Dummy.
    /// </summary>
    public class IsExternalInit
    {
    }
}
\ No newline at end of file

A Core/NosSmooth.Packets/NosCore.Packets.csproj.DotSettings => Core/NosSmooth.Packets/NosCore.Packets.csproj.DotSettings +2 -0
@@ 0,0 1,2 @@
<wpf:ResourceDictionary xml:space="preserve" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:s="clr-namespace:System;assembly=mscorlib" xmlns:ss="urn:shemas-jetbrains-com:settings-storage-xaml" xmlns:wpf="http://schemas.microsoft.com/winfx/2006/xaml/presentation">
	<s:String x:Key="/Default/CodeInspection/CSharpLanguageProject/LanguageLevel/@EntryValue">CSharp100</s:String></wpf:ResourceDictionary>
\ No newline at end of file

A Core/NosSmooth.Packets/PacketSerializer.cs => Core/NosSmooth.Packets/PacketSerializer.cs +40 -0
@@ 0,0 1,40 @@
//
//  PacketSerializer.cs
//
//  Copyright (c) František Boháček. All rights reserved.
//  Licensed under the MIT license. See LICENSE file in the project root for full license information.

using System;
using NosSmooth.Packets.Attributes;
using NosSmooth.Packets.Converters;
using NosSmooth.Packets.Packets;
using Remora.Results;

namespace NosSmooth.Packets;

/// <summary>
/// Serializer of packets.
/// </summary>
public class PacketSerializer
{
    /// <summary>
    /// Serializes the given object to string by appending to the packet string builder.
    /// </summary>
    /// <param name="obj">The packet to serialize.</param>
    /// <returns>A result that may or may not have succeeded.</returns>
    public Result<string> Serialize(IPacket obj)
    {
        return Result<string>.FromSuccess("as");
    }

    /// <summary>
    /// Convert the data from the enumerator to the given type.
    /// </summary>
    /// <param name="packetString">The packet string to deserialize.</param>
    /// <param name="preferredSource">The preferred source to check first. If packet with the given header is not found there, other sources will be checked as well.</param>
    /// <returns>The parsed object or an error.</returns>
    public Result<IPacket> Deserialize(string packetString, PacketSource preferredSource)
    {
        return Result<IPacket>.FromError(new ArgumentInvalidError("asdf", "asdf"));
    }
}
\ No newline at end of file

A Core/NosSmooth.Packets/PacketStringBuilder.cs => Core/NosSmooth.Packets/PacketStringBuilder.cs +165 -0
@@ 0,0 1,165 @@
//
//  PacketStringBuilder.cs
//
//  Copyright (c) František Boháček. All rights reserved.
//  Licensed under the MIT license. See LICENSE file in the project root for full license information.

using System.Collections.Generic;
using System.Text;
using Remora.Results;

namespace NosSmooth.Packets;

/// <summary>
/// String builder for packets.
/// </summary>
public class PacketStringBuilder
{
    private readonly StringBuilder _builder;
    private StringBuilderLevel _currentLevel;
    private char? _preparedLevelSeparator;
    private char? _insertSeparator;

    /// <summary>
    /// Initializes a new instance of the <see cref="PacketStringBuilder"/> class.
    /// </summary>
    /// <param name="separator">The top level separator.</param>
    public PacketStringBuilder(char separator = ' ')
    {
        _currentLevel = new StringBuilderLevel(null, separator);
        _preparedLevelSeparator = _insertSeparator = null;
        _builder = new StringBuilder();
    }

    /// <summary>
    /// Sets the separator to search for only once.
    /// </summary>
    /// <remarks>
    /// This separator will have the most priority.
    /// </remarks>
    /// <param name="separator">The separator to look for.</param>
    public void SetAfterSeparatorOnce(char separator)
    {
        _currentLevel.SeparatorOnce = separator;
    }

    /// <summary>
    /// Prepare the given level that can be set when needed.
    /// </summary>
    /// <param name="separator">The separator of the prepared level.</param>
    public void PrepareLevel(char separator)
    {
        _preparedLevelSeparator = separator;
    }

    /// <summary>
    /// Reset the prepared level, if there is any.
    /// </summary>
    public void RemovePreparedLevel()
    {
        _preparedLevelSeparator = null;
    }

    /// <summary>
    /// Create next level with the separator given in the prepared level.
    /// </summary>
    /// <remarks>
    /// Level of the current builder will stay the same.
    /// Will return null, if there is not a level prepared.
    /// </remarks>
    /// <returns>An enumerator with the new level pushed.</returns>
    public bool PushPreparedLevel()
    {
        if (_preparedLevelSeparator is null)
        {
            return false;
        }

        _currentLevel = new StringBuilderLevel(_currentLevel, _preparedLevelSeparator.Value);
        return true;
    }

    /// <summary>
    /// Push new separator level to the stack.
    /// </summary>
    /// <remarks>
    /// This will change the current enumerator.
    /// It has to be <see cref="PopLevel"/> after parent level should be used.
    /// </remarks>
    /// <param name="separator">The separator of the new level.</param>
    public void PushLevel(char separator)
    {
        _preparedLevelSeparator = null;
        _currentLevel = new StringBuilderLevel(_currentLevel, separator);
    }

    /// <summary>
    /// Pop the current level.
    /// </summary>
    /// <returns>A result that may or may not have succeeded. There will be an error if the current level is the top one.</returns>
    public Result PopLevel()
    {
        if (_currentLevel.Parent is null)
        {
            return new InvalidOperationError("The level cannot be popped, the stack is already at the top level.");
        }

        _preparedLevelSeparator = null;
        _currentLevel = _currentLevel.Parent;

        return Result.FromSuccess();
    }

    /// <summary>
    /// Appends the value to the string.
    /// </summary>
    /// <param name="value">The value to append.</param>
    public void Append(string value)
    {
        if (_insertSeparator is not null)
        {
            _builder.Append(_insertSeparator);
            _insertSeparator = null;
        }

        _builder.Append(value);
        _insertSeparator = _currentLevel.SeparatorOnce ?? _currentLevel.Separator;
        _currentLevel.SeparatorOnce = null;
    }

    /// <summary>
    /// Replace the last separator with the parent separator.
    /// </summary>
    public void ReplaceWithParentSeparator()
    {
        var parent = _currentLevel.Parent;
        if (_insertSeparator is null || parent is null)
        {
            return;
        }

        _insertSeparator = parent.SeparatorOnce ?? parent.Separator;
        parent.SeparatorOnce = null;
    }

    /// <inheritdoc />
    public override string ToString()
    {
        return _builder.ToString();
    }

    private class StringBuilderLevel
    {
        public StringBuilderLevel(StringBuilderLevel? parent, char separator)
        {
            Parent = parent;
            Separator = separator;
        }

        public StringBuilderLevel? Parent { get; }

        public char Separator { get; }

        public char? SeparatorOnce { get; set; }
    }
}
\ No newline at end of file

A Core/NosSmooth.Packets/PacketStringEnumerator.cs => Core/NosSmooth.Packets/PacketStringEnumerator.cs +399 -0
@@ 0,0 1,399 @@
//
//  PacketStringEnumerator.cs
//
//  Copyright (c) František Boháček. All rights reserved.
//  Licensed under the MIT license. See LICENSE file in the project root for full license information.

using System.Collections.Generic;
using System.Text;
using NosSmooth.Packets.Errors;
using Remora.Results;

namespace NosSmooth.Packets;

/// <summary>
/// Enumerator for packet strings.
/// </summary>
public struct PacketStringEnumerator
{
    private readonly EnumeratorData _data;
    private readonly Dictionary<char, ushort> _numberOfSeparators;
    private EnumeratorLevel _currentLevel;
    private (char Separator, uint? MaxTokens)? _preparedLevel;
    private PacketToken? _currentToken;

    /// <summary>
    /// Initializes a new instance of the <see cref="PacketStringEnumerator"/> struct.
    /// </summary>
    /// <param name="data">The packet string data.</param>
    /// <param name="separator">The separator to use on the highest level.</param>
    public PacketStringEnumerator(string data, char separator = ' ')
    {
        _currentLevel = new EnumeratorLevel(null, separator);
        _data = new EnumeratorData(data);
        _numberOfSeparators = new Dictionary<char, ushort>();
        _numberOfSeparators.Add(separator, 1);
        _currentToken = null;
        _preparedLevel = null;
    }

    /// <summary>
    /// Initializes a new instance of the <see cref="PacketStringEnumerator"/> struct.
    /// </summary>
    /// <param name="data">The data of the enumerator.</param>
    /// <param name="level">The current enumerator level.</param>
    /// <param name="numberOfSeparators">The number of separators.</param>
    private PacketStringEnumerator(EnumeratorData data, EnumeratorLevel level, Dictionary<char, ushort> numberOfSeparators)
    {
        _currentLevel = level;
        _data = data;

        // TODO: use something less heavy than copying everything from the dictionary.
        _numberOfSeparators = new Dictionary<char, ushort>(numberOfSeparators);
        _currentToken = null;
        _preparedLevel = null;
    }

    /// <summary>
    /// Sets the separator to search for only once.
    /// </summary>
    /// <remarks>
    /// This separator will have the most priority.
    /// </remarks>
    /// <param name="separator">The separator to look for.</param>
    public void SetAfterSeparatorOnce(char separator)
    {
        _currentToken = null;
        _currentLevel.SeparatorOnce = separator;
    }

    /// <summary>
    /// Prepare the given level that can be set when needed.
    /// </summary>
    /// <param name="separator">The separator of the prepared level.</param>
    /// <param name="maxTokens">The maximum number of tokens for the level.</param>
    public void PrepareLevel(char separator, uint? maxTokens = null)
    {
        _preparedLevel = (separator, maxTokens);
    }

    /// <summary>
    /// Reset the prepared level, if there is any.
    /// </summary>
    public void RemovePreparedLevel()
    {
        _preparedLevel = null;
    }

    /// <summary>
    /// Create next level with the separator given in the prepared level.
    /// </summary>
    /// <remarks>
    /// Level of the current enumerator will stay the same.
    /// Will return null, if there is not a level prepared.
    /// </remarks>
    /// <returns>An enumerator with the new level pushed.</returns>
    public PacketStringEnumerator? CreatePreparedLevel()
    {
        return _preparedLevel is not null ? CreateLevel(_preparedLevel.Value.Separator, _preparedLevel.Value.MaxTokens) : null;
    }

    /// <summary>
    /// Push next level with the separator given in the prepared level.
    /// </summary>
    /// <returns>Whether there is a prepared level present.</returns>
    public bool PushPreparedLevel()
    {
        if (_preparedLevel is null)
        {
            return false;
        }

        _currentToken = null;
        _currentLevel = new EnumeratorLevel(_currentLevel, _preparedLevel.Value.Separator, _preparedLevel.Value.MaxTokens)
        {
            ReachedEnd = _currentLevel.ReachedEnd
        };

        if (!_numberOfSeparators.ContainsKey(_preparedLevel.Value.Separator))
        {
            _numberOfSeparators.Add(_preparedLevel.Value.Separator, 0);
        }

        _numberOfSeparators[_preparedLevel.Value.Separator]++;
        return true;
    }

    /// <summary>
    /// Create next level with the given separator and maximum number of tokens.
    /// </summary>
    /// <remarks>
    /// Level of the current enumerator will stay the same.
    /// The maximum number of tokens indicates how many tokens can be read ie. in lists,
    /// the enumerator won't allow reading more than that many tokens, error will be thrown if the user tries to read more.
    /// </remarks>
    /// <param name="separator">The separator of the new level.</param>
    /// <param name="maxTokens">The maximum number of tokens to read.</param>
    /// <returns>An enumerator with the new level pushed.</returns>
    public PacketStringEnumerator CreateLevel(char separator, uint? maxTokens = default)
    {
        _currentToken = null;
        var stringEnumerator = new PacketStringEnumerator(_data, _currentLevel, _numberOfSeparators);
        stringEnumerator.PushLevel(separator, maxTokens);
        return stringEnumerator;
    }

    /// <summary>
    /// Push new separator level to the stack.
    /// </summary>
    /// <remarks>
    /// This will change the current enumerator.
    /// It has to be <see cref="PopLevel"/> after parent level should be used.
    /// </remarks>
    /// <param name="separator">The separator of the new level.</param>
    /// <param name="maxTokens">The maximum number of tokens to read.</param>
    public void PushLevel(char separator, uint? maxTokens = default)
    {
        _preparedLevel = null;
        _currentToken = null;
        _currentLevel = new EnumeratorLevel(_currentLevel, separator, maxTokens)
        {
            ReachedEnd = _currentLevel.ReachedEnd
        };

        if (!_numberOfSeparators.ContainsKey(separator))
        {
            _numberOfSeparators.Add(separator, 0);
        }

        _numberOfSeparators[separator]++;
    }

    /// <summary>
    /// Pop the current level.
    /// </summary>
    /// <returns>A result that may or may not have succeeded. There will be an error if the current level is the top one.</returns>
    public Result PopLevel()
    {
        if (_currentLevel.Parent is null)
        {
            return new InvalidOperationError("The level cannot be popped, the stack is already at the top level.");
        }

        _preparedLevel = null;
        _numberOfSeparators[_currentLevel.Separator]--;
        _currentLevel = _currentLevel.Parent;
        return Result.FromSuccess();
    }

    /// <summary>
    /// Get the next token.
    /// </summary>
    /// <param name="seek">Whether to seek the cursor to the end of the token.</param>
    /// <returns>The found token.</returns>
    public Result<PacketToken> GetNextToken(bool seek = true)
    {
        // The token is cached if seek was false to speed things up.
        if (_currentToken != null)
        {
            var cachedToken = _currentToken.Value;
            if (seek)
            {
                UpdateCurrentAndParentLevels(cachedToken);
                _currentLevel.TokensRead++;
                _currentToken = null;
                _data.Cursor += cachedToken.Token.Length + 1;
                _currentLevel.SeparatorOnce = null;
            }

            return cachedToken;
        }

        if (_data.ReachedEnd || _currentLevel.ReachedEnd)
        {
            return new PacketEndReachedError(_data.Data, _currentLevel.ReachedEnd);
        }

        var currentIndex = _data.Cursor;
        char currentCharacter = _data.Data[currentIndex];
        StringBuilder tokenString = new StringBuilder();

        bool? isLast, encounteredUpperLevel;

        // If the current character is a separator, stop, else, add it to the builder.
        while (!IsSeparator(currentCharacter, out isLast, out encounteredUpperLevel))
        {
            tokenString.Append(currentCharacter);
            currentIndex++;

            if (currentIndex == _data.Data.Length)
            {
                isLast = true;
                encounteredUpperLevel = true;
                break;
            }

            currentCharacter = _data.Data[currentIndex];
        }

        currentIndex++;

        var token = new PacketToken(tokenString.ToString(), isLast, encounteredUpperLevel, _data.ReachedEnd);
        if (seek)
        {
            UpdateCurrentAndParentLevels(token);
            _data.Cursor = currentIndex;
            _currentLevel.TokensRead++;
        }
        else
        {
            _currentToken = token;
        }

        return token;
    }

    /// <summary>
    /// Update fields that are used in the process.
    /// </summary>
    /// <param name="token">The token.</param>
    private void UpdateCurrentAndParentLevels(PacketToken token)
    {
        // If the token is last in the current level, then set reached end of the current level.
        if (token.IsLast ?? false)
        {
            _currentLevel.ReachedEnd = true;
        }

        // IsLast is set if parent separator was encountered. The parent needs to be updated.
        if (_currentLevel.Parent is not null && (token.IsLast ?? false))
        {
            var parent = _currentLevel.Parent;

            // Adding TokensRead is supported only one layer currently.
            parent.TokensRead++; // Add read tokens of the parent, because we encountered its separator.
            if (parent.TokensRead >= parent.MaxTokens)
            {
                parent.ReachedEnd = true;
                _currentLevel.ReachedEnd = true;
            }
            _currentLevel.Parent = parent;
        }

        // Encountered Upper Level is set if the reaached separator is not from neither the current level neither the parent
        if ((token.EncounteredUpperLevel ?? false) && _currentLevel.Parent is not null)
        {
            // Just treat it as last, even though that may be incorrect.
            var parent = _currentLevel.Parent;
            parent.ReachedEnd = true;
            _currentLevel.ReachedEnd = true;
            _currentLevel.Parent = parent;
        }

        // The once separator is always used just once, whatever happens.
        _currentLevel.SeparatorOnce = null;
    }

    /// <summary>
    /// Whether the last token of the current level was read.
    /// </summary>
    /// <returns>Whether the last token was read. Null if cannot determine (ie. there are multiple levels with the same separator.)</returns>
    public bool? IsOnLastToken()
        => _data.ReachedEnd || _currentLevel.ReachedEnd;

    /// <summary>
    /// Checks if the given character is a separator.
    /// </summary>
    /// <param name="c">The character to check.</param>
    /// <param name="isLast">Whether the separator indicates last separator in this level. True if numberOfSeparators is exactly one and this is the parent's separator.</param>
    /// <param name="encounteredUpperLevel">Whether higher level than the parent was encountered. That could indicate some kind of an error if this is not the last token.</param>
    /// <returns>Whether the character is a separator.</returns>
    private bool IsSeparator(char c, out bool? isLast, out bool? encounteredUpperLevel)
    {
        isLast = null;
        encounteredUpperLevel = null;

        // Separator once has the highest preference
        if (_currentLevel.SeparatorOnce == c)
        {
            isLast = false;
            return true;
        }

        // The separator is not in any level.
        if (!_numberOfSeparators.ContainsKey(c))
        {
            return false;
        }

        var number = _numberOfSeparators[c];
        if (number == 0)
        { // The separator is not in any level.
            return false;
        }

        // The separator is in one level, we can correctly determine which level it corresponds to.
        // If the number is higher, we cannot know which level the separator corresponds to,
        // thus we have to let encounteredUpperLevel and isLast be null.
        // Typical for arrays that are at the end of packets or of specified length.
        if (number == 1)
        {
            if (_currentLevel.Parent?.Separator == c)
            {
                isLast = true;
                encounteredUpperLevel = false;
            }
            else if (_currentLevel.Separator == c)
            {
                isLast = false;
                encounteredUpperLevel = false;
            }
            else
            {
                encounteredUpperLevel = isLast = true;
            }
        }

        return true;
    }

    private class EnumeratorData
    {
        public EnumeratorData(string data)
        {
            Data = data;
            Cursor = 0;
        }

        public string Data { get; }

        public int Cursor { get; set; }

        public bool ReachedEnd => Cursor >= Data.Length;
    }

    private class EnumeratorLevel
    {
        public EnumeratorLevel(EnumeratorLevel? parent, char separator, uint? maxTokens = default)
        {
            Parent = parent;
            Separator = separator;
            SeparatorOnce = null;
            MaxTokens = maxTokens;
            TokensRead = 0;
            ReachedEnd = false;
        }

        public EnumeratorLevel? Parent { get; set; }

        public char Separator { get; }

        public char? SeparatorOnce { get; set; }

        public uint? MaxTokens { get; set; }

        public uint TokensRead { get; set; }

        public bool ReachedEnd { get; set; }
    }
}
\ No newline at end of file

A Core/NosSmooth.Packets/PacketToken.cs => Core/NosSmooth.Packets/PacketToken.cs +19 -0
@@ 0,0 1,19 @@
//
//  PacketToken.cs
//
//  Copyright (c) František Boháček. All rights reserved.
//  Licensed under the MIT license. See LICENSE file in the project root for full license information.

using System.Diagnostics.CodeAnalysis;

namespace NosSmooth.Packets;

/// <summary>
/// The single token from a packet.
/// </summary>
/// <param name="Token">The token.</param>
/// <param name="IsLast">Whether the token is last in the current level. Null if it cannot be determined.</param>
/// <param name="EncounteredUpperLevel">Whether the current separator was from an upper stack level than the parent. That could mean some kind of an error if not etc. at the end of parsing a last entry of a list and last entry of a subpacket.</param>
/// <param name="PacketEndReached">Whether the packet's end was reached.</param>
[SuppressMessage("StyleCop.CSharp.NamingRules", "SA1313:Parameter names should begin with lower-case letter", Justification = "Record struct creates the underlying properties.")]
public readonly record struct PacketToken(string Token, bool? IsLast, bool? EncounteredUpperLevel, bool PacketEndReached);
\ No newline at end of file

A Core/NosSmooth.Packets/Packets/IPacket.cs => Core/NosSmooth.Packets/Packets/IPacket.cs +14 -0
@@ 0,0 1,14 @@
//
//  IPacket.cs
//
//  Copyright (c) František Boháček. All rights reserved.
//  Licensed under the MIT license. See LICENSE file in the project root for full license information.

namespace NosSmooth.Packets.Packets;

/// <summary>
/// Base packet interface that must be implemented in every packet.
/// </summary>
public interface IPacket
{
}
\ No newline at end of file

A Core/NosSmooth.Packets/Packets/PacketInfo.cs => Core/NosSmooth.Packets/Packets/PacketInfo.cs +18 -0
@@ 0,0 1,18 @@
//
//  PacketInfo.cs
//
//  Copyright (c) František Boháček. All rights reserved.
//  Licensed under the MIT license. See LICENSE file in the project root for full license information.

using System;
using NosSmooth.Packets.Converters;

namespace NosSmooth.Packets.Packets;

/// <summary>
/// Information about a packet type.
/// </summary>
/// <param name="Header">The packet's header, if any.</param>
/// <param name="PacketType">The packet's type.</param>
/// <param name="PacketConverter">The packet's converter.</param>
public record PacketInfo(string? Header, Type PacketType, ITypeConverter PacketConverter);
\ No newline at end of file

A Core/NosSmooth.Packets/Packets/PacketTypesRepository.cs => Core/NosSmooth.Packets/Packets/PacketTypesRepository.cs +150 -0
@@ 0,0 1,150 @@
//
//  PacketTypesRepository.cs
//
//  Copyright (c) František Boháček. All rights reserved.
//  Licensed under the MIT license. See LICENSE file in the project root for full license information.

using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using Microsoft.Extensions.DependencyInjection;
using NosSmooth.Packets.Attributes;
using NosSmooth.Packets.Converters;
using NosSmooth.Packets.Errors;
using Remora.Results;

namespace NosSmooth.Packets.Packets;

/// <summary>
/// Repository of packet types for finding information about packets.
/// </summary>
public class PacketTypesRepository
{
    private readonly TypeConverterRepository _typeConverterRepository;
    private readonly Dictionary<PacketSource, Dictionary<string, PacketInfo>> _headerToPacket;
    private readonly Dictionary<string, PacketInfo> _typeToPacket;

    /// <summary>
    /// Initializes a new instance of the <see cref="PacketTypesRepository"/> class.
    /// </summary>
    /// <param name="typeConverterRepository">The type converter repository.</param>
    public PacketTypesRepository(TypeConverterRepository typeConverterRepository)
    {
        _typeConverterRepository = typeConverterRepository;
        _headerToPacket = new Dictionary<PacketSource, Dictionary<string, PacketInfo>>();
        _typeToPacket = new Dictionary<string, PacketInfo>();
    }

    /// <summary>
    /// Add the given packet type to the repository.
    /// </summary>
    /// <param name="type">The type of the packet.</param>
    /// <returns>A result that may or may not have succeeded.</returns>
    public Result AddPacketType(Type type)
    {
        if (typeof(IPacket).IsAssignableFrom(type))
        {
            return new ArgumentInvalidError(nameof(type), "The type has to be assignable to IPacket.");
        }

        var header = type.GetCustomAttribute<PacketHeaderAttribute>();
        if (header is null)
        {
            return new ArgumentInvalidError(nameof(type), "Every packet has to specify the header.");
        }

        var converterResult = _typeConverterRepository.GetTypeConverter(type);
        if (!converterResult.IsSuccess)
        {
            return Result.FromError(converterResult);
        }

        var info = new PacketInfo(header.Identifier, type, converterResult.Entity);

        if (_headerToPacket.ContainsKey(header.Source))
        {
            _headerToPacket[header.Source] = new Dictionary<string, PacketInfo>();
        }

        if (type.FullName is not null)
        {
            _typeToPacket[type.FullName] = info;
        }

        if (header.Identifier is not null)
        {
            if (_headerToPacket[header.Source].ContainsKey(header.Identifier))
            {
                return new AmbiguousHeaderError
                (
                    header.Identifier,
                    header.Source,
                    new[] { _headerToPacket[header.Source][header.Identifier], info }
                );
            }

            _headerToPacket[header.Source][header.Identifier] = info;
        }

        return Result.FromSuccess();
    }

    /// <summary>
    /// Gets the type of a packet that corresponds to the given header.
    /// </summary>
    /// <param name="header">The header of the packet.</param>
    /// <param name="preferredSource">The preferred source, first this source will be checked for the header and if packet with that source is not found, other sources will be accpeted as well.</param>
    /// <returns>Info that stores the packet's info. Or an error, if not found.</returns>
    public Result<PacketInfo> FindPacketInfo(string header, PacketSource preferredSource)
    {
        if (_headerToPacket[preferredSource].ContainsKey(header))
        {
            return _headerToPacket[preferredSource][header];
        }

        var foundPackets = _headerToPacket.Values
            .Where(x => x.ContainsKey(header))
            .Select(x => x[header]).ToArray();

        return foundPackets.Length switch
        {
            1 => foundPackets[0],
            0 => new PacketConverterNotFoundError(header),
            _ => new AmbiguousHeaderError(header, null, foundPackets)
        };
    }

    /// <summary>
    /// Gets the packet info from the given packet type.
    /// </summary>
    /// <typeparam name="TPacket">The type of the packet.</typeparam>
    /// <returns>Info that stores the packet's info. Or an error, if not found.</returns>
    public Result<PacketInfo> FindPacketInfo<TPacket>() where TPacket : IPacket
        => FindPacketInfo(typeof(TPacket));

    /// <summary>
    /// Gets the packet info from the given packet type.
    /// </summary>
    /// <param name="packetType">The type of the packet.</param>
    /// <returns>Info that stores the packet's info. Or an error, if not found.</returns>
    public Result<PacketInfo> FindPacketInfo(Type packetType)
        => packetType.FullName is null
            ? new ArgumentInvalidError(nameof(packetType), "The type has to have a name to get packet info.")
            : FindPacketInfoByTypeName(packetType.FullName);

    /// <summary>
    /// Gets the packet info from the given packet type name.
    /// </summary>
    /// <param name="packetTypeFullName">The full name of the type of the packet.</param>
    /// <returns>Info that stores the packet's info. Or an error, if not found.</returns>
    public Result<PacketInfo> FindPacketInfoByTypeName(string packetTypeFullName)
    {
        if (!_typeToPacket.ContainsKey(packetTypeFullName))
        {
            return new PacketConverterNotFoundError(packetTypeFullName);
        }

        return _typeToPacket[packetTypeFullName];
    }
}
\ No newline at end of file

A Core/NosSmooth.Packets/Packets/UnresolvedPacket.cs => Core/NosSmooth.Packets/Packets/UnresolvedPacket.cs +14 -0
@@ 0,0 1,14 @@
//
//  UnresolvedPacket.cs
//
//  Copyright (c) František Boháček. All rights reserved.
//  Licensed under the MIT license. See LICENSE file in the project root for full license information.

namespace NosSmooth.Packets.Packets;

/// <summary>
/// Unresolved packet that is not supported.
/// </summary>
/// <param name="Header">The header of the packet.</param>
/// <param name="Body">The body of the packet.</param>
public record UnresolvedPacket(string Header, string Body) : IPacket;
\ No newline at end of file

A Tests/NosSmooth.Packets.Tests/PacketStringBuilderTests.cs => Tests/NosSmooth.Packets.Tests/PacketStringBuilderTests.cs +64 -0
@@ 0,0 1,64 @@
//
//  PacketStringBuilderTests.cs
//
//  Copyright (c) František Boháček. All rights reserved.
//  Licensed under the MIT license. See LICENSE file in the project root for full license information.

using Xunit;

namespace NosSmooth.Packets.Tests;

/// <summary>
/// Tests for <see cref="PacketStringBuilder"/>.
/// </summary>
public class PacketStringBuilderTests
{
    /// <summary>
    /// Tests that the builder correctly builds array of complex types.
    /// </summary>
    [Fact]
    public void BuilderCorrectlyBuildsComplexArray()
    {
        // in 1 11.12.13|14.15.16|17.18.19
        var stringBuilder = new PacketStringBuilder();
        stringBuilder.Append("in");
        stringBuilder.Append("1");

        stringBuilder.PushLevel('|');
        for (int i = 0; i < 3; i++)
        {
            stringBuilder.PushLevel('.');
            for (int j = 0; j < 3; j++)
            {
                stringBuilder.Append((1 + (i * 3) + j + 10).ToString());
            }
            stringBuilder.ReplaceWithParentSeparator();
            stringBuilder.PopLevel();
        }

        stringBuilder.PopLevel();

        Assert.Equal("in 1 11.12.13|14.15.16|17.18.19", stringBuilder.ToString());
    }

    /// <summary>
    /// Tests that the builder correctly uses once separator.
    /// </summary>
    [Fact]
    public void BuilderCorrectlyUsesOnceSeparator()
    {
        var stringBuilder = new PacketStringBuilder();
        stringBuilder.Append("in");

        stringBuilder.SetAfterSeparatorOnce('.');
        stringBuilder.PushLevel('|');
        stringBuilder.Append("a");
        stringBuilder.Append("b");
        stringBuilder.Append("c");
        stringBuilder.ReplaceWithParentSeparator();
        stringBuilder.PopLevel();
        stringBuilder.Append("d");

        Assert.Equal("in a|b|c.d", stringBuilder.ToString());
    }
}
\ No newline at end of file

A Tests/NosSmooth.Packets.Tests/PacketStringEnumeratorTests.cs => Tests/NosSmooth.Packets.Tests/PacketStringEnumeratorTests.cs +196 -0
@@ 0,0 1,196 @@
//
//  PacketStringEnumeratorTests.cs
//
//  Copyright (c) František Boháček. All rights reserved.
//  Licensed under the MIT license. See LICENSE file in the project root for full license information.

using System;
using System.Linq;
using NosSmooth.Packets.Errors;
using Xunit;

namespace NosSmooth.Packets.Tests;

/// <summary>
/// Test for <see cref="PacketStringEnumerator"/>.
/// </summary>
public class PacketStringEnumeratorTests
{
    /// <summary>
    /// Test that array of complex types can be parsed.
    /// </summary>
    [Fact]
    public void EnumeratorListComplexStringGivesCorrectResult()
    {
        var stringEnumerator = new PacketStringEnumerator("in 1 11.12.13|14.15.16|17.18.19");
        var headerTokenResult = stringEnumerator.GetNextToken();
        Assert.True(headerTokenResult.IsSuccess);
        Assert.False(headerTokenResult.Entity.PacketEndReached);
        Assert.NotNull(headerTokenResult.Entity.IsLast);
        Assert.NotNull(headerTokenResult.Entity.EncounteredUpperLevel);
        Assert.False(headerTokenResult.Entity.IsLast);
        Assert.False(headerTokenResult.Entity.EncounteredUpperLevel);
        Assert.Matches("in", headerTokenResult.Entity.Token);

        var firstToken = stringEnumerator.GetNextToken();
        Assert.True(firstToken.IsSuccess);
        Assert.False(firstToken.Entity.PacketEndReached);
        Assert.NotNull(firstToken.Entity.IsLast);
        Assert.NotNull(firstToken.Entity.EncounteredUpperLevel);
        Assert.False(firstToken.Entity.IsLast);
        Assert.False(firstToken.Entity.EncounteredUpperLevel);
        Assert.Matches("1", firstToken.Entity.Token);

        var listEnumerator = stringEnumerator.CreateLevel('|');
        listEnumerator.PrepareLevel('.');

        for (int i = 0; i < 3; i++)
        {
            var preparedLevel = listEnumerator.CreatePreparedLevel();
            Assert.NotNull(preparedLevel);

            for (int j = 0; j < 3; j++)
            {
                string currentNum = (j + (i * 3) + 1 + 10).ToString();
                var currentToken = preparedLevel!.Value.GetNextToken();
                Assert.True(currentToken.IsSuccess);
                Assert.False(currentToken.Entity.PacketEndReached);
                Assert.NotNull(currentToken.Entity.IsLast);
                Assert.NotNull(currentToken.Entity.EncounteredUpperLevel);
                if (j == 2 && i == 2)
                {
                    Assert.True(currentToken.Entity.EncounteredUpperLevel);
                }
                else
                {
                    Assert.False(currentToken.Entity.EncounteredUpperLevel);
                }

                if (j != 2)
                {
                    Assert.False(currentToken.Entity.IsLast);
                }
                else
                {
                    Assert.True(currentToken.Entity.IsLast);
                }

                Assert.Matches(currentNum, currentToken.Entity.Token);
            }

            Assert.True(preparedLevel!.Value.IsOnLastToken());
        }

        Assert.True(stringEnumerator.IsOnLastToken());
    }

    /// <summary>
    /// Test that over reaching the end is not allowed.
    /// </summary>
    [Fact]
    public void EnumeratorDoesNotAllowOvereachingPacketEnd()
    {
        var stringEnumerator = new PacketStringEnumerator("in 1 2 3 4");
        var tokenResult = stringEnumerator.GetNextToken();
        Assert.True(tokenResult.IsSuccess); // in
        tokenResult = stringEnumerator.GetNextToken();
        Assert.True(tokenResult.IsSuccess); // 1
        tokenResult = stringEnumerator.GetNextToken();
        Assert.True(tokenResult.IsSuccess); // 2
        tokenResult = stringEnumerator.GetNextToken();
        Assert.True(tokenResult.IsSuccess); // 3
        tokenResult = stringEnumerator.GetNextToken();
        Assert.True(tokenResult.IsSuccess); // 4

        tokenResult = stringEnumerator.GetNextToken();
        Assert.False(tokenResult.IsSuccess);
        Assert.IsType<PacketEndReachedError>(tokenResult.Error);
    }

    /// <summary>
    /// Test that over reaching the end of a list is not allowed.
    /// </summary>
    [Fact]
    public void EnumeratorDoesNotAllowOvereachingListComplexTypeEnd()
    {
        var stringEnumerator = new PacketStringEnumerator("in 1|2.2|3.3|4.4|5");
        var tokenResult = stringEnumerator.GetNextToken();
        Assert.True(tokenResult.IsSuccess); // in

        var listEnumerator = stringEnumerator.CreateLevel('.');
        var itemEnumerator = listEnumerator.CreateLevel('|');

        tokenResult = itemEnumerator.GetNextToken();
        Assert.True(tokenResult.IsSuccess); // 1

        tokenResult = itemEnumerator.GetNextToken();
        Assert.True(tokenResult.IsSuccess); // 2
        Assert.True(tokenResult.Entity.IsLast);

        tokenResult = itemEnumerator.GetNextToken();
        Assert.False(tokenResult.IsSuccess);
        Assert.IsType<PacketEndReachedError>(tokenResult.Error);
        Assert.True(((PacketEndReachedError)tokenResult.Error!).LevelEnd);
    }

    /// <summary>
    /// Test that over reaching the length of a list is not allowed.
    /// </summary>
    [Fact]
    public void EnumeratorDoesNotAllowOvereachingListLength()
    {
        var stringEnumerator = new PacketStringEnumerator("in 1|2.2|3.3|4.4|5");
        var tokenResult = stringEnumerator.GetNextToken();
        Assert.True(tokenResult.IsSuccess); // in

        var listEnumerator = stringEnumerator.CreateLevel('.', 2);
        var itemEnumerator = listEnumerator.CreateLevel('|');

        // first item
        tokenResult = itemEnumerator.GetNextToken();
        Assert.True(tokenResult.IsSuccess);

        tokenResult = itemEnumerator.GetNextToken();
        Assert.True(tokenResult.IsSuccess);
        Assert.True(tokenResult.Entity.IsLast);

        // second item
        itemEnumerator = listEnumerator.CreateLevel('|');
        tokenResult = itemEnumerator.GetNextToken();
        Assert.True(tokenResult.IsSuccess);

        tokenResult = itemEnumerator.GetNextToken();
        Assert.True(tokenResult.IsSuccess);
        Assert.True(tokenResult.Entity.IsLast);

        // cannot reach third item
        Assert.True(listEnumerator.IsOnLastToken());
        itemEnumerator = listEnumerator.CreateLevel('|');
        tokenResult = itemEnumerator.GetNextToken();
        Assert.False(tokenResult.IsSuccess);
        Assert.IsType<PacketEndReachedError>(tokenResult.Error);
        Assert.True(((PacketEndReachedError)tokenResult.Error!).LevelEnd);
    }

    /// <summary>
    /// Test that EncounteredUpperLevel is returned if appropriate.
    /// </summary>
    [Fact]
    public void EnumeratorReturnsEncounteredUpperLevel()
    {
        var stringEnumerator = new PacketStringEnumerator("in 1|2 1");
        var tokenResult = stringEnumerator.GetNextToken();
        Assert.True(tokenResult.IsSuccess); // in

        var listEnumerator = stringEnumerator.CreateLevel('.');
        var itemEnumerator = listEnumerator.CreateLevel('|');

        tokenResult = itemEnumerator.GetNextToken();
        Assert.True(tokenResult.IsSuccess);

        tokenResult = itemEnumerator.GetNextToken();
        Assert.True(tokenResult.IsSuccess);
        Assert.True(tokenResult.Entity.IsLast);
        Assert.True(tokenResult.Entity.EncounteredUpperLevel);
    }
}
\ No newline at end of file

Do not follow this link