diff --git a/src/KubeOps/Operator/Entities/Extensions/EntityToCrdExtensions.cs b/src/KubeOps/Operator/Entities/Extensions/EntityToCrdExtensions.cs index 79808573..7e1d0e18 100644 --- a/src/KubeOps/Operator/Entities/Extensions/EntityToCrdExtensions.cs +++ b/src/KubeOps/Operator/Entities/Extensions/EntityToCrdExtensions.cs @@ -1,6 +1,7 @@ using System; using System.Collections; using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; using System.Linq; using System.Reflection; using DotnetKubernetesClient.Entities; @@ -214,6 +215,8 @@ private static V1JSONSchemaProps MapType( // this description is on the class props.Description ??= type.GetCustomAttributes(true).FirstOrDefault()?.Description; + var isSimpleType = IsSimpleType(type); + if (type == typeof(V1ObjectMeta)) { props.Type = Object; @@ -226,7 +229,7 @@ private static V1JSONSchemaProps MapType( additionalColumns, jsonPath); } - else if (!IsSimpleType(type) && + else if (!isSimpleType && (typeof(IDictionary).IsAssignableFrom(type) || (type.IsGenericType && type.GetGenericTypeDefinition() == typeof(IDictionary<,>)) || (type.IsGenericType && @@ -237,18 +240,16 @@ private static V1JSONSchemaProps MapType( props.Type = Object; props.XKubernetesPreserveUnknownFields = true; } - else if (!IsSimpleType(type) && - type.IsGenericType && - typeof(IEnumerable<>).IsAssignableFrom(type.GetGenericTypeDefinition())) + else if (!isSimpleType && IsGenericEnumerableType(type, out Type? closingType)) { props.Type = Array; - props.Items = MapType(type.GetGenericArguments()[0], additionalColumns, jsonPath); + props.Items = MapType(closingType, additionalColumns, jsonPath); } else if (type == typeof(IntstrIntOrString)) { props.XKubernetesIntOrString = true; } - else if (!IsSimpleType(type)) + else if (!isSimpleType) { ProcessType(type, props, additionalColumns, jsonPath); } @@ -348,5 +349,22 @@ private static bool IsSimpleType(Type type) => IsSimpleType(type.GetGenericArguments()[0])); private static string CamelCase(string str) => $"{str.Substring(0, 1).ToLower()}{str.Substring(1)}"; + + private static bool IsGenericEnumerableType(Type type, [NotNullWhen(true)] out Type? closingType) + { + if (type.IsGenericType && typeof(IEnumerable<>).IsAssignableFrom(type.GetGenericTypeDefinition())) + { + closingType = type.GetGenericArguments()[0]; + return true; + } + + closingType = type + .GetInterfaces() + .Where(t => t.IsGenericType && t.GetGenericTypeDefinition() == typeof(IEnumerable<>)) + .Select(t => t.GetGenericArguments()[0]) + .FirstOrDefault(); + + return closingType != null; + } } } diff --git a/tests/KubeOps.Test/Operator/Entities/CrdGeneration.Test.cs b/tests/KubeOps.Test/Operator/Entities/CrdGeneration.Test.cs index 6c03b00a..484a7c6b 100644 --- a/tests/KubeOps.Test/Operator/Entities/CrdGeneration.Test.cs +++ b/tests/KubeOps.Test/Operator/Entities/CrdGeneration.Test.cs @@ -7,7 +7,6 @@ using KubeOps.Operator.Commands.Generators; using KubeOps.Operator.Entities.Extensions; using KubeOps.Operator.Errors; -using KubeOps.Operator.Services; using KubeOps.Test.TestEntities; using Xunit; @@ -74,30 +73,43 @@ public void Should_Set_The_Correct_Type_And_Format_For_Types(string fieldName, s nullableField.Nullable.Should().BeTrue(); } - [Fact] - public void Should_Set_The_Correct_Array_Type() - { + [Theory] + [InlineData(nameof(TestSpecEntitySpec.StringArray), "string", null)] + [InlineData(nameof(TestSpecEntitySpec.NullableStringArray), "string", true)] + [InlineData(nameof(TestSpecEntitySpec.EnumerableInteger), "integer", null)] + [InlineData(nameof(TestSpecEntitySpec.EnumerableNullableInteger), "integer", null)] + [InlineData(nameof(TestSpecEntitySpec.IntegerList), "integer", null)] + [InlineData(nameof(TestSpecEntitySpec.IntegerHashSet), "integer", null)] + [InlineData(nameof(TestSpecEntitySpec.IntegerISet), "integer", null)] + [InlineData(nameof(TestSpecEntitySpec.IntegerIReadOnlySet), "integer", null)] + public void Should_Set_The_Correct_Array_Type(string property, string expectedType, bool? expectedNullable) + { + var propertyName = char.ToLowerInvariant(property[0]) + property[1..]; var crd = _testSpecEntity.CreateCrd(); var specProperties = crd.Spec.Versions.First().Schema.OpenAPIV3Schema.Properties["spec"]; - var normalField = specProperties.Properties["stringArray"]; + var normalField = specProperties.Properties[propertyName]; normalField.Type.Should().Be("array"); - (normalField.Items as V1JSONSchemaProps)?.Type?.Should().Be("string"); - normalField.Nullable.Should().BeNull(); - - var nullableField = specProperties.Properties["nullableStringArray"]; - nullableField.Type.Should().Be("array"); - (nullableField.Items as V1JSONSchemaProps)?.Type?.Should().Be("string"); - nullableField.Nullable.Should().BeTrue(); + (normalField.Items as V1JSONSchemaProps)?.Type?.Should().Be(expectedType); + normalField.Nullable.Should().Be(expectedNullable); } - [Fact] - public void Should_Set_The_Correct_Complex_Array_Type() - { + [Theory] + [InlineData(nameof(TestSpecEntitySpec.ComplexItemsEnumerable))] + [InlineData(nameof(TestSpecEntitySpec.ComplexItemsList))] + [InlineData(nameof(TestSpecEntitySpec.ComplexItemsIList))] + [InlineData(nameof(TestSpecEntitySpec.ComplexItemsReadOnlyList))] + [InlineData(nameof(TestSpecEntitySpec.ComplexItemsCollection))] + [InlineData(nameof(TestSpecEntitySpec.ComplexItemsICollection))] + [InlineData(nameof(TestSpecEntitySpec.ComplexItemsReadOnlyCollection))] + [InlineData(nameof(TestSpecEntitySpec.ComplexItemsDerivedList))] + public void Should_Set_The_Correct_Complex_Array_Type(string property) + { + var propertyName = char.ToLowerInvariant(property[0]) + property[1..]; var crd = _testSpecEntity.CreateCrd(); var specProperties = crd.Spec.Versions.First().Schema.OpenAPIV3Schema.Properties["spec"]; - var complexItemsArray = specProperties.Properties["complexItems"]; + var complexItemsArray = specProperties.Properties[propertyName]; complexItemsArray.Type.Should().Be("array"); (complexItemsArray.Items as V1JSONSchemaProps)?.Type?.Should().Be("object"); complexItemsArray.Nullable.Should().BeNull(); diff --git a/tests/KubeOps.Test/TestEntities/TestSpecEntity.cs b/tests/KubeOps.Test/TestEntities/TestSpecEntity.cs index a76bb257..5d0ea93a 100644 --- a/tests/KubeOps.Test/TestEntities/TestSpecEntity.cs +++ b/tests/KubeOps.Test/TestEntities/TestSpecEntity.cs @@ -1,6 +1,7 @@ using System; using System.Collections; using System.Collections.Generic; +using System.Collections.ObjectModel; using System.Linq; using k8s.Models; using KubeOps.Operator.Entities; @@ -11,10 +12,22 @@ namespace KubeOps.Test.TestEntities [Description("This is the Spec Class Description")] public class TestSpecEntitySpec { - public string[] StringArray { get; set; } = new string[0]; + public string[] StringArray { get; set; } = Array.Empty(); public string[]? NullableStringArray { get; set; } + public IEnumerable EnumerableInteger { get; set; } = Array.Empty(); + + public IEnumerable EnumerableNullableInteger { get; set; } = Array.Empty(); + + public IntegerList IntegerList { get; set; } = new(); + + public HashSet IntegerHashSet { get; set; } = new(); + + public ISet IntegerISet { get; set; } = new HashSet(); + + public IReadOnlySet IntegerIReadOnlySet { get; set; } = new HashSet(); + [AdditionalPrinterColumn] public string NormalString { get; set; } = string.Empty; @@ -80,7 +93,21 @@ public class TestSpecEntitySpec [Required] public int Required { get; set; } - public IEnumerable ComplexItems { get; set; } = Enumerable.Empty(); + public IEnumerable ComplexItemsEnumerable { get; set; } = Enumerable.Empty(); + + public List ComplexItemsList { get; set; } = new(); + + public IList ComplexItemsIList { get; set; } = Array.Empty(); + + public IReadOnlyList ComplexItemsReadOnlyList { get; set; } = Array.Empty(); + + public Collection ComplexItemsCollection { get; set; } = new(); + + public ICollection ComplexItemsICollection { get; set; } = Array.Empty(); + + public IReadOnlyCollection ComplexItemsReadOnlyCollection { get; set; } = Array.Empty(); + + public TestItemList ComplexItemsDerivedList { get; set; } = new(); public IDictionary Dictionary { get; set; } = new Dictionary(); @@ -117,4 +144,12 @@ public class TestItem public string Item { get; set; } = null!; public string Extra { get; set; } = null!; } + + public class TestItemList : List + { + } + + public class IntegerList : Collection + { + } }