From e0d7901d99513b4a92a5152ccb1eb68ca6cc02a5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Franti=C5=A1ek=20Boh=C3=A1=C4=8Dek?= Date: Tue, 28 Dec 2021 23:46:03 +0100 Subject: [PATCH] feat: add base packet types converting --- .../Attributes/GenerateSerializerAttribute.cs | 17 + .../Attributes/PacketContextListAttribute.cs | 35 ++ .../Attributes/PacketHeaderAttribute.cs | 37 ++ .../Attributes/PacketIndexAttribute.cs | 40 ++ .../Attributes/PacketListIndexAttribute.cs | 27 ++ .../Attributes/PacketSource.cs | 23 + .../Converters/BaseTypeConverter.cs | 38 ++ .../Converters/Basic/BasicTypeConverter.cs | 43 ++ .../Converters/Basic/ByteTypeConverter.cs | 27 ++ .../Converters/Basic/IntTypeConverter.cs | 27 ++ .../Converters/Basic/LongTypeConverter.cs | 27 ++ .../Converters/Basic/ShortTypeConverter.cs | 27 ++ .../Converters/Basic/UIntTypeConverter.cs | 27 ++ .../Converters/Basic/ULongTypeConverter.cs | 27 ++ .../Converters/Basic/UShortTypeConverter.cs | 27 ++ .../Converters/ITypeConverter.cs | 55 +++ .../Converters/Packets/InPacketConverter.cs | 28 ++ .../Converters/Special/EnumTypeConverter.cs | 45 ++ .../Special/ISpecialTypeConverter.cs | 41 ++ .../Converters/Special/ListTypeConverter.cs | 138 ++++++ .../Converters/TypeConverterRepository.cs | 217 ++++++++++ .../Errors/AmbiguousHeaderError.cs | 23 + .../Errors/CouldNotConvertError.cs | 21 + .../Errors/DeserializedValueNullError.cs | 15 + .../Errors/ListSerializerError.cs | 16 + .../Errors/PacketConverterNotFoundError.cs | 16 + .../Errors/PacketEndReachedError.cs | 17 + .../Errors/TypeConverterNotFoundError.cs | 16 + .../Errors/WrongTypeError.cs | 20 + .../Extensions/ServiceCollectionExtensions.cs | 16 + Core/NosSmooth.Packets/IsExternalInit.cs | 15 + .../NosCore.Packets.csproj.DotSettings | 2 + Core/NosSmooth.Packets/PacketSerializer.cs | 40 ++ Core/NosSmooth.Packets/PacketStringBuilder.cs | 165 ++++++++ .../PacketStringEnumerator.cs | 399 ++++++++++++++++++ Core/NosSmooth.Packets/PacketToken.cs | 19 + Core/NosSmooth.Packets/Packets/IPacket.cs | 14 + Core/NosSmooth.Packets/Packets/PacketInfo.cs | 18 + .../Packets/PacketTypesRepository.cs | 150 +++++++ .../Packets/UnresolvedPacket.cs | 14 + .../PacketStringBuilderTests.cs | 64 +++ .../PacketStringEnumeratorTests.cs | 196 +++++++++ 42 files changed, 2229 insertions(+) create mode 100644 Core/NosSmooth.Packets/Attributes/GenerateSerializerAttribute.cs create mode 100644 Core/NosSmooth.Packets/Attributes/PacketContextListAttribute.cs create mode 100644 Core/NosSmooth.Packets/Attributes/PacketHeaderAttribute.cs create mode 100644 Core/NosSmooth.Packets/Attributes/PacketIndexAttribute.cs create mode 100644 Core/NosSmooth.Packets/Attributes/PacketListIndexAttribute.cs create mode 100644 Core/NosSmooth.Packets/Attributes/PacketSource.cs create mode 100644 Core/NosSmooth.Packets/Converters/BaseTypeConverter.cs create mode 100644 Core/NosSmooth.Packets/Converters/Basic/BasicTypeConverter.cs create mode 100644 Core/NosSmooth.Packets/Converters/Basic/ByteTypeConverter.cs create mode 100644 Core/NosSmooth.Packets/Converters/Basic/IntTypeConverter.cs create mode 100644 Core/NosSmooth.Packets/Converters/Basic/LongTypeConverter.cs create mode 100644 Core/NosSmooth.Packets/Converters/Basic/ShortTypeConverter.cs create mode 100644 Core/NosSmooth.Packets/Converters/Basic/UIntTypeConverter.cs create mode 100644 Core/NosSmooth.Packets/Converters/Basic/ULongTypeConverter.cs create mode 100644 Core/NosSmooth.Packets/Converters/Basic/UShortTypeConverter.cs create mode 100644 Core/NosSmooth.Packets/Converters/ITypeConverter.cs create mode 100644 Core/NosSmooth.Packets/Converters/Packets/InPacketConverter.cs create mode 100644 Core/NosSmooth.Packets/Converters/Special/EnumTypeConverter.cs create mode 100644 Core/NosSmooth.Packets/Converters/Special/ISpecialTypeConverter.cs create mode 100644 Core/NosSmooth.Packets/Converters/Special/ListTypeConverter.cs create mode 100644 Core/NosSmooth.Packets/Converters/TypeConverterRepository.cs create mode 100644 Core/NosSmooth.Packets/Errors/AmbiguousHeaderError.cs create mode 100644 Core/NosSmooth.Packets/Errors/CouldNotConvertError.cs create mode 100644 Core/NosSmooth.Packets/Errors/DeserializedValueNullError.cs create mode 100644 Core/NosSmooth.Packets/Errors/ListSerializerError.cs create mode 100644 Core/NosSmooth.Packets/Errors/PacketConverterNotFoundError.cs create mode 100644 Core/NosSmooth.Packets/Errors/PacketEndReachedError.cs create mode 100644 Core/NosSmooth.Packets/Errors/TypeConverterNotFoundError.cs create mode 100644 Core/NosSmooth.Packets/Errors/WrongTypeError.cs create mode 100644 Core/NosSmooth.Packets/Extensions/ServiceCollectionExtensions.cs create mode 100644 Core/NosSmooth.Packets/IsExternalInit.cs create mode 100644 Core/NosSmooth.Packets/NosCore.Packets.csproj.DotSettings create mode 100644 Core/NosSmooth.Packets/PacketSerializer.cs create mode 100644 Core/NosSmooth.Packets/PacketStringBuilder.cs create mode 100644 Core/NosSmooth.Packets/PacketStringEnumerator.cs create mode 100644 Core/NosSmooth.Packets/PacketToken.cs create mode 100644 Core/NosSmooth.Packets/Packets/IPacket.cs create mode 100644 Core/NosSmooth.Packets/Packets/PacketInfo.cs create mode 100644 Core/NosSmooth.Packets/Packets/PacketTypesRepository.cs create mode 100644 Core/NosSmooth.Packets/Packets/UnresolvedPacket.cs create mode 100644 Tests/NosSmooth.Packets.Tests/PacketStringBuilderTests.cs create mode 100644 Tests/NosSmooth.Packets.Tests/PacketStringEnumeratorTests.cs diff --git a/Core/NosSmooth.Packets/Attributes/GenerateSerializerAttribute.cs b/Core/NosSmooth.Packets/Attributes/GenerateSerializerAttribute.cs new file mode 100644 index 0000000..f94ad0e --- /dev/null +++ b/Core/NosSmooth.Packets/Attributes/GenerateSerializerAttribute.cs @@ -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; + +/// +/// Attribute for marking packets that should have their generator generated using Roslyn code generator. +/// +[AttributeUsage(AttributeTargets.Class)] +public class GenerateSerializerAttribute : Attribute +{ +} \ No newline at end of file diff --git a/Core/NosSmooth.Packets/Attributes/PacketContextListAttribute.cs b/Core/NosSmooth.Packets/Attributes/PacketContextListAttribute.cs new file mode 100644 index 0000000..7f54b2a --- /dev/null +++ b/Core/NosSmooth.Packets/Attributes/PacketContextListAttribute.cs @@ -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; + +/// +/// Attribute for marking properties as a contextual list. +/// +/// +/// Contextual list gets its length from another property that was already set. +/// +[AttributeUsage(AttributeTargets.Property | AttributeTargets.Parameter)] +public class PacketContextListAttribute : PacketListIndexAttribute +{ + /// + /// Initializes a new instance of the class. + /// + /// The position in the packet. + /// The name of the property the length is stored in. + public PacketContextListAttribute(ushort index, string lengthStoredIn) + : base(index) + { + LengthStoredIn = lengthStoredIn; + } + + /// + /// Gets or sets the attribute name that stores the length of this list. + /// + public string LengthStoredIn { get; set; } +} \ No newline at end of file diff --git a/Core/NosSmooth.Packets/Attributes/PacketHeaderAttribute.cs b/Core/NosSmooth.Packets/Attributes/PacketHeaderAttribute.cs new file mode 100644 index 0000000..095be47 --- /dev/null +++ b/Core/NosSmooth.Packets/Attributes/PacketHeaderAttribute.cs @@ -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; + +/// +/// Attribute for specifying the header identifier of the packet. +/// +[AttributeUsage(AttributeTargets.Class)] +public class PacketHeaderAttribute : Attribute +{ + /// + /// Initializes a new instance of the class. + /// + /// The packet identifier (the first entry). + /// The source of the packet. + public PacketHeaderAttribute(string? identifier, PacketSource source) + { + Identifier = identifier; + Source = source; + } + + /// + /// Gets the identifier of the packet (the first entry in the packet). + /// + public string? Identifier { get; } + + /// + /// Gets the source of the packet used for determining where the packet is from. + /// + public PacketSource Source { get; } +} \ No newline at end of file diff --git a/Core/NosSmooth.Packets/Attributes/PacketIndexAttribute.cs b/Core/NosSmooth.Packets/Attributes/PacketIndexAttribute.cs new file mode 100644 index 0000000..562ffc3 --- /dev/null +++ b/Core/NosSmooth.Packets/Attributes/PacketIndexAttribute.cs @@ -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; + +/// +/// Attribute for marking properties in packets with their position in the packet. +/// +[AttributeUsage(AttributeTargets.Property | AttributeTargets.Parameter)] +public class PacketIndexAttribute : Attribute +{ + /// + /// Initializes a new instance of the class. + /// + /// The position of the property. + public PacketIndexAttribute(ushort index) + { + Index = index; + } + + /// + /// Gets the index of the current property. + /// + public ushort Index { get; } + + /// + /// Gets the inner separator used for complex types such as sub packets. + /// + public char? InnerSeparator { get; set; } + + /// + /// Gets the separator after this field. + /// + public char? AfterSeparator { get; set; } +} \ No newline at end of file diff --git a/Core/NosSmooth.Packets/Attributes/PacketListIndexAttribute.cs b/Core/NosSmooth.Packets/Attributes/PacketListIndexAttribute.cs new file mode 100644 index 0000000..593dc40 --- /dev/null +++ b/Core/NosSmooth.Packets/Attributes/PacketListIndexAttribute.cs @@ -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; + +/// +/// Attribute for marking property as a packet list. +/// +public class PacketListIndexAttribute : PacketIndexAttribute +{ + /// + /// Initializes a new instance of the class. + /// + /// The position in the packet. + public PacketListIndexAttribute(ushort index) + : base(index) + { + } + + /// + /// Gets or sets the separator of the items in the array. + /// + public string ListSeparator { get; set; } = "|"; +} \ No newline at end of file diff --git a/Core/NosSmooth.Packets/Attributes/PacketSource.cs b/Core/NosSmooth.Packets/Attributes/PacketSource.cs new file mode 100644 index 0000000..3a924ae --- /dev/null +++ b/Core/NosSmooth.Packets/Attributes/PacketSource.cs @@ -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; + +/// +/// Specifies the source of the packet. +/// +public enum PacketSource +{ + /// + /// The packet is from the server. + /// + Server, + + /// + /// The packet is from the client. + /// + Client +} \ No newline at end of file diff --git a/Core/NosSmooth.Packets/Converters/BaseTypeConverter.cs b/Core/NosSmooth.Packets/Converters/BaseTypeConverter.cs new file mode 100644 index 0000000..c578bfb --- /dev/null +++ b/Core/NosSmooth.Packets/Converters/BaseTypeConverter.cs @@ -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; + +/// +/// Base type for converting objects that maps object converting methods to the actual type. +/// +/// The type of the object that this converts. +public abstract class BaseTypeConverter : ITypeConverter +{ + /// + public abstract Result Serialize(TParseType obj, PacketStringBuilder builder); + + /// + public abstract Result Deserialize(PacketStringEnumerator stringEnumerator); + + /// + Result ITypeConverter.Deserialize(PacketStringEnumerator stringEnumerator) + => Deserialize(stringEnumerator); + + /// + 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 diff --git a/Core/NosSmooth.Packets/Converters/Basic/BasicTypeConverter.cs b/Core/NosSmooth.Packets/Converters/Basic/BasicTypeConverter.cs new file mode 100644 index 0000000..c941f45 --- /dev/null +++ b/Core/NosSmooth.Packets/Converters/Basic/BasicTypeConverter.cs @@ -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; + +/// +/// Basic type converter for converting using . +/// +/// The basic type, that contains correct to string. +public abstract class BasicTypeConverter : BaseTypeConverter +{ + /// + public override Result Serialize(TBasicType obj, PacketStringBuilder builder) + { + builder.Append(obj?.ToString() ?? "-"); + return Result.FromSuccess(); + } + + /// + public override Result Deserialize(PacketStringEnumerator stringEnumerator) + { + var nextTokenResult = stringEnumerator.GetNextToken(); + if (!nextTokenResult.IsSuccess) + { + return Result.FromError(nextTokenResult); + } + + return Deserialize(nextTokenResult.Entity.Token); + } + + /// + /// Deserialize the given string value. + /// + /// The value to deserialize. + /// The deserialized value or an error. + protected abstract Result Deserialize(string value); +} \ No newline at end of file diff --git a/Core/NosSmooth.Packets/Converters/Basic/ByteTypeConverter.cs b/Core/NosSmooth.Packets/Converters/Basic/ByteTypeConverter.cs new file mode 100644 index 0000000..baab668 --- /dev/null +++ b/Core/NosSmooth.Packets/Converters/Basic/ByteTypeConverter.cs @@ -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; + +/// +/// Converter of . +/// +public class ByteTypeConverter : BasicTypeConverter +{ + /// + protected override Result 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 diff --git a/Core/NosSmooth.Packets/Converters/Basic/IntTypeConverter.cs b/Core/NosSmooth.Packets/Converters/Basic/IntTypeConverter.cs new file mode 100644 index 0000000..fc73fba --- /dev/null +++ b/Core/NosSmooth.Packets/Converters/Basic/IntTypeConverter.cs @@ -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; + +/// +/// Converter of . +/// +public class IntTypeConverter : BasicTypeConverter +{ + /// + protected override Result 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 diff --git a/Core/NosSmooth.Packets/Converters/Basic/LongTypeConverter.cs b/Core/NosSmooth.Packets/Converters/Basic/LongTypeConverter.cs new file mode 100644 index 0000000..c996dc3 --- /dev/null +++ b/Core/NosSmooth.Packets/Converters/Basic/LongTypeConverter.cs @@ -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; + +/// +/// Converter of . +/// +public class LongTypeConverter : BasicTypeConverter +{ + /// + protected override Result 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 diff --git a/Core/NosSmooth.Packets/Converters/Basic/ShortTypeConverter.cs b/Core/NosSmooth.Packets/Converters/Basic/ShortTypeConverter.cs new file mode 100644 index 0000000..f726f8e --- /dev/null +++ b/Core/NosSmooth.Packets/Converters/Basic/ShortTypeConverter.cs @@ -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; + +/// +/// Converter of . +/// +public class ShortTypeConverter : BasicTypeConverter +{ + /// + protected override Result 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 diff --git a/Core/NosSmooth.Packets/Converters/Basic/UIntTypeConverter.cs b/Core/NosSmooth.Packets/Converters/Basic/UIntTypeConverter.cs new file mode 100644 index 0000000..37c4f28 --- /dev/null +++ b/Core/NosSmooth.Packets/Converters/Basic/UIntTypeConverter.cs @@ -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; + +/// +/// Converter of . +/// +public class UIntTypeConverter : BasicTypeConverter +{ + /// + protected override Result 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 diff --git a/Core/NosSmooth.Packets/Converters/Basic/ULongTypeConverter.cs b/Core/NosSmooth.Packets/Converters/Basic/ULongTypeConverter.cs new file mode 100644 index 0000000..c0f7e15 --- /dev/null +++ b/Core/NosSmooth.Packets/Converters/Basic/ULongTypeConverter.cs @@ -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; + +/// +/// Converter of . +/// +public class ULongTypeConverter : BasicTypeConverter +{ + /// + protected override Result 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 diff --git a/Core/NosSmooth.Packets/Converters/Basic/UShortTypeConverter.cs b/Core/NosSmooth.Packets/Converters/Basic/UShortTypeConverter.cs new file mode 100644 index 0000000..818770a --- /dev/null +++ b/Core/NosSmooth.Packets/Converters/Basic/UShortTypeConverter.cs @@ -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; + +/// +/// Converter of . +/// +public class UShortTypeConverter : BasicTypeConverter +{ + /// + protected override Result 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 diff --git a/Core/NosSmooth.Packets/Converters/ITypeConverter.cs b/Core/NosSmooth.Packets/Converters/ITypeConverter.cs new file mode 100644 index 0000000..691089c --- /dev/null +++ b/Core/NosSmooth.Packets/Converters/ITypeConverter.cs @@ -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; + +/// +/// Base type for converting types. +/// +public interface ITypeConverter +{ + /// + /// Convert the data from the enumerator to the given type. + /// + /// The packet string enumerator with the current position. + /// The parsed object or an error. + public Result Deserialize(PacketStringEnumerator stringEnumerator); + + /// + /// Serializes the given object to string by appending to the packet string builder. + /// + /// The object to serialize. + /// The string builder to append to. + /// A result that may or may not have succeeded. + public Result Serialize(object obj, PacketStringBuilder builder); +} + +/// +/// Converts string to an object. +/// +/// +/// Used for converting packets. +/// +/// The type that can be parsed. +public interface ITypeConverter : ITypeConverter +{ + /// + /// Convert the data from the enumerator to the given type. + /// + /// The packet string enumerator with the current position. + /// The parsed object or an error. + public new Result Deserialize(PacketStringEnumerator stringEnumerator); + + /// + /// Serializes the given object to string by appending to the packet string builder. + /// + /// The object to serialize. + /// The string builder to append to. + /// A result that may or may not have succeeded. + public Result Serialize(TParseType obj, PacketStringBuilder builder); +} \ No newline at end of file diff --git a/Core/NosSmooth.Packets/Converters/Packets/InPacketConverter.cs b/Core/NosSmooth.Packets/Converters/Packets/InPacketConverter.cs new file mode 100644 index 0000000..5126684 --- /dev/null +++ b/Core/NosSmooth.Packets/Converters/Packets/InPacketConverter.cs @@ -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; + +/// +/// Converter for "in" packet. . +/// +public class InPacketConverter : BaseTypeConverter +{ + /// + public override Result Serialize(InPacket obj, PacketStringBuilder builder) + { + throw new System.NotImplementedException(); + } + + /// + public override Result Deserialize(PacketStringEnumerator stringEnumerator) + { + throw new System.NotImplementedException(); + } +} \ No newline at end of file diff --git a/Core/NosSmooth.Packets/Converters/Special/EnumTypeConverter.cs b/Core/NosSmooth.Packets/Converters/Special/EnumTypeConverter.cs new file mode 100644 index 0000000..ff012a2 --- /dev/null +++ b/Core/NosSmooth.Packets/Converters/Special/EnumTypeConverter.cs @@ -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; + +/// +/// Converts all enums. +/// +public class EnumTypeConverter : ISpecialTypeConverter +{ + /// + public bool ShouldHandle(Type type) + => type.IsEnum; + + /// + public Result Deserialize(Type type, PacketStringEnumerator stringEnumerator) + { + var tokenResult = stringEnumerator.GetNextToken(); + if (!tokenResult.IsSuccess) + { + return Result.FromError(tokenResult); + } + + return Enum.Parse(type, tokenResult.Entity.Token); + } + + /// + 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 diff --git a/Core/NosSmooth.Packets/Converters/Special/ISpecialTypeConverter.cs b/Core/NosSmooth.Packets/Converters/Special/ISpecialTypeConverter.cs new file mode 100644 index 0000000..6dc4f8a --- /dev/null +++ b/Core/NosSmooth.Packets/Converters/Special/ISpecialTypeConverter.cs @@ -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; + +/// +/// Converts special types such as enums or lists. +/// +public interface ISpecialTypeConverter +{ + /// + /// Whether this type converter should handle the given type. + /// + /// The type to handle. + /// Whether the type should be handled. + public bool ShouldHandle(Type type); + + /// + /// Deserialize the given string to the object. + /// + /// The type to deserialize. + /// The packets string enumerator. + /// A result that may or may not have succeeded. + public Result Deserialize(Type type, PacketStringEnumerator stringEnumerator); + + /// + /// Deserialize the given object into string. + /// + /// The type to serialize. + /// The object to serialize. + /// The packet string builder. + /// A result that may or may not have succeeded. + public Result Serialize(Type type, object? obj, PacketStringBuilder builder); +} \ No newline at end of file diff --git a/Core/NosSmooth.Packets/Converters/Special/ListTypeConverter.cs b/Core/NosSmooth.Packets/Converters/Special/ListTypeConverter.cs new file mode 100644 index 0000000..7473aee --- /dev/null +++ b/Core/NosSmooth.Packets/Converters/Special/ListTypeConverter.cs @@ -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; + +/// +/// Converts lists. +/// +public class ListTypeConverter : ISpecialTypeConverter +{ + private readonly TypeConverterRepository _typeConverterRepository; + + /// + /// Initializes a new instance of the class. + /// + /// The type converter repository. + public ListTypeConverter(TypeConverterRepository typeConverterRepository) + { + _typeConverterRepository = typeConverterRepository; + } + + /// + public bool ShouldHandle(Type type) + => typeof(IEnumerable).IsAssignableFrom(type); + + /// + public Result Deserialize(Type type, PacketStringEnumerator stringEnumerator) + { + var data = new List(); + 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.FromError(new ListSerializerError(result, data.Count)); + } + + data.Add(result.Entity); + } + while (!(stringEnumerator.IsOnLastToken() ?? false)); + + return GetAndFillListMethod(genericType)(data); + } + + /// + 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? + + /// + /// From https://stackoverflow.com/questions/35913495/moving-from-reflection-to-expression-tree. + /// + /// The generic type. + /// The function. + private Func, object> GetAndFillListMethod(Type genericType) + { + var listType = typeof(List<>); + var listGenericType = listType.MakeGenericType(genericType); + + var values = Expression.Parameter(typeof(IEnumerable), "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 + 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, object>)Expression.Lambda(block, values).Compile(); + } +} \ No newline at end of file diff --git a/Core/NosSmooth.Packets/Converters/TypeConverterRepository.cs b/Core/NosSmooth.Packets/Converters/TypeConverterRepository.cs new file mode 100644 index 0000000..3357f76 --- /dev/null +++ b/Core/NosSmooth.Packets/Converters/TypeConverterRepository.cs @@ -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; + +/// +/// Repository for . +/// +public class TypeConverterRepository +{ + private readonly IServiceProvider _serviceProvider; + + /// + /// Initializes a new instance of the class. + /// + /// The dependency injection service provider. + public TypeConverterRepository(IServiceProvider serviceProvider) + { + _serviceProvider = serviceProvider; + } + + /// + /// Gets the type converter for the given type. + /// + /// The type to find converter for. + /// The type converter or an error. + public Result 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.FromSuccess(typeConverter); + } + + /// + /// Gets the type converter for the given type. + /// + /// The type to find converter for. + /// The type converter or an error. + public Result> GetTypeConverter() + { + var typeConverter = _serviceProvider.GetService>(); + + if (typeConverter is null) + { + return new TypeConverterNotFoundError(typeof(TParseType)); + } + + return Result>.FromSuccess(typeConverter); + } + + /// + /// Convert the data from the enumerator to the given type. + /// + /// The type of the object to serialize. + /// The packet string enumerator with the current position. + /// The parsed object or an error. + public Result 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.FromError(deserializeResult); + } + + if (deserializeResult.Entity is null) + { + if (parseType.DeclaringType == typeof(Nullable<>)) + { + return default; + } + + return Result.FromError(new DeserializedValueNullError(parseType)); + } + + return deserializeResult.Entity; + } + + var converterResult = GetTypeConverter(parseType); + if (!converterResult.IsSuccess) + { + return Result.FromError(converterResult); + } + + var deserializedResult = converterResult.Entity.Deserialize(stringEnumerator); + if (!deserializedResult.IsSuccess) + { + return Result.FromError(deserializedResult); + } + + return Result.FromSuccess(deserializedResult); + } + + /// + /// Serializes the given object to string by appending to the packet string builder. + /// + /// The type of the object to serialize. + /// The object to serialize. + /// The string builder to append to. + /// A result that may or may not have succeeded. + 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); + } + + /// + /// Convert the data from the enumerator to the given type. + /// + /// The packet string enumerator with the current position. + /// The type of the object to serialize. + /// The parsed object or an error. + public Result Deserialize(PacketStringEnumerator stringEnumerator) + { + var specialConverter = GetSpecialConverter(typeof(TParseType)); + if (specialConverter is not null) + { + var deserializeResult = specialConverter.Deserialize(typeof(TParseType), stringEnumerator); + if (!deserializeResult.IsSuccess) + { + return Result.FromError(deserializeResult); + } + + if (deserializeResult.Entity is null) + { + if (typeof(TParseType).DeclaringType == typeof(Nullable<>)) + { + return default; + } + + return Result.FromError(new DeserializedValueNullError(typeof(TParseType))); + } + + return (TParseType)deserializeResult.Entity; + } + + var converterResult = GetTypeConverter(); + if (!converterResult.IsSuccess) + { + return Result.FromError(converterResult); + } + + return converterResult.Entity.Deserialize(stringEnumerator); + } + + /// + /// Serializes the given object to string by appending to the packet string builder. + /// + /// The object to serialize. + /// The string builder to append to. + /// The type of the object to deserialize. + /// A result that may or may not have succeeded. + public Result Serialize(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(); + if (!converterResult.IsSuccess) + { + return Result.FromError(converterResult); + } + + return converterResult.Entity.Serialize(obj, builder); + } + + private ISpecialTypeConverter? GetSpecialConverter(Type type) + { + var specialConverters = _serviceProvider.GetServices(); + foreach (var specialConverter in specialConverters) + { + if (specialConverter.ShouldHandle(type)) + { + return specialConverter; + } + } + + return null; + } +} \ No newline at end of file diff --git a/Core/NosSmooth.Packets/Errors/AmbiguousHeaderError.cs b/Core/NosSmooth.Packets/Errors/AmbiguousHeaderError.cs new file mode 100644 index 0000000..5ad82c4 --- /dev/null +++ b/Core/NosSmooth.Packets/Errors/AmbiguousHeaderError.cs @@ -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; + +/// +/// The header was ambiguous, there were at least two packets with the same header and source. +/// +/// The packet's header. +/// The packet's source. +/// The types that were ambiguous. +public record AmbiguousHeaderError(string Header, PacketSource? Source, IReadOnlyList 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 diff --git a/Core/NosSmooth.Packets/Errors/CouldNotConvertError.cs b/Core/NosSmooth.Packets/Errors/CouldNotConvertError.cs new file mode 100644 index 0000000..99a1ab1 --- /dev/null +++ b/Core/NosSmooth.Packets/Errors/CouldNotConvertError.cs @@ -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; + +/// +/// The value could not be converted. +/// +/// The converter that failed the parsing. +/// The value that failed to parse. +/// The reason for the error. +/// The underlying exception, if any. +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 diff --git a/Core/NosSmooth.Packets/Errors/DeserializedValueNullError.cs b/Core/NosSmooth.Packets/Errors/DeserializedValueNullError.cs new file mode 100644 index 0000000..93cfe08 --- /dev/null +++ b/Core/NosSmooth.Packets/Errors/DeserializedValueNullError.cs @@ -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; + +/// +/// Deserialized value is null, but it cannot be. +/// +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 diff --git a/Core/NosSmooth.Packets/Errors/ListSerializerError.cs b/Core/NosSmooth.Packets/Errors/ListSerializerError.cs new file mode 100644 index 0000000..8f25ded --- /dev/null +++ b/Core/NosSmooth.Packets/Errors/ListSerializerError.cs @@ -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; + +/// +/// Could not parse an item in the list. +/// +/// The errorful result. +/// The index in the list. +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 diff --git a/Core/NosSmooth.Packets/Errors/PacketConverterNotFoundError.cs b/Core/NosSmooth.Packets/Errors/PacketConverterNotFoundError.cs new file mode 100644 index 0000000..3be5bd5 --- /dev/null +++ b/Core/NosSmooth.Packets/Errors/PacketConverterNotFoundError.cs @@ -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; + +/// +/// The converter for the given packet was not found. +/// +/// The header of the packet. +public record PacketConverterNotFoundError(string Header) + : ResultError($"Could not find converter for packet with header {Header}."); \ No newline at end of file diff --git a/Core/NosSmooth.Packets/Errors/PacketEndReachedError.cs b/Core/NosSmooth.Packets/Errors/PacketEndReachedError.cs new file mode 100644 index 0000000..17c5c4b --- /dev/null +++ b/Core/NosSmooth.Packets/Errors/PacketEndReachedError.cs @@ -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; + +/// +/// The end of a packet was reached already. +/// +/// The packet string. +/// Whether this indicates end of a level instead of the whole packet. +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 diff --git a/Core/NosSmooth.Packets/Errors/TypeConverterNotFoundError.cs b/Core/NosSmooth.Packets/Errors/TypeConverterNotFoundError.cs new file mode 100644 index 0000000..e7c99d0 --- /dev/null +++ b/Core/NosSmooth.Packets/Errors/TypeConverterNotFoundError.cs @@ -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; + +/// +/// Could not find type converter for the given type. +/// +/// The type of the object. +public record TypeConverterNotFoundError(Type Type) : ResultError($"Could not find converter for {Type.FullName}."); \ No newline at end of file diff --git a/Core/NosSmooth.Packets/Errors/WrongTypeError.cs b/Core/NosSmooth.Packets/Errors/WrongTypeError.cs new file mode 100644 index 0000000..8e9ac86 --- /dev/null +++ b/Core/NosSmooth.Packets/Errors/WrongTypeError.cs @@ -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; + +/// +/// The wrong type was passed to a type converter. +/// +/// The converter that failed to convert the object. +/// The expected type of the converting object. +/// The actual object the converter got. +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 diff --git a/Core/NosSmooth.Packets/Extensions/ServiceCollectionExtensions.cs b/Core/NosSmooth.Packets/Extensions/ServiceCollectionExtensions.cs new file mode 100644 index 0000000..813e9b6 --- /dev/null +++ b/Core/NosSmooth.Packets/Extensions/ServiceCollectionExtensions.cs @@ -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; + +/// +/// Extensions for . +/// +public static class ServiceCollectionExtensions +{ +} \ No newline at end of file diff --git a/Core/NosSmooth.Packets/IsExternalInit.cs b/Core/NosSmooth.Packets/IsExternalInit.cs new file mode 100644 index 0000000..5c2fc46 --- /dev/null +++ b/Core/NosSmooth.Packets/IsExternalInit.cs @@ -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 +{ + /// + /// Dummy. + /// + public class IsExternalInit + { + } +} \ No newline at end of file diff --git a/Core/NosSmooth.Packets/NosCore.Packets.csproj.DotSettings b/Core/NosSmooth.Packets/NosCore.Packets.csproj.DotSettings new file mode 100644 index 0000000..4887f94 --- /dev/null +++ b/Core/NosSmooth.Packets/NosCore.Packets.csproj.DotSettings @@ -0,0 +1,2 @@ + + CSharp100 \ No newline at end of file diff --git a/Core/NosSmooth.Packets/PacketSerializer.cs b/Core/NosSmooth.Packets/PacketSerializer.cs new file mode 100644 index 0000000..b87913d --- /dev/null +++ b/Core/NosSmooth.Packets/PacketSerializer.cs @@ -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; + +/// +/// Serializer of packets. +/// +public class PacketSerializer +{ + /// + /// Serializes the given object to string by appending to the packet string builder. + /// + /// The packet to serialize. + /// A result that may or may not have succeeded. + public Result Serialize(IPacket obj) + { + return Result.FromSuccess("as"); + } + + /// + /// Convert the data from the enumerator to the given type. + /// + /// The packet string to deserialize. + /// The preferred source to check first. If packet with the given header is not found there, other sources will be checked as well. + /// The parsed object or an error. + public Result Deserialize(string packetString, PacketSource preferredSource) + { + return Result.FromError(new ArgumentInvalidError("asdf", "asdf")); + } +} \ No newline at end of file diff --git a/Core/NosSmooth.Packets/PacketStringBuilder.cs b/Core/NosSmooth.Packets/PacketStringBuilder.cs new file mode 100644 index 0000000..20c42ff --- /dev/null +++ b/Core/NosSmooth.Packets/PacketStringBuilder.cs @@ -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; + +/// +/// String builder for packets. +/// +public class PacketStringBuilder +{ + private readonly StringBuilder _builder; + private StringBuilderLevel _currentLevel; + private char? _preparedLevelSeparator; + private char? _insertSeparator; + + /// + /// Initializes a new instance of the class. + /// + /// The top level separator. + public PacketStringBuilder(char separator = ' ') + { + _currentLevel = new StringBuilderLevel(null, separator); + _preparedLevelSeparator = _insertSeparator = null; + _builder = new StringBuilder(); + } + + /// + /// Sets the separator to search for only once. + /// + /// + /// This separator will have the most priority. + /// + /// The separator to look for. + public void SetAfterSeparatorOnce(char separator) + { + _currentLevel.SeparatorOnce = separator; + } + + /// + /// Prepare the given level that can be set when needed. + /// + /// The separator of the prepared level. + public void PrepareLevel(char separator) + { + _preparedLevelSeparator = separator; + } + + /// + /// Reset the prepared level, if there is any. + /// + public void RemovePreparedLevel() + { + _preparedLevelSeparator = null; + } + + /// + /// Create next level with the separator given in the prepared level. + /// + /// + /// Level of the current builder will stay the same. + /// Will return null, if there is not a level prepared. + /// + /// An enumerator with the new level pushed. + public bool PushPreparedLevel() + { + if (_preparedLevelSeparator is null) + { + return false; + } + + _currentLevel = new StringBuilderLevel(_currentLevel, _preparedLevelSeparator.Value); + return true; + } + + /// + /// Push new separator level to the stack. + /// + /// + /// This will change the current enumerator. + /// It has to be after parent level should be used. + /// + /// The separator of the new level. + public void PushLevel(char separator) + { + _preparedLevelSeparator = null; + _currentLevel = new StringBuilderLevel(_currentLevel, separator); + } + + /// + /// Pop the current level. + /// + /// A result that may or may not have succeeded. There will be an error if the current level is the top one. + 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(); + } + + /// + /// Appends the value to the string. + /// + /// The value to append. + 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; + } + + /// + /// Replace the last separator with the parent separator. + /// + public void ReplaceWithParentSeparator() + { + var parent = _currentLevel.Parent; + if (_insertSeparator is null || parent is null) + { + return; + } + + _insertSeparator = parent.SeparatorOnce ?? parent.Separator; + parent.SeparatorOnce = null; + } + + /// + 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 diff --git a/Core/NosSmooth.Packets/PacketStringEnumerator.cs b/Core/NosSmooth.Packets/PacketStringEnumerator.cs new file mode 100644 index 0000000..18ab4d7 --- /dev/null +++ b/Core/NosSmooth.Packets/PacketStringEnumerator.cs @@ -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; + +/// +/// Enumerator for packet strings. +/// +public struct PacketStringEnumerator +{ + private readonly EnumeratorData _data; + private readonly Dictionary _numberOfSeparators; + private EnumeratorLevel _currentLevel; + private (char Separator, uint? MaxTokens)? _preparedLevel; + private PacketToken? _currentToken; + + /// + /// Initializes a new instance of the struct. + /// + /// The packet string data. + /// The separator to use on the highest level. + public PacketStringEnumerator(string data, char separator = ' ') + { + _currentLevel = new EnumeratorLevel(null, separator); + _data = new EnumeratorData(data); + _numberOfSeparators = new Dictionary(); + _numberOfSeparators.Add(separator, 1); + _currentToken = null; + _preparedLevel = null; + } + + /// + /// Initializes a new instance of the struct. + /// + /// The data of the enumerator. + /// The current enumerator level. + /// The number of separators. + private PacketStringEnumerator(EnumeratorData data, EnumeratorLevel level, Dictionary numberOfSeparators) + { + _currentLevel = level; + _data = data; + + // TODO: use something less heavy than copying everything from the dictionary. + _numberOfSeparators = new Dictionary(numberOfSeparators); + _currentToken = null; + _preparedLevel = null; + } + + /// + /// Sets the separator to search for only once. + /// + /// + /// This separator will have the most priority. + /// + /// The separator to look for. + public void SetAfterSeparatorOnce(char separator) + { + _currentToken = null; + _currentLevel.SeparatorOnce = separator; + } + + /// + /// Prepare the given level that can be set when needed. + /// + /// The separator of the prepared level. + /// The maximum number of tokens for the level. + public void PrepareLevel(char separator, uint? maxTokens = null) + { + _preparedLevel = (separator, maxTokens); + } + + /// + /// Reset the prepared level, if there is any. + /// + public void RemovePreparedLevel() + { + _preparedLevel = null; + } + + /// + /// Create next level with the separator given in the prepared level. + /// + /// + /// Level of the current enumerator will stay the same. + /// Will return null, if there is not a level prepared. + /// + /// An enumerator with the new level pushed. + public PacketStringEnumerator? CreatePreparedLevel() + { + return _preparedLevel is not null ? CreateLevel(_preparedLevel.Value.Separator, _preparedLevel.Value.MaxTokens) : null; + } + + /// + /// Push next level with the separator given in the prepared level. + /// + /// Whether there is a prepared level present. + 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; + } + + /// + /// Create next level with the given separator and maximum number of tokens. + /// + /// + /// 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. + /// + /// The separator of the new level. + /// The maximum number of tokens to read. + /// An enumerator with the new level pushed. + public PacketStringEnumerator CreateLevel(char separator, uint? maxTokens = default) + { + _currentToken = null; + var stringEnumerator = new PacketStringEnumerator(_data, _currentLevel, _numberOfSeparators); + stringEnumerator.PushLevel(separator, maxTokens); + return stringEnumerator; + } + + /// + /// Push new separator level to the stack. + /// + /// + /// This will change the current enumerator. + /// It has to be after parent level should be used. + /// + /// The separator of the new level. + /// The maximum number of tokens to read. + 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]++; + } + + /// + /// Pop the current level. + /// + /// A result that may or may not have succeeded. There will be an error if the current level is the top one. + 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(); + } + + /// + /// Get the next token. + /// + /// Whether to seek the cursor to the end of the token. + /// The found token. + public Result 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; + } + + /// + /// Update fields that are used in the process. + /// + /// The token. + 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; + } + + /// + /// Whether the last token of the current level was read. + /// + /// Whether the last token was read. Null if cannot determine (ie. there are multiple levels with the same separator.) + public bool? IsOnLastToken() + => _data.ReachedEnd || _currentLevel.ReachedEnd; + + /// + /// Checks if the given character is a separator. + /// + /// The character to check. + /// Whether the separator indicates last separator in this level. True if numberOfSeparators is exactly one and this is the parent's separator. + /// Whether higher level than the parent was encountered. That could indicate some kind of an error if this is not the last token. + /// Whether the character is a separator. + 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 diff --git a/Core/NosSmooth.Packets/PacketToken.cs b/Core/NosSmooth.Packets/PacketToken.cs new file mode 100644 index 0000000..412c341 --- /dev/null +++ b/Core/NosSmooth.Packets/PacketToken.cs @@ -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; + +/// +/// The single token from a packet. +/// +/// The token. +/// Whether the token is last in the current level. Null if it cannot be determined. +/// 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. +/// Whether the packet's end was reached. +[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 diff --git a/Core/NosSmooth.Packets/Packets/IPacket.cs b/Core/NosSmooth.Packets/Packets/IPacket.cs new file mode 100644 index 0000000..64b3537 --- /dev/null +++ b/Core/NosSmooth.Packets/Packets/IPacket.cs @@ -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; + +/// +/// Base packet interface that must be implemented in every packet. +/// +public interface IPacket +{ +} \ No newline at end of file diff --git a/Core/NosSmooth.Packets/Packets/PacketInfo.cs b/Core/NosSmooth.Packets/Packets/PacketInfo.cs new file mode 100644 index 0000000..b250027 --- /dev/null +++ b/Core/NosSmooth.Packets/Packets/PacketInfo.cs @@ -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; + +/// +/// Information about a packet type. +/// +/// The packet's header, if any. +/// The packet's type. +/// The packet's converter. +public record PacketInfo(string? Header, Type PacketType, ITypeConverter PacketConverter); \ No newline at end of file diff --git a/Core/NosSmooth.Packets/Packets/PacketTypesRepository.cs b/Core/NosSmooth.Packets/Packets/PacketTypesRepository.cs new file mode 100644 index 0000000..0a42f0d --- /dev/null +++ b/Core/NosSmooth.Packets/Packets/PacketTypesRepository.cs @@ -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; + +/// +/// Repository of packet types for finding information about packets. +/// +public class PacketTypesRepository +{ + private readonly TypeConverterRepository _typeConverterRepository; + private readonly Dictionary> _headerToPacket; + private readonly Dictionary _typeToPacket; + + /// + /// Initializes a new instance of the class. + /// + /// The type converter repository. + public PacketTypesRepository(TypeConverterRepository typeConverterRepository) + { + _typeConverterRepository = typeConverterRepository; + _headerToPacket = new Dictionary>(); + _typeToPacket = new Dictionary(); + } + + /// + /// Add the given packet type to the repository. + /// + /// The type of the packet. + /// A result that may or may not have succeeded. + 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(); + 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(); + } + + 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(); + } + + /// + /// Gets the type of a packet that corresponds to the given header. + /// + /// The header of the packet. + /// 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. + /// Info that stores the packet's info. Or an error, if not found. + public Result 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) + }; + } + + /// + /// Gets the packet info from the given packet type. + /// + /// The type of the packet. + /// Info that stores the packet's info. Or an error, if not found. + public Result FindPacketInfo() where TPacket : IPacket + => FindPacketInfo(typeof(TPacket)); + + /// + /// Gets the packet info from the given packet type. + /// + /// The type of the packet. + /// Info that stores the packet's info. Or an error, if not found. + public Result 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); + + /// + /// Gets the packet info from the given packet type name. + /// + /// The full name of the type of the packet. + /// Info that stores the packet's info. Or an error, if not found. + public Result FindPacketInfoByTypeName(string packetTypeFullName) + { + if (!_typeToPacket.ContainsKey(packetTypeFullName)) + { + return new PacketConverterNotFoundError(packetTypeFullName); + } + + return _typeToPacket[packetTypeFullName]; + } +} \ No newline at end of file diff --git a/Core/NosSmooth.Packets/Packets/UnresolvedPacket.cs b/Core/NosSmooth.Packets/Packets/UnresolvedPacket.cs new file mode 100644 index 0000000..fc2b6cc --- /dev/null +++ b/Core/NosSmooth.Packets/Packets/UnresolvedPacket.cs @@ -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; + +/// +/// Unresolved packet that is not supported. +/// +/// The header of the packet. +/// The body of the packet. +public record UnresolvedPacket(string Header, string Body) : IPacket; \ No newline at end of file diff --git a/Tests/NosSmooth.Packets.Tests/PacketStringBuilderTests.cs b/Tests/NosSmooth.Packets.Tests/PacketStringBuilderTests.cs new file mode 100644 index 0000000..bb30058 --- /dev/null +++ b/Tests/NosSmooth.Packets.Tests/PacketStringBuilderTests.cs @@ -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; + +/// +/// Tests for . +/// +public class PacketStringBuilderTests +{ + /// + /// Tests that the builder correctly builds array of complex types. + /// + [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()); + } + + /// + /// Tests that the builder correctly uses once separator. + /// + [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 diff --git a/Tests/NosSmooth.Packets.Tests/PacketStringEnumeratorTests.cs b/Tests/NosSmooth.Packets.Tests/PacketStringEnumeratorTests.cs new file mode 100644 index 0000000..a196b27 --- /dev/null +++ b/Tests/NosSmooth.Packets.Tests/PacketStringEnumeratorTests.cs @@ -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; + +/// +/// Test for . +/// +public class PacketStringEnumeratorTests +{ + /// + /// Test that array of complex types can be parsed. + /// + [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()); + } + + /// + /// Test that over reaching the end is not allowed. + /// + [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(tokenResult.Error); + } + + /// + /// Test that over reaching the end of a list is not allowed. + /// + [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(tokenResult.Error); + Assert.True(((PacketEndReachedError)tokenResult.Error!).LevelEnd); + } + + /// + /// Test that over reaching the length of a list is not allowed. + /// + [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(tokenResult.Error); + Assert.True(((PacketEndReachedError)tokenResult.Error!).LevelEnd); + } + + /// + /// Test that EncounteredUpperLevel is returned if appropriate. + /// + [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 -- 2.48.1