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 => +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 => +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