Skip to content

Commit

Permalink
Merge pull request #5024 from microsoft/andrueastman/nameSanitization
Browse files Browse the repository at this point in the history
Fixes file name and namespace sanitization
  • Loading branch information
baywet authored Jul 26, 2024
2 parents 7a81340 + fbe351c commit 827d97d
Show file tree
Hide file tree
Showing 3 changed files with 28 additions and 17 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
14 changes: 10 additions & 4 deletions src/Kiota.Builder/Plugins/PluginsGenerationService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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;
Expand All @@ -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);
Expand All @@ -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)
{
Expand Down Expand Up @@ -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);
Expand All @@ -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<AppManifestModel> GetAppManifestModelAsync(string pluginFileName, string manifestFullPath, CancellationToken cancellationToken)
{
var manifestInfo = ExtractInfoFromDocument(OAIDocument.Info);
Expand Down
30 changes: 17 additions & 13 deletions tests/Kiota.Builder.Tests/Plugins/PluginsGenerationServiceTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down Expand Up @@ -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);
Expand All @@ -87,40 +90,41 @@ 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<OpenApiRuntime>().First().Spec.Url);
Assert.Equal($"{expectedPluginName.ToLower()}-openapi.yml", resultingManifest.Document.Runtimes.OfType<OpenApiRuntime>().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
var v1ManifestContent = await File.ReadAllTextAsync(Path.Combine(outputDirectory, OpenAIPluginFileName));
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";
Expand Down

0 comments on commit 827d97d

Please sign in to comment.