diff --git a/src/Kiota.Builder/LanguageInformation.cs b/src/Kiota.Builder/LanguageInformation.cs index a7114808df..3f32f8fe6c 100644 --- a/src/Kiota.Builder/LanguageInformation.cs +++ b/src/Kiota.Builder/LanguageInformation.cs @@ -85,6 +85,11 @@ public static LanguageInformation Parse(JsonNode source) } return extension; } + + public override bool Equals(object obj) + { + return Equals(obj as LanguageInformation); + } } public record LanguageDependency : IOpenApiSerializable { @@ -130,6 +135,11 @@ public static LanguageDependency Parse(JsonNode source) } return extension; } + + public override bool Equals(object obj) + { + return Equals(obj as LanguageDependency); + } } public enum LanguageMaturityLevel diff --git a/src/Kiota.Builder/OpenApiExtensions/OpenApiAiAdaptiveCardExtension.cs b/src/Kiota.Builder/OpenApiExtensions/OpenApiAiAdaptiveCardExtension.cs index 22a8707226..cdafd8f279 100644 --- a/src/Kiota.Builder/OpenApiExtensions/OpenApiAiAdaptiveCardExtension.cs +++ b/src/Kiota.Builder/OpenApiExtensions/OpenApiAiAdaptiveCardExtension.cs @@ -63,9 +63,10 @@ public static OpenApiAiAdaptiveCardExtension Parse(JsonNode source) public void Write(IOpenApiWriter writer, OpenApiSpecVersion specVersion) { ArgumentNullException.ThrowIfNull(writer); - writer.WriteStartObject(); + // Only write the object if there's actual content to write if (!string.IsNullOrEmpty(DataPath) && !string.IsNullOrEmpty(File) && !string.IsNullOrEmpty(Title)) { + writer.WriteStartObject(); writer.WritePropertyName(nameof(DataPath).ToFirstCharacterLowerCase().ToSnakeCase()); writer.WriteValue(DataPath); writer.WritePropertyName(nameof(File).ToFirstCharacterLowerCase()); @@ -77,7 +78,7 @@ public void Write(IOpenApiWriter writer, OpenApiSpecVersion specVersion) writer.WritePropertyName(nameof(Url).ToFirstCharacterLowerCase()); writer.WriteValue(Url); } + writer.WriteEndObject(); } - writer.WriteEndObject(); } } diff --git a/src/Kiota.Builder/OpenApiExtensions/OpenApiAiCapabilitiesExtension.cs b/src/Kiota.Builder/OpenApiExtensions/OpenApiAiCapabilitiesExtension.cs index 5e8193f587..38c9839b77 100644 --- a/src/Kiota.Builder/OpenApiExtensions/OpenApiAiCapabilitiesExtension.cs +++ b/src/Kiota.Builder/OpenApiExtensions/OpenApiAiCapabilitiesExtension.cs @@ -45,9 +45,10 @@ public static OpenApiAiCapabilitiesExtension Parse(JsonNode source) public void Write(IOpenApiWriter writer, OpenApiSpecVersion specVersion) { ArgumentNullException.ThrowIfNull(writer); - writer.WriteStartObject(); + // Only write the object if there's actual content to write if (ResponseSemantics != null || Confirmation != null || SecurityInfo != null) { + writer.WriteStartObject(); if (ResponseSemantics is not null) { @@ -64,8 +65,9 @@ public void Write(IOpenApiWriter writer, OpenApiSpecVersion specVersion) writer.WritePropertyName(nameof(SecurityInfo).ToFirstCharacterLowerCase().ToSnakeCase()); WriteSecurityInfo(writer, SecurityInfo); } + + writer.WriteEndObject(); } - writer.WriteEndObject(); } private static ExtensionConfirmation ParseConfirmation(JsonObject source) diff --git a/src/Kiota.Builder/OpenApiExtensions/OpenApiLogoExtension.cs b/src/Kiota.Builder/OpenApiExtensions/OpenApiLogoExtension.cs index 5afa47d663..d4303315fc 100644 --- a/src/Kiota.Builder/OpenApiExtensions/OpenApiLogoExtension.cs +++ b/src/Kiota.Builder/OpenApiExtensions/OpenApiLogoExtension.cs @@ -29,12 +29,13 @@ public static OpenApiLogoExtension Parse(JsonNode source) public void Write(IOpenApiWriter writer, OpenApiSpecVersion specVersion) { ArgumentNullException.ThrowIfNull(writer); - writer.WriteStartObject(); + // Only write the object if there's actual content to write if (!string.IsNullOrEmpty(Url)) { + writer.WriteStartObject(); writer.WritePropertyName(nameof(Url).ToFirstCharacterLowerCase()); writer.WriteValue(Url); + writer.WriteEndObject(); } - writer.WriteEndObject(); } } diff --git a/src/Kiota.Builder/Plugins/PluginsGenerationService.cs b/src/Kiota.Builder/Plugins/PluginsGenerationService.cs index 78432e58f6..aa6688b327 100644 --- a/src/Kiota.Builder/Plugins/PluginsGenerationService.cs +++ b/src/Kiota.Builder/Plugins/PluginsGenerationService.cs @@ -993,10 +993,22 @@ private static (OpenApiRuntime[], Function[], ConversationStarter[]) GetRuntimes Capabilities = GetFunctionCapabilitiesFromOperation(operation, configuration, logger), }); - conversationStarters.Add(new ConversationStarter + + // Don't create conversation starters for operations that have empty x-ai-capabilities + var hasEmptyCapabilities = operation.Extensions is not null && + operation.Extensions.TryGetValue(OpenApiAiCapabilitiesExtension.Name, out var capExt) && + capExt is OpenApiAiCapabilitiesExtension cap && + cap.ResponseSemantics == null && + cap.Confirmation == null && + cap.SecurityInfo == null; + + if (!hasEmptyCapabilities) { - Text = !string.IsNullOrEmpty(summary) ? summary : description - }); + conversationStarters.Add(new ConversationStarter + { + Text = !string.IsNullOrEmpty(summary) ? summary : description + }); + } } } @@ -1136,6 +1148,7 @@ rExtRaw is T rExt && capabilitiesExtension is OpenApiAiCapabilitiesExtension capabilities) { var functionCapabilities = new FunctionCapabilities(); + bool hasContent = false; // Set ResponseSemantics if (capabilities.ResponseSemantics is not null) @@ -1163,6 +1176,7 @@ rExtRaw is T rExt && } responseSemantics.OAuthCardPath = capabilities.ResponseSemantics.OauthCardPath; functionCapabilities.ResponseSemantics = responseSemantics; + hasContent = true; } // Set Confirmation @@ -1175,6 +1189,7 @@ rExtRaw is T rExt && Body = capabilities.Confirmation.Body, }; functionCapabilities.Confirmation = confirmation; + hasContent = true; } // Set SecurityInfo @@ -1185,8 +1200,11 @@ rExtRaw is T rExt && DataHandling = capabilities.SecurityInfo.DataHandling, }; functionCapabilities.SecurityInfo = securityInfo; + hasContent = true; } - return functionCapabilities; + + // Only return the capabilities object if it actually has content + return hasContent ? functionCapabilities : null; } return null; @@ -1242,6 +1260,18 @@ rExtRaw is T rExt && } } + // Don't create capabilities for operations that have empty x-ai-capabilities + if (openApiOperation.Extensions is not null && + openApiOperation.Extensions.TryGetValue(OpenApiAiCapabilitiesExtension.Name, out var capabilitiesExtension) && + capabilitiesExtension is OpenApiAiCapabilitiesExtension capabilities && + capabilities.ResponseSemantics == null && + capabilities.Confirmation == null && + capabilities.SecurityInfo == null) + { + // If the capabilities extension exists but has no content, don't create response semantics + return null; + } + string functionName = openApiOperation.OperationId; string fileName = $"{functionName}.json"; string staticTemplateJson = $"{{\"file\": \"./adaptiveCards/{fileName}\"}}"; diff --git a/tests/Kiota.Builder.Tests/Extensions/OpenApiSchemaExtensionsTests.cs b/tests/Kiota.Builder.Tests/Extensions/OpenApiSchemaExtensionsTests.cs index 48657c3b19..dc4c87e334 100644 --- a/tests/Kiota.Builder.Tests/Extensions/OpenApiSchemaExtensionsTests.cs +++ b/tests/Kiota.Builder.Tests/Extensions/OpenApiSchemaExtensionsTests.cs @@ -198,8 +198,8 @@ public void GetSchemaNameAnyOfReference() { AnyOf = [ new OpenApiSchemaReference("microsoft.graph.entity"), - new OpenApiSchemaReference("microsoft.graph.user"), - ] + new OpenApiSchemaReference("microsoft.graph.user"), + ] }; var names = schema.GetSchemaNames(); Assert.Contains("entity", names); @@ -233,8 +233,8 @@ public void GetSchemaNameOneOfReference() { OneOf = [ new OpenApiSchemaReference("microsoft.graph.entity"), - new OpenApiSchemaReference("microsoft.graph.user"), - ] + new OpenApiSchemaReference("microsoft.graph.user"), + ] }; var names = schema.GetSchemaNames(); Assert.Contains("entity", names); @@ -315,7 +315,8 @@ public void GetReferenceIdsAllOfNested() var schema = new OpenApiSchema { AllOf = [ - new OpenApiSchema() { + new OpenApiSchema() + { AllOf = [ new OpenApiSchemaReference("microsoft.graph.entity"), new OpenApiSchemaReference("microsoft.graph.user") @@ -388,9 +389,11 @@ public void IsInherited() { AllOf = [ new OpenApiSchemaReference("microsoft.graph.entity"), - new OpenApiSchema() { + new OpenApiSchema() + { Type = JsonSchemaType.Object, - Properties = new Dictionary() { + Properties = new Dictionary() + { ["firstName"] = new OpenApiSchema() } } @@ -441,15 +444,19 @@ public void IsIntersection() schema = new OpenApiSchema { AllOf = [ - new OpenApiSchema() { + new OpenApiSchema() + { Type = JsonSchemaType.Object, - Properties = new Dictionary() { + Properties = new Dictionary() + { ["id"] = new OpenApiSchema() } }, - new OpenApiSchema() { + new OpenApiSchema() + { Type = JsonSchemaType.Object, - Properties = new Dictionary() { + Properties = new Dictionary() + { ["firstName"] = new OpenApiSchema() } } @@ -461,9 +468,11 @@ public void IsIntersection() schema = new OpenApiSchema { AllOf = [ - new OpenApiSchema() { + new OpenApiSchema() + { Type = JsonSchemaType.Object, - Properties = new Dictionary() { + Properties = new Dictionary() + { ["id"] = new OpenApiSchema() } } @@ -503,15 +512,19 @@ public void MergesIntersection() Description = "description", Deprecated = true, AllOf = [ - new OpenApiSchema() { + new OpenApiSchema() + { Type = JsonSchemaType.Object, - Properties = new Dictionary() { + Properties = new Dictionary() + { ["id"] = new OpenApiSchema() } }, - new OpenApiSchema() { + new OpenApiSchema() + { Type = JsonSchemaType.Object, - Properties = new Dictionary() { + Properties = new Dictionary() + { ["firstName"] = new OpenApiSchema() } } @@ -533,25 +546,32 @@ public void MergesIntersectionRecursively() Description = "description", Deprecated = true, AllOf = [ - new OpenApiSchema() { + new OpenApiSchema() + { Type = JsonSchemaType.Object, - Properties = new Dictionary() { + Properties = new Dictionary() + { ["id"] = new OpenApiSchema() } }, - new OpenApiSchema() { + new OpenApiSchema() + { Type = JsonSchemaType.Object, AllOf = [ - new OpenApiSchema() { + new OpenApiSchema() + { Type = JsonSchemaType.Object, - Properties = new Dictionary() { + Properties = new Dictionary() + { ["firstName"] = new OpenApiSchema(), ["lastName"] = new OpenApiSchema() } }, - new OpenApiSchema() { + new OpenApiSchema() + { Type = JsonSchemaType.Object, - Properties = new Dictionary() { + Properties = new Dictionary() + { ["lastName"] = new OpenApiSchema() } }, @@ -947,11 +967,11 @@ public void IsEnumFailsOnEmptyMembers() Enum = [ "low", - "medium", - "high", - "hidden", - "none", - "unknownFutureValue" + "medium", + "high", + "hidden", + "none", + "unknownFutureValue" ], Type = JsonSchemaType.String }; diff --git a/tests/Kiota.Builder.Tests/Extensions/OpenApiUrlTreeNodeExtensionsTests.cs b/tests/Kiota.Builder.Tests/Extensions/OpenApiUrlTreeNodeExtensionsTests.cs index 14eb987dd2..29016fa1a0 100644 --- a/tests/Kiota.Builder.Tests/Extensions/OpenApiUrlTreeNodeExtensionsTests.cs +++ b/tests/Kiota.Builder.Tests/Extensions/OpenApiUrlTreeNodeExtensionsTests.cs @@ -330,30 +330,36 @@ public void GeneratesRequiredQueryParametersAndOptionalMixInPathItem() doc.Paths.Add("users\\{id}\\manager", new OpenApiPathItem() { Parameters = [ - new OpenApiParameter { + new OpenApiParameter + { Name = "id", In = ParameterLocation.Path, Required = true, - Schema = new OpenApiSchema { - Type = JsonSchemaType.String - } - }, - new OpenApiParameter { - Name = "filter", - In = ParameterLocation.Query, - Required = false, - Schema = new OpenApiSchema { + Schema = new OpenApiSchema + { Type = JsonSchemaType.String } }, - new OpenApiParameter { - Name = "apikey", - In = ParameterLocation.Query, - Required = true, - Schema = new OpenApiSchema { - Type = JsonSchemaType.String - } - } + new OpenApiParameter + { + Name = "filter", + In = ParameterLocation.Query, + Required = false, + Schema = new OpenApiSchema + { + Type = JsonSchemaType.String + } + }, + new OpenApiParameter + { + Name = "apikey", + In = ParameterLocation.Query, + Required = true, + Schema = new OpenApiSchema + { + Type = JsonSchemaType.String + } + } ], Operations = new Dictionary { { HttpMethod.Get, new() { @@ -418,29 +424,35 @@ public void GeneratesOnlyOptionalQueryParametersInPathItem() doc.Paths.Add("users\\{id}\\manager", new OpenApiPathItem() { Parameters = [ - new OpenApiParameter { + new OpenApiParameter + { Name = "id", In = ParameterLocation.Path, Required = true, - Schema = new OpenApiSchema { - Type = JsonSchemaType.String - } - }, - new OpenApiParameter { - Name = "filter", - In = ParameterLocation.Query, - Required = false, - Schema = new OpenApiSchema { + Schema = new OpenApiSchema + { Type = JsonSchemaType.String } }, - new OpenApiParameter { - Name = "apikey", - In = ParameterLocation.Query, - Schema = new OpenApiSchema { - Type = JsonSchemaType.String - } - } + new OpenApiParameter + { + Name = "filter", + In = ParameterLocation.Query, + Required = false, + Schema = new OpenApiSchema + { + Type = JsonSchemaType.String + } + }, + new OpenApiParameter + { + Name = "apikey", + In = ParameterLocation.Query, + Schema = new OpenApiSchema + { + Type = JsonSchemaType.String + } + } ], Operations = new Dictionary { { HttpMethod.Get, new() { @@ -503,30 +515,36 @@ public void GeneratesOnlyRequiredQueryParametersInPathItem() doc.Paths.Add("users\\{id}\\manager", new OpenApiPathItem() { Parameters = [ - new OpenApiParameter { + new OpenApiParameter + { Name = "id", In = ParameterLocation.Path, Required = true, - Schema = new OpenApiSchema { - Type = JsonSchemaType.String - } - }, - new OpenApiParameter { - Name = "filter", - In = ParameterLocation.Query, - Required = true, - Schema = new OpenApiSchema { + Schema = new OpenApiSchema + { Type = JsonSchemaType.String } }, - new OpenApiParameter { - Name = "apikey", - Required = true, - In = ParameterLocation.Query, - Schema = new OpenApiSchema { - Type = JsonSchemaType.String - } - } + new OpenApiParameter + { + Name = "filter", + In = ParameterLocation.Query, + Required = true, + Schema = new OpenApiSchema + { + Type = JsonSchemaType.String + } + }, + new OpenApiParameter + { + Name = "apikey", + Required = true, + In = ParameterLocation.Query, + Schema = new OpenApiSchema + { + Type = JsonSchemaType.String + } + } ], Operations = new Dictionary { { HttpMethod.Get, new() { @@ -782,11 +800,13 @@ public void SinglePathParametersAreDeduplicated() ["users/{foo}/careerAdvisor/{id}"] = new OpenApiPathItem { Parameters = [ - new OpenApiParameter { + new OpenApiParameter + { Name = "foo", In = ParameterLocation.Path, Required = true, - Schema = new OpenApiSchema { + Schema = new OpenApiSchema + { Type = JsonSchemaType.String } }, @@ -814,11 +834,13 @@ public void SinglePathParametersAreDeduplicated() ["users/{id}/careerAdvisor"] = new OpenApiPathItem { Parameters = [ - new OpenApiParameter { + new OpenApiParameter + { Name = "id", In = ParameterLocation.Path, Required = true, - Schema = new OpenApiSchema { + Schema = new OpenApiSchema + { Type = JsonSchemaType.String } }, @@ -850,11 +872,13 @@ public void SinglePathParametersAreDeduplicated() [HttpMethod.Get] = new OpenApiOperation { Parameters = [ - new OpenApiParameter { + new OpenApiParameter + { Name = "user-id", In = ParameterLocation.Path, Required = true, - Schema = new OpenApiSchema { + Schema = new OpenApiSchema + { Type = JsonSchemaType.String } }, diff --git a/tests/Kiota.Builder.Tests/KiotaBuilderTests.cs b/tests/Kiota.Builder.Tests/KiotaBuilderTests.cs index f638c7bdfa..561c458cd4 100644 --- a/tests/Kiota.Builder.Tests/KiotaBuilderTests.cs +++ b/tests/Kiota.Builder.Tests/KiotaBuilderTests.cs @@ -1878,7 +1878,8 @@ public void Supports_Path_Parameters() Name = "scope", In = ParameterLocation.Path, Required = true, - Schema = new OpenApiSchema { + Schema = new OpenApiSchema + { Type = JsonSchemaType.String } } @@ -6824,15 +6825,19 @@ public void AddReservedPathParameterSymbol() ["users/{id}/manager"] = new OpenApiPathItem { Parameters = [ - new OpenApiParameter { + new OpenApiParameter + { Name = "id", In = ParameterLocation.Path, Required = true, - Schema = new OpenApiSchema { + Schema = new OpenApiSchema + { Type = JsonSchemaType.String }, - Extensions = new Dictionary (){ - ["x-ms-reserved-parameter"] = new OpenApiReservedParameterExtension { + Extensions = new Dictionary() + { + ["x-ms-reserved-parameter"] = new OpenApiReservedParameterExtension + { IsReserved = true } } @@ -6898,15 +6903,19 @@ public void DoesNotAddReservedPathParameterSymbol() ["users/{id}/manager"] = new OpenApiPathItem { Parameters = [ - new OpenApiParameter { + new OpenApiParameter + { Name = "id", In = ParameterLocation.Path, Required = true, - Schema = new OpenApiSchema { + Schema = new OpenApiSchema + { Type = JsonSchemaType.String }, - Extensions = new Dictionary() { - ["x-ms-reserved-parameter"] = new OpenApiReservedParameterExtension { + Extensions = new Dictionary() + { + ["x-ms-reserved-parameter"] = new OpenApiReservedParameterExtension + { IsReserved = false } } diff --git a/tests/Kiota.Builder.Tests/OpenApiExtensions/OpenApiAiAdaptiveCardExtensionTests.cs b/tests/Kiota.Builder.Tests/OpenApiExtensions/OpenApiAiAdaptiveCardExtensionTests.cs index accbd82f42..5bb5c261c0 100644 --- a/tests/Kiota.Builder.Tests/OpenApiExtensions/OpenApiAiAdaptiveCardExtensionTests.cs +++ b/tests/Kiota.Builder.Tests/OpenApiExtensions/OpenApiAiAdaptiveCardExtensionTests.cs @@ -138,4 +138,24 @@ public void Serializes() var result = sWriter.ToString(); Assert.Equal("{\"data_path\":\"$.items\",\"file\":\"path_to_file\",\"title\":\"title\",\"url\":\"https://example.com\"}", result); } + + [Fact] + public void WritesNothingForEmptyAdaptiveCard() + { + var value = new OpenApiAiAdaptiveCardExtension + { + DataPath = null, + File = null, + Title = null, + Url = null + }; + using var sWriter = new StringWriter(); + OpenApiJsonWriter writer = new(sWriter, new OpenApiJsonWriterSettings { Terse = true }); + + value.Write(writer, OpenApiSpecVersion.OpenApi3_0); + var result = sWriter.ToString(); + + // When required properties are null/empty, nothing should be written + Assert.Equal(string.Empty, result); + } } diff --git a/tests/Kiota.Builder.Tests/OpenApiExtensions/OpenApiAiCapabilitiesExtensionTests.cs b/tests/Kiota.Builder.Tests/OpenApiExtensions/OpenApiAiCapabilitiesExtensionTests.cs index 5a69322d34..bc6fec6289 100644 --- a/tests/Kiota.Builder.Tests/OpenApiExtensions/OpenApiAiCapabilitiesExtensionTests.cs +++ b/tests/Kiota.Builder.Tests/OpenApiExtensions/OpenApiAiCapabilitiesExtensionTests.cs @@ -236,4 +236,43 @@ public void Serializes() Assert.Contains("some data handling", result); } + + [Fact] + public void WritesNothingForEmptyCapabilities() + { + var value = new OpenApiAiCapabilitiesExtension + { + ResponseSemantics = null, + Confirmation = null, + SecurityInfo = null + }; + using var sWriter = new StringWriter(); + OpenApiJsonWriter writer = new(sWriter, new OpenApiJsonWriterSettings { Terse = true }); + + value.Write(writer, OpenApiSpecVersion.OpenApi3_0); + var result = sWriter.ToString(); + + // When all properties are null, nothing should be written + Assert.Equal(string.Empty, result); + } + + [Fact] + public void WritesNothingForCapabilitiesWithEmptyNestedObjects() + { + var value = new OpenApiAiCapabilitiesExtension + { + ResponseSemantics = new ExtensionResponseSemantics { DataPath = string.Empty }, + Confirmation = new ExtensionConfirmation(), + SecurityInfo = new ExtensionSecurityInfo() + }; + using var sWriter = new StringWriter(); + OpenApiJsonWriter writer = new(sWriter, new OpenApiJsonWriterSettings { Terse = true }); + + value.Write(writer, OpenApiSpecVersion.OpenApi3_0); + var result = sWriter.ToString(); + + // Should write the object structure even if nested objects are empty + // since the properties are not null + Assert.NotEqual(string.Empty, result); + } } diff --git a/tests/Kiota.Builder.Tests/OpenApiExtensions/OpenApiLogoExtensionTests.cs b/tests/Kiota.Builder.Tests/OpenApiExtensions/OpenApiLogoExtensionTests.cs new file mode 100644 index 0000000000..8c300dc3f5 --- /dev/null +++ b/tests/Kiota.Builder.Tests/OpenApiExtensions/OpenApiLogoExtensionTests.cs @@ -0,0 +1,77 @@ +using System.IO; +using System.Text.Json.Nodes; +using Kiota.Builder.OpenApiExtensions; +using Microsoft.OpenApi; +using Xunit; + +namespace Kiota.Builder.Tests.OpenApiExtensions; + +public sealed class OpenApiLogoExtensionTest +{ + [Fact] + public void Parses() + { + var oaiValueRepresentation = + """ + { + "url": "https://example.com/logo.png" + } + """; + using var stream = new MemoryStream(System.Text.Encoding.UTF8.GetBytes(oaiValueRepresentation)); + var oaiValue = JsonNode.Parse(stream); + var value = OpenApiLogoExtension.Parse(oaiValue); + + Assert.NotNull(value); + Assert.Equal("https://example.com/logo.png", value.Url); + } + + [Fact] + public void Serializes() + { + var value = new OpenApiLogoExtension + { + Url = "https://example.com/logo.png" + }; + using var sWriter = new StringWriter(); + OpenApiJsonWriter writer = new(sWriter, new OpenApiJsonWriterSettings { Terse = true }); + + value.Write(writer, OpenApiSpecVersion.OpenApi3_0); + var result = sWriter.ToString(); + + Assert.Equal("{\"url\":\"https://example.com/logo.png\"}", result); + } + + [Fact] + public void WritesNothingForEmptyLogo() + { + var value = new OpenApiLogoExtension + { + Url = null + }; + using var sWriter = new StringWriter(); + OpenApiJsonWriter writer = new(sWriter, new OpenApiJsonWriterSettings { Terse = true }); + + value.Write(writer, OpenApiSpecVersion.OpenApi3_0); + var result = sWriter.ToString(); + + // When Url is null/empty, nothing should be written + Assert.Equal(string.Empty, result); + } + + [Fact] + public void WritesNothingForEmptyUrlString() + { + var value = new OpenApiLogoExtension + { + Url = string.Empty + }; + using var sWriter = new StringWriter(); + OpenApiJsonWriter writer = new(sWriter, new OpenApiJsonWriterSettings { Terse = true }); + + value.Write(writer, OpenApiSpecVersion.OpenApi3_0); + var result = sWriter.ToString(); + + // When Url is empty string, nothing should be written + Assert.Equal(string.Empty, result); + } +} diff --git a/tests/Kiota.Builder.Tests/Plugins/PluginsGenerationServiceTests.cs b/tests/Kiota.Builder.Tests/Plugins/PluginsGenerationServiceTests.cs index 99ee451755..8ec8b059c6 100644 --- a/tests/Kiota.Builder.Tests/Plugins/PluginsGenerationServiceTests.cs +++ b/tests/Kiota.Builder.Tests/Plugins/PluginsGenerationServiceTests.cs @@ -2567,6 +2567,72 @@ public void GetFirstPartialFileName_ThrowsArgumentException_WhenInputIsNullOrEmp Assert.Throws(() => PluginsGenerationService.GetFirstPartialFileName(string.Empty)); } + [Fact] + public async Task GeneratesManifestWithoutEmptyCapabilitiesAsync() + { + var simpleDescriptionContent = +""" +openapi: 3.0.0 +info: + title: test + version: 1.0 + description: test description +servers: + - url: http://localhost/ +paths: + /test: + get: + operationId: getTest + summary: Get test data + description: description for test path + x-ai-capabilities: {} + responses: + '200': + description: test +"""; + var workingDirectory = Path.Combine(Path.GetTempPath(), Path.GetRandomFileName()); + var simpleDescriptionPath = Path.Combine(workingDirectory) + "description.yaml"; + await File.WriteAllTextAsync(simpleDescriptionPath, simpleDescriptionContent); + var openAPIDocumentDS = new OpenApiDocumentDownloadService(_httpClient, _logger); + var outputDirectory = Path.Combine(workingDirectory, "output"); + var generationConfiguration = new GenerationConfiguration + { + OutputPath = outputDirectory, + OpenAPIFilePath = simpleDescriptionPath, + PluginTypes = [PluginType.APIPlugin], + ClientClassName = "testPlugin", + ApiRootUrl = "http://localhost/", + }; + var (openAPIDocumentStream, _) = await openAPIDocumentDS.LoadStreamAsync(simpleDescriptionPath, generationConfiguration, null, false); + var openApiDocument = await openAPIDocumentDS.GetDocumentFromStreamAsync(openAPIDocumentStream, generationConfiguration); + KiotaBuilder.CleanupOperationIdForPlugins(openApiDocument); + var urlTreeNode = OpenApiUrlTreeNode.Create(openApiDocument, Constants.DefaultOpenApiLabel); + + var pluginsGenerationService = new PluginsGenerationService(openApiDocument, urlTreeNode, generationConfiguration, workingDirectory, _logger); + + var manifestPaths = await pluginsGenerationService.GenerateManifestAsync(); + + // Verify the manifest was generated + var apiPluginPath = manifestPaths[PluginType.APIPlugin]; + Assert.True(File.Exists(apiPluginPath)); + + // Read and validate the manifest content + var manifestContent = await File.ReadAllTextAsync(apiPluginPath); + using var jsonDocument = JsonDocument.Parse(manifestContent); + var resultingManifest = PluginManifestDocument.Load(jsonDocument.RootElement); + + Assert.NotNull(resultingManifest.Document); + Assert.Single(resultingManifest.Document.Functions); // One function should be generated + + // Verify that the function does not have a capabilities property when it's empty + var function = resultingManifest.Document.Functions[0]; + Assert.Equal("getTest", function.Name); + Assert.Null(function.Capabilities); // Capabilities should be null for empty x-ai-capabilities + + // Also verify the JSON doesn't contain "capabilities": {} + Assert.DoesNotContain("\"capabilities\":", manifestContent); + } + #endregion }