From c25992eab618437b2355eea204a713df8174c557 Mon Sep 17 00:00:00 2001 From: Vincent Biret Date: Wed, 27 Nov 2024 13:00:17 -0500 Subject: [PATCH 01/12] chore: removes unused variable Signed-off-by: Vincent Biret --- .../Kiota.Builder.Tests/Plugins/PluginsGenerationServiceTests.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/tests/Kiota.Builder.Tests/Plugins/PluginsGenerationServiceTests.cs b/tests/Kiota.Builder.Tests/Plugins/PluginsGenerationServiceTests.cs index cd5af624ad..ae2ccc8a38 100644 --- a/tests/Kiota.Builder.Tests/Plugins/PluginsGenerationServiceTests.cs +++ b/tests/Kiota.Builder.Tests/Plugins/PluginsGenerationServiceTests.cs @@ -747,7 +747,6 @@ public async Task MergesAllOfRequestBodyAsync(string content, Action>(); var openAPIDocumentDS = new OpenApiDocumentDownloadService(_httpClient, _logger); var outputDirectory = Path.Combine(workingDirectory, "output"); var generationConfiguration = new GenerationConfiguration From 4bd8a76f887053573b8bad5c36cbd74fe4e59828 Mon Sep 17 00:00:00 2001 From: Vincent Biret Date: Wed, 27 Nov 2024 13:12:01 -0500 Subject: [PATCH 02/12] fix: removes invalid discriminator mappings for plugin export --- .../Plugins/PluginsGenerationService.cs | 23 +++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/src/Kiota.Builder/Plugins/PluginsGenerationService.cs b/src/Kiota.Builder/Plugins/PluginsGenerationService.cs index 6a1e73c972..56efe89985 100644 --- a/src/Kiota.Builder/Plugins/PluginsGenerationService.cs +++ b/src/Kiota.Builder/Plugins/PluginsGenerationService.cs @@ -61,6 +61,7 @@ public async Task GenerateManifestAsync(CancellationToken cancellationToken = de var descriptionWriter = new OpenApiYamlWriter(fileWriter); var trimmedPluginDocument = GetDocumentWithTrimmedComponentsAndResponses(OAIDocument); trimmedPluginDocument = InlineRequestBodyAllOf(trimmedPluginDocument); + RemoveDiscriminatorMappingEntriesReferencingAbsentSchemas(trimmedPluginDocument); trimmedPluginDocument.SerializeAsV3(descriptionWriter); descriptionWriter.Flush(); @@ -105,6 +106,28 @@ public async Task GenerateManifestAsync(CancellationToken cancellationToken = de } } + private sealed class MappingCleanupVisitor(OpenApiDocument openApiDocument) : OpenApiVisitorBase + { + private readonly OpenApiDocument _document = openApiDocument; + + public override void Visit(OpenApiSchema schema) + { + if (schema.Discriminator?.Mapping is null) + return; + var keysToRemove = schema.Discriminator.Mapping.Where(x => !_document.Components.Schemas.ContainsKey(x.Value.Split('/', StringSplitOptions.RemoveEmptyEntries)[^1])).Select(static x => x.Key).ToArray(); + foreach (var key in keysToRemove) + schema.Discriminator.Mapping.Remove(key); + base.Visit(schema); + } + } + + private static void RemoveDiscriminatorMappingEntriesReferencingAbsentSchemas(OpenApiDocument document) + { + var visitor = new MappingCleanupVisitor(document); + var walker = new OpenApiWalker(visitor); + walker.Walk(document); + } + private static OpenApiDocument InlineRequestBodyAllOf(OpenApiDocument openApiDocument) { if (openApiDocument.Paths is null) return openApiDocument; From 7e438201709166c7c907e9788a8e6791422e0b9f Mon Sep 17 00:00:00 2001 From: Vincent Biret Date: Wed, 27 Nov 2024 13:31:02 -0500 Subject: [PATCH 03/12] fix: inlining of allOf for plugins now emits right reference fix: inlining of allOf for plugins now includes all properties Signed-off-by: Vincent Biret --- .../Plugins/PluginsGenerationService.cs | 122 +++++------------- 1 file changed, 32 insertions(+), 90 deletions(-) diff --git a/src/Kiota.Builder/Plugins/PluginsGenerationService.cs b/src/Kiota.Builder/Plugins/PluginsGenerationService.cs index 56efe89985..f1f85477e2 100644 --- a/src/Kiota.Builder/Plugins/PluginsGenerationService.cs +++ b/src/Kiota.Builder/Plugins/PluginsGenerationService.cs @@ -61,7 +61,9 @@ public async Task GenerateManifestAsync(CancellationToken cancellationToken = de var descriptionWriter = new OpenApiYamlWriter(fileWriter); var trimmedPluginDocument = GetDocumentWithTrimmedComponentsAndResponses(OAIDocument); trimmedPluginDocument = InlineRequestBodyAllOf(trimmedPluginDocument); - RemoveDiscriminatorMappingEntriesReferencingAbsentSchemas(trimmedPluginDocument); + PrepareDescriptionForCopilot(trimmedPluginDocument); + // trimming a second time to remove any components that are no longer used after the inlining + trimmedPluginDocument = GetDocumentWithTrimmedComponentsAndResponses(trimmedPluginDocument); trimmedPluginDocument.SerializeAsV3(descriptionWriter); descriptionWriter.Flush(); @@ -121,11 +123,36 @@ public override void Visit(OpenApiSchema schema) } } - private static void RemoveDiscriminatorMappingEntriesReferencingAbsentSchemas(OpenApiDocument document) + private sealed class AllOfPropertiesRetrievalVisitor : OpenApiVisitorBase { - var visitor = new MappingCleanupVisitor(document); - var walker = new OpenApiWalker(visitor); - walker.Walk(document); + public override void Visit(OpenApiSchema schema) + { + if (schema.AllOf is not { Count: > 0 }) + return; + var allPropertiesToAdd = GetAllProperties(schema).ToArray(); + foreach (var (key, value) in allPropertiesToAdd) + schema.Properties.TryAdd(key, value); + schema.AllOf.Clear(); + base.Visit(schema); + } + + private static IEnumerable> GetAllProperties(OpenApiSchema schema) + { + return schema.AllOf is not null ? + schema.AllOf.SelectMany(static x => GetAllProperties(x)).Union(schema.Properties) : + schema.Properties; + } + } + + private static void PrepareDescriptionForCopilot(OpenApiDocument document) + { + var allOfPropertiesRetrievalVisitor = new AllOfPropertiesRetrievalVisitor(); + var allOfPropertiesRetrievalWalker = new OpenApiWalker(allOfPropertiesRetrievalVisitor); + allOfPropertiesRetrievalWalker.Walk(document); + + var mappingCleanupVisitor = new MappingCleanupVisitor(document); + var mappingCleanupWalker = new OpenApiWalker(mappingCleanupVisitor); + mappingCleanupWalker.Walk(document); } private static OpenApiDocument InlineRequestBodyAllOf(OpenApiDocument openApiDocument) @@ -137,8 +164,6 @@ private static OpenApiDocument InlineRequestBodyAllOf(OpenApiDocument openApiDoc foreach (var contentItem in contentItems) { var schema = contentItem.Schema; - // Merge all schemas in allOf `schema.MergeAllOfSchemaEntries()` doesn't seem to do the right thing. - schema = MergeAllOfInSchema(schema); schema = SelectFirstAnyOfOrOneOf(schema); contentItem.Schema = schema; } @@ -163,89 +188,6 @@ private static OpenApiDocument InlineRequestBodyAllOf(OpenApiDocument openApiDoc } return newSchema; } - static OpenApiSchema? MergeAllOfInSchema(OpenApiSchema? schema) - { - if (schema?.AllOf is not { Count: > 0 }) return schema; - var newSchema = new OpenApiSchema(); - foreach (var apiSchema in schema.AllOf) - { - if (apiSchema.Title is not null) newSchema.Title = apiSchema.Title; - if (!string.IsNullOrEmpty(apiSchema.Type)) - { - if (!string.IsNullOrEmpty(newSchema.Type) && newSchema.Type != apiSchema.Type) - { - throw new InvalidOperationException( - $"The schemas in allOf cannot have different types: '{newSchema.Type}' and '{apiSchema.Type}'."); - } - newSchema.Type = apiSchema.Type; - } - if (apiSchema.Format is not null) newSchema.Format = apiSchema.Format; - if (!string.IsNullOrEmpty(apiSchema.Description)) newSchema.Description = apiSchema.Description; - if (apiSchema.Maximum is not null) newSchema.Maximum = apiSchema.Maximum; - if (apiSchema.ExclusiveMaximum is not null) newSchema.ExclusiveMaximum = apiSchema.ExclusiveMaximum; - if (apiSchema.Minimum is not null) newSchema.Minimum = apiSchema.Minimum; - if (apiSchema.ExclusiveMinimum is not null) newSchema.ExclusiveMinimum = apiSchema.ExclusiveMinimum; - if (apiSchema.MaxLength is not null) newSchema.MaxLength = apiSchema.MaxLength; - if (apiSchema.MinLength is not null) newSchema.MinLength = apiSchema.MinLength; - if (!string.IsNullOrEmpty(apiSchema.Pattern)) newSchema.Pattern = apiSchema.Pattern; - if (apiSchema.MultipleOf is not null) newSchema.MultipleOf = apiSchema.MultipleOf; - if (apiSchema.Default is not null) newSchema.Default = apiSchema.Default; - if (apiSchema.ReadOnly) newSchema.ReadOnly = apiSchema.ReadOnly; - if (apiSchema.WriteOnly) newSchema.WriteOnly = apiSchema.WriteOnly; - if (apiSchema.Not is not null) newSchema.Not = apiSchema.Not; - if (apiSchema.Required is { Count: > 0 }) - { - foreach (var r in apiSchema.Required.Where(static r => !string.IsNullOrEmpty(r))) - { - newSchema.Required.Add(r); - } - } - if (apiSchema.Items is not null) newSchema.Items = apiSchema.Items; - if (apiSchema.MaxItems is not null) newSchema.MaxItems = apiSchema.MaxItems; - if (apiSchema.MinItems is not null) newSchema.MinItems = apiSchema.MinItems; - if (apiSchema.UniqueItems is not null) newSchema.UniqueItems = apiSchema.UniqueItems; - if (apiSchema.Properties is not null) - { - foreach (var property in apiSchema.Properties) - { - newSchema.Properties.TryAdd(property.Key, property.Value); - } - } - if (apiSchema.MaxProperties is not null) newSchema.MaxProperties = apiSchema.MaxProperties; - if (apiSchema.MinProperties is not null) newSchema.MinProperties = apiSchema.MinProperties; - if (apiSchema.AdditionalPropertiesAllowed) newSchema.AdditionalPropertiesAllowed = true; - if (apiSchema.AdditionalProperties is not null) newSchema.AdditionalProperties = apiSchema.AdditionalProperties; - if (apiSchema.Discriminator is not null) newSchema.Discriminator = apiSchema.Discriminator; - if (apiSchema.Example is not null) newSchema.Example = apiSchema.Example; - if (apiSchema.Enum is not null) - { - foreach (var enumValue in apiSchema.Enum) - { - newSchema.Enum.Add(enumValue); - } - } - if (apiSchema.Nullable) newSchema.Nullable = apiSchema.Nullable; - if (apiSchema.ExternalDocs is not null) newSchema.ExternalDocs = apiSchema.ExternalDocs; - if (apiSchema.Deprecated) newSchema.Deprecated = apiSchema.Deprecated; - if (apiSchema.Xml is not null) newSchema.Xml = apiSchema.Xml; - if (apiSchema.Extensions is not null) - { - foreach (var extension in apiSchema.Extensions) - { - newSchema.Extensions.Add(extension.Key, extension.Value); - } - } - if (apiSchema.Reference is not null) newSchema.Reference = apiSchema.Reference; - if (apiSchema.Annotations is not null) - { - foreach (var annotation in apiSchema.Annotations) - { - newSchema.Annotations.Add(annotation.Key, annotation.Value); - } - } - } - return newSchema; - } } [GeneratedRegex(@"[^a-zA-Z0-9_]+", RegexOptions.IgnoreCase | RegexOptions.Singleline, 2000)] From 72d00e56eaceea83318634e4ee5556dbfa0de175 Mon Sep 17 00:00:00 2001 From: Vincent Biret Date: Wed, 27 Nov 2024 14:13:01 -0500 Subject: [PATCH 04/12] fix any/oneOf selection for plugins generation Signed-off-by: Vincent Biret --- .../Plugins/PluginsGenerationService.cs | 127 +++++++++++++----- 1 file changed, 91 insertions(+), 36 deletions(-) diff --git a/src/Kiota.Builder/Plugins/PluginsGenerationService.cs b/src/Kiota.Builder/Plugins/PluginsGenerationService.cs index f1f85477e2..2af5acbe33 100644 --- a/src/Kiota.Builder/Plugins/PluginsGenerationService.cs +++ b/src/Kiota.Builder/Plugins/PluginsGenerationService.cs @@ -11,6 +11,7 @@ using Kiota.Builder.OpenApiExtensions; using Microsoft.Extensions.Logging; using Microsoft.OpenApi.ApiManifest; +using Microsoft.OpenApi.Interfaces; using Microsoft.OpenApi.Models; using Microsoft.OpenApi.Services; using Microsoft.OpenApi.Writers; @@ -60,10 +61,10 @@ public async Task GenerateManifestAsync(CancellationToken cancellationToken = de #pragma warning restore CA2007 // Consider calling ConfigureAwait on the awaited task var descriptionWriter = new OpenApiYamlWriter(fileWriter); var trimmedPluginDocument = GetDocumentWithTrimmedComponentsAndResponses(OAIDocument); - trimmedPluginDocument = InlineRequestBodyAllOf(trimmedPluginDocument); PrepareDescriptionForCopilot(trimmedPluginDocument); // trimming a second time to remove any components that are no longer used after the inlining trimmedPluginDocument = GetDocumentWithTrimmedComponentsAndResponses(trimmedPluginDocument); + trimmedPluginDocument.Info.Title = trimmedPluginDocument.Info.Title[..^9]; // removing the second ` - Subset` suffix from the title trimmedPluginDocument.SerializeAsV3(descriptionWriter); descriptionWriter.Flush(); @@ -144,8 +145,97 @@ private static IEnumerable> GetAllProperties } } + private sealed class SelectFirstAnyOneOfVisitor : OpenApiVisitorBase + { + public override void Visit(OpenApiSchema schema) + { + if (schema.AnyOf is { Count: > 0 }) + { + CopyRelevantInformation(schema.AnyOf[0], schema); + schema.AnyOf.Clear(); + } + if (schema.OneOf is { Count: > 0 }) + { + CopyRelevantInformation(schema.OneOf[0], schema); + schema.OneOf.Clear(); + } + base.Visit(schema); + } + private static void CopyRelevantInformation(OpenApiSchema source, OpenApiSchema target) + { + if (!string.IsNullOrEmpty(source.Type)) + target.Type = source.Type; + if (!string.IsNullOrEmpty(source.Format)) + target.Format = source.Format; + if (source.Items is not null) + target.Items = source.Items; + if (source.Properties is not null) + target.Properties = new Dictionary(source.Properties); + if (source.Required is not null) + target.Required = new HashSet(source.Required); + if (source.AdditionalProperties is not null) + target.AdditionalProperties = source.AdditionalProperties; + if (source.Enum is not null) + target.Enum = [.. source.Enum]; + if (source.ExclusiveMaximum is not null) + target.ExclusiveMaximum = source.ExclusiveMaximum; + if (source.ExclusiveMinimum is not null) + target.ExclusiveMinimum = source.ExclusiveMinimum; + if (source.Maximum is not null) + target.Maximum = source.Maximum; + if (source.Minimum is not null) + target.Minimum = source.Minimum; + if (source.MaxItems is not null) + target.MaxItems = source.MaxItems; + if (source.MinItems is not null) + target.MinItems = source.MinItems; + if (source.MaxLength is not null) + target.MaxLength = source.MaxLength; + if (source.MinLength is not null) + target.MinLength = source.MinLength; + if (source.Pattern is not null) + target.Pattern = source.Pattern; + if (source.MaxProperties is not null) + target.MaxProperties = source.MaxProperties; + if (source.MinProperties is not null) + target.MinProperties = source.MinProperties; + if (source.UniqueItems is not null) + target.UniqueItems = source.UniqueItems; + if (source.Nullable) + target.Nullable = true; + if (source.ReadOnly) + target.ReadOnly = true; + if (source.WriteOnly) + target.WriteOnly = true; + if (source.Deprecated) + target.Deprecated = true; + if (source.Xml is not null) + target.Xml = source.Xml; + if (source.ExternalDocs is not null) + target.ExternalDocs = source.ExternalDocs; + if (source.Example is not null) + target.Example = source.Example; + if (source.Extensions is not null) + target.Extensions = new Dictionary(source.Extensions); + if (source.Discriminator is not null) + target.Discriminator = source.Discriminator; + if (!string.IsNullOrEmpty(source.Description)) + target.Description = source.Description; + if (!string.IsNullOrEmpty(source.Title)) + target.Title = source.Title; + if (source.Default is not null) + target.Default = source.Default; + if (source.Reference is not null) + target.Reference = source.Reference; + } + } + private static void PrepareDescriptionForCopilot(OpenApiDocument document) { + var selectFirstAnyOneOfVisitor = new SelectFirstAnyOneOfVisitor(); + var selectFirstAnyOneOfWalker = new OpenApiWalker(selectFirstAnyOneOfVisitor); + selectFirstAnyOneOfWalker.Walk(document); + var allOfPropertiesRetrievalVisitor = new AllOfPropertiesRetrievalVisitor(); var allOfPropertiesRetrievalWalker = new OpenApiWalker(allOfPropertiesRetrievalVisitor); allOfPropertiesRetrievalWalker.Walk(document); @@ -155,41 +245,6 @@ private static void PrepareDescriptionForCopilot(OpenApiDocument document) mappingCleanupWalker.Walk(document); } - private static OpenApiDocument InlineRequestBodyAllOf(OpenApiDocument openApiDocument) - { - if (openApiDocument.Paths is null) return openApiDocument; - var contentItems = openApiDocument.Paths.Values.Where(static x => x?.Operations is not null) - .SelectMany(static x => x.Operations.Values.Where(static x => x?.RequestBody?.Content is not null) - .SelectMany(static x => x.RequestBody.Content.Values)); - foreach (var contentItem in contentItems) - { - var schema = contentItem.Schema; - schema = SelectFirstAnyOfOrOneOf(schema); - contentItem.Schema = schema; - } - - return openApiDocument; - - static OpenApiSchema? SelectFirstAnyOfOrOneOf(OpenApiSchema? schema) - { - if (schema?.AnyOf is not { Count: > 0 } && schema?.OneOf is not { Count: > 0 }) return schema; - OpenApiSchema newSchema; - if (schema.AnyOf is { Count: > 0 }) - { - newSchema = schema.AnyOf[0]; - } - else if (schema.OneOf is { Count: > 0 }) - { - newSchema = schema.OneOf[0]; - } - else - { - newSchema = schema; - } - return newSchema; - } - } - [GeneratedRegex(@"[^a-zA-Z0-9_]+", RegexOptions.IgnoreCase | RegexOptions.Singleline, 2000)] private static partial Regex PluginNameCleanupRegex(); From 1459e8d868f53165801c0f6422cd44a3c16766f2 Mon Sep 17 00:00:00 2001 From: Vincent Biret Date: Wed, 27 Nov 2024 14:21:45 -0500 Subject: [PATCH 05/12] fix: remove error status description Signed-off-by: Vincent Biret --- .../Plugins/PluginsGenerationService.cs | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/src/Kiota.Builder/Plugins/PluginsGenerationService.cs b/src/Kiota.Builder/Plugins/PluginsGenerationService.cs index 2af5acbe33..0a4b527717 100644 --- a/src/Kiota.Builder/Plugins/PluginsGenerationService.cs +++ b/src/Kiota.Builder/Plugins/PluginsGenerationService.cs @@ -230,8 +230,25 @@ private static void CopyRelevantInformation(OpenApiSchema source, OpenApiSchema } } + private sealed class ErrorResponsesCleanupVisitor : OpenApiVisitorBase + { + public override void Visit(OpenApiOperation operation) + { + if (operation.Responses is null) + return; + var errorResponses = operation.Responses.Where(static x => x.Key.StartsWith('4') || x.Key.StartsWith('5')).ToArray(); + foreach (var (key, value) in errorResponses) + operation.Responses.Remove(key); + base.Visit(operation); + } + } + private static void PrepareDescriptionForCopilot(OpenApiDocument document) { + var errorResponsesCleanupVisitor = new ErrorResponsesCleanupVisitor(); + var errorResponsesCleanupWalker = new OpenApiWalker(errorResponsesCleanupVisitor); + errorResponsesCleanupWalker.Walk(document); + var selectFirstAnyOneOfVisitor = new SelectFirstAnyOneOfVisitor(); var selectFirstAnyOneOfWalker = new OpenApiWalker(selectFirstAnyOneOfVisitor); selectFirstAnyOneOfWalker.Walk(document); From d150295ecec68a3f9ea06fe3044d5a3b448a17e4 Mon Sep 17 00:00:00 2001 From: Vincent Biret Date: Wed, 27 Nov 2024 14:29:00 -0500 Subject: [PATCH 06/12] feat: removes external documentation links Signed-off-by: Vincent Biret --- .../Plugins/PluginsGenerationService.cs | 32 +++++++++++++++++++ 1 file changed, 32 insertions(+) diff --git a/src/Kiota.Builder/Plugins/PluginsGenerationService.cs b/src/Kiota.Builder/Plugins/PluginsGenerationService.cs index 0a4b527717..b5f07b3494 100644 --- a/src/Kiota.Builder/Plugins/PluginsGenerationService.cs +++ b/src/Kiota.Builder/Plugins/PluginsGenerationService.cs @@ -243,8 +243,40 @@ public override void Visit(OpenApiOperation operation) } } + private sealed class ExternalDocumentationCleanupVisitor : OpenApiVisitorBase + { + public override void Visit(OpenApiDocument doc) + { + if (doc.ExternalDocs is not null) + doc.ExternalDocs = null; + base.Visit(doc); + } + public override void Visit(OpenApiOperation operation) + { + if (operation.ExternalDocs is not null) + operation.ExternalDocs = null; + base.Visit(operation); + } + public override void Visit(OpenApiSchema schema) + { + if (schema.ExternalDocs is not null) + schema.ExternalDocs = null; + base.Visit(schema); + } + public override void Visit(OpenApiTag tag) + { + if (tag.ExternalDocs is not null) + tag.ExternalDocs = null; + base.Visit(tag); + } + } + private static void PrepareDescriptionForCopilot(OpenApiDocument document) { + var externalDocumentationCleanupVisitor = new ExternalDocumentationCleanupVisitor(); + var externalDocumentationCleanupWalker = new OpenApiWalker(externalDocumentationCleanupVisitor); + externalDocumentationCleanupWalker.Walk(document); + var errorResponsesCleanupVisitor = new ErrorResponsesCleanupVisitor(); var errorResponsesCleanupWalker = new OpenApiWalker(errorResponsesCleanupVisitor); errorResponsesCleanupWalker.Walk(document); From f672a59d920aa672acbc3ee7553fbd389f171dc4 Mon Sep 17 00:00:00 2001 From: Vincent Biret Date: Wed, 27 Nov 2024 14:51:28 -0500 Subject: [PATCH 07/12] fix: failing unit tests Signed-off-by: Vincent Biret --- src/Kiota.Builder/Plugins/PluginsGenerationService.cs | 10 ++++++---- .../Plugins/PluginsGenerationServiceTests.cs | 5 ++--- 2 files changed, 8 insertions(+), 7 deletions(-) diff --git a/src/Kiota.Builder/Plugins/PluginsGenerationService.cs b/src/Kiota.Builder/Plugins/PluginsGenerationService.cs index b5f07b3494..1406aa91c7 100644 --- a/src/Kiota.Builder/Plugins/PluginsGenerationService.cs +++ b/src/Kiota.Builder/Plugins/PluginsGenerationService.cs @@ -131,6 +131,8 @@ public override void Visit(OpenApiSchema schema) if (schema.AllOf is not { Count: > 0 }) return; var allPropertiesToAdd = GetAllProperties(schema).ToArray(); + foreach (var allOfEntry in schema.AllOf) + SelectFirstAnyOneOfVisitor.CopyRelevantInformation(allOfEntry, schema, false, false, false); foreach (var (key, value) in allPropertiesToAdd) schema.Properties.TryAdd(key, value); schema.AllOf.Clear(); @@ -161,7 +163,7 @@ public override void Visit(OpenApiSchema schema) } base.Visit(schema); } - private static void CopyRelevantInformation(OpenApiSchema source, OpenApiSchema target) + internal static void CopyRelevantInformation(OpenApiSchema source, OpenApiSchema target, bool includeProperties = true, bool includeReference = true, bool includeDiscriminator = true) { if (!string.IsNullOrEmpty(source.Type)) target.Type = source.Type; @@ -169,7 +171,7 @@ private static void CopyRelevantInformation(OpenApiSchema source, OpenApiSchema target.Format = source.Format; if (source.Items is not null) target.Items = source.Items; - if (source.Properties is not null) + if (source.Properties is not null && includeProperties) target.Properties = new Dictionary(source.Properties); if (source.Required is not null) target.Required = new HashSet(source.Required); @@ -217,7 +219,7 @@ private static void CopyRelevantInformation(OpenApiSchema source, OpenApiSchema target.Example = source.Example; if (source.Extensions is not null) target.Extensions = new Dictionary(source.Extensions); - if (source.Discriminator is not null) + if (source.Discriminator is not null && includeDiscriminator) target.Discriminator = source.Discriminator; if (!string.IsNullOrEmpty(source.Description)) target.Description = source.Description; @@ -225,7 +227,7 @@ private static void CopyRelevantInformation(OpenApiSchema source, OpenApiSchema target.Title = source.Title; if (source.Default is not null) target.Default = source.Default; - if (source.Reference is not null) + if (source.Reference is not null && includeReference) target.Reference = source.Reference; } } diff --git a/tests/Kiota.Builder.Tests/Plugins/PluginsGenerationServiceTests.cs b/tests/Kiota.Builder.Tests/Plugins/PluginsGenerationServiceTests.cs index ae2ccc8a38..652d9d1c14 100644 --- a/tests/Kiota.Builder.Tests/Plugins/PluginsGenerationServiceTests.cs +++ b/tests/Kiota.Builder.Tests/Plugins/PluginsGenerationServiceTests.cs @@ -210,7 +210,6 @@ public async Task GeneratesManifestAndCleansUpInputDescriptionAsync() var workingDirectory = Path.Combine(Path.GetTempPath(), Path.GetRandomFileName()); var simpleDescriptionPath = Path.Combine(workingDirectory) + "description.yaml"; await File.WriteAllTextAsync(simpleDescriptionPath, simpleDescriptionContent); - var mockLogger = new Mock>(); var openAPIDocumentDS = new OpenApiDocumentDownloadService(_httpClient, _logger); var outputDirectory = Path.Combine(workingDirectory, "output"); var generationConfiguration = new GenerationConfiguration @@ -268,10 +267,10 @@ public async Task GeneratesManifestAndCleansUpInputDescriptionAsync() Assert.Empty(resultDocument.Components.Schemas);// no schema is referenced. so ensure they are all removed Assert.Empty(resultDocument.Extensions); // no extension at root (unsupported extension is removed) Assert.Equal(2, resultDocument.Paths.Count); // document has only two paths - Assert.Equal(originalDocument.Paths["/test"].Operations[OperationType.Get].Responses.Count, resultDocument.Paths["/test"].Operations[OperationType.Get].Responses.Count); // Responses are still intact. + Assert.Equal(originalDocument.Paths["/test"].Operations[OperationType.Get].Responses.Count - 1, resultDocument.Paths["/test"].Operations[OperationType.Get].Responses.Count); // We removed the error response Assert.NotEmpty(resultDocument.Paths["/test"].Operations[OperationType.Get].Responses["200"].Description); // response description string is not empty Assert.Empty(resultDocument.Paths["/test"].Operations[OperationType.Get].Extensions); // NO UNsupported extension - Assert.Equal(originalDocument.Paths["/test/{id}"].Operations[OperationType.Get].Responses.Count, resultDocument.Paths["/test/{id}"].Operations[OperationType.Get].Responses.Count); // Responses are still intact. + Assert.Equal(originalDocument.Paths["/test/{id}"].Operations[OperationType.Get].Responses.Count - 1, resultDocument.Paths["/test/{id}"].Operations[OperationType.Get].Responses.Count); // Responses are still intact. Assert.NotEmpty(resultDocument.Paths["/test/{id}"].Operations[OperationType.Get].Responses["200"].Description);// response description string is not empty Assert.Single(resultDocument.Paths["/test/{id}"].Operations[OperationType.Get].Extensions); // 1 supported extension still present in operation } From 3962cb79a6f6ba510edd47ed355d27ccd92fc47f Mon Sep 17 00:00:00 2001 From: Vincent Biret Date: Wed, 27 Nov 2024 15:17:56 -0500 Subject: [PATCH 08/12] chore: adds test data for all of cleanup Signed-off-by: Vincent Biret --- .../Plugins/PluginsGenerationServiceTests.cs | 21 ++++++++++++++++--- 1 file changed, 18 insertions(+), 3 deletions(-) diff --git a/tests/Kiota.Builder.Tests/Plugins/PluginsGenerationServiceTests.cs b/tests/Kiota.Builder.Tests/Plugins/PluginsGenerationServiceTests.cs index 652d9d1c14..d2f2ac9549 100644 --- a/tests/Kiota.Builder.Tests/Plugins/PluginsGenerationServiceTests.cs +++ b/tests/Kiota.Builder.Tests/Plugins/PluginsGenerationServiceTests.cs @@ -193,6 +193,10 @@ public async Task GeneratesManifestAndCleansUpInputDescriptionAsync() responses: '200': description: + content: + application/json: + schema: + $ref: '#/components/schemas/microsoft.graph.message' '500': description: api error response components: @@ -206,7 +210,17 @@ public async Task GeneratesManifestAndCleansUpInputDescriptionAsync() id: type: string '@odata.type': - type: string"; + type: string + microsoft.graph.message: + allOf: + - $ref: '#/components/schemas/microsoft.graph.entity' + - type: object + title: message + properties: + subject: + type: string + body: + type: string"; var workingDirectory = Path.Combine(Path.GetTempPath(), Path.GetRandomFileName()); var simpleDescriptionPath = Path.Combine(workingDirectory) + "description.yaml"; await File.WriteAllTextAsync(simpleDescriptionPath, simpleDescriptionContent); @@ -250,7 +264,7 @@ public async Task GeneratesManifestAndCleansUpInputDescriptionAsync() Assert.Equal(originalDocument.Paths["/test"].Operations[OperationType.Get].Description, resultingManifest.Document.Functions[0].Description);// pulls from description Assert.Equal(originalDocument.Paths["/test/{id}"].Operations[OperationType.Get].Summary, resultingManifest.Document.Functions[1].Description);// pulls from summary - Assert.Single(originalDocument.Components.Schemas);// one schema originally + Assert.Equal(2, originalDocument.Components.Schemas.Count);// one schema originally Assert.Single(originalDocument.Extensions); // single unsupported extension at root Assert.Equal(2, originalDocument.Paths.Count); // document has only two paths Assert.Equal(2, originalDocument.Paths["/test"].Operations[OperationType.Get].Responses.Count); // 2 responses originally @@ -264,7 +278,7 @@ public async Task GeneratesManifestAndCleansUpInputDescriptionAsync() Assert.Empty(diagnostic.Errors); // Assertions / validations - Assert.Empty(resultDocument.Components.Schemas);// no schema is referenced. so ensure they are all removed + Assert.Single(resultDocument.Components.Schemas);// no schema is referenced. so ensure they are all removed Assert.Empty(resultDocument.Extensions); // no extension at root (unsupported extension is removed) Assert.Equal(2, resultDocument.Paths.Count); // document has only two paths Assert.Equal(originalDocument.Paths["/test"].Operations[OperationType.Get].Responses.Count - 1, resultDocument.Paths["/test"].Operations[OperationType.Get].Responses.Count); // We removed the error response @@ -273,6 +287,7 @@ public async Task GeneratesManifestAndCleansUpInputDescriptionAsync() Assert.Equal(originalDocument.Paths["/test/{id}"].Operations[OperationType.Get].Responses.Count - 1, resultDocument.Paths["/test/{id}"].Operations[OperationType.Get].Responses.Count); // Responses are still intact. Assert.NotEmpty(resultDocument.Paths["/test/{id}"].Operations[OperationType.Get].Responses["200"].Description);// response description string is not empty Assert.Single(resultDocument.Paths["/test/{id}"].Operations[OperationType.Get].Extensions); // 1 supported extension still present in operation + Assert.Empty(resultDocument.Paths["/test/{id}"].Operations[OperationType.Get].Responses["200"].Content["application/json"].Schema.AllOf); // allOf were merged } #region Security From 9482a8455e419c463c44a9a26cf234897770fa5b Mon Sep 17 00:00:00 2001 From: Vincent Biret Date: Wed, 27 Nov 2024 15:20:37 -0500 Subject: [PATCH 09/12] chore: adds test data for external docs removal Signed-off-by: Vincent Biret --- .../Plugins/PluginsGenerationServiceTests.cs | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/tests/Kiota.Builder.Tests/Plugins/PluginsGenerationServiceTests.cs b/tests/Kiota.Builder.Tests/Plugins/PluginsGenerationServiceTests.cs index d2f2ac9549..c524d7d6b1 100644 --- a/tests/Kiota.Builder.Tests/Plugins/PluginsGenerationServiceTests.cs +++ b/tests/Kiota.Builder.Tests/Plugins/PluginsGenerationServiceTests.cs @@ -171,6 +171,9 @@ public async Task GeneratesManifestAndCleansUpInputDescriptionAsync() /test: get: description: description for test path + externalDocs: + description: external docs for test path + url: http://localhost/test x-random-extension: true responses: '200': @@ -264,6 +267,7 @@ public async Task GeneratesManifestAndCleansUpInputDescriptionAsync() Assert.Equal(originalDocument.Paths["/test"].Operations[OperationType.Get].Description, resultingManifest.Document.Functions[0].Description);// pulls from description Assert.Equal(originalDocument.Paths["/test/{id}"].Operations[OperationType.Get].Summary, resultingManifest.Document.Functions[1].Description);// pulls from summary + Assert.NotNull(originalDocument.Paths["/test"].Operations[OperationType.Get].ExternalDocs); // existing external docs Assert.Equal(2, originalDocument.Components.Schemas.Count);// one schema originally Assert.Single(originalDocument.Extensions); // single unsupported extension at root Assert.Equal(2, originalDocument.Paths.Count); // document has only two paths @@ -283,6 +287,7 @@ public async Task GeneratesManifestAndCleansUpInputDescriptionAsync() Assert.Equal(2, resultDocument.Paths.Count); // document has only two paths Assert.Equal(originalDocument.Paths["/test"].Operations[OperationType.Get].Responses.Count - 1, resultDocument.Paths["/test"].Operations[OperationType.Get].Responses.Count); // We removed the error response Assert.NotEmpty(resultDocument.Paths["/test"].Operations[OperationType.Get].Responses["200"].Description); // response description string is not empty + Assert.Null(resultDocument.Paths["/test"].Operations[OperationType.Get].ExternalDocs); // external docs are removed Assert.Empty(resultDocument.Paths["/test"].Operations[OperationType.Get].Extensions); // NO UNsupported extension Assert.Equal(originalDocument.Paths["/test/{id}"].Operations[OperationType.Get].Responses.Count - 1, resultDocument.Paths["/test/{id}"].Operations[OperationType.Get].Responses.Count); // Responses are still intact. Assert.NotEmpty(resultDocument.Paths["/test/{id}"].Operations[OperationType.Get].Responses["200"].Description);// response description string is not empty From 13c04769a3d78e7297d30bdc2583e44fbfd853ec Mon Sep 17 00:00:00 2001 From: Vincent Biret Date: Wed, 27 Nov 2024 15:24:01 -0500 Subject: [PATCH 10/12] chore: adds test for any of selection Signed-off-by: Vincent Biret --- .../Plugins/PluginsGenerationServiceTests.cs | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/tests/Kiota.Builder.Tests/Plugins/PluginsGenerationServiceTests.cs b/tests/Kiota.Builder.Tests/Plugins/PluginsGenerationServiceTests.cs index c524d7d6b1..b5f86312b5 100644 --- a/tests/Kiota.Builder.Tests/Plugins/PluginsGenerationServiceTests.cs +++ b/tests/Kiota.Builder.Tests/Plugins/PluginsGenerationServiceTests.cs @@ -211,7 +211,9 @@ public async Task GeneratesManifestAndCleansUpInputDescriptionAsync() type: object properties: id: - type: string + anyOf: + - type: string + - type: integer '@odata.type': type: string microsoft.graph.message: @@ -275,6 +277,7 @@ public async Task GeneratesManifestAndCleansUpInputDescriptionAsync() Assert.Single(originalDocument.Paths["/test"].Operations[OperationType.Get].Extensions); // 1 UNsupported extension Assert.Equal(2, originalDocument.Paths["/test/{id}"].Operations[OperationType.Get].Responses.Count); // 2 responses originally Assert.Single(originalDocument.Paths["/test/{id}"].Operations[OperationType.Get].Extensions); // 1 supported extension + Assert.Equal(2, originalDocument.Paths["/test/{id}"].Operations[OperationType.Get].Responses["200"].Content["application/json"].Schema.AllOf[0].Properties["id"].AnyOf.Count); // anyOf we selected // Validate the output open api file var resultOpenApiFile = File.OpenRead(Path.Combine(outputDirectory, OpenApiFileName)); @@ -293,6 +296,8 @@ public async Task GeneratesManifestAndCleansUpInputDescriptionAsync() Assert.NotEmpty(resultDocument.Paths["/test/{id}"].Operations[OperationType.Get].Responses["200"].Description);// response description string is not empty Assert.Single(resultDocument.Paths["/test/{id}"].Operations[OperationType.Get].Extensions); // 1 supported extension still present in operation Assert.Empty(resultDocument.Paths["/test/{id}"].Operations[OperationType.Get].Responses["200"].Content["application/json"].Schema.AllOf); // allOf were merged + Assert.Empty(resultDocument.Paths["/test/{id}"].Operations[OperationType.Get].Responses["200"].Content["application/json"].Schema.Properties["id"].AnyOf); // anyOf we selected + Assert.Equal("string", resultDocument.Paths["/test/{id}"].Operations[OperationType.Get].Responses["200"].Content["application/json"].Schema.Properties["id"].Type); } #region Security From 59ffddab1254b44c56c261adc85b9622387b7a92 Mon Sep 17 00:00:00 2001 From: Vincent Biret Date: Wed, 27 Nov 2024 15:25:48 -0500 Subject: [PATCH 11/12] chore: adds a test for error responses removal Signed-off-by: Vincent Biret --- .../Kiota.Builder.Tests/Plugins/PluginsGenerationServiceTests.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/Kiota.Builder.Tests/Plugins/PluginsGenerationServiceTests.cs b/tests/Kiota.Builder.Tests/Plugins/PluginsGenerationServiceTests.cs index b5f86312b5..6743f5c63a 100644 --- a/tests/Kiota.Builder.Tests/Plugins/PluginsGenerationServiceTests.cs +++ b/tests/Kiota.Builder.Tests/Plugins/PluginsGenerationServiceTests.cs @@ -298,6 +298,7 @@ public async Task GeneratesManifestAndCleansUpInputDescriptionAsync() Assert.Empty(resultDocument.Paths["/test/{id}"].Operations[OperationType.Get].Responses["200"].Content["application/json"].Schema.AllOf); // allOf were merged Assert.Empty(resultDocument.Paths["/test/{id}"].Operations[OperationType.Get].Responses["200"].Content["application/json"].Schema.Properties["id"].AnyOf); // anyOf we selected Assert.Equal("string", resultDocument.Paths["/test/{id}"].Operations[OperationType.Get].Responses["200"].Content["application/json"].Schema.Properties["id"].Type); + Assert.DoesNotContain("500", resultDocument.Paths["/test/{id}"].Operations[OperationType.Get].Responses.Keys, StringComparer.OrdinalIgnoreCase); // We removed the error response } #region Security From 494a05d38bc3f984840bd9e18a69ba28a965040d Mon Sep 17 00:00:00 2001 From: Vincent Biret Date: Wed, 27 Nov 2024 15:28:30 -0500 Subject: [PATCH 12/12] docs: adds changelog entry for openapi plugins generation Signed-off-by: Vincent Biret --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index d85163d3e4..3256dc56bf 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -21,6 +21,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Fixed invalid code in Php caused by "*/*/" in property description. [5635](https://github.com/microsoft/kiota/issues/5635) - Fixed a bug where discriminator property name lookup could end up in an infinite loop. [#5771](https://github.com/microsoft/kiota/issues/5771) - Fixed TypeScript generation error when generating usings from shaken serializers. [#5634](https://github.com/microsoft/kiota/issues/5634) +- Multiple fixed and improvements in OpenAPI description generation for plugins. [#5806](https://github.com/microsoft/kiota/issues/5806) ## [1.20.0] - 2024-11-07