diff --git a/CHANGELOG.md b/CHANGELOG.md index 30df685762..7fdb78e754 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -17,6 +17,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Fixed a bug where the copilot teams toolkit integration would serialize empty declarative copilots. [#4974](https://github.com/microsoft/kiota/issues/4974) - Fixed a bug for the docker image where the volume path would not match the expected configuration for the description. - Fixed a bug in Go where certain namespaces were escaped unexpectedly. [#5012](https://github.com/microsoft/kiota/issues/5012) +- Fixed file name and namespace sanitization when generating plugins. [#5019](https://github.com/microsoft/kiota/issues/5019) ## [1.16.0] - 2024-07-05 diff --git a/src/Kiota.Builder/Plugins/PluginsGenerationService.cs b/src/Kiota.Builder/Plugins/PluginsGenerationService.cs index 0bb6641c57..48f4132beb 100644 --- a/src/Kiota.Builder/Plugins/PluginsGenerationService.cs +++ b/src/Kiota.Builder/Plugins/PluginsGenerationService.cs @@ -4,6 +4,7 @@ using System.Linq; using System.Text.Json; using System.Text.Json.Serialization; +using System.Text.RegularExpressions; using System.Threading; using System.Threading.Tasks; using Kiota.Builder.Configuration; @@ -18,7 +19,7 @@ using Microsoft.Plugins.Manifest; namespace Kiota.Builder.Plugins; -public class PluginsGenerationService +public partial class PluginsGenerationService { private readonly OpenApiDocument OAIDocument; private readonly OpenApiUrlTreeNode TreeNode; @@ -43,7 +44,9 @@ public PluginsGenerationService(OpenApiDocument document, OpenApiUrlTreeNode ope private const string AppManifestFileName = "manifest.json"; public async Task GenerateManifestAsync(CancellationToken cancellationToken = default) { - // 1. write the OpenApi description + // 1. cleanup any namings to be used later on. + Configuration.ClientClassName = PluginNameCleanupRegex().Replace(Configuration.ClientClassName, string.Empty); //drop any special characters + // 2. write the OpenApi description var descriptionRelativePath = $"{Configuration.ClientClassName.ToLowerInvariant()}-{DescriptionPathSuffix}"; var descriptionFullPath = Path.Combine(Configuration.OutputPath, descriptionRelativePath); var directory = Path.GetDirectoryName(descriptionFullPath); @@ -58,7 +61,7 @@ public async Task GenerateManifestAsync(CancellationToken cancellationToken = de trimmedPluginDocument.SerializeAsV3(descriptionWriter); descriptionWriter.Flush(); - // 2. write the plugins + // 3. write the plugins foreach (var pluginType in Configuration.PluginTypes) { @@ -98,7 +101,7 @@ public async Task GenerateManifestAsync(CancellationToken cancellationToken = de await writer.FlushAsync(cancellationToken).ConfigureAwait(false); } - // 3. write the app manifest if its an Api Plugin + // 4. write the app manifest if its an Api Plugin if (Configuration.PluginTypes.Any(static plugin => plugin == PluginType.APIPlugin)) { var manifestFullPath = Path.Combine(Configuration.OutputPath, AppManifestFileName); @@ -111,6 +114,9 @@ public async Task GenerateManifestAsync(CancellationToken cancellationToken = de } } + [GeneratedRegex(@"[^a-zA-Z0-9_]+", RegexOptions.IgnoreCase | RegexOptions.Singleline, 2000)] + private static partial Regex PluginNameCleanupRegex(); + private async Task GetAppManifestModelAsync(string pluginFileName, string manifestFullPath, CancellationToken cancellationToken) { var manifestInfo = ExtractInfoFromDocument(OAIDocument.Info); diff --git a/tests/Kiota.Builder.Tests/Plugins/PluginsGenerationServiceTests.cs b/tests/Kiota.Builder.Tests/Plugins/PluginsGenerationServiceTests.cs index c04055de9e..bb39cb9396 100644 --- a/tests/Kiota.Builder.Tests/Plugins/PluginsGenerationServiceTests.cs +++ b/tests/Kiota.Builder.Tests/Plugins/PluginsGenerationServiceTests.cs @@ -33,8 +33,11 @@ public void Dispose() _httpClient.Dispose(); } - [Fact] - public async Task GeneratesManifest() + [Theory] + [InlineData("client", "client")] + [InlineData("Budget Tracker", "BudgetTracker")]//drop the space + [InlineData("My-Super complex() %@#$& Name", "MySupercomplexName")]//drop the space and special characters + public async Task GeneratesManifest(string inputPluginName, string expectedPluginName) { var simpleDescriptionContent = @"openapi: 3.0.0 info: @@ -76,7 +79,7 @@ public async Task GeneratesManifest() OutputPath = outputDirectory, OpenAPIFilePath = "openapiPath", PluginTypes = [PluginType.APIPlugin, PluginType.APIManifest, PluginType.OpenAI], - ClientClassName = "client", + ClientClassName = inputPluginName, ApiRootUrl = "http://localhost/", //Kiota builder would set this for us }; var (openAPIDocumentStream, _) = await openAPIDocumentDS.LoadStreamAsync(simpleDescriptionPath, generationConfiguration, null, false); @@ -87,19 +90,20 @@ public async Task GeneratesManifest() var pluginsGenerationService = new PluginsGenerationService(openApiDocument, urlTreeNode, generationConfiguration, workingDirectory); await pluginsGenerationService.GenerateManifestAsync(); - Assert.True(File.Exists(Path.Combine(outputDirectory, ManifestFileName))); - Assert.True(File.Exists(Path.Combine(outputDirectory, "client-apimanifest.json"))); + Assert.True(File.Exists(Path.Combine(outputDirectory, $"{expectedPluginName.ToLower()}-apiplugin.json"))); + Assert.True(File.Exists(Path.Combine(outputDirectory, $"{expectedPluginName.ToLower()}-apimanifest.json"))); Assert.True(File.Exists(Path.Combine(outputDirectory, OpenAIPluginFileName))); - Assert.True(File.Exists(Path.Combine(outputDirectory, OpenApiFileName))); + Assert.True(File.Exists(Path.Combine(outputDirectory, $"{expectedPluginName.ToLower()}-openapi.yml"))); Assert.True(File.Exists(Path.Combine(outputDirectory, AppManifestFileName))); // Validate the v2 plugin - var manifestContent = await File.ReadAllTextAsync(Path.Combine(outputDirectory, ManifestFileName)); + var manifestContent = await File.ReadAllTextAsync(Path.Combine(outputDirectory, $"{expectedPluginName.ToLower()}-apiplugin.json")); using var jsonDocument = JsonDocument.Parse(manifestContent); var resultingManifest = PluginManifestDocument.Load(jsonDocument.RootElement); Assert.NotNull(resultingManifest.Document); - Assert.Equal(OpenApiFileName, resultingManifest.Document.Runtimes.OfType().First().Spec.Url); + Assert.Equal($"{expectedPluginName.ToLower()}-openapi.yml", resultingManifest.Document.Runtimes.OfType().First().Spec.Url); Assert.Equal(2, resultingManifest.Document.Functions.Count);// all functions are generated despite missing operationIds + Assert.Equal(expectedPluginName, resultingManifest.Document.Namespace);// namespace is cleaned up. Assert.Empty(resultingManifest.Problems);// no problems are expected with names // Validate the v1 plugin @@ -107,20 +111,20 @@ public async Task GeneratesManifest() using var v1JsonDocument = JsonDocument.Parse(v1ManifestContent); var v1Manifest = PluginManifestDocument.Load(v1JsonDocument.RootElement); Assert.NotNull(resultingManifest.Document); - Assert.Equal(OpenApiFileName, v1Manifest.Document.Api.URL); + Assert.Equal($"{expectedPluginName.ToLower()}-openapi.yml", v1Manifest.Document.Api.URL); Assert.Empty(v1Manifest.Problems); // Validate the manifest file var appManifestFile = await File.ReadAllTextAsync(Path.Combine(outputDirectory, AppManifestFileName)); var appManifestModelObject = JsonSerializer.Deserialize(appManifestFile, PluginsGenerationService.AppManifestModelGenerationContext.AppManifestModel); - Assert.Equal("com.microsoft.kiota.plugin.client", appManifestModelObject.PackageName); - Assert.Equal("client", appManifestModelObject.Name.ShortName); + Assert.Equal($"com.microsoft.kiota.plugin.{expectedPluginName}", appManifestModelObject.PackageName); + Assert.Equal(expectedPluginName, appManifestModelObject.Name.ShortName); Assert.Equal("Microsoft Kiota.", appManifestModelObject.Developer.Name); Assert.Equal("color.png", appManifestModelObject.Icons.Color); Assert.NotNull(appManifestModelObject.CopilotExtensions.Plugins); Assert.Single(appManifestModelObject.CopilotExtensions.Plugins); - Assert.Equal("client", appManifestModelObject.CopilotExtensions.Plugins[0].Id); - Assert.Equal(ManifestFileName, appManifestModelObject.CopilotExtensions.Plugins[0].File); + Assert.Equal(expectedPluginName, appManifestModelObject.CopilotExtensions.Plugins[0].Id); + Assert.Equal($"{expectedPluginName.ToLower()}-apiplugin.json", appManifestModelObject.CopilotExtensions.Plugins[0].File); } private const string ManifestFileName = "client-apiplugin.json"; private const string OpenAIPluginFileName = "openai-plugins.json";