From d791407064472fa106114dbd5be7311a86f51da5 Mon Sep 17 00:00:00 2001 From: Jerome Haltom Date: Fri, 3 Jan 2025 22:52:11 -0600 Subject: [PATCH 1/8] Add EnumMemberAttribute and .NET 9's JsonStringEnumMemberNameAttribute handling for CRD generation. --- src/KubeOps.Transpiler/Crds.cs | 37 ++++++++++++++++++- .../KubeOps.Transpiler.csproj | 2 +- 2 files changed, 37 insertions(+), 2 deletions(-) diff --git a/src/KubeOps.Transpiler/Crds.cs b/src/KubeOps.Transpiler/Crds.cs index e41d9f0d..42116320 100644 --- a/src/KubeOps.Transpiler/Crds.cs +++ b/src/KubeOps.Transpiler/Crds.cs @@ -1,6 +1,7 @@ using System.Collections; using System.Collections.ObjectModel; using System.Reflection; +using System.Runtime.Serialization; using System.Text.Json.Serialization; using k8s; @@ -336,12 +337,46 @@ private static V1JSONSchemaProps Map(this MetadataLoadContext context, Type type "System.Enum" => new V1JSONSchemaProps { Type = String, - EnumProperty = Enum.GetNames(type).Cast().ToList(), + EnumProperty = GetEnumNames(context, type), }, _ => throw InvalidType(type), }; } + private static IList GetEnumNames(this MetadataLoadContext context, Type type) + { + var attributeNameByFieldName = new Dictionary(); + foreach (var field in type.GetFields(BindingFlags.Public | BindingFlags.Static)) + { + if (field.GetCustomAttribute() is { Value: not null } enumMemberAtribute) + { + attributeNameByFieldName.Add(field.Name, enumMemberAtribute.Value); + } + +#if NET9_0_OR_GREATER + if (field.GetCustomAttribute() is { Name: not null } stringEnumMemberAttribute) + { + attributeNameByFieldName.Add(field.Name, stringEnumMemberAttribute.Name); + } +#endif + } + + var enumName = new List(); + foreach (var value in Enum.GetNames(type)) + { + if (attributeNameByFieldName.TryGetValue(value, out var name)) + { + enumName.Add(name); + } + else + { + enumName.Add(value); + } + } + + return enumName; + } + private static V1JSONSchemaProps MapObjectType(this MetadataLoadContext context, Type type) { switch (type.FullName) diff --git a/src/KubeOps.Transpiler/KubeOps.Transpiler.csproj b/src/KubeOps.Transpiler/KubeOps.Transpiler.csproj index a71a5557..25af1db5 100644 --- a/src/KubeOps.Transpiler/KubeOps.Transpiler.csproj +++ b/src/KubeOps.Transpiler/KubeOps.Transpiler.csproj @@ -1,7 +1,7 @@ - net6.0;net7.0;net8.0 + net6.0;net7.0;net8.0;net9.0 From 2c3d3d4c7c92d66041ec300a169da162a50226cd Mon Sep 17 00:00:00 2001 From: Jerome Haltom Date: Fri, 3 Jan 2025 22:57:00 -0600 Subject: [PATCH 2/8] Add .NET 9 requirement for build. --- .github/workflows/dotnet-release.yml | 5 +++++ .github/workflows/dotnet-test.yml | 8 ++++++++ 2 files changed, 13 insertions(+) diff --git a/.github/workflows/dotnet-release.yml b/.github/workflows/dotnet-release.yml index 7e2184ce..d38b4f34 100644 --- a/.github/workflows/dotnet-release.yml +++ b/.github/workflows/dotnet-release.yml @@ -16,6 +16,11 @@ jobs: with: dotnet-version: 8.x + - name: Setup .NET + uses: actions/setup-dotnet@v3 + with: + dotnet-version: 9.x + - name: Semantic Release uses: cycjimmy/semantic-release-action@v4 with: diff --git a/.github/workflows/dotnet-test.yml b/.github/workflows/dotnet-test.yml index 6c15906a..b7ee92e4 100644 --- a/.github/workflows/dotnet-test.yml +++ b/.github/workflows/dotnet-test.yml @@ -1,5 +1,8 @@ name: .NET Testing +on: + workflow_dispatch: + on: pull_request: branches: @@ -21,6 +24,11 @@ jobs: with: dotnet-version: 8.x + - name: Setup .NET + uses: actions/setup-dotnet@v3 + with: + dotnet-version: 9.x + - name: Linting run: dotnet format --verify-no-changes From 512ad39e456fab3accc6abb01f71133301fa79c5 Mon Sep 17 00:00:00 2001 From: Jerome Haltom Date: Fri, 3 Jan 2025 22:58:17 -0600 Subject: [PATCH 3/8] Syntax error. --- .github/workflows/dotnet-test.yml | 2 -- 1 file changed, 2 deletions(-) diff --git a/.github/workflows/dotnet-test.yml b/.github/workflows/dotnet-test.yml index b7ee92e4..a64f645f 100644 --- a/.github/workflows/dotnet-test.yml +++ b/.github/workflows/dotnet-test.yml @@ -2,8 +2,6 @@ name: .NET Testing on: workflow_dispatch: - -on: pull_request: branches: - "**" From 6e6587e3a39e67b3456e8df9165a45232b33520b Mon Sep 17 00:00:00 2001 From: Jerome Haltom Date: Sat, 4 Jan 2025 07:39:51 -0600 Subject: [PATCH 4/8] Enable preview features? --- src/Directory.Build.props | 1 + 1 file changed, 1 insertion(+) diff --git a/src/Directory.Build.props b/src/Directory.Build.props index 7ca7c9f6..45d4a78d 100644 --- a/src/Directory.Build.props +++ b/src/Directory.Build.props @@ -21,6 +21,7 @@ true + True From ea6f49e5c5548a316ffd85b1f5dc0c0bac2b0a4a Mon Sep 17 00:00:00 2001 From: Jerome Haltom Date: Sat, 4 Jan 2025 09:26:49 -0600 Subject: [PATCH 5/8] Revert some stuff. --- .github/workflows/dotnet-release.yml | 5 ----- .github/workflows/dotnet-test.yml | 6 ------ src/Directory.Build.props | 1 - 3 files changed, 12 deletions(-) diff --git a/.github/workflows/dotnet-release.yml b/.github/workflows/dotnet-release.yml index d38b4f34..7e2184ce 100644 --- a/.github/workflows/dotnet-release.yml +++ b/.github/workflows/dotnet-release.yml @@ -16,11 +16,6 @@ jobs: with: dotnet-version: 8.x - - name: Setup .NET - uses: actions/setup-dotnet@v3 - with: - dotnet-version: 9.x - - name: Semantic Release uses: cycjimmy/semantic-release-action@v4 with: diff --git a/.github/workflows/dotnet-test.yml b/.github/workflows/dotnet-test.yml index a64f645f..6c15906a 100644 --- a/.github/workflows/dotnet-test.yml +++ b/.github/workflows/dotnet-test.yml @@ -1,7 +1,6 @@ name: .NET Testing on: - workflow_dispatch: pull_request: branches: - "**" @@ -22,11 +21,6 @@ jobs: with: dotnet-version: 8.x - - name: Setup .NET - uses: actions/setup-dotnet@v3 - with: - dotnet-version: 9.x - - name: Linting run: dotnet format --verify-no-changes diff --git a/src/Directory.Build.props b/src/Directory.Build.props index 45d4a78d..7ca7c9f6 100644 --- a/src/Directory.Build.props +++ b/src/Directory.Build.props @@ -21,7 +21,6 @@ true - True From e376d1310e1133b03f16814da3389861c7d59440 Mon Sep 17 00:00:00 2001 From: Jerome Haltom Date: Sat, 4 Jan 2025 09:27:32 -0600 Subject: [PATCH 6/8] Remove NET9 stuff. --- src/KubeOps.Transpiler/Crds.cs | 7 ------- src/KubeOps.Transpiler/KubeOps.Transpiler.csproj | 2 +- 2 files changed, 1 insertion(+), 8 deletions(-) diff --git a/src/KubeOps.Transpiler/Crds.cs b/src/KubeOps.Transpiler/Crds.cs index 42116320..a188675f 100644 --- a/src/KubeOps.Transpiler/Crds.cs +++ b/src/KubeOps.Transpiler/Crds.cs @@ -352,13 +352,6 @@ private static IList GetEnumNames(this MetadataLoadContext context, Type { attributeNameByFieldName.Add(field.Name, enumMemberAtribute.Value); } - -#if NET9_0_OR_GREATER - if (field.GetCustomAttribute() is { Name: not null } stringEnumMemberAttribute) - { - attributeNameByFieldName.Add(field.Name, stringEnumMemberAttribute.Name); - } -#endif } var enumName = new List(); diff --git a/src/KubeOps.Transpiler/KubeOps.Transpiler.csproj b/src/KubeOps.Transpiler/KubeOps.Transpiler.csproj index 25af1db5..a71a5557 100644 --- a/src/KubeOps.Transpiler/KubeOps.Transpiler.csproj +++ b/src/KubeOps.Transpiler/KubeOps.Transpiler.csproj @@ -1,7 +1,7 @@ - net6.0;net7.0;net8.0;net9.0 + net6.0;net7.0;net8.0 From 65638581071f98c3eceba65980e9edf673ca9b33 Mon Sep 17 00:00:00 2001 From: Jerome Haltom Date: Sat, 4 Jan 2025 17:48:45 -0600 Subject: [PATCH 7/8] Add NET9. Evaluate JsonStringEnumMemberNameAttribute. --- src/KubeOps.Transpiler/Crds.cs | 6 ++++++ src/KubeOps.Transpiler/KubeOps.Transpiler.csproj | 2 +- 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/src/KubeOps.Transpiler/Crds.cs b/src/KubeOps.Transpiler/Crds.cs index a188675f..40b924f7 100644 --- a/src/KubeOps.Transpiler/Crds.cs +++ b/src/KubeOps.Transpiler/Crds.cs @@ -352,6 +352,12 @@ private static IList GetEnumNames(this MetadataLoadContext context, Type { attributeNameByFieldName.Add(field.Name, enumMemberAtribute.Value); } +#if NET9_0_OR_GREATER + if (field.GetCustomAttribute() is { Name: not null } jsonMemberNameAtribute) + { + attributeNameByFieldName.Add(field.Name, jsonMemberNameAtribute.Name); + } +#endif } var enumName = new List(); diff --git a/src/KubeOps.Transpiler/KubeOps.Transpiler.csproj b/src/KubeOps.Transpiler/KubeOps.Transpiler.csproj index a71a5557..25af1db5 100644 --- a/src/KubeOps.Transpiler/KubeOps.Transpiler.csproj +++ b/src/KubeOps.Transpiler/KubeOps.Transpiler.csproj @@ -1,7 +1,7 @@ - net6.0;net7.0;net8.0 + net6.0;net7.0;net8.0;net9.0 From ecdb22d790a1f7debee024928cda155ec316bd82 Mon Sep 17 00:00:00 2001 From: Jerome Haltom Date: Sat, 4 Jan 2025 18:33:46 -0600 Subject: [PATCH 8/8] Properly get attributes. Remove EnumMember. STJ doesn't actually use this. Add a unit test. Test in .NET 9. Unsure if author wants to test both 8 and 9. --- src/KubeOps.Transpiler/Crds.cs | 25 ++++++++++--------- src/KubeOps.Transpiler/Utilities.cs | 12 +++++++++ test/Directory.Build.props | 2 +- test/KubeOps.Transpiler.Test/Crds.Mlc.Test.cs | 23 +++++++++++++++++ 4 files changed, 49 insertions(+), 13 deletions(-) diff --git a/src/KubeOps.Transpiler/Crds.cs b/src/KubeOps.Transpiler/Crds.cs index 40b924f7..2f24d433 100644 --- a/src/KubeOps.Transpiler/Crds.cs +++ b/src/KubeOps.Transpiler/Crds.cs @@ -345,35 +345,36 @@ private static V1JSONSchemaProps Map(this MetadataLoadContext context, Type type private static IList GetEnumNames(this MetadataLoadContext context, Type type) { +#if NET9_0_OR_GREATER var attributeNameByFieldName = new Dictionary(); + foreach (var field in type.GetFields(BindingFlags.Public | BindingFlags.Static)) { - if (field.GetCustomAttribute() is { Value: not null } enumMemberAtribute) - { - attributeNameByFieldName.Add(field.Name, enumMemberAtribute.Value); - } -#if NET9_0_OR_GREATER - if (field.GetCustomAttribute() is { Name: not null } jsonMemberNameAtribute) + if (field.GetCustomAttributeData() is { } jsonMemberNameAttribute && + jsonMemberNameAttribute.GetCustomAttributeCtorArg(context, 0) is { } jsonMemberNameAtributeName) { - attributeNameByFieldName.Add(field.Name, jsonMemberNameAtribute.Name); + attributeNameByFieldName.Add(field.Name, jsonMemberNameAtributeName); } -#endif } - var enumName = new List(); + var enumNames = new List(); + foreach (var value in Enum.GetNames(type)) { if (attributeNameByFieldName.TryGetValue(value, out var name)) { - enumName.Add(name); + enumNames.Add(name); } else { - enumName.Add(value); + enumNames.Add(value); } } - return enumName; + return enumNames; +#else + return Enum.GetNames(type); +#endif } private static V1JSONSchemaProps MapObjectType(this MetadataLoadContext context, Type type) diff --git a/src/KubeOps.Transpiler/Utilities.cs b/src/KubeOps.Transpiler/Utilities.cs index 67c97ee2..8593a40b 100644 --- a/src/KubeOps.Transpiler/Utilities.cs +++ b/src/KubeOps.Transpiler/Utilities.cs @@ -20,6 +20,18 @@ public static class Utilities .GetCustomAttributes(type) .FirstOrDefault(a => a.AttributeType.Name == typeof(TAttribute).Name); + /// + /// Load a custom attribute from a read-only-reflected field. + /// + /// The field. + /// The type of the attribute to load. + /// The custom attribute data if an attribute is found. + public static CustomAttributeData? GetCustomAttributeData(this FieldInfo field) + where TAttribute : Attribute + => CustomAttributeData + .GetCustomAttributes(field) + .FirstOrDefault(a => a.AttributeType.Name == typeof(TAttribute).Name); + /// /// Load a custom attribute from a read-only-reflected property. /// diff --git a/test/Directory.Build.props b/test/Directory.Build.props index bcea30f7..1c833b76 100644 --- a/test/Directory.Build.props +++ b/test/Directory.Build.props @@ -1,6 +1,6 @@ - net8.0 + net9.0 enable enable false diff --git a/test/KubeOps.Transpiler.Test/Crds.Mlc.Test.cs b/test/KubeOps.Transpiler.Test/Crds.Mlc.Test.cs index 17a72d2f..d31f4662 100644 --- a/test/KubeOps.Transpiler.Test/Crds.Mlc.Test.cs +++ b/test/KubeOps.Transpiler.Test/Crds.Mlc.Test.cs @@ -40,6 +40,7 @@ public class CrdsMlcTest(MlcProvider provider) : TranspilerTestBase(provider) [InlineData(typeof(SetIntEntity), "array", null, false)] [InlineData(typeof(InheritedEnumerableEntity), "array", null, false)] [InlineData(typeof(EnumEntity), "string", null, false)] + [InlineData(typeof(NamedEnumEntity), "string", null, false)] [InlineData(typeof(NullableEnumEntity), "string", null, true)] [InlineData(typeof(DictionaryEntity), "object", null, false)] [InlineData(typeof(EnumerableKeyPairsEntity), "object", null, false)] @@ -475,6 +476,14 @@ public void Should_Correctly_Use_Entity_Scope_Attribute() clusterCrd.Spec.Scope.Should().Be("Cluster"); } + [Fact] + public void Should_Correctly_Get_Enum_Value_From_JsonStringEnumMemberNameAttribute() + { + var crd = _mlc.Transpile(typeof(NamedEnumEntity)); + var specProperties = crd.Spec.Versions.First().Schema.OpenAPIV3Schema.Properties["property"]; + specProperties.EnumProperty.Should().BeEquivalentTo(["enumValue1", "enumValue2"]); + } + #region Test Entity Classes [KubernetesEntity(Group = "testing.dev", ApiVersion = "v1", Kind = "TestEntity")] @@ -659,6 +668,20 @@ public enum TestSpecEnum } } + [KubernetesEntity(Group = "testing.dev", ApiVersion = "v1", Kind = "TestEntity")] + private class NamedEnumEntity : CustomKubernetesEntity + { + public TestSpecEnum Property { get; set; } + + public enum TestSpecEnum + { + [JsonStringEnumMemberName("enumValue1")] + Value1, + [JsonStringEnumMemberName("enumValue2")] + Value2, + } + } + [KubernetesEntity(Group = "testing.dev", ApiVersion = "v1", Kind = "TestEntity")] private class SimpleDictionaryEntity : CustomKubernetesEntity {