diff --git a/Directory.Build.props b/Directory.Build.props index 2a5ed0116..38297f8ce 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -29,6 +29,7 @@ true portable 13.0 + enable diff --git a/src/LibKubernetesGenerator/TypeHelper.cs b/src/LibKubernetesGenerator/TypeHelper.cs index db27dab97..5d4bb5e28 100644 --- a/src/LibKubernetesGenerator/TypeHelper.cs +++ b/src/LibKubernetesGenerator/TypeHelper.cs @@ -23,7 +23,7 @@ public void RegisterHelper(ScriptObject scriptObject) scriptObject.Import(nameof(IfType), new Func(IfType)); } - private string GetDotNetType(JsonObjectType jsonType, string name, bool required, string format) + private string GetDotNetType(JsonObjectType jsonType, string name, bool required, string? format) { if (name == "pretty" && !required) { @@ -81,7 +81,7 @@ private string GetDotNetType(JsonObjectType jsonType, string name, bool required switch (format) { case "byte": - return "byte[]"; + return required ? "byte[]" : "byte[]?"; case "date-time": // eventTime is required but should be optional, see https://github.com/kubernetes-client/csharp/issues/1197 @@ -100,58 +100,58 @@ private string GetDotNetType(JsonObjectType jsonType, string name, bool required } } - return "string"; + return required ? "string" : "string?"; case JsonObjectType.Object: - return "object"; + return required ? "object" : "object?"; default: throw new NotSupportedException(); } } - private string GetDotNetType(JsonSchema schema, JsonSchemaProperty parent) + private string GetDotNetType(JsonSchema? schema, JsonSchemaProperty parent, bool isCollectionItem = false) { if (schema != null) { - if (schema.IsArray) + if (schema.IsArray && schema.Item != null) { - return $"IList<{GetDotNetType(schema.Item, parent)}>"; + return $"IList<{GetDotNetType(schema.Item, parent, isCollectionItem: true)}>?"; } if (schema.IsDictionary && schema.AdditionalPropertiesSchema != null) { - return $"IDictionary"; + return $"IDictionary?"; } - if (schema?.Reference != null) + if (schema.Reference != null) { - return classNameHelper.GetClassNameForSchemaDefinition(schema.Reference); + var typeName = classNameHelper.GetClassNameForSchemaDefinition(schema.Reference); + // Collection items are always non-nullable, unless we're at the root level + return (isCollectionItem || parent.IsRequired) ? typeName : typeName + "?"; } - if (schema != null) - { - return GetDotNetType(schema.Type, parent.Name, parent.IsRequired, schema.Format); - } + return GetDotNetType(schema.Type, parent.Name, isCollectionItem || parent.IsRequired, schema.Format); } - return GetDotNetType(parent.Type, parent.Name, parent.IsRequired, parent.Format); + return GetDotNetType(parent.Type, parent.Name, isCollectionItem || parent.IsRequired, parent.Format); } public string GetDotNetType(JsonSchemaProperty p) { if (p.Reference != null) { - return classNameHelper.GetClassNameForSchemaDefinition(p.Reference); + var typeName = classNameHelper.GetClassNameForSchemaDefinition(p.Reference); + return p.IsRequired ? typeName : typeName + "?"; } - if (p.IsArray) + if (p.IsArray && p.Item != null) { - // getType - return $"IList<{GetDotNetType(p.Item, p)}>"; + // getType - items in arrays are non-nullable + return $"IList<{GetDotNetType(p.Item, p, isCollectionItem: true)}>?"; } if (p.IsDictionary && p.AdditionalPropertiesSchema != null) { - return $"IDictionary"; + return $"IDictionary?"; } return GetDotNetType(p.Type, p.Name, p.IsRequired, p.Format); @@ -161,7 +161,8 @@ public string GetDotNetTypeOpenApiParameter(OpenApiParameter parameter) { if (parameter.Schema?.Reference != null) { - return classNameHelper.GetClassNameForSchemaDefinition(parameter.Schema.Reference); + var typeName = classNameHelper.GetClassNameForSchemaDefinition(parameter.Schema.Reference); + return parameter.IsRequired ? typeName : typeName + "?"; } else if (parameter.Schema != null) { diff --git a/tests/KubernetesClient.Tests/NullableReferenceTypesTests.cs b/tests/KubernetesClient.Tests/NullableReferenceTypesTests.cs new file mode 100644 index 000000000..a1f142932 --- /dev/null +++ b/tests/KubernetesClient.Tests/NullableReferenceTypesTests.cs @@ -0,0 +1,95 @@ +using System.Collections.Generic; +using k8s.Models; +using Xunit; + +namespace k8s.Tests +{ + public class NullableReferenceTypesTests + { + [Fact] + public void ContainerVolumeMountsIsNullableProperty() + { + // Arrange & Act + var container = new V1Container(); + + // Assert + // VolumeMounts should be null by default (nullable property) + Assert.Null(container.VolumeMounts); + + // This should not throw NullReferenceException anymore - users should check for null + // container.VolumeMounts.Add(new V1VolumeMount()); // This would throw + + // Proper usage: Initialize the list first + container.VolumeMounts = new List + { + new V1VolumeMount(), + }; + + Assert.NotNull(container.VolumeMounts); + Assert.Single(container.VolumeMounts); + } + + [Fact] + public void ContainerNameIsRequiredProperty() + { + // Arrange & Act + var container = new V1Container + { + Name = "test-container", + }; + + // Assert + // Name is a required property (non-nullable string) + Assert.NotNull(container.Name); + Assert.Equal("test-container", container.Name); + } + + [Fact] + public void ContainerImageIsOptionalProperty() + { + // Arrange & Act + var container = new V1Container(); + + // Assert + // Image is optional (nullable string) + Assert.Null(container.Image); + + container.Image = "nginx:latest"; + Assert.Equal("nginx:latest", container.Image); + } + + [Fact] + public void ContainerLifecycleIsOptionalComplexProperty() + { + // Arrange & Act + var container = new V1Container(); + + // Assert + // Lifecycle is optional (nullable reference type) + Assert.Null(container.Lifecycle); + + container.Lifecycle = new V1Lifecycle(); + Assert.NotNull(container.Lifecycle); + } + + [Fact] + public void ContainerCollectionItemsAreNonNullable() + { + // Arrange + var container = new V1Container + { + // Initialize the list - the list itself can be null, but items cannot be null + VolumeMounts = new List + { + new V1VolumeMount { Name = "vol1", MountPath = "/data", }, + new V1VolumeMount { Name = "vol2", MountPath = "/config", }, + }, + }; + + // Act & Assert + Assert.NotNull(container.VolumeMounts); + Assert.Equal(2, container.VolumeMounts.Count); + Assert.All(container.VolumeMounts, vm => Assert.NotNull(vm)); + } + } +}