Skip to content

Commit cd169e3

Browse files
authored
fix: Expand code generator support for enumerable and collection types in JSON Schema (#153)
1 parent 437a52b commit cd169e3

File tree

3 files changed

+89
-24
lines changed

3 files changed

+89
-24
lines changed

src/KubeOps/Operator/Entities/Extensions/EntityToCrdExtensions.cs

Lines changed: 24 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
using System;
22
using System.Collections;
33
using System.Collections.Generic;
4+
using System.Diagnostics.CodeAnalysis;
45
using System.Linq;
56
using System.Reflection;
67
using DotnetKubernetesClient.Entities;
@@ -214,6 +215,8 @@ private static V1JSONSchemaProps MapType(
214215
// this description is on the class
215216
props.Description ??= type.GetCustomAttributes<DescriptionAttribute>(true).FirstOrDefault()?.Description;
216217

218+
var isSimpleType = IsSimpleType(type);
219+
217220
if (type == typeof(V1ObjectMeta))
218221
{
219222
props.Type = Object;
@@ -226,7 +229,7 @@ private static V1JSONSchemaProps MapType(
226229
additionalColumns,
227230
jsonPath);
228231
}
229-
else if (!IsSimpleType(type) &&
232+
else if (!isSimpleType &&
230233
(typeof(IDictionary).IsAssignableFrom(type) ||
231234
(type.IsGenericType && type.GetGenericTypeDefinition() == typeof(IDictionary<,>)) ||
232235
(type.IsGenericType &&
@@ -237,18 +240,16 @@ private static V1JSONSchemaProps MapType(
237240
props.Type = Object;
238241
props.XKubernetesPreserveUnknownFields = true;
239242
}
240-
else if (!IsSimpleType(type) &&
241-
type.IsGenericType &&
242-
typeof(IEnumerable<>).IsAssignableFrom(type.GetGenericTypeDefinition()))
243+
else if (!isSimpleType && IsGenericEnumerableType(type, out Type? closingType))
243244
{
244245
props.Type = Array;
245-
props.Items = MapType(type.GetGenericArguments()[0], additionalColumns, jsonPath);
246+
props.Items = MapType(closingType, additionalColumns, jsonPath);
246247
}
247248
else if (type == typeof(IntstrIntOrString))
248249
{
249250
props.XKubernetesIntOrString = true;
250251
}
251-
else if (!IsSimpleType(type))
252+
else if (!isSimpleType)
252253
{
253254
ProcessType(type, props, additionalColumns, jsonPath);
254255
}
@@ -348,5 +349,22 @@ private static bool IsSimpleType(Type type) =>
348349
IsSimpleType(type.GetGenericArguments()[0]));
349350

350351
private static string CamelCase(string str) => $"{str.Substring(0, 1).ToLower()}{str.Substring(1)}";
352+
353+
private static bool IsGenericEnumerableType(Type type, [NotNullWhen(true)] out Type? closingType)
354+
{
355+
if (type.IsGenericType && typeof(IEnumerable<>).IsAssignableFrom(type.GetGenericTypeDefinition()))
356+
{
357+
closingType = type.GetGenericArguments()[0];
358+
return true;
359+
}
360+
361+
closingType = type
362+
.GetInterfaces()
363+
.Where(t => t.IsGenericType && t.GetGenericTypeDefinition() == typeof(IEnumerable<>))
364+
.Select(t => t.GetGenericArguments()[0])
365+
.FirstOrDefault();
366+
367+
return closingType != null;
368+
}
351369
}
352370
}

tests/KubeOps.Test/Operator/Entities/CrdGeneration.Test.cs

Lines changed: 28 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,6 @@
77
using KubeOps.Operator.Commands.Generators;
88
using KubeOps.Operator.Entities.Extensions;
99
using KubeOps.Operator.Errors;
10-
using KubeOps.Operator.Services;
1110
using KubeOps.Test.TestEntities;
1211
using Xunit;
1312

@@ -74,30 +73,43 @@ public void Should_Set_The_Correct_Type_And_Format_For_Types(string fieldName, s
7473
nullableField.Nullable.Should().BeTrue();
7574
}
7675

77-
[Fact]
78-
public void Should_Set_The_Correct_Array_Type()
79-
{
76+
[Theory]
77+
[InlineData(nameof(TestSpecEntitySpec.StringArray), "string", null)]
78+
[InlineData(nameof(TestSpecEntitySpec.NullableStringArray), "string", true)]
79+
[InlineData(nameof(TestSpecEntitySpec.EnumerableInteger), "integer", null)]
80+
[InlineData(nameof(TestSpecEntitySpec.EnumerableNullableInteger), "integer", null)]
81+
[InlineData(nameof(TestSpecEntitySpec.IntegerList), "integer", null)]
82+
[InlineData(nameof(TestSpecEntitySpec.IntegerHashSet), "integer", null)]
83+
[InlineData(nameof(TestSpecEntitySpec.IntegerISet), "integer", null)]
84+
[InlineData(nameof(TestSpecEntitySpec.IntegerIReadOnlySet), "integer", null)]
85+
public void Should_Set_The_Correct_Array_Type(string property, string expectedType, bool? expectedNullable)
86+
{
87+
var propertyName = char.ToLowerInvariant(property[0]) + property[1..];
8088
var crd = _testSpecEntity.CreateCrd();
8189
var specProperties = crd.Spec.Versions.First().Schema.OpenAPIV3Schema.Properties["spec"];
8290

83-
var normalField = specProperties.Properties["stringArray"];
91+
var normalField = specProperties.Properties[propertyName];
8492
normalField.Type.Should().Be("array");
85-
(normalField.Items as V1JSONSchemaProps)?.Type?.Should().Be("string");
86-
normalField.Nullable.Should().BeNull();
87-
88-
var nullableField = specProperties.Properties["nullableStringArray"];
89-
nullableField.Type.Should().Be("array");
90-
(nullableField.Items as V1JSONSchemaProps)?.Type?.Should().Be("string");
91-
nullableField.Nullable.Should().BeTrue();
93+
(normalField.Items as V1JSONSchemaProps)?.Type?.Should().Be(expectedType);
94+
normalField.Nullable.Should().Be(expectedNullable);
9295
}
9396

94-
[Fact]
95-
public void Should_Set_The_Correct_Complex_Array_Type()
96-
{
97+
[Theory]
98+
[InlineData(nameof(TestSpecEntitySpec.ComplexItemsEnumerable))]
99+
[InlineData(nameof(TestSpecEntitySpec.ComplexItemsList))]
100+
[InlineData(nameof(TestSpecEntitySpec.ComplexItemsIList))]
101+
[InlineData(nameof(TestSpecEntitySpec.ComplexItemsReadOnlyList))]
102+
[InlineData(nameof(TestSpecEntitySpec.ComplexItemsCollection))]
103+
[InlineData(nameof(TestSpecEntitySpec.ComplexItemsICollection))]
104+
[InlineData(nameof(TestSpecEntitySpec.ComplexItemsReadOnlyCollection))]
105+
[InlineData(nameof(TestSpecEntitySpec.ComplexItemsDerivedList))]
106+
public void Should_Set_The_Correct_Complex_Array_Type(string property)
107+
{
108+
var propertyName = char.ToLowerInvariant(property[0]) + property[1..];
97109
var crd = _testSpecEntity.CreateCrd();
98110
var specProperties = crd.Spec.Versions.First().Schema.OpenAPIV3Schema.Properties["spec"];
99111

100-
var complexItemsArray = specProperties.Properties["complexItems"];
112+
var complexItemsArray = specProperties.Properties[propertyName];
101113
complexItemsArray.Type.Should().Be("array");
102114
(complexItemsArray.Items as V1JSONSchemaProps)?.Type?.Should().Be("object");
103115
complexItemsArray.Nullable.Should().BeNull();

tests/KubeOps.Test/TestEntities/TestSpecEntity.cs

Lines changed: 37 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
using System;
22
using System.Collections;
33
using System.Collections.Generic;
4+
using System.Collections.ObjectModel;
45
using System.Linq;
56
using k8s.Models;
67
using KubeOps.Operator.Entities;
@@ -11,10 +12,22 @@ namespace KubeOps.Test.TestEntities
1112
[Description("This is the Spec Class Description")]
1213
public class TestSpecEntitySpec
1314
{
14-
public string[] StringArray { get; set; } = new string[0];
15+
public string[] StringArray { get; set; } = Array.Empty<string>();
1516

1617
public string[]? NullableStringArray { get; set; }
1718

19+
public IEnumerable<int> EnumerableInteger { get; set; } = Array.Empty<int>();
20+
21+
public IEnumerable<int?> EnumerableNullableInteger { get; set; } = Array.Empty<int?>();
22+
23+
public IntegerList IntegerList { get; set; } = new();
24+
25+
public HashSet<int> IntegerHashSet { get; set; } = new();
26+
27+
public ISet<int> IntegerISet { get; set; } = new HashSet<int>();
28+
29+
public IReadOnlySet<int> IntegerIReadOnlySet { get; set; } = new HashSet<int>();
30+
1831
[AdditionalPrinterColumn]
1932
public string NormalString { get; set; } = string.Empty;
2033

@@ -80,7 +93,21 @@ public class TestSpecEntitySpec
8093
[Required]
8194
public int Required { get; set; }
8295

83-
public IEnumerable<TestItem> ComplexItems { get; set; } = Enumerable.Empty<TestItem>();
96+
public IEnumerable<TestItem> ComplexItemsEnumerable { get; set; } = Enumerable.Empty<TestItem>();
97+
98+
public List<TestItem> ComplexItemsList { get; set; } = new();
99+
100+
public IList<TestItem> ComplexItemsIList { get; set; } = Array.Empty<TestItem>();
101+
102+
public IReadOnlyList<TestItem> ComplexItemsReadOnlyList { get; set; } = Array.Empty<TestItem>();
103+
104+
public Collection<TestItem> ComplexItemsCollection { get; set; } = new();
105+
106+
public ICollection<TestItem> ComplexItemsICollection { get; set; } = Array.Empty<TestItem>();
107+
108+
public IReadOnlyCollection<TestItem> ComplexItemsReadOnlyCollection { get; set; } = Array.Empty<TestItem>();
109+
110+
public TestItemList ComplexItemsDerivedList { get; set; } = new();
84111

85112
public IDictionary Dictionary { get; set; } = new Dictionary<string, string>();
86113

@@ -117,4 +144,12 @@ public class TestItem
117144
public string Item { get; set; } = null!;
118145
public string Extra { get; set; } = null!;
119146
}
147+
148+
public class TestItemList : List<TestItem>
149+
{
150+
}
151+
152+
public class IntegerList : Collection<int>
153+
{
154+
}
120155
}

0 commit comments

Comments
 (0)