From 7bb2a1833d38fde47dbca48325de9006873b3f9a Mon Sep 17 00:00:00 2001 From: Mykhailo Matviiv Date: Wed, 10 Jan 2024 00:41:35 +0100 Subject: [PATCH] Implement UPPER_SNAKE_CASE enum converter for control plane API --- .../JsonConverters/DdbEnumJsonConverter.cs | 42 +++++++++++++++++++ .../Benchmarks/Query/QueryBenchmarkBase.cs | 2 +- .../Mocks/DescribeTableResponseFactory.cs | 2 +- .../DynamoDbLowLevelContext.cs | 4 +- .../Extensions/DateTimeConverterExtensions.cs | 2 +- .../StringNormalizationExtensions.cs | 16 +++++++ .../JsonConverters/DdbEnumJsonConverter.cs | 20 +++++---- .../Internal/TypeParsers/EnumParser.cs | 29 +++++++++++++ .../DescribeTable/Models/Enums/BillingMode.cs | 6 +-- .../DescribeTable/Models/Enums/IndexStatus.cs | 10 ++--- .../DescribeTable/Models/Enums/KeyType.cs | 6 +-- .../Models/Enums/ProjectionType.cs | 8 ++-- .../Models/Enums/ReplicaStatus.cs | 12 +++--- .../DescribeTable/Models/Enums/SseStatus.cs | 12 +++--- .../DescribeTable/Models/Enums/SseType.cs | 6 +-- .../Models/Enums/StreamViewType.cs | 10 ++--- .../DescribeTable/Models/Enums/TableStatus.cs | 16 +++---- 17 files changed, 148 insertions(+), 55 deletions(-) create mode 100644 EfficientDynamoDb.Tests/Internal/JsonConverters/DdbEnumJsonConverter.cs diff --git a/EfficientDynamoDb.Tests/Internal/JsonConverters/DdbEnumJsonConverter.cs b/EfficientDynamoDb.Tests/Internal/JsonConverters/DdbEnumJsonConverter.cs new file mode 100644 index 00000000..46307e59 --- /dev/null +++ b/EfficientDynamoDb.Tests/Internal/JsonConverters/DdbEnumJsonConverter.cs @@ -0,0 +1,42 @@ +using System; +using System.Text.Json; +using EfficientDynamoDb.Internal.JsonConverters; +using EfficientDynamoDb.Operations.DescribeTable.Models.Enums; +using NUnit.Framework; + +namespace EfficientDynamoDb.Tests.Internal.JsonConverters +{ + [TestFixture] + public class DdbEnumJsonConverter + { + private readonly JsonSerializerOptions _options = new JsonSerializerOptions + { Converters = { new DdbEnumJsonConverterFactory() } }; + + [TestCase(KeyType.Hash, "HASH")] + [TestCase(SseType.Kms, "KMS")] + [TestCase(SseType.Aes256, "AES256")] + [TestCase(StreamViewType.NewAndOldImages, "NEW_AND_OLD_IMAGES")] + [TestCase(StreamViewType.KeysOnly, "KEYS_ONLY")] + public void EnumDeserializationTest(T enumValue, string jsonValue) where T : struct, Enum + { + var json = $"\"{jsonValue}\""; + + var result = JsonSerializer.Deserialize(json, _options); + + Assert.That(result, Is.EqualTo(enumValue)); + } + + [TestCase(KeyType.Hash, "HASH")] + [TestCase(SseType.Kms, "KMS")] + [TestCase(SseType.Aes256, "AES256")] + [TestCase(StreamViewType.NewAndOldImages, "NEW_AND_OLD_IMAGES")] + [TestCase(StreamViewType.KeysOnly, "KEYS_ONLY")] + public void EnumSerializationTest(T enumValue, string expectedJsonValue) where T : struct, Enum + { + var result = JsonSerializer.Serialize(enumValue, _options); + var expectedJson = $"\"{expectedJsonValue}\""; + + Assert.That(result, Is.EqualTo(expectedJson)); + } + } +} \ No newline at end of file diff --git a/src/Benchmarks/Benchmarks/Query/QueryBenchmarkBase.cs b/src/Benchmarks/Benchmarks/Query/QueryBenchmarkBase.cs index 6535bd55..ae4e024b 100644 --- a/src/Benchmarks/Benchmarks/Query/QueryBenchmarkBase.cs +++ b/src/Benchmarks/Benchmarks/Query/QueryBenchmarkBase.cs @@ -91,7 +91,7 @@ public abstract class QueryBenchmarkBase _describeTableBytes = Encoding.UTF8.GetBytes(JsonSerializer.Serialize(new DescribeTableResponse(new TableDescription { TableName = "production_" + Tables.TestTable, - KeySchema = new[] {new KeySchemaElement("pk", KeyType.HASH), new KeySchemaElement("sk", KeyType.RANGE)}, + KeySchema = new[] {new KeySchemaElement("pk", KeyType.Hash), new KeySchemaElement("sk", KeyType.Range)}, AttributeDefinitions = new[] {new AttributeDefinition("pk", "S"), new AttributeDefinition("sk", "S")} }), new JsonSerializerOptions { diff --git a/src/Benchmarks/Mocks/DescribeTableResponseFactory.cs b/src/Benchmarks/Mocks/DescribeTableResponseFactory.cs index 25e632f3..c78b471d 100644 --- a/src/Benchmarks/Mocks/DescribeTableResponseFactory.cs +++ b/src/Benchmarks/Mocks/DescribeTableResponseFactory.cs @@ -15,7 +15,7 @@ public static byte[] CreateResponse() return Encoding.UTF8.GetBytes(JsonSerializer.Serialize(new DescribeTableResponse(new TableDescription { TableName = "production_" + Tables.TestTable, - KeySchema = new[] {new KeySchemaElement("pk", EfficientDynamoDb.Operations.DescribeTable.Models.Enums.KeyType.HASH), new KeySchemaElement("sk", KeyType.RANGE)}, + KeySchema = new[] {new KeySchemaElement("pk", EfficientDynamoDb.Operations.DescribeTable.Models.Enums.KeyType.Hash), new KeySchemaElement("sk", KeyType.Range)}, AttributeDefinitions = new[] {new AttributeDefinition("pk", "S"), new AttributeDefinition("sk", "S")} }), new JsonSerializerOptions { diff --git a/src/EfficientDynamoDb/DynamoDbContext/DynamoDbLowLevelContext.cs b/src/EfficientDynamoDb/DynamoDbContext/DynamoDbLowLevelContext.cs index 416b139b..9888613f 100644 --- a/src/EfficientDynamoDb/DynamoDbContext/DynamoDbLowLevelContext.cs +++ b/src/EfficientDynamoDb/DynamoDbContext/DynamoDbLowLevelContext.cs @@ -193,8 +193,8 @@ private async ValueTask BuildHttpContentAsync(UpdateItemRequest req .ConfigureAwait(false); var keySchema = response.Table.KeySchema; - return (keySchema.First(x => x.KeyType == KeyType.HASH).AttributeName, - keySchema.FirstOrDefault(x => x.KeyType == KeyType.RANGE)?.AttributeName); + return (keySchema.First(x => x.KeyType == KeyType.Hash).AttributeName, + keySchema.FirstOrDefault(x => x.KeyType == KeyType.Range)?.AttributeName); } } diff --git a/src/EfficientDynamoDb/Internal/Extensions/DateTimeConverterExtensions.cs b/src/EfficientDynamoDb/Internal/Extensions/DateTimeConverterExtensions.cs index c27ebe6f..1ff5d12a 100644 --- a/src/EfficientDynamoDb/Internal/Extensions/DateTimeConverterExtensions.cs +++ b/src/EfficientDynamoDb/Internal/Extensions/DateTimeConverterExtensions.cs @@ -4,7 +4,7 @@ namespace EfficientDynamoDb.Internal.Extensions { internal static class DateTimeConverterExtensions { - public static long ToUnixSeconds(this DateTime dateTime) => (long) (dateTime - UnixEpochStart).TotalSeconds; + public static double ToUnixSeconds(this DateTime dateTime) => (dateTime - UnixEpochStart).TotalSeconds; public static DateTime FromUnixSeconds(this double seconds) => UnixEpochStart.AddSeconds(seconds); diff --git a/src/EfficientDynamoDb/Internal/Extensions/StringNormalizationExtensions.cs b/src/EfficientDynamoDb/Internal/Extensions/StringNormalizationExtensions.cs index 261484cf..cd5f9885 100644 --- a/src/EfficientDynamoDb/Internal/Extensions/StringNormalizationExtensions.cs +++ b/src/EfficientDynamoDb/Internal/Extensions/StringNormalizationExtensions.cs @@ -1,3 +1,5 @@ +using EfficientDynamoDb.Internal.Core; + namespace EfficientDynamoDb.Internal.Extensions { internal static class StringNormalizationExtensions @@ -39,5 +41,19 @@ public static string NormalizeWhiteSpace(this string self) return new string(output, 0, currentIndex); } + + /// + /// Converts the string to UPPER_SNAKE_CASE and appends it to the . + /// + public static void ToUpperSnakeCase(this string self, ref NoAllocStringBuilder builder) + { + for (var i = 0; i < self.Length; i++) + { + var c = self[i]; + if (i != 0 && char.IsUpper(c)) + builder.Append("_"); + builder.Append(char.ToUpperInvariant(c)); + } + } } } \ No newline at end of file diff --git a/src/EfficientDynamoDb/Internal/JsonConverters/DdbEnumJsonConverter.cs b/src/EfficientDynamoDb/Internal/JsonConverters/DdbEnumJsonConverter.cs index 911c6d7e..669b2f16 100644 --- a/src/EfficientDynamoDb/Internal/JsonConverters/DdbEnumJsonConverter.cs +++ b/src/EfficientDynamoDb/Internal/JsonConverters/DdbEnumJsonConverter.cs @@ -1,6 +1,8 @@ using System; using System.Text.Json; using System.Text.Json.Serialization; +using EfficientDynamoDb.Internal.Core; +using EfficientDynamoDb.Internal.Extensions; using EfficientDynamoDb.Internal.TypeParsers; namespace EfficientDynamoDb.Internal.JsonConverters @@ -11,17 +13,21 @@ public override T Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerial { var enumString = reader.GetString(); - if (!EnumParser.TryParseCaseInsensitive(enumString, out T value)) - { - return default; - } - - return value; + return EnumParser.TryParseUpperSnakeCase(enumString, out T value) + ? value + : default; } public override void Write(Utf8JsonWriter writer, T value, JsonSerializerOptions options) { - writer.WriteStringValue(value.ToString()); + var enumString = value.ToString(); + + Span buffer = stackalloc char[enumString.Length * 2]; // Allocate enough space to account for new underscores + var sb = new NoAllocStringBuilder(in buffer, true); + + enumString.ToUpperSnakeCase(ref sb); + + writer.WriteStringValue(sb.GetBuffer()); } } } \ No newline at end of file diff --git a/src/EfficientDynamoDb/Internal/TypeParsers/EnumParser.cs b/src/EfficientDynamoDb/Internal/TypeParsers/EnumParser.cs index e4ac3867..d56e0242 100644 --- a/src/EfficientDynamoDb/Internal/TypeParsers/EnumParser.cs +++ b/src/EfficientDynamoDb/Internal/TypeParsers/EnumParser.cs @@ -1,5 +1,6 @@ using System; using System.Runtime.CompilerServices; +using EfficientDynamoDb.Internal.Core; namespace EfficientDynamoDb.Internal.TypeParsers { @@ -8,5 +9,33 @@ public static class EnumParser [MethodImpl(MethodImplOptions.AggressiveInlining)] public static bool TryParseCaseInsensitive(string? value, out TEnum result) where TEnum : struct, Enum => Enum.TryParse(value, out result) || Enum.TryParse(value, true, out result); + + public static bool TryParseUpperSnakeCase(string? value, out TEnum result) where TEnum : struct, Enum + { + if (value == null) + { + result = default; + return false; + } + + Span buffer = stackalloc char[value.Length]; + var sb = new NoAllocStringBuilder(in buffer, true); + + var isNextUpper = false; + foreach (var c in value) + { + if (c == '_') + { + isNextUpper = true; + continue; + } + + var nextChar = isNextUpper ? c : char.ToLowerInvariant(c); + sb.Append(nextChar); + isNextUpper = false; + } + + return Enum.TryParse(sb.ToString(), true, out result); + } } } \ No newline at end of file diff --git a/src/EfficientDynamoDb/Operations/DescribeTable/Models/Enums/BillingMode.cs b/src/EfficientDynamoDb/Operations/DescribeTable/Models/Enums/BillingMode.cs index 7e7400e0..0c8ad7f2 100644 --- a/src/EfficientDynamoDb/Operations/DescribeTable/Models/Enums/BillingMode.cs +++ b/src/EfficientDynamoDb/Operations/DescribeTable/Models/Enums/BillingMode.cs @@ -2,8 +2,8 @@ namespace EfficientDynamoDb.Operations.DescribeTable.Models.Enums { public enum BillingMode { - _UNKNOWN = 0, - PROVISIONED = 10, - PAY_PER_REQUEST = 20 + Undefined = 0, + Provisioned = 10, + PayPerRequest = 20 } } \ No newline at end of file diff --git a/src/EfficientDynamoDb/Operations/DescribeTable/Models/Enums/IndexStatus.cs b/src/EfficientDynamoDb/Operations/DescribeTable/Models/Enums/IndexStatus.cs index 5f6a0741..eaf6de77 100644 --- a/src/EfficientDynamoDb/Operations/DescribeTable/Models/Enums/IndexStatus.cs +++ b/src/EfficientDynamoDb/Operations/DescribeTable/Models/Enums/IndexStatus.cs @@ -2,10 +2,10 @@ namespace EfficientDynamoDb.Operations.DescribeTable.Models.Enums { public enum IndexStatus { - _UNKNOWN = 0, - CREATING = 10, - UPDATING = 20, - DELETING = 30, - ACTIVE = 40 + Undefined = 0, + Creating = 10, + Updating = 20, + Deleting = 30, + Active = 40 } } \ No newline at end of file diff --git a/src/EfficientDynamoDb/Operations/DescribeTable/Models/Enums/KeyType.cs b/src/EfficientDynamoDb/Operations/DescribeTable/Models/Enums/KeyType.cs index f7aaeaa2..82f9dfa1 100644 --- a/src/EfficientDynamoDb/Operations/DescribeTable/Models/Enums/KeyType.cs +++ b/src/EfficientDynamoDb/Operations/DescribeTable/Models/Enums/KeyType.cs @@ -2,8 +2,8 @@ namespace EfficientDynamoDb.Operations.DescribeTable.Models.Enums { public enum KeyType { - _UNKNOWN = 0, - HASH = 10, - RANGE = 20 + Undefined = 0, + Hash = 10, + Range = 20 } } \ No newline at end of file diff --git a/src/EfficientDynamoDb/Operations/DescribeTable/Models/Enums/ProjectionType.cs b/src/EfficientDynamoDb/Operations/DescribeTable/Models/Enums/ProjectionType.cs index 39c545cc..6c00679b 100644 --- a/src/EfficientDynamoDb/Operations/DescribeTable/Models/Enums/ProjectionType.cs +++ b/src/EfficientDynamoDb/Operations/DescribeTable/Models/Enums/ProjectionType.cs @@ -2,9 +2,9 @@ namespace EfficientDynamoDb.Operations.DescribeTable.Models.Enums { public enum ProjectionType { - _UNKNOWN = 0, - KEYS_ONLY = 10, - INCLUDE = 20, - ALL = 30 + Undefined = 0, + KeysOnly = 10, + Include = 20, + All = 30 } } \ No newline at end of file diff --git a/src/EfficientDynamoDb/Operations/DescribeTable/Models/Enums/ReplicaStatus.cs b/src/EfficientDynamoDb/Operations/DescribeTable/Models/Enums/ReplicaStatus.cs index 4215f05f..a26c8495 100644 --- a/src/EfficientDynamoDb/Operations/DescribeTable/Models/Enums/ReplicaStatus.cs +++ b/src/EfficientDynamoDb/Operations/DescribeTable/Models/Enums/ReplicaStatus.cs @@ -2,11 +2,11 @@ namespace EfficientDynamoDb.Operations.DescribeTable.Models.Enums { public enum ReplicaStatus { - _UNKNOWN = 0, - CREATING = 10, - UPDATING = 20, - DELETING = 30, - ACTIVE = 40, - REGION_DISABLED = 50 + Undefined = 0, + Creating = 10, + Updating = 20, + Deleting = 30, + Active = 40, + RegionDisabled = 50 } } \ No newline at end of file diff --git a/src/EfficientDynamoDb/Operations/DescribeTable/Models/Enums/SseStatus.cs b/src/EfficientDynamoDb/Operations/DescribeTable/Models/Enums/SseStatus.cs index 83d15050..e51b7f56 100644 --- a/src/EfficientDynamoDb/Operations/DescribeTable/Models/Enums/SseStatus.cs +++ b/src/EfficientDynamoDb/Operations/DescribeTable/Models/Enums/SseStatus.cs @@ -4,14 +4,14 @@ namespace EfficientDynamoDb.Operations.DescribeTable.Models.Enums { public enum SseStatus { - _UNKNOWN = 0, - ENABLED = 10, + Undefined = 0, + Enabled = 10, [Obsolete("Not supported according to DDB docs")] - ENABLING = 20, + Enabling = 20, [Obsolete("Not supported according to DDB docs")] - DISABLED = 30, + Disabled = 30, [Obsolete("Not supported according to DDB docs")] - DISABLING = 40, - UPDATING = 50 + Disabling = 40, + Updating = 50 } } \ No newline at end of file diff --git a/src/EfficientDynamoDb/Operations/DescribeTable/Models/Enums/SseType.cs b/src/EfficientDynamoDb/Operations/DescribeTable/Models/Enums/SseType.cs index bd17fc4a..2f3885bb 100644 --- a/src/EfficientDynamoDb/Operations/DescribeTable/Models/Enums/SseType.cs +++ b/src/EfficientDynamoDb/Operations/DescribeTable/Models/Enums/SseType.cs @@ -4,9 +4,9 @@ namespace EfficientDynamoDb.Operations.DescribeTable.Models.Enums { public enum SseType { - _UNKNOWN = 0, - KMS = 10, + Undefined = 0, + Kms = 10, [Obsolete("Not supported according to DDB docs")] - AES256 = 20 + Aes256 = 20 } } \ No newline at end of file diff --git a/src/EfficientDynamoDb/Operations/DescribeTable/Models/Enums/StreamViewType.cs b/src/EfficientDynamoDb/Operations/DescribeTable/Models/Enums/StreamViewType.cs index 95edac4b..694e0a90 100644 --- a/src/EfficientDynamoDb/Operations/DescribeTable/Models/Enums/StreamViewType.cs +++ b/src/EfficientDynamoDb/Operations/DescribeTable/Models/Enums/StreamViewType.cs @@ -2,10 +2,10 @@ namespace EfficientDynamoDb.Operations.DescribeTable.Models.Enums { public enum StreamViewType { - _UNKNOWN = 0, - KEYS_ONLY = 10, - NEW_IMAGE = 20, - OLD_IMAGE = 30, - NEW_AND_OLD_IMAGES = 40 + Undefined = 0, + KeysOnly = 10, + NewImage = 20, + OldImage = 30, + NewAndOldImages = 40 } } \ No newline at end of file diff --git a/src/EfficientDynamoDb/Operations/DescribeTable/Models/Enums/TableStatus.cs b/src/EfficientDynamoDb/Operations/DescribeTable/Models/Enums/TableStatus.cs index f0f99ef7..c08500f1 100644 --- a/src/EfficientDynamoDb/Operations/DescribeTable/Models/Enums/TableStatus.cs +++ b/src/EfficientDynamoDb/Operations/DescribeTable/Models/Enums/TableStatus.cs @@ -2,13 +2,13 @@ namespace EfficientDynamoDb.Operations.DescribeTable.Models.Enums { public enum TableStatus { - _UNKNOWN = 0, - CREATING = 10, - UPDATING = 20, - DELETING = 30, - ACTIVE = 40, - INACCESSIBLE_ENCRYPTION_CREDENTIALS = 50, - ARCHIVING = 60, - ARCHIVED = 70, + Undefined = 0, + Creating = 10, + Updating = 20, + Deleting = 30, + Active = 40, + InaccessibleEncryptionCredentials = 50, + Archiving = 60, + Archived = 70, } } \ No newline at end of file