diff --git a/src/Kiota.Builder/CodeDOM/CodeEnum.cs b/src/Kiota.Builder/CodeDOM/CodeEnum.cs index c4c48550e5..5352b2a186 100644 --- a/src/Kiota.Builder/CodeDOM/CodeEnum.cs +++ b/src/Kiota.Builder/CodeDOM/CodeEnum.cs @@ -2,6 +2,7 @@ using System.Collections.Concurrent; using System.Collections.Generic; using System.Linq; +using Microsoft.OpenApi; namespace Kiota.Builder.CodeDOM; #pragma warning disable CA1711 @@ -42,4 +43,9 @@ public CodeConstant? CodeEnumObject { get; set; } + + public JsonSchemaType? BackingType + { + get; set; + } } diff --git a/src/Kiota.Builder/Extensions/OpenApiSchemaExtensions.cs b/src/Kiota.Builder/Extensions/OpenApiSchemaExtensions.cs index 8d8f93bf25..72408a9758 100644 --- a/src/Kiota.Builder/Extensions/OpenApiSchemaExtensions.cs +++ b/src/Kiota.Builder/Extensions/OpenApiSchemaExtensions.cs @@ -33,6 +33,8 @@ public static IEnumerable GetSchemaNames(this IOpenApiSchema schema, boo return schema switch { OpenApiSchemaReference reference => reference.Reference?.Id, + // Quickfix for https://github.com/microsoft/kiota/issues/6776 ToDo: Properly handle nullable types in accordance with OpenAPI 3. + OpenApiSchema {OneOf: [OpenApiSchema{Type: JsonSchemaType.Null}, OpenApiSchemaReference reference]} => reference.Reference?.Id, OpenApiSchema s when s.GetMergedSchemaOriginalReferenceId() is string originalReferenceId => originalReferenceId, _ => null, }; @@ -59,6 +61,8 @@ public static bool IsReferencedSchema(this IOpenApiSchema schema) return schema switch { OpenApiSchemaReference => true, + // Quickfix for https://github.com/microsoft/kiota/issues/6776 ToDo: Properly handle nullable types in accordance with OpenAPI 3. + OpenApiSchema {OneOf: [OpenApiSchema{Type: JsonSchemaType.Null}, OpenApiSchemaReference reference]} => true, _ => false, }; } @@ -318,7 +322,7 @@ public static bool IsSemanticallyMeaningful(this IOpenApiSchema schema, bool ign return schema.HasAnyProperty() || (!ignoreEnums && schema.Enum is { Count: > 0 }) || (!ignoreArrays && schema.Items != null) || - (!ignoreType && schema.Type is not null && + (!ignoreType && schema.Type is not null and not JsonSchemaType.Null && ((ignoreNullableObjects && !schema.IsObjectType()) || !ignoreNullableObjects)) || !string.IsNullOrEmpty(schema.Format) || diff --git a/src/Kiota.Builder/KiotaBuilder.cs b/src/Kiota.Builder/KiotaBuilder.cs index 94a77921f2..28fa2eb7a5 100644 --- a/src/Kiota.Builder/KiotaBuilder.cs +++ b/src/Kiota.Builder/KiotaBuilder.cs @@ -2021,6 +2021,7 @@ private CodeElement AddModelDeclarationIfDoesntExist(OpenApiUrlTreeNode currentN currentNode.GetPathItemDescription(Constants.DefaultOpenApiLabel), }, Deprecation = schema.GetDeprecationInformation(), + BackingType = schema.Type, }; SetEnumOptions(schema, newEnum); return currentNamespace.AddEnum(newEnum).First(); diff --git a/src/Kiota.Builder/Writers/CSharp/CodeMethodWriter.cs b/src/Kiota.Builder/Writers/CSharp/CodeMethodWriter.cs index f80d43ceb3..0e6befab48 100644 --- a/src/Kiota.Builder/Writers/CSharp/CodeMethodWriter.cs +++ b/src/Kiota.Builder/Writers/CSharp/CodeMethodWriter.cs @@ -4,6 +4,7 @@ using Kiota.Builder.CodeDOM; using Kiota.Builder.Extensions; using Kiota.Builder.OrderComparers; +using Microsoft.OpenApi; namespace Kiota.Builder.Writers.CSharp; @@ -141,10 +142,21 @@ private void WriteFactoryMethodBodyForUnionModel(CodeMethod codeElement, CodeCla } else if (propertyType.TypeDefinition is CodeClass && propertyType.IsCollection || propertyType.TypeDefinition is null || propertyType.TypeDefinition is CodeEnum) { + var readerReference = parseNodeParameter.Name.ToFirstCharacterLowerCase(); + var selfReference = $"{ResultVarName}.{property.Name.ToFirstCharacterUpperCase()}"; + var deserializationMethodName = GetDeserializationMethodName(propertyType, codeElement); var typeName = conventions.GetTypeString(propertyType, codeElement, true, (propertyType.TypeDefinition is CodeEnum || conventions.IsPrimitiveType(propertyType.Name)) && propertyType.CollectionKind is not CodeTypeBase.CodeTypeCollectionKind.None); var valueVarName = $"{property.Name.ToFirstCharacterLowerCase()}Value"; - writer.WriteLine($"{(includeElse ? "else " : string.Empty)}if({parseNodeParameter.Name.ToFirstCharacterLowerCase()}.{GetDeserializationMethodName(propertyType, codeElement)} is {typeName} {valueVarName})"); - writer.WriteBlock(lines: $"{ResultVarName}.{property.Name.ToFirstCharacterUpperCase()} = {valueVarName};"); + if (propertyType.TypeDefinition is CodeType { TypeDefinition: CodeEnum {BackingType: JsonSchemaType.Integer}}) + { + writer.WriteLine($"{(includeElse ? "else " : string.Empty)}if({readerReference}.{deserializationMethodName} is int {valueVarName})"); + writer.WriteBlock(lines: $"{selfReference} = ({typeName}){valueVarName};"); + } + else + { + writer.WriteLine($"{(includeElse ? "else " : string.Empty)}if({readerReference}.{deserializationMethodName} is {typeName} {valueVarName})"); + writer.WriteBlock(lines: $"{selfReference} = {valueVarName};"); + } } if (!includeElse) includeElse = true; @@ -162,10 +174,21 @@ private void WriteFactoryMethodBodyForIntersectionModel(CodeMethod codeElement, { if (property.Type is CodeType propertyType) { + var readerReference = parseNodeParameter.Name.ToFirstCharacterLowerCase(); + var selfReference = $"{ResultVarName}.{property.Name.ToFirstCharacterUpperCase()}"; + var deserializationMethodName = GetDeserializationMethodName(propertyType, codeElement); var typeName = conventions.GetTypeString(propertyType, codeElement, true, propertyType.TypeDefinition is CodeEnum && propertyType.CollectionKind is not CodeTypeBase.CodeTypeCollectionKind.None); var valueVarName = $"{property.Name.ToFirstCharacterLowerCase()}Value"; - writer.WriteLine($"{(includeElse ? "else " : string.Empty)}if({parseNodeParameter.Name.ToFirstCharacterLowerCase()}.{GetDeserializationMethodName(propertyType, codeElement)} is {typeName} {valueVarName})"); - writer.WriteBlock(lines: $"{ResultVarName}.{property.Name.ToFirstCharacterUpperCase()} = {valueVarName};"); + if (propertyType.TypeDefinition is CodeType { TypeDefinition: CodeEnum {BackingType: JsonSchemaType.Integer}}) + { + writer.WriteLine($"{(includeElse ? "else " : string.Empty)}if({readerReference}.{deserializationMethodName} is int {valueVarName})"); + writer.WriteBlock(lines: $"{selfReference} = ({typeName}){valueVarName};"); + } + else + { + writer.WriteLine($"{(includeElse ? "else " : string.Empty)}if({readerReference}.{deserializationMethodName} is {typeName} {valueVarName})"); + writer.WriteBlock(lines: $"{selfReference} = {valueVarName};"); + } } if (!includeElse) includeElse = true; @@ -349,7 +372,13 @@ private void WriteDeserializerBodyForInheritedModel(bool shouldHide, CodeMethod .Where(static x => !x.ExistsInBaseType) .OrderBy(static x => x.Name, StringComparer.Ordinal)) { - writer.WriteLine($"{{ \"{otherProp.WireName}\", n => {{ {otherProp.Name.ToFirstCharacterUpperCase()} = n.{GetDeserializationMethodName(otherProp.Type, codeElement)}; }} }},"); + if (otherProp is {Type: CodeType { TypeDefinition: CodeEnum {BackingType: JsonSchemaType.Integer}} propertyType}) + { + var typeName = conventions.GetTypeString(propertyType, codeElement, true, (propertyType.TypeDefinition is CodeEnum || conventions.IsPrimitiveType(propertyType.Name)) && propertyType.CollectionKind is not CodeTypeBase.CodeTypeCollectionKind.None); + writer.WriteLine($"{{ \"{otherProp.WireName}\", n => {{ {otherProp.Name.ToFirstCharacterUpperCase()} = ({typeName}?) n.{GetDeserializationMethodName(otherProp.Type, codeElement)}; }} }},"); + } + else + writer.WriteLine($"{{ \"{otherProp.WireName}\", n => {{ {otherProp.Name.ToFirstCharacterUpperCase()} = n.{GetDeserializationMethodName(otherProp.Type, codeElement)}; }} }},"); } writer.CloseBlock("};"); } @@ -370,7 +399,10 @@ private string GetDeserializationMethodName(CodeTypeBase propType, CodeMethod me return $"GetCollectionOfObjectValues<{propertyType}>({propertyType}.CreateFromDiscriminatorValue){collectionMethod}"; } else if (currentType.TypeDefinition is CodeEnum enumType) - return $"GetEnumValue<{enumType.GetFullName()}>()"; + if (enumType.BackingType is JsonSchemaType.Integer) + return $"GetIntValue()"; + else + return $"GetEnumValue<{enumType.GetFullName()}>()"; } return propertyType switch { @@ -482,7 +514,10 @@ private void WriteSerializerBodyForInheritedModel(bool shouldHide, CodeMethod me .OrderBy(static x => x.Name)) { var serializationMethodName = GetSerializationMethodName(otherProp.Type, method); - writer.WriteLine($"writer.{serializationMethodName}(\"{otherProp.WireName}\", {otherProp.Name.ToFirstCharacterUpperCase()});"); + if (otherProp.Type is CodeType{TypeDefinition: CodeEnum {BackingType: JsonSchemaType.Integer}}) + writer.WriteLine($"writer.{serializationMethodName}(\"{otherProp.WireName}\", (int?){otherProp.Name.ToFirstCharacterUpperCase()});"); + else + writer.WriteLine($"writer.{serializationMethodName}(\"{otherProp.WireName}\", {otherProp.Name.ToFirstCharacterUpperCase()});"); } } private void WriteSerializerBodyForUnionModel(CodeMethod method, CodeClass parentClass, LanguageWriter writer) @@ -494,8 +529,12 @@ private void WriteSerializerBodyForUnionModel(CodeMethod method, CodeClass paren .OrderBy(static x => x, CodePropertyTypeForwardComparer) .ThenBy(static x => x.Name)) { + var serializationMethodName = GetSerializationMethodName(otherProp.Type, method); writer.WriteLine($"{(includeElse ? "else " : string.Empty)}if({otherProp.Name.ToFirstCharacterUpperCase()} != null)"); - writer.WriteBlock(lines: $"writer.{GetSerializationMethodName(otherProp.Type, method)}(null, {otherProp.Name.ToFirstCharacterUpperCase()});"); + if (otherProp.Type is CodeType{TypeDefinition: CodeEnum {BackingType: JsonSchemaType.Integer}}) + writer.WriteBlock(lines: $"writer.{serializationMethodName}(\"{otherProp.WireName}\", (int?){otherProp.Name.ToFirstCharacterUpperCase()});"); + else + writer.WriteBlock(lines: $"writer.{serializationMethodName}(null, {otherProp.Name.ToFirstCharacterUpperCase()});"); if (!includeElse) includeElse = true; } @@ -510,8 +549,12 @@ private void WriteSerializerBodyForIntersectionModel(CodeMethod method, CodeClas .OrderBy(static x => x, CodePropertyTypeBackwardComparer) .ThenBy(static x => x.Name)) { + var serializationMethodName = GetSerializationMethodName(otherProp.Type, method); writer.WriteLine($"{(includeElse ? "else " : string.Empty)}if({otherProp.Name.ToFirstCharacterUpperCase()} != null)"); - writer.WriteBlock(lines: $"writer.{GetSerializationMethodName(otherProp.Type, method)}(null, {otherProp.Name.ToFirstCharacterUpperCase()});"); + if (otherProp.Type is CodeType{TypeDefinition: CodeEnum {BackingType: JsonSchemaType.Integer}}) + writer.WriteBlock(lines: $"writer.{serializationMethodName}(\"{otherProp.WireName}\", (int?){otherProp.Name.ToFirstCharacterUpperCase()});"); + else + writer.WriteBlock(lines: $"writer.{serializationMethodName}(null, {otherProp.Name.ToFirstCharacterUpperCase()});"); if (!includeElse) includeElse = true; } @@ -529,7 +572,12 @@ private void WriteSerializerBodyForIntersectionModel(CodeMethod method, CodeClas .Select(static x => x.Name.ToFirstCharacterUpperCase()) .OrderBy(static x => x) .Aggregate(static (x, y) => $"{x}, {y}"); - writer.WriteLine($"writer.{GetSerializationMethodName(complexProperties.First().Type, method)}(null, {propertiesNames});"); + var prop = complexProperties.First(); + var serializationMethodName = GetSerializationMethodName(prop.Type, method); + if (prop.Type is CodeType{TypeDefinition: CodeEnum {BackingType: JsonSchemaType.Integer}}) + writer.WriteLine($"writer.{serializationMethodName}(\"{prop.WireName}\", (int?){prop.Name.ToFirstCharacterUpperCase()});"); + else + writer.WriteLine($"writer.{GetSerializationMethodName(complexProperties.First().Type, method)}(null, {propertiesNames});"); if (includeElse) { writer.CloseBlock(); @@ -678,7 +726,10 @@ private string GetSerializationMethodName(CodeTypeBase propType, CodeMethod meth else return $"WriteCollectionOfObjectValues<{propertyType}>"; else if (currentType.TypeDefinition is CodeEnum enumType) - return $"WriteEnumValue<{enumType.GetFullName()}>"; + if (enumType.BackingType is JsonSchemaType.Integer) + return $"WriteIntValue"; + else + return $"WriteEnumValue<{enumType.GetFullName()}>"; } return propertyType switch diff --git a/tests/Kiota.Builder.Tests/Extensions/OpenApiSchemaExtensionsTests.cs b/tests/Kiota.Builder.Tests/Extensions/OpenApiSchemaExtensionsTests.cs index 48657c3b19..8374fed434 100644 --- a/tests/Kiota.Builder.Tests/Extensions/OpenApiSchemaExtensionsTests.cs +++ b/tests/Kiota.Builder.Tests/Extensions/OpenApiSchemaExtensionsTests.cs @@ -1,4 +1,5 @@ using System.Collections.Generic; +using System.Linq; using Kiota.Builder.Extensions; using Microsoft.OpenApi; using Xunit; @@ -80,6 +81,18 @@ public void ExternalReferencesAreSupported() Assert.True(mockSchema.IsReferencedSchema()); } [Fact] + public void ExternalNullableReferencesAreSupported() + { + var mockSchema = new OpenApiSchema + { + OneOf = [ + new OpenApiSchema{Type = JsonSchemaType.Null}, + new OpenApiSchemaReference("example.json#/path/to/component", null, "http://example.com/example.json") + ] + }; + Assert.True(mockSchema.IsReferencedSchema()); + } + [Fact] public void SchemasAreNotConsideredReferences() { var mockSchema = new OpenApiSchema(); @@ -356,6 +369,20 @@ public void GetReferenceIdsOneOf() Assert.Contains("microsoft.graph.user", names); } [Fact] + public void GetReferenceIdsOneOfNullable() + { + var schema = new OpenApiSchema + { + OneOf = [ + new OpenApiSchema{Type = JsonSchemaType.Null}, + new OpenApiSchemaReference("microsoft.graph.user") + ] + }; + var names = schema.GetSchemaReferenceIds(); + var name = Assert.Single(names); + Assert.Contains("microsoft.graph.user", name); + } + [Fact] public void GetReferenceIdsItems() { var schema = new OpenApiSchema diff --git a/tests/Kiota.Builder.Tests/Writers/CSharp/YamlToCSharpTests.cs b/tests/Kiota.Builder.Tests/Writers/CSharp/YamlToCSharpTests.cs new file mode 100644 index 0000000000..e489a3edfc --- /dev/null +++ b/tests/Kiota.Builder.Tests/Writers/CSharp/YamlToCSharpTests.cs @@ -0,0 +1,776 @@ +#nullable enable +using System; +using System.IO; +using System.Linq; +using System.Net.Http; +using System.Text; +using System.Threading.Tasks; +using Kiota.Builder.Configuration; +using Kiota.Builder.Writers.CSharp; +using Microsoft.Extensions.Logging; +using Microsoft.OpenApi; +using Moq; +using Xunit; + +namespace Kiota.Builder.Tests.Writers.CSharp; + +public sealed class YamlToCSharpTests : IDisposable +{ + public void Dispose() + { + _httpClient.Dispose(); + } + + private readonly HttpClient _httpClient = new(); + + public const string NullableObjectInOpenApi3 = """ + { + "openapi": "3.1.1", + "info": { + "title": "WebApplication2 | v1", + "version": "1.0.0" + }, + "servers": [ + { + "url": "http://localhost:5088/" + } + ], + "paths": { + "/Sample": { + "get": { + "tags": [ + "Sample" + ], + "operationId": "GetWeatherForecast", + "responses": { + "200": { + "description": "OK", + "content": { + "text/plain": { + "schema": { + "$ref": "#/components/schemas/Sample" + } + }, + "application/json": { + "schema": { + "$ref": "#/components/schemas/Sample" + } + }, + "text/json": { + "schema": { + "$ref": "#/components/schemas/Sample" + } + } + } + } + } + } + } + }, + "components": { + "schemas": { + "Nested": { + "required": [ + "value" + ], + "type": "object", + "properties": { + "value": { + "type": "string" + } + } + }, + "Sample": { + "required": [ + "immediate", + "nestedNonNullable", + "nestedNullable" + ], + "type": "object", + "properties": { + "immediate": { + "type": "string" + }, + "nestedNonNullable": { + "$ref": "#/components/schemas/Nested" + }, + "nestedNullable": { + "oneOf": [ + { + "type": "null" + }, + { + "$ref": "#/components/schemas/Nested" + } + ] + } + } + } + } + }, + "tags": [ + { + "name": "Sample" + } + ] + } + """; + + public const string NullableObjectInOpenApi3_Models_Sample = """ + // + #pragma warning disable CS0618 + using Microsoft.Kiota.Abstractions.Extensions; + using Microsoft.Kiota.Abstractions.Serialization; + using System.Collections.Generic; + using System.IO; + using System; + namespace ApiSdk.Models + { + [global::System.CodeDom.Compiler.GeneratedCode("Kiota", "1.0.0")] + #pragma warning disable CS1591 + public partial class Sample : IAdditionalDataHolder, IParsable + #pragma warning restore CS1591 + { + /// Stores additional data not described in the OpenAPI description found when deserializing. Can be used for serialization as well. + public IDictionary AdditionalData { get; set; } + /// The immediate property + #if NETSTANDARD2_1_OR_GREATER || NETCOREAPP3_1_OR_GREATER + #nullable enable + public string? Immediate { get; set; } + #nullable restore + #else + public string Immediate { get; set; } + #endif + /// The nestedNonNullable property + #if NETSTANDARD2_1_OR_GREATER || NETCOREAPP3_1_OR_GREATER + #nullable enable + public global::ApiSdk.Models.Nested? NestedNonNullable { get; set; } + #nullable restore + #else + public global::ApiSdk.Models.Nested NestedNonNullable { get; set; } + #endif + /// The nestedNullable property + #if NETSTANDARD2_1_OR_GREATER || NETCOREAPP3_1_OR_GREATER + #nullable enable + public global::ApiSdk.Models.Nested? NestedNullable { get; set; } + #nullable restore + #else + public global::ApiSdk.Models.Nested NestedNullable { get; set; } + #endif + /// + /// Instantiates a new and sets the default values. + /// + public Sample() + { + AdditionalData = new Dictionary(); + } + /// + /// Creates a new instance of the appropriate class based on discriminator value + /// + /// A + /// The parse node to use to read the discriminator value and create the object + public static global::ApiSdk.Models.Sample CreateFromDiscriminatorValue(IParseNode parseNode) + { + if(ReferenceEquals(parseNode, null)) throw new ArgumentNullException(nameof(parseNode)); + return new global::ApiSdk.Models.Sample(); + } + /// + /// The deserialization information for the current model + /// + /// A IDictionary<string, Action<IParseNode>> + public virtual IDictionary> GetFieldDeserializers() + { + return new Dictionary> + { + { "immediate", n => { Immediate = n.GetStringValue(); } }, + { "nestedNonNullable", n => { NestedNonNullable = n.GetObjectValue(global::ApiSdk.Models.Nested.CreateFromDiscriminatorValue); } }, + { "nestedNullable", n => { NestedNullable = n.GetObjectValue(global::ApiSdk.Models.Nested.CreateFromDiscriminatorValue); } }, + }; + } + /// + /// Serializes information the current object + /// + /// Serialization writer to use to serialize this model + public virtual void Serialize(ISerializationWriter writer) + { + if(ReferenceEquals(writer, null)) throw new ArgumentNullException(nameof(writer)); + writer.WriteStringValue("immediate", Immediate); + writer.WriteObjectValue("nestedNonNullable", NestedNonNullable); + writer.WriteObjectValue("nestedNullable", NestedNullable); + writer.WriteAdditionalData(AdditionalData); + } + } + } + #pragma warning restore CS0618 + + """; + + public const string NullableObjectInOpenApi3_Reverse = """ + { + "openapi": "3.1.1", + "info": { + "title": "WebApplication2 | v1", + "version": "1.0.0" + }, + "servers": [ + { + "url": "http://localhost:5088/" + } + ], + "paths": { + "/Sample": { + "get": { + "tags": [ + "Sample" + ], + "operationId": "GetWeatherForecast", + "responses": { + "200": { + "description": "OK", + "content": { + "text/plain": { + "schema": { + "$ref": "#/components/schemas/Sample" + } + }, + "application/json": { + "schema": { + "$ref": "#/components/schemas/Sample" + } + }, + "text/json": { + "schema": { + "$ref": "#/components/schemas/Sample" + } + } + } + } + } + } + } + }, + "components": { + "schemas": { + "Nested": { + "required": [ + "value" + ], + "type": "object", + "properties": { + "value": { + "type": "string" + } + } + }, + "Sample": { + "required": [ + "immediate", + "nestedNonNullable", + "nestedNullable" + ], + "type": "object", + "properties": { + "immediate": { + "type": "string" + }, + "nestedNonNullable": { + "$ref": "#/components/schemas/Nested" + }, + "nestedNullable": { + "oneOf": [ + { + "$ref": "#/components/schemas/Nested" + }, + { + "type": "null" + } + ] + } + } + } + } + }, + "tags": [ + { + "name": "Sample" + } + ] + } + """; + + public const string NullableObjectInOpenApi3_Reverse_Models_Sample = """ + // + #pragma warning disable CS0618 + using Microsoft.Kiota.Abstractions.Extensions; + using Microsoft.Kiota.Abstractions.Serialization; + using System.Collections.Generic; + using System.IO; + using System; + namespace ApiSdk.Models + { + [global::System.CodeDom.Compiler.GeneratedCode("Kiota", "1.0.0")] + #pragma warning disable CS1591 + public partial class Sample : IAdditionalDataHolder, IParsable + #pragma warning restore CS1591 + { + /// Stores additional data not described in the OpenAPI description found when deserializing. Can be used for serialization as well. + public IDictionary AdditionalData { get; set; } + /// The immediate property + #if NETSTANDARD2_1_OR_GREATER || NETCOREAPP3_1_OR_GREATER + #nullable enable + public string? Immediate { get; set; } + #nullable restore + #else + public string Immediate { get; set; } + #endif + /// The nestedNonNullable property + #if NETSTANDARD2_1_OR_GREATER || NETCOREAPP3_1_OR_GREATER + #nullable enable + public global::ApiSdk.Models.Nested? NestedNonNullable { get; set; } + #nullable restore + #else + public global::ApiSdk.Models.Nested NestedNonNullable { get; set; } + #endif + /// The nestedNullable property + #if NETSTANDARD2_1_OR_GREATER || NETCOREAPP3_1_OR_GREATER + #nullable enable + public global::ApiSdk.Models.Nested? NestedNullable { get; set; } + #nullable restore + #else + public global::ApiSdk.Models.Nested NestedNullable { get; set; } + #endif + /// + /// Instantiates a new and sets the default values. + /// + public Sample() + { + AdditionalData = new Dictionary(); + } + /// + /// Creates a new instance of the appropriate class based on discriminator value + /// + /// A + /// The parse node to use to read the discriminator value and create the object + public static global::ApiSdk.Models.Sample CreateFromDiscriminatorValue(IParseNode parseNode) + { + if(ReferenceEquals(parseNode, null)) throw new ArgumentNullException(nameof(parseNode)); + return new global::ApiSdk.Models.Sample(); + } + /// + /// The deserialization information for the current model + /// + /// A IDictionary<string, Action<IParseNode>> + public virtual IDictionary> GetFieldDeserializers() + { + return new Dictionary> + { + { "immediate", n => { Immediate = n.GetStringValue(); } }, + { "nestedNonNullable", n => { NestedNonNullable = n.GetObjectValue(global::ApiSdk.Models.Nested.CreateFromDiscriminatorValue); } }, + { "nestedNullable", n => { NestedNullable = n.GetObjectValue(global::ApiSdk.Models.Nested.CreateFromDiscriminatorValue); } }, + }; + } + /// + /// Serializes information the current object + /// + /// Serialization writer to use to serialize this model + public virtual void Serialize(ISerializationWriter writer) + { + if(ReferenceEquals(writer, null)) throw new ArgumentNullException(nameof(writer)); + writer.WriteStringValue("immediate", Immediate); + writer.WriteObjectValue("nestedNonNullable", NestedNonNullable); + writer.WriteObjectValue("nestedNullable", NestedNullable); + writer.WriteAdditionalData(AdditionalData); + } + } + } + #pragma warning restore CS0618 + + """; + + public const string NullableEnumInOpenApi3 = """ + { + "openapi": "3.1.1", + "info": { + "title": "WebApplication2 | v1", + "version": "1.0.0" + }, + "servers": [ + { + "url": "http://localhost:5088/" + } + ], + "paths": { + "/Sample": { + "get": { + "tags": [ + "Sample" + ], + "operationId": "GetWeatherForecast", + "responses": { + "200": { + "description": "OK", + "content": { + "text/plain": { + "schema": { + "$ref": "#/components/schemas/SampleEnum" + } + }, + "application/json": { + "schema": { + "$ref": "#/components/schemas/SampleEnum" + } + }, + "text/json": { + "schema": { + "$ref": "#/components/schemas/SampleEnum" + } + } + } + } + } + } + } + }, + "components": { + "schemas": { + "ESample": { + "$comment": "Applied FixEnumsSchemaTransformer", + "enum": [ + "A", + "B" + ], + "type": "integer", + "x-ms-enum": { + "name": "ESample", + "modelAsString": false, + "values": [ + { + "name": "A", + "value": 0, + "description": "" + }, + { + "name": "B", + "value": 1, + "description": "" + } + ] + } + }, + "SampleEnum": { + "required": [ + "notNullable", + "nullable" + ], + "type": "object", + "properties": { + "notNullable": { + "$ref": "#/components/schemas/ESample" + }, + "nullable": { + "oneOf": [ + { + "type": "null" + }, + { + "$ref": "#/components/schemas/ESample" + } + ] + } + } + } + } + }, + "tags": [ + { + "name": "Sample" + } + ] + } + """; + + public const string NullableEnumInOpenApi3_Models_SampleEnum = """ + // + #pragma warning disable CS0618 + using Microsoft.Kiota.Abstractions.Extensions; + using Microsoft.Kiota.Abstractions.Serialization; + using System.Collections.Generic; + using System.IO; + using System; + namespace ApiSdk.Models + { + [global::System.CodeDom.Compiler.GeneratedCode("Kiota", "1.0.0")] + #pragma warning disable CS1591 + public partial class SampleEnum : IAdditionalDataHolder, IParsable + #pragma warning restore CS1591 + { + /// Stores additional data not described in the OpenAPI description found when deserializing. Can be used for serialization as well. + public IDictionary AdditionalData { get; set; } + /// The notNullable property + public global::ApiSdk.Models.ESample? NotNullable { get; set; } + /// The nullable property + public global::ApiSdk.Models.ESample? Nullable { get; set; } + /// + /// Instantiates a new and sets the default values. + /// + public SampleEnum() + { + AdditionalData = new Dictionary(); + } + /// + /// Creates a new instance of the appropriate class based on discriminator value + /// + /// A + /// The parse node to use to read the discriminator value and create the object + public static global::ApiSdk.Models.SampleEnum CreateFromDiscriminatorValue(IParseNode parseNode) + { + if(ReferenceEquals(parseNode, null)) throw new ArgumentNullException(nameof(parseNode)); + return new global::ApiSdk.Models.SampleEnum(); + } + /// + /// The deserialization information for the current model + /// + /// A IDictionary<string, Action<IParseNode>> + public virtual IDictionary> GetFieldDeserializers() + { + return new Dictionary> + { + { "notNullable", n => { NotNullable = (global::ApiSdk.Models.ESample?) n.GetIntValue(); } }, + { "nullable", n => { Nullable = (global::ApiSdk.Models.ESample?) n.GetIntValue(); } }, + }; + } + /// + /// Serializes information the current object + /// + /// Serialization writer to use to serialize this model + public virtual void Serialize(ISerializationWriter writer) + { + if(ReferenceEquals(writer, null)) throw new ArgumentNullException(nameof(writer)); + writer.WriteIntValue("notNullable", (int?)NotNullable); + writer.WriteIntValue("nullable", (int?)Nullable); + writer.WriteAdditionalData(AdditionalData); + } + } + } + #pragma warning restore CS0618 + + """; + + public const string NullableEnumInOpenApi3_Reverse = """ + { + "openapi": "3.1.1", + "info": { + "title": "WebApplication2 | v1", + "version": "1.0.0" + }, + "servers": [ + { + "url": "http://localhost:5088/" + } + ], + "paths": { + "/Sample": { + "get": { + "tags": [ + "Sample" + ], + "operationId": "GetWeatherForecast", + "responses": { + "200": { + "description": "OK", + "content": { + "text/plain": { + "schema": { + "$ref": "#/components/schemas/SampleEnum" + } + }, + "application/json": { + "schema": { + "$ref": "#/components/schemas/SampleEnum" + } + }, + "text/json": { + "schema": { + "$ref": "#/components/schemas/SampleEnum" + } + } + } + } + } + } + } + }, + "components": { + "schemas": { + "ESample": { + "$comment": "Applied FixEnumsSchemaTransformer", + "enum": [ + "A", + "B" + ], + "type": "integer", + "x-ms-enum": { + "name": "ESample", + "modelAsString": false, + "values": [ + { + "name": "A", + "value": 0, + "description": "" + }, + { + "name": "B", + "value": 1, + "description": "" + } + ] + } + }, + "SampleEnum": { + "required": [ + "notNullable", + "nullable" + ], + "type": "object", + "properties": { + "notNullable": { + "$ref": "#/components/schemas/ESample" + }, + "nullable": { + "oneOf": [ + { + "$ref": "#/components/schemas/ESample" + }, + { + "type": "null" + } + ] + } + } + } + } + }, + "tags": [ + { + "name": "Sample" + } + ] + } + """; + + public const string NullableEnumInOpenApi3_Reverse_Models_SampleEnum = """ + // + #pragma warning disable CS0618 + using Microsoft.Kiota.Abstractions.Extensions; + using Microsoft.Kiota.Abstractions.Serialization; + using System.Collections.Generic; + using System.IO; + using System; + namespace ApiSdk.Models + { + [global::System.CodeDom.Compiler.GeneratedCode("Kiota", "1.0.0")] + #pragma warning disable CS1591 + public partial class SampleEnum : IAdditionalDataHolder, IParsable + #pragma warning restore CS1591 + { + /// Stores additional data not described in the OpenAPI description found when deserializing. Can be used for serialization as well. + public IDictionary AdditionalData { get; set; } + /// The notNullable property + public global::ApiSdk.Models.ESample? NotNullable { get; set; } + /// The nullable property + public global::ApiSdk.Models.ESample? Nullable { get; set; } + /// + /// Instantiates a new and sets the default values. + /// + public SampleEnum() + { + AdditionalData = new Dictionary(); + } + /// + /// Creates a new instance of the appropriate class based on discriminator value + /// + /// A + /// The parse node to use to read the discriminator value and create the object + public static global::ApiSdk.Models.SampleEnum CreateFromDiscriminatorValue(IParseNode parseNode) + { + if(ReferenceEquals(parseNode, null)) throw new ArgumentNullException(nameof(parseNode)); + return new global::ApiSdk.Models.SampleEnum(); + } + /// + /// The deserialization information for the current model + /// + /// A IDictionary<string, Action<IParseNode>> + public virtual IDictionary> GetFieldDeserializers() + { + return new Dictionary> + { + { "notNullable", n => { NotNullable = (global::ApiSdk.Models.ESample?) n.GetIntValue(); } }, + { "nullable", n => { Nullable = (global::ApiSdk.Models.ESample?) n.GetIntValue(); } }, + }; + } + /// + /// Serializes information the current object + /// + /// Serialization writer to use to serialize this model + public virtual void Serialize(ISerializationWriter writer) + { + if(ReferenceEquals(writer, null)) throw new ArgumentNullException(nameof(writer)); + writer.WriteIntValue("notNullable", (int?)NotNullable); + writer.WriteIntValue("nullable", (int?)Nullable); + writer.WriteAdditionalData(AdditionalData); + } + } + } + #pragma warning restore CS0618 + + """; + + + [Theory] + [InlineData("NullableObjectInOpenApi3", NullableObjectInOpenApi3, new[] {"Models/Sample.cs", NullableObjectInOpenApi3_Models_Sample})] + [InlineData("NullableObjectInOpenApi3 Reverse", NullableObjectInOpenApi3_Reverse, new[] {"Models/Sample.cs", NullableObjectInOpenApi3_Reverse_Models_Sample})] + [InlineData("NullableEnumInOpenApi3", NullableEnumInOpenApi3, new[] {"Models/SampleEnum.cs", NullableEnumInOpenApi3_Models_SampleEnum})] + [InlineData("NullableEnumInOpenApi3 Reverse", NullableEnumInOpenApi3_Reverse, new[] {"Models/SampleEnum.cs", NullableEnumInOpenApi3_Reverse_Models_SampleEnum})] + public async Task CreateOpenApiDocumentWithResultAsync_ReturnsDiagnostics( + string description, + string input, + string[] expectedData) + { + if (expectedData.Length % 2 != 0) + Assert.Fail("Invalid test data"); + var expectedList = expectedData.Chunk(2).Select(e => (fileName: e[0], expected: e[1])).ToList(); + + string? tempInputFile = null; + string? tempOutputDirectory = null; + try + { + tempInputFile = Path.GetTempFileName(); + tempOutputDirectory = Path.Combine(Path.GetTempPath(), Path.GetRandomFileName()); + Directory.CreateDirectory(tempOutputDirectory); + await File.WriteAllTextAsync(tempInputFile, input); + var mockLogger = new Mock>(); + var builder = new KiotaBuilder(mockLogger.Object, + new GenerationConfiguration + { + ClientClassName = "Graph", + OpenAPIFilePath = tempInputFile, + OutputPath = tempOutputDirectory, + Language = GenerationLanguage.CSharp + }, _httpClient); + var result = await builder.GenerateClientAsync(default); + Assert.True(result); + foreach (var (fileName, expected) in expectedList) + { + var contents = await File.ReadAllTextAsync(Path.Combine(tempOutputDirectory, fileName)); + Assert.Equal(expected, contents); + } + } + finally + { + if (tempInputFile is not null && File.Exists(tempInputFile)) + File.Delete(tempInputFile); + if (tempOutputDirectory is not null && Directory.Exists(tempOutputDirectory)) + Directory.Delete(tempOutputDirectory, true); + } + } +}