From 1be70d47ef21485a7a50053cc5df4ab973492328 Mon Sep 17 00:00:00 2001 From: Andrew Omondi Date: Fri, 28 Jun 2024 16:33:45 +0300 Subject: [PATCH 1/8] WIP: todo more testing --- .../Plugins/Models/AppManifestModel.cs | 71 +++++++++++++++++++ .../Plugins/PluginsGenerationService.cs | 54 ++++++++++++-- 2 files changed, 121 insertions(+), 4 deletions(-) create mode 100644 src/Kiota.Builder/Plugins/Models/AppManifestModel.cs diff --git a/src/Kiota.Builder/Plugins/Models/AppManifestModel.cs b/src/Kiota.Builder/Plugins/Models/AppManifestModel.cs new file mode 100644 index 0000000000..369008212e --- /dev/null +++ b/src/Kiota.Builder/Plugins/Models/AppManifestModel.cs @@ -0,0 +1,71 @@ +using System; +using System.Collections.Generic; +using System.Text.Json.Serialization; + +namespace Kiota.Builder.Plugins.Models; + +public class AppManifestModel(string pluginName, string documentName, string documentDescription) +{ + [JsonPropertyName("$schema")] + public string Schema { get; set; } = "https://developer.microsoft.com/json-schemas/teams/vDevPreview/MicrosoftTeams.schema.json"; + public string ManifestVersion { get; set; } = "devPreview"; + public string Version { get; set; } = "1.0.0"; + public string Id { get; set; } = Guid.NewGuid().ToString(); + public Developer Developer { get; init; } = new (); + public string PackageName { get; set; } = $"com.microsoft.kiota.plugin.{pluginName}"; + public Name Name { get; set; } = new (pluginName,documentName); + public Description Description { get; set; } = new (documentDescription,documentName); + public Icons Icons { get; set; } = new (); + public string AccentColor { get; set; } = "#FFFFFF"; + public CopilotExtensions CopilotExtensions { get; set; } = new (); +} + +[JsonSerializable(typeof(AppManifestModel))] +internal partial class AppManifestModelGenerationContext : JsonSerializerContext +{ +} + +#pragma warning disable CA1054 +public class Developer(string? name = null, string? websiteUrl= null, string? privacyUrl =null, string? termsOfUseUrl=null) +#pragma warning restore CA1054 +{ + public string Name { get; set; } = !string.IsNullOrEmpty(name) ? name: "Kiota Generator, Inc."; +#pragma warning disable CA1056 + public string WebsiteUrl { get; set; } = !string.IsNullOrEmpty(websiteUrl) ? websiteUrl:"https://www.example.com/contact/"; + public string PrivacyUrl { get; set; } = !string.IsNullOrEmpty(privacyUrl) ? privacyUrl:"https://www.example.com/privacy/"; + public string TermsOfUseUrl { get; set; } = !string.IsNullOrEmpty(termsOfUseUrl) ? termsOfUseUrl:"https://www.example.com/terms/"; +#pragma warning restore CA1056 +} + +public class Name(string pluginName, string documentName) +{ + [JsonPropertyName("short")] + public string ShortName { get; private set; } = pluginName; + [JsonPropertyName("full")] + public string FullName { get; private set; } = $"API Plugin {pluginName} for {documentName}"; +} + +public class Description(string description, string documentName) +{ + [JsonPropertyName("short")] + public string ShortName { get; private set; } = !string.IsNullOrEmpty(description) ? $"API Plugin for {description}." : documentName; + [JsonPropertyName("full")] + public string FullName { get; private set; } = !string.IsNullOrEmpty(description) ? $"API Plugin for {description}." : documentName; +} + +public class Icons +{ + public string Color { get; set; } = "color.png"; + public string Outline { get; set; } = "outline.png"; +} + +public class CopilotExtensions +{ + public IList Plugins { get; } = new List(); +} + +public class Plugin(string pluginName, string fileName) +{ + public string Id { get; set; } = pluginName; + public string File { get; set; } = fileName; +} diff --git a/src/Kiota.Builder/Plugins/PluginsGenerationService.cs b/src/Kiota.Builder/Plugins/PluginsGenerationService.cs index 026f14b47f..7acafdc9b7 100644 --- a/src/Kiota.Builder/Plugins/PluginsGenerationService.cs +++ b/src/Kiota.Builder/Plugins/PluginsGenerationService.cs @@ -8,6 +8,7 @@ using Kiota.Builder.Configuration; using Kiota.Builder.Extensions; using Kiota.Builder.OpenApiExtensions; +using Kiota.Builder.Plugins.Models; using Microsoft.Kiota.Abstractions.Extensions; using Microsoft.OpenApi.ApiManifest; using Microsoft.OpenApi.Models; @@ -38,9 +39,10 @@ public PluginsGenerationService(OpenApiDocument document, OpenApiUrlTreeNode ope private const string ManifestFileNameSuffix = ".json"; private const string DescriptionPathSuffix = "openapi.yml"; private const string OpenAIManifestFileName = "openai-plugins"; + private const string AppManifestFileName = "manifest.json"; public async Task GenerateManifestAsync(CancellationToken cancellationToken = default) { - // write the description + // 1. write the OpenApi description var descriptionRelativePath = $"{Configuration.ClientClassName.ToLowerInvariant()}-{DescriptionPathSuffix}"; var descriptionFullPath = Path.Combine(Configuration.OutputPath, descriptionRelativePath); var directory = Path.GetDirectoryName(descriptionFullPath); @@ -55,11 +57,13 @@ public async Task GenerateManifestAsync(CancellationToken cancellationToken = de trimmedPluginDocument.SerializeAsV3(descriptionWriter); descriptionWriter.Flush(); - // write the plugins + // 2. write the plugins + var pluginFileName = string.Empty; foreach (var pluginType in Configuration.PluginTypes) { var manifestFileName = pluginType == PluginType.OpenAI ? OpenAIManifestFileName : $"{Configuration.ClientClassName.ToLowerInvariant()}-{pluginType.ToString().ToLowerInvariant()}"; - var manifestOutputPath = Path.Combine(Configuration.OutputPath, $"{manifestFileName}{ManifestFileNameSuffix}"); + pluginFileName = $"{manifestFileName}{ManifestFileNameSuffix}"; + var manifestOutputPath = Path.Combine(Configuration.OutputPath, pluginFileName); #pragma warning disable CA2007 // Consider calling ConfigureAwait on the awaited task await using var fileStream = File.Create(manifestOutputPath, 4096); await using var writer = new Utf8JsonWriter(fileStream, new JsonWriterOptions { Indented = true }); @@ -93,9 +97,51 @@ public async Task GenerateManifestAsync(CancellationToken cancellationToken = de } await writer.FlushAsync(cancellationToken).ConfigureAwait(false); } - } + + // 3. write the app manifest + var manifestFullPath = Path.Combine(Configuration.OutputPath, AppManifestFileName); + var manifestInfo = ExtractInfoFromDocument(OAIDocument.Info); + var developerInfo = new Developer(OAIDocument.Info?.Contact?.Name, OAIDocument.Info?.Contact?.Url?.OriginalString , manifestInfo.PrivacyUrl,OAIDocument.Info?.TermsOfService?.OriginalString); + // create default model + AppManifestModel manifestModel = new AppManifestModel( + Configuration.ClientClassName, + OAIDocument.Info?.Title.CleanupXMLString() ?? "OpenApi Document", + OAIDocument.Info?.Description.CleanupXMLString() ?? "OpenApi Description") + { + Developer = developerInfo + }; + + if (File.Exists(manifestFullPath)) // No need for default, try to update the model from the file + { +#pragma warning disable CA2007 // Consider calling ConfigureAwait on the awaited task + await using var fileStream = File.OpenRead(manifestFullPath); +#pragma warning restore CA2007 // Consider calling ConfigureAwait on the awaited task + var manifestModelFromFile = await JsonSerializer.DeserializeAsync(fileStream,AppManifestModelGenerationContext.AppManifestModel,cancellationToken).ConfigureAwait(false); + if (manifestModelFromFile != null) + manifestModel = manifestModelFromFile; + } + if (manifestModel.CopilotExtensions.Plugins.FirstOrDefault(pluginItem => + pluginItem.Id.Equals(Configuration.ClientClassName, StringComparison.OrdinalIgnoreCase)) is { } plugin) + { + plugin.File = pluginFileName; + } + else + { + manifestModel.CopilotExtensions.Plugins.Add(new Plugin(Configuration.ClientClassName,pluginFileName)); + } + +#pragma warning disable CA2007 + await using var appManifestStream = File.Open(manifestFullPath, FileMode.Create); +#pragma warning restore CA2007 + await JsonSerializer.SerializeAsync(appManifestStream, manifestModel, AppManifestModelGenerationContext.AppManifestModel, cancellationToken).ConfigureAwait(false); + } + private static readonly AppManifestModelGenerationContext AppManifestModelGenerationContext = new(new JsonSerializerOptions{ + PropertyNamingPolicy = JsonNamingPolicy.CamelCase, + WriteIndented = true, + }); + private OpenApiDocument GetDocumentWithTrimmedComponentsAndResponses(OpenApiDocument doc) { // ensure the info and components are not null From ba27a478269216accc18abb66d9e3909f8eee789 Mon Sep 17 00:00:00 2001 From: Andrew Omondi Date: Fri, 28 Jun 2024 16:48:35 +0300 Subject: [PATCH 2/8] format --- .../Plugins/Models/AppManifestModel.cs | 28 +++++++++---------- .../Plugins/PluginsGenerationService.cs | 17 +++++------ 2 files changed, 23 insertions(+), 22 deletions(-) diff --git a/src/Kiota.Builder/Plugins/Models/AppManifestModel.cs b/src/Kiota.Builder/Plugins/Models/AppManifestModel.cs index 369008212e..4c5e70dc41 100644 --- a/src/Kiota.Builder/Plugins/Models/AppManifestModel.cs +++ b/src/Kiota.Builder/Plugins/Models/AppManifestModel.cs @@ -11,13 +11,13 @@ public class AppManifestModel(string pluginName, string documentName, string doc public string ManifestVersion { get; set; } = "devPreview"; public string Version { get; set; } = "1.0.0"; public string Id { get; set; } = Guid.NewGuid().ToString(); - public Developer Developer { get; init; } = new (); + public Developer Developer { get; init; } = new(); public string PackageName { get; set; } = $"com.microsoft.kiota.plugin.{pluginName}"; - public Name Name { get; set; } = new (pluginName,documentName); - public Description Description { get; set; } = new (documentDescription,documentName); - public Icons Icons { get; set; } = new (); + public Name Name { get; set; } = new(pluginName, documentName); + public Description Description { get; set; } = new(documentDescription, documentName); + public Icons Icons { get; set; } = new(); public string AccentColor { get; set; } = "#FFFFFF"; - public CopilotExtensions CopilotExtensions { get; set; } = new (); + public CopilotExtensions CopilotExtensions { get; set; } = new(); } [JsonSerializable(typeof(AppManifestModel))] @@ -26,16 +26,16 @@ internal partial class AppManifestModelGenerationContext : JsonSerializerContext } #pragma warning disable CA1054 -public class Developer(string? name = null, string? websiteUrl= null, string? privacyUrl =null, string? termsOfUseUrl=null) +public class Developer(string? name = null, string? websiteUrl = null, string? privacyUrl = null, string? termsOfUseUrl = null) #pragma warning restore CA1054 -{ - public string Name { get; set; } = !string.IsNullOrEmpty(name) ? name: "Kiota Generator, Inc."; +{ + public string Name { get; set; } = !string.IsNullOrEmpty(name) ? name : "Kiota Generator, Inc."; #pragma warning disable CA1056 - public string WebsiteUrl { get; set; } = !string.IsNullOrEmpty(websiteUrl) ? websiteUrl:"https://www.example.com/contact/"; - public string PrivacyUrl { get; set; } = !string.IsNullOrEmpty(privacyUrl) ? privacyUrl:"https://www.example.com/privacy/"; - public string TermsOfUseUrl { get; set; } = !string.IsNullOrEmpty(termsOfUseUrl) ? termsOfUseUrl:"https://www.example.com/terms/"; + public string WebsiteUrl { get; set; } = !string.IsNullOrEmpty(websiteUrl) ? websiteUrl : "https://www.example.com/contact/"; + public string PrivacyUrl { get; set; } = !string.IsNullOrEmpty(privacyUrl) ? privacyUrl : "https://www.example.com/privacy/"; + public string TermsOfUseUrl { get; set; } = !string.IsNullOrEmpty(termsOfUseUrl) ? termsOfUseUrl : "https://www.example.com/terms/"; #pragma warning restore CA1056 -} +} public class Name(string pluginName, string documentName) { @@ -43,7 +43,7 @@ public class Name(string pluginName, string documentName) public string ShortName { get; private set; } = pluginName; [JsonPropertyName("full")] public string FullName { get; private set; } = $"API Plugin {pluginName} for {documentName}"; -} +} public class Description(string description, string documentName) { @@ -51,7 +51,7 @@ public class Description(string description, string documentName) public string ShortName { get; private set; } = !string.IsNullOrEmpty(description) ? $"API Plugin for {description}." : documentName; [JsonPropertyName("full")] public string FullName { get; private set; } = !string.IsNullOrEmpty(description) ? $"API Plugin for {description}." : documentName; -} +} public class Icons { diff --git a/src/Kiota.Builder/Plugins/PluginsGenerationService.cs b/src/Kiota.Builder/Plugins/PluginsGenerationService.cs index 7acafdc9b7..f873694892 100644 --- a/src/Kiota.Builder/Plugins/PluginsGenerationService.cs +++ b/src/Kiota.Builder/Plugins/PluginsGenerationService.cs @@ -97,11 +97,11 @@ public async Task GenerateManifestAsync(CancellationToken cancellationToken = de } await writer.FlushAsync(cancellationToken).ConfigureAwait(false); } - + // 3. write the app manifest var manifestFullPath = Path.Combine(Configuration.OutputPath, AppManifestFileName); var manifestInfo = ExtractInfoFromDocument(OAIDocument.Info); - var developerInfo = new Developer(OAIDocument.Info?.Contact?.Name, OAIDocument.Info?.Contact?.Url?.OriginalString , manifestInfo.PrivacyUrl,OAIDocument.Info?.TermsOfService?.OriginalString); + var developerInfo = new Developer(OAIDocument.Info?.Contact?.Name, OAIDocument.Info?.Contact?.Url?.OriginalString, manifestInfo.PrivacyUrl, OAIDocument.Info?.TermsOfService?.OriginalString); // create default model AppManifestModel manifestModel = new AppManifestModel( Configuration.ClientClassName, @@ -110,13 +110,13 @@ public async Task GenerateManifestAsync(CancellationToken cancellationToken = de { Developer = developerInfo }; - + if (File.Exists(manifestFullPath)) // No need for default, try to update the model from the file { #pragma warning disable CA2007 // Consider calling ConfigureAwait on the awaited task await using var fileStream = File.OpenRead(manifestFullPath); #pragma warning restore CA2007 // Consider calling ConfigureAwait on the awaited task - var manifestModelFromFile = await JsonSerializer.DeserializeAsync(fileStream,AppManifestModelGenerationContext.AppManifestModel,cancellationToken).ConfigureAwait(false); + var manifestModelFromFile = await JsonSerializer.DeserializeAsync(fileStream, AppManifestModelGenerationContext.AppManifestModel, cancellationToken).ConfigureAwait(false); if (manifestModelFromFile != null) manifestModel = manifestModelFromFile; } @@ -128,20 +128,21 @@ public async Task GenerateManifestAsync(CancellationToken cancellationToken = de } else { - manifestModel.CopilotExtensions.Plugins.Add(new Plugin(Configuration.ClientClassName,pluginFileName)); + manifestModel.CopilotExtensions.Plugins.Add(new Plugin(Configuration.ClientClassName, pluginFileName)); } #pragma warning disable CA2007 await using var appManifestStream = File.Open(manifestFullPath, FileMode.Create); #pragma warning restore CA2007 - await JsonSerializer.SerializeAsync(appManifestStream, manifestModel, AppManifestModelGenerationContext.AppManifestModel, cancellationToken).ConfigureAwait(false); + await JsonSerializer.SerializeAsync(appManifestStream, manifestModel, AppManifestModelGenerationContext.AppManifestModel, cancellationToken).ConfigureAwait(false); } - private static readonly AppManifestModelGenerationContext AppManifestModelGenerationContext = new(new JsonSerializerOptions{ + private static readonly AppManifestModelGenerationContext AppManifestModelGenerationContext = new(new JsonSerializerOptions + { PropertyNamingPolicy = JsonNamingPolicy.CamelCase, WriteIndented = true, }); - + private OpenApiDocument GetDocumentWithTrimmedComponentsAndResponses(OpenApiDocument doc) { // ensure the info and components are not null From 99516b833f51cef8f26bb2313c698c64d24fcdc9 Mon Sep 17 00:00:00 2001 From: Andrew Omondi Date: Mon, 1 Jul 2024 10:30:36 +0300 Subject: [PATCH 3/8] More cleanup/testing --- .../Plugins/Models/AppManifestModel.cs | 79 +++++++++++++++---- .../Plugins/PluginsGenerationService.cs | 29 ++++--- .../Plugins/PluginsGenerationServiceTests.cs | 65 +++++++++++++++ 3 files changed, 146 insertions(+), 27 deletions(-) diff --git a/src/Kiota.Builder/Plugins/Models/AppManifestModel.cs b/src/Kiota.Builder/Plugins/Models/AppManifestModel.cs index 4c5e70dc41..220abe22bc 100644 --- a/src/Kiota.Builder/Plugins/Models/AppManifestModel.cs +++ b/src/Kiota.Builder/Plugins/Models/AppManifestModel.cs @@ -4,17 +4,29 @@ namespace Kiota.Builder.Plugins.Models; -public class AppManifestModel(string pluginName, string documentName, string documentDescription) +internal class AppManifestModel { + public AppManifestModel() + { + // empty constructor to not mess with deserializers + } + + public AppManifestModel(string pluginName, string documentName, string documentDescription) + { + PackageName = $"com.microsoft.kiota.plugin.{pluginName}"; + Name = new(pluginName, documentName); + Description = new(documentDescription, documentName); + } + [JsonPropertyName("$schema")] public string Schema { get; set; } = "https://developer.microsoft.com/json-schemas/teams/vDevPreview/MicrosoftTeams.schema.json"; public string ManifestVersion { get; set; } = "devPreview"; public string Version { get; set; } = "1.0.0"; public string Id { get; set; } = Guid.NewGuid().ToString(); public Developer Developer { get; init; } = new(); - public string PackageName { get; set; } = $"com.microsoft.kiota.plugin.{pluginName}"; - public Name Name { get; set; } = new(pluginName, documentName); - public Description Description { get; set; } = new(documentDescription, documentName); + public string PackageName { get; set; } = String.Empty; + public Name Name { get; set; } = new(); + public Description Description { get; set; } = new(); public Icons Icons { get; set; } = new(); public string AccentColor { get; set; } = "#FFFFFF"; public CopilotExtensions CopilotExtensions { get; set; } = new(); @@ -26,7 +38,7 @@ internal partial class AppManifestModelGenerationContext : JsonSerializerContext } #pragma warning disable CA1054 -public class Developer(string? name = null, string? websiteUrl = null, string? privacyUrl = null, string? termsOfUseUrl = null) +internal class Developer(string? name = null, string? websiteUrl = null, string? privacyUrl = null, string? termsOfUseUrl = null) #pragma warning restore CA1054 { public string Name { get; set; } = !string.IsNullOrEmpty(name) ? name : "Kiota Generator, Inc."; @@ -37,35 +49,68 @@ public class Developer(string? name = null, string? websiteUrl = null, string? p #pragma warning restore CA1056 } -public class Name(string pluginName, string documentName) +internal class Name { + public Name() + { + // empty constructor to not mess with deserializers + } + + public Name(string pluginName, string documentName) + { + ShortName = pluginName; + FullName = $"API Plugin {pluginName} for {documentName}"; + } + [JsonPropertyName("short")] - public string ShortName { get; private set; } = pluginName; + public string ShortName { get; set; } = string.Empty; [JsonPropertyName("full")] - public string FullName { get; private set; } = $"API Plugin {pluginName} for {documentName}"; + public string FullName { get; set; } = string.Empty; } -public class Description(string description, string documentName) +internal class Description { + public Description() + { + // empty constructor to not mess with deserializers + } + + public Description(string description, string documentName) + { + ShortName = !string.IsNullOrEmpty(description) ? $"API Plugin for {description}." : documentName; + FullName = !string.IsNullOrEmpty(description) ? $"API Plugin for {description}." : documentName; + } + [JsonPropertyName("short")] - public string ShortName { get; private set; } = !string.IsNullOrEmpty(description) ? $"API Plugin for {description}." : documentName; + public string ShortName { get; set; } = string.Empty; [JsonPropertyName("full")] - public string FullName { get; private set; } = !string.IsNullOrEmpty(description) ? $"API Plugin for {description}." : documentName; + public string FullName { get; set; } = string.Empty; } -public class Icons +internal class Icons { public string Color { get; set; } = "color.png"; public string Outline { get; set; } = "outline.png"; } -public class CopilotExtensions +internal class CopilotExtensions { - public IList Plugins { get; } = new List(); + public IList Plugins { get; set; } = new List(); } -public class Plugin(string pluginName, string fileName) +internal class Plugin { - public string Id { get; set; } = pluginName; - public string File { get; set; } = fileName; + public Plugin() + { + // empty constructor to not mess with deserializers + } + + public Plugin(string pluginName, string fileName) + { + Id = pluginName; + File = fileName; + } + + public string Id { get; set; } = string.Empty; + public string File { get; set; } = string.Empty; } diff --git a/src/Kiota.Builder/Plugins/PluginsGenerationService.cs b/src/Kiota.Builder/Plugins/PluginsGenerationService.cs index f873694892..0313ecb3ce 100644 --- a/src/Kiota.Builder/Plugins/PluginsGenerationService.cs +++ b/src/Kiota.Builder/Plugins/PluginsGenerationService.cs @@ -58,12 +58,11 @@ public async Task GenerateManifestAsync(CancellationToken cancellationToken = de descriptionWriter.Flush(); // 2. write the plugins - var pluginFileName = string.Empty; + foreach (var pluginType in Configuration.PluginTypes) { var manifestFileName = pluginType == PluginType.OpenAI ? OpenAIManifestFileName : $"{Configuration.ClientClassName.ToLowerInvariant()}-{pluginType.ToString().ToLowerInvariant()}"; - pluginFileName = $"{manifestFileName}{ManifestFileNameSuffix}"; - var manifestOutputPath = Path.Combine(Configuration.OutputPath, pluginFileName); + var manifestOutputPath = Path.Combine(Configuration.OutputPath, $"{manifestFileName}{ManifestFileNameSuffix}"); #pragma warning disable CA2007 // Consider calling ConfigureAwait on the awaited task await using var fileStream = File.Create(manifestOutputPath, 4096); await using var writer = new Utf8JsonWriter(fileStream, new JsonWriterOptions { Indented = true }); @@ -98,8 +97,21 @@ public async Task GenerateManifestAsync(CancellationToken cancellationToken = de await writer.FlushAsync(cancellationToken).ConfigureAwait(false); } - // 3. write the app manifest - var manifestFullPath = Path.Combine(Configuration.OutputPath, AppManifestFileName); + // 3. 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); + var pluginFileName = $"{Configuration.ClientClassName.ToLowerInvariant()}-{PluginType.APIPlugin.ToString().ToLowerInvariant()}{ManifestFileNameSuffix}"; + var appManifestModel = await GetAppManifestModelAsync(pluginFileName, manifestFullPath, cancellationToken).ConfigureAwait(false); +#pragma warning disable CA2007 + await using var appManifestStream = File.Open(manifestFullPath, FileMode.Create); +#pragma warning restore CA2007 + await JsonSerializer.SerializeAsync(appManifestStream, appManifestModel, AppManifestModelGenerationContext.AppManifestModel, cancellationToken).ConfigureAwait(false); + } + } + + private async Task GetAppManifestModelAsync(string pluginFileName, string manifestFullPath, CancellationToken cancellationToken) + { var manifestInfo = ExtractInfoFromDocument(OAIDocument.Info); var developerInfo = new Developer(OAIDocument.Info?.Contact?.Name, OAIDocument.Info?.Contact?.Url?.OriginalString, manifestInfo.PrivacyUrl, OAIDocument.Info?.TermsOfService?.OriginalString); // create default model @@ -131,13 +143,10 @@ public async Task GenerateManifestAsync(CancellationToken cancellationToken = de manifestModel.CopilotExtensions.Plugins.Add(new Plugin(Configuration.ClientClassName, pluginFileName)); } -#pragma warning disable CA2007 - await using var appManifestStream = File.Open(manifestFullPath, FileMode.Create); -#pragma warning restore CA2007 - await JsonSerializer.SerializeAsync(appManifestStream, manifestModel, AppManifestModelGenerationContext.AppManifestModel, cancellationToken).ConfigureAwait(false); + return manifestModel; } - private static readonly AppManifestModelGenerationContext AppManifestModelGenerationContext = new(new JsonSerializerOptions + internal static readonly AppManifestModelGenerationContext AppManifestModelGenerationContext = new(new JsonSerializerOptions { PropertyNamingPolicy = JsonNamingPolicy.CamelCase, WriteIndented = true, diff --git a/tests/Kiota.Builder.Tests/Plugins/PluginsGenerationServiceTests.cs b/tests/Kiota.Builder.Tests/Plugins/PluginsGenerationServiceTests.cs index 509004f814..6656d65ebd 100644 --- a/tests/Kiota.Builder.Tests/Plugins/PluginsGenerationServiceTests.cs +++ b/tests/Kiota.Builder.Tests/Plugins/PluginsGenerationServiceTests.cs @@ -6,6 +6,7 @@ using System.Threading.Tasks; using Kiota.Builder.Configuration; using Kiota.Builder.Plugins; +using Kiota.Builder.Plugins.Models; using Microsoft.Extensions.Logging; using Microsoft.OpenApi.Models; using Microsoft.OpenApi.Readers; @@ -90,6 +91,7 @@ public async Task GeneratesManifest() Assert.True(File.Exists(Path.Combine(outputDirectory, "client-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, AppManifestFileName))); // Validate the v2 plugin var manifestContent = await File.ReadAllTextAsync(Path.Combine(outputDirectory, ManifestFileName)); @@ -107,11 +109,74 @@ public async Task GeneratesManifest() Assert.NotNull(resultingManifest.Document); Assert.Equal(OpenApiFileName, 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("client", appManifestModelObject.CopilotExtensions.Plugins[0].Id); + Assert.Equal(ManifestFileName, appManifestModelObject.CopilotExtensions.Plugins[0].File); } private const string ManifestFileName = "client-apiplugin.json"; private const string OpenAIPluginFileName = "openai-plugins.json"; private const string OpenApiFileName = "client-openapi.yml"; + private const string AppManifestFileName = "manifest.json"; + [Fact] + public async Task GeneratesManifestAndUpdatesExistingAppManifest() + { + var simpleDescriptionContent = @"openapi: 3.0.0 +info: + title: test + version: 1.0 +servers: + - url: http://localhost/ + description: There's no place like home +paths: + /test/{id}: + get: + description: description for test path with id + operationId: test.WithId + parameters: + - name: id + in: path + required: true + description: The id of the test + schema: + type: integer + format: int32 + 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 mockLogger = new Mock>(); + var openAPIDocumentDS = new OpenApiDocumentDownloadService(_httpClient, mockLogger.Object); + var outputDirectory = Path.Combine(workingDirectory, "output"); + var generationConfiguration = new GenerationConfiguration + { + OutputPath = outputDirectory, + OpenAPIFilePath = "openapiPath", + PluginTypes = [PluginType.APIPlugin], + ClientClassName = "client", + ApiRootUrl = "http://localhost/", //Kiota builder would set this for us + }; + 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); + 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, OpenApiFileName))); + Assert.True(File.Exists(Path.Combine(outputDirectory, "maniffest.json"))); + + } [Fact] public async Task GeneratesManifestAndCleansUpInputDescription() { From 8c194b00ef98e60e481ad6d84d0a9334f52cb0c2 Mon Sep 17 00:00:00 2001 From: Andrew Omondi Date: Mon, 1 Jul 2024 11:38:03 +0300 Subject: [PATCH 4/8] format. --- .../Plugins/Models/AppManifestModel.cs | 14 +- .../Plugins/PluginsGenerationService.cs | 2 + .../Plugins/PluginsGenerationServiceTests.cs | 342 +++++++++++++++++- 3 files changed, 355 insertions(+), 3 deletions(-) diff --git a/src/Kiota.Builder/Plugins/Models/AppManifestModel.cs b/src/Kiota.Builder/Plugins/Models/AppManifestModel.cs index 220abe22bc..15ad32d06c 100644 --- a/src/Kiota.Builder/Plugins/Models/AppManifestModel.cs +++ b/src/Kiota.Builder/Plugins/Models/AppManifestModel.cs @@ -1,5 +1,7 @@ using System; using System.Collections.Generic; +using System.IO; +using System.Text.Json; using System.Text.Json.Serialization; namespace Kiota.Builder.Plugins.Models; @@ -24,15 +26,22 @@ public AppManifestModel(string pluginName, string documentName, string documentD public string Version { get; set; } = "1.0.0"; public string Id { get; set; } = Guid.NewGuid().ToString(); public Developer Developer { get; init; } = new(); - public string PackageName { get; set; } = String.Empty; + public string? PackageName + { + get; set; + } public Name Name { get; set; } = new(); public Description Description { get; set; } = new(); public Icons Icons { get; set; } = new(); public string AccentColor { get; set; } = "#FFFFFF"; public CopilotExtensions CopilotExtensions { get; set; } = new(); + + [JsonExtensionData] + public Dictionary AdditionalData { get; set; } = new(); } [JsonSerializable(typeof(AppManifestModel))] +[JsonSerializable(typeof(JsonElement))] internal partial class AppManifestModelGenerationContext : JsonSerializerContext { } @@ -47,6 +56,9 @@ internal class Developer(string? name = null, string? websiteUrl = null, string? public string PrivacyUrl { get; set; } = !string.IsNullOrEmpty(privacyUrl) ? privacyUrl : "https://www.example.com/privacy/"; public string TermsOfUseUrl { get; set; } = !string.IsNullOrEmpty(termsOfUseUrl) ? termsOfUseUrl : "https://www.example.com/terms/"; #pragma warning restore CA1056 + + [JsonExtensionData] + public Dictionary AdditionalData { get; set; } = new(); } internal class Name diff --git a/src/Kiota.Builder/Plugins/PluginsGenerationService.cs b/src/Kiota.Builder/Plugins/PluginsGenerationService.cs index 0313ecb3ce..212e89a252 100644 --- a/src/Kiota.Builder/Plugins/PluginsGenerationService.cs +++ b/src/Kiota.Builder/Plugins/PluginsGenerationService.cs @@ -3,6 +3,7 @@ using System.IO; using System.Linq; using System.Text.Json; +using System.Text.Json.Serialization; using System.Threading; using System.Threading.Tasks; using Kiota.Builder.Configuration; @@ -150,6 +151,7 @@ private async Task GetAppManifestModelAsync(string pluginFileN { PropertyNamingPolicy = JsonNamingPolicy.CamelCase, WriteIndented = true, + DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull }); private OpenApiDocument GetDocumentWithTrimmedComponentsAndResponses(OpenApiDocument doc) diff --git a/tests/Kiota.Builder.Tests/Plugins/PluginsGenerationServiceTests.cs b/tests/Kiota.Builder.Tests/Plugins/PluginsGenerationServiceTests.cs index 6656d65ebd..39df9954b4 100644 --- a/tests/Kiota.Builder.Tests/Plugins/PluginsGenerationServiceTests.cs +++ b/tests/Kiota.Builder.Tests/Plugins/PluginsGenerationServiceTests.cs @@ -155,6 +155,321 @@ public async Task GeneratesManifestAndUpdatesExistingAppManifest() var mockLogger = new Mock>(); var openAPIDocumentDS = new OpenApiDocumentDownloadService(_httpClient, mockLogger.Object); var outputDirectory = Path.Combine(workingDirectory, "output"); + Directory.CreateDirectory(outputDirectory); + var preExistingManifestContents = @"{ + ""$schema"": ""https://developer.microsoft.com/json-schemas/teams/v1.17/MicrosoftTeams.schema.json"", + ""manifestVersion"": ""1.17"", + ""version"": ""1.0.0"", + ""id"": ""%MICROSOFT-APP-ID%"", + ""localizationInfo"": { + ""defaultLanguageTag"": ""en-us"", + ""additionalLanguages"": [ + { + ""languageTag"": ""es-es"", + ""file"": ""en-us.json"" + } + ] + }, + ""developer"": { + ""name"": ""Publisher Name"", + ""websiteUrl"": ""https://example.com/"", + ""privacyUrl"": ""https://example.com/privacy"", + ""termsOfUseUrl"": ""https://example.com/app-tos"", + ""mpnId"": ""1234567890"" + }, + ""name"": { + ""short"": ""Name of your app"", + ""full"": ""Full name of app, if longer than 30 characters"" + }, + ""description"": { + ""short"": ""Short description of your app (<= 80 chars)"", + ""full"": ""Full description of your app (<= 4000 chars)"" + }, + ""icons"": { + ""outline"": ""A relative path to a transparent .png icon — 32px X 32px"", + ""color"": ""A relative path to a full color .png icon — 192px X 192px"" + }, + ""accentColor"": ""A valid HTML color code."", + ""configurableTabs"": [ + { + ""configurationUrl"": ""https://contoso.com/teamstab/configure"", + ""scopes"": [ + ""team"", + ""groupChat"" + ], + ""canUpdateConfiguration"": true, + ""context"": [ + ""channelTab"", + ""privateChatTab"", + ""meetingChatTab"", + ""meetingDetailsTab"", + ""meetingSidePanel"", + ""meetingStage"" + ], + ""sharePointPreviewImage"": ""Relative path to a tab preview image for use in SharePoint — 1024px X 768"", + ""supportedSharePointHosts"": [ + ""sharePointFullPage"", + ""sharePointWebPart"" + ] + } + ], + ""staticTabs"": [ + { + ""entityId"": ""unique Id for the page entity"", + ""scopes"": [ + ""personal"" + ], + ""context"": [ + ""personalTab"", + ""channelTab"" + ], + ""name"": ""Display name of tab"", + ""contentUrl"": ""https://contoso.com/content (displayed in Teams canvas)"", + ""websiteUrl"": ""https://contoso.com/content (displayed in web browser)"", + ""searchUrl"": ""https://contoso.com/content (displayed in web browser)"" + } + ], + ""supportedChannelTypes"": [ + ""sharedChannels"", + ""privateChannels"" + ], + ""bots"": [ + { + ""botId"": ""%MICROSOFT-APP-ID-REGISTERED-WITH-BOT-FRAMEWORK%"", + ""scopes"": [ + ""team"", + ""personal"", + ""groupChat"" + ], + ""needsChannelSelector"": false, + ""isNotificationOnly"": false, + ""supportsFiles"": true, + ""supportsCalling"": false, + ""supportsVideo"": true, + ""commandLists"": [ + { + ""scopes"": [ + ""team"", + ""groupChat"" + ], + ""commands"": [ + { + ""title"": ""Command 1"", + ""description"": ""Description of Command 1"" + }, + { + ""title"": ""Command 2"", + ""description"": ""Description of Command 2"" + } + ] + }, + { + ""scopes"": [ + ""personal"", + ""groupChat"" + ], + ""commands"": [ + { + ""title"": ""Personal command 1"", + ""description"": ""Description of Personal command 1"" + }, + { + ""title"": ""Personal command N"", + ""description"": ""Description of Personal command N"" + } + ] + } + ] + } + ], + ""connectors"": [ + { + ""connectorId"": ""GUID-FROM-CONNECTOR-DEV-PORTAL%"", + ""scopes"": [ + ""team"" + ], + ""configurationUrl"": ""https://contoso.com/teamsconnector/configure"" + } + ], + ""composeExtensions"": [ + { + ""canUpdateConfiguration"": true, + ""botId"": ""%MICROSOFT-APP-ID-REGISTERED-WITH-BOT-FRAMEWORK%"", + ""commands"": [ + { + ""id"": ""exampleCmd1"", + ""title"": ""Example Command"", + ""type"": ""query"", + ""context"": [ + ""compose"", + ""commandBox"" + ], + ""description"": ""Command Description; e.g., Search on the web"", + ""initialRun"": true, + ""fetchTask"": false, + ""parameters"": [ + { + ""name"": ""keyword"", + ""title"": ""Search keywords"", + ""inputType"": ""choiceset"", + ""description"": ""Enter the keywords to search for"", + ""value"": ""Initial value for the parameter"", + ""choices"": [ + { + ""title"": ""Title of the choice"", + ""value"": ""Value of the choice"" + } + ] + } + ] + }, + { + ""id"": ""exampleCmd2"", + ""title"": ""Example Command 2"", + ""type"": ""action"", + ""context"": [ + ""message"" + ], + ""description"": ""Command Description; e.g., Add a customer"", + ""initialRun"": true, + ""fetchTask"": false , + ""parameters"": [ + { + ""name"": ""custinfo"", + ""title"": ""Customer name"", + ""description"": ""Enter a customer name"", + ""inputType"": ""text"" + } + ] + }, + { + ""id"": ""exampleCmd3"", + ""title"": ""Example Command 3"", + ""type"": ""action"", + ""context"": [ + ""compose"", + ""commandBox"", + ""message"" + ], + ""description"": ""Command Description; e.g., Add a customer"", + ""fetchTask"": false, + ""taskInfo"": { + ""title"": ""Initial dialog title"", + ""width"": ""Dialog width"", + ""height"": ""Dialog height"", + ""url"": ""Initial webview URL"" + } + } + ], + ""messageHandlers"": [ + { + ""type"": ""link"", + ""value"": { + ""domains"": [ + ""mysite.someplace.com"", + ""othersite.someplace.com"" + ], + ""supportsAnonymizedPayloads"": false + } + } + ] + } + ], + ""permissions"": [ + ""identity"", + ""messageTeamMembers"" + ], + ""devicePermissions"": [ + ""geolocation"", + ""media"", + ""notifications"", + ""midi"", + ""openExternal"" + ], + ""validDomains"": [ + ""contoso.com"", + ""mysite.someplace.com"", + ""othersite.someplace.com"" + ], + ""webApplicationInfo"": { + ""id"": ""AAD App ID"", + ""resource"": ""Resource URL for acquiring auth token for SSO"" + }, + ""authorization"": { + ""permissions"": { + ""resourceSpecific"": [ + { + ""type"": ""Application"", + ""name"": ""ChannelSettings.Read.Group"" + }, + { + ""type"": ""Delegated"", + ""name"": ""ChannelMeetingParticipant.Read.Group"" + } + ] + } + }, + ""showLoadingIndicator"": false, + ""isFullScreen"": false, + ""activities"": { + ""activityTypes"": [ + { + ""type"": ""taskCreated"", + ""description"": ""Task created activity"", + ""templateText"": "" created task for you"" + }, + { + ""type"": ""userMention"", + ""description"": ""Personal mention activity"", + ""templateText"": "" mentioned you"" + } + ] + }, + ""defaultBlockUntilAdminAction"": true, + ""publisherDocsUrl"": ""https://example.com/app-info"", + ""defaultInstallScope"": ""meetings"", + ""defaultGroupCapability"": { + ""meetings"": ""tab"", + ""team"": ""bot"", + ""groupChat"": ""bot"" + }, + ""configurableProperties"": [ + ""name"", + ""shortDescription"", + ""longDescription"", + ""smallImageUrl"", + ""largeImageUrl"", + ""accentColor"", + ""developerUrl"", + ""privacyUrl"", + ""termsOfUseUrl"" + ], + ""subscriptionOffer"": { + ""offerId"": ""publisherId.offerId"" + }, + ""meetingExtensionDefinition"": { + ""scenes"": [ + { + ""id"": ""9082c811-7e6a-4174-8173-6ccd57d377e6"", + ""name"": ""Getting started sample"", + ""file"": ""scenes/sceneMetadata.json"", + ""preview"": ""scenes/scenePreview.png"", + ""maxAudience"": 15, + ""seatsReservedForOrganizersOrPresenters"": 0 + }, + { + ""id"": ""afeaed22-f89b-48e1-98b4-46a514344e4a"", + ""name"": ""Sample-1"", + ""file"": ""scenes/sceneMetadata.json"", + ""preview"": ""scenes/scenePreview.png"", + ""maxAudience"": 15, + ""seatsReservedForOrganizersOrPresenters"": 3 + } + ] + } +}"; + var preExistingManifestPath = Path.Combine(outputDirectory, "manifest.json"); + await File.WriteAllTextAsync(preExistingManifestPath, preExistingManifestContents); var generationConfiguration = new GenerationConfiguration { OutputPath = outputDirectory, @@ -168,14 +483,37 @@ public async Task GeneratesManifestAndUpdatesExistingAppManifest() KiotaBuilder.CleanupOperationIdForPlugins(openApiDocument); var urlTreeNode = OpenApiUrlTreeNode.Create(openApiDocument, Constants.DefaultOpenApiLabel); + // Assert manifest exists before generation and is parsable + Assert.True(File.Exists(Path.Combine(outputDirectory, "manifest.json"))); + var originalManifestFile = await File.ReadAllTextAsync(Path.Combine(outputDirectory, AppManifestFileName)); + var originalAppManifestModelObject = JsonSerializer.Deserialize(originalManifestFile, PluginsGenerationService.AppManifestModelGenerationContext.AppManifestModel); + Assert.Null(originalAppManifestModelObject.PackageName);// package wasn't present + Assert.Equal("Name of your app", originalAppManifestModelObject.Name.ShortName); // app name is same + Assert.Equal("Publisher Name", originalAppManifestModelObject.Developer.Name); // app name is same + Assert.Empty(originalAppManifestModelObject.CopilotExtensions.Plugins); // no plugins present + + // Run the plugin generation 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, OpenApiFileName))); - Assert.True(File.Exists(Path.Combine(outputDirectory, "maniffest.json"))); + Assert.True(File.Exists(Path.Combine(outputDirectory, "manifest.json")));// Assert manifest exists after generation + // Validate the manifest file + var appManifestFile = await File.ReadAllTextAsync(Path.Combine(outputDirectory, AppManifestFileName)); + var appManifestModelObject = JsonSerializer.Deserialize(appManifestFile, PluginsGenerationService.AppManifestModelGenerationContext.AppManifestModel); + Assert.Null(appManifestModelObject.PackageName);// package wasn't present + Assert.Equal("Name of your app", appManifestModelObject.Name.ShortName); // app name is same + Assert.Equal("Publisher Name", originalAppManifestModelObject.Developer.Name); // developer name is same + Assert.Equal("client", appManifestModelObject.CopilotExtensions.Plugins[0].Id); + Assert.Equal(ManifestFileName, appManifestModelObject.CopilotExtensions.Plugins[0].File); + var rootJsonElement = JsonDocument.Parse(appManifestFile).RootElement; + Assert.True(rootJsonElement.TryGetProperty("subscriptionOffer", out _));// no loss of information + Assert.True(rootJsonElement.TryGetProperty("meetingExtensionDefinition", out _));// no loss of information + Assert.True(rootJsonElement.TryGetProperty("activities", out _));// no loss of information + Assert.True(rootJsonElement.TryGetProperty("devicePermissions", out _));// no loss of information + Assert.True(rootJsonElement.TryGetProperty("composeExtensions", out _));// no loss of information } [Fact] public async Task GeneratesManifestAndCleansUpInputDescription() From b33af2876edf64b9b3cb94698fdb6a108142233b Mon Sep 17 00:00:00 2001 From: Andrew Omondi Date: Mon, 1 Jul 2024 12:05:35 +0300 Subject: [PATCH 5/8] Testing and working --- .../Plugins/Models/AppManifestModel.cs | 136 ++++++++++-------- .../Plugins/PluginsGenerationService.cs | 49 +++++-- .../Plugins/PluginsGenerationServiceTests.cs | 2 +- 3 files changed, 118 insertions(+), 69 deletions(-) diff --git a/src/Kiota.Builder/Plugins/Models/AppManifestModel.cs b/src/Kiota.Builder/Plugins/Models/AppManifestModel.cs index 15ad32d06c..74c6cf981c 100644 --- a/src/Kiota.Builder/Plugins/Models/AppManifestModel.cs +++ b/src/Kiota.Builder/Plugins/Models/AppManifestModel.cs @@ -1,6 +1,5 @@ using System; using System.Collections.Generic; -using System.IO; using System.Text.Json; using System.Text.Json.Serialization; @@ -8,33 +7,51 @@ namespace Kiota.Builder.Plugins.Models; internal class AppManifestModel { - public AppManifestModel() + [JsonPropertyName("$schema")] + public string? Schema { - // empty constructor to not mess with deserializers + get; set; } - - public AppManifestModel(string pluginName, string documentName, string documentDescription) + public string? ManifestVersion { - PackageName = $"com.microsoft.kiota.plugin.{pluginName}"; - Name = new(pluginName, documentName); - Description = new(documentDescription, documentName); + get; set; + } + public string? Version + { + get; set; + } + public string? Id + { + get; set; + } + public Developer? Developer + { + get; init; } - - [JsonPropertyName("$schema")] - public string Schema { get; set; } = "https://developer.microsoft.com/json-schemas/teams/vDevPreview/MicrosoftTeams.schema.json"; - public string ManifestVersion { get; set; } = "devPreview"; - public string Version { get; set; } = "1.0.0"; - public string Id { get; set; } = Guid.NewGuid().ToString(); - public Developer Developer { get; init; } = new(); public string? PackageName { get; set; } - public Name Name { get; set; } = new(); - public Description Description { get; set; } = new(); - public Icons Icons { get; set; } = new(); - public string AccentColor { get; set; } = "#FFFFFF"; - public CopilotExtensions CopilotExtensions { get; set; } = new(); + public Name? Name + { + get; set; + } + public Description? Description + { + get; set; + } + public Icons? Icons + { + get; set; + } + public string? AccentColor + { + get; set; + } + public CopilotExtensions? CopilotExtensions + { + get; set; + } [JsonExtensionData] public Dictionary AdditionalData { get; set; } = new(); @@ -47,14 +64,26 @@ internal partial class AppManifestModelGenerationContext : JsonSerializerContext } #pragma warning disable CA1054 -internal class Developer(string? name = null, string? websiteUrl = null, string? privacyUrl = null, string? termsOfUseUrl = null) +internal class Developer #pragma warning restore CA1054 { - public string Name { get; set; } = !string.IsNullOrEmpty(name) ? name : "Kiota Generator, Inc."; + public string? Name + { + get; set; + } #pragma warning disable CA1056 - public string WebsiteUrl { get; set; } = !string.IsNullOrEmpty(websiteUrl) ? websiteUrl : "https://www.example.com/contact/"; - public string PrivacyUrl { get; set; } = !string.IsNullOrEmpty(privacyUrl) ? privacyUrl : "https://www.example.com/privacy/"; - public string TermsOfUseUrl { get; set; } = !string.IsNullOrEmpty(termsOfUseUrl) ? termsOfUseUrl : "https://www.example.com/terms/"; + public string? WebsiteUrl + { + get; set; + } + public string? PrivacyUrl + { + get; set; + } + public string? TermsOfUseUrl + { + get; set; + } #pragma warning restore CA1056 [JsonExtensionData] @@ -63,46 +92,42 @@ internal class Developer(string? name = null, string? websiteUrl = null, string? internal class Name { - public Name() + [JsonPropertyName("short")] + public string? ShortName { - // empty constructor to not mess with deserializers + get; set; } - - public Name(string pluginName, string documentName) + [JsonPropertyName("full")] + public string? FullName { - ShortName = pluginName; - FullName = $"API Plugin {pluginName} for {documentName}"; + get; set; } - - [JsonPropertyName("short")] - public string ShortName { get; set; } = string.Empty; - [JsonPropertyName("full")] - public string FullName { get; set; } = string.Empty; } internal class Description { - public Description() + [JsonPropertyName("short")] + public string? ShortName { - // empty constructor to not mess with deserializers + get; set; } - - public Description(string description, string documentName) + [JsonPropertyName("full")] + public string? FullName { - ShortName = !string.IsNullOrEmpty(description) ? $"API Plugin for {description}." : documentName; - FullName = !string.IsNullOrEmpty(description) ? $"API Plugin for {description}." : documentName; + get; set; } - - [JsonPropertyName("short")] - public string ShortName { get; set; } = string.Empty; - [JsonPropertyName("full")] - public string FullName { get; set; } = string.Empty; } internal class Icons { - public string Color { get; set; } = "color.png"; - public string Outline { get; set; } = "outline.png"; + public string? Color + { + get; set; + } + public string? Outline + { + get; set; + } } internal class CopilotExtensions @@ -112,17 +137,12 @@ internal class CopilotExtensions internal class Plugin { - public Plugin() + public string? Id { - // empty constructor to not mess with deserializers + get; set; } - - public Plugin(string pluginName, string fileName) + public string? File { - Id = pluginName; - File = fileName; + get; set; } - - public string Id { get; set; } = string.Empty; - public string File { get; set; } = string.Empty; } diff --git a/src/Kiota.Builder/Plugins/PluginsGenerationService.cs b/src/Kiota.Builder/Plugins/PluginsGenerationService.cs index 212e89a252..b86bf1c63a 100644 --- a/src/Kiota.Builder/Plugins/PluginsGenerationService.cs +++ b/src/Kiota.Builder/Plugins/PluginsGenerationService.cs @@ -114,14 +114,37 @@ public async Task GenerateManifestAsync(CancellationToken cancellationToken = de private async Task GetAppManifestModelAsync(string pluginFileName, string manifestFullPath, CancellationToken cancellationToken) { var manifestInfo = ExtractInfoFromDocument(OAIDocument.Info); - var developerInfo = new Developer(OAIDocument.Info?.Contact?.Name, OAIDocument.Info?.Contact?.Url?.OriginalString, manifestInfo.PrivacyUrl, OAIDocument.Info?.TermsOfService?.OriginalString); // create default model - AppManifestModel manifestModel = new AppManifestModel( - Configuration.ClientClassName, - OAIDocument.Info?.Title.CleanupXMLString() ?? "OpenApi Document", - OAIDocument.Info?.Description.CleanupXMLString() ?? "OpenApi Description") + var manifestModel = new AppManifestModel { - Developer = developerInfo + Schema = "https://developer.microsoft.com/json-schemas/teams/vDevPreview/MicrosoftTeams.schema.json", + ManifestVersion = "devPreview", + Version = "1.0.0", + Id = Guid.NewGuid().ToString(), + Developer = new Developer + { + Name = !string.IsNullOrEmpty(OAIDocument.Info?.Contact?.Name) ? OAIDocument.Info?.Contact?.Name : "Kiota Generator, Inc.", + WebsiteUrl = !string.IsNullOrEmpty(OAIDocument.Info?.Contact?.Url?.OriginalString) ? OAIDocument.Info?.Contact?.Url?.OriginalString : "https://www.example.com/contact/", + PrivacyUrl = !string.IsNullOrEmpty(manifestInfo.PrivacyUrl) ? manifestInfo.PrivacyUrl : "https://www.example.com/privacy/", + TermsOfUseUrl = !string.IsNullOrEmpty(OAIDocument.Info?.TermsOfService?.OriginalString) ? OAIDocument.Info?.TermsOfService?.OriginalString : "https://www.example.com/terms/", + }, + PackageName = $"com.microsoft.kiota.plugin.{Configuration.ClientClassName}", + Name = new Name + { + ShortName = Configuration.ClientClassName, + FullName = $"API Plugin {Configuration.ClientClassName} for {OAIDocument.Info?.Title.CleanupXMLString() ?? "OpenApi Document"}" + }, + Description = new Description + { + ShortName = !string.IsNullOrEmpty(OAIDocument.Info?.Description.CleanupXMLString()) ? $"API Plugin for {OAIDocument.Info?.Description.CleanupXMLString()}." : OAIDocument.Info?.Title.CleanupXMLString() ?? "OpenApi Document", + FullName = !string.IsNullOrEmpty(OAIDocument.Info?.Description.CleanupXMLString()) ? $"API Plugin for {OAIDocument.Info?.Description.CleanupXMLString()}." : OAIDocument.Info?.Title.CleanupXMLString() ?? "OpenApi Document" + }, + Icons = new Icons + { + Color = "color.png", + Outline = "outline.png" + }, + AccentColor = "#FFFFFF" }; if (File.Exists(manifestFullPath)) // No need for default, try to update the model from the file @@ -134,14 +157,20 @@ private async Task GetAppManifestModelAsync(string pluginFileN manifestModel = manifestModelFromFile; } - if (manifestModel.CopilotExtensions.Plugins.FirstOrDefault(pluginItem => - pluginItem.Id.Equals(Configuration.ClientClassName, StringComparison.OrdinalIgnoreCase)) is { } plugin) + manifestModel.CopilotExtensions ??= new CopilotExtensions();// ensure its not null. + + if (manifestModel.CopilotExtensions.Plugins.FirstOrDefault(pluginItem => Configuration.ClientClassName.Equals(pluginItem.Id, StringComparison.OrdinalIgnoreCase)) is { } plugin) { - plugin.File = pluginFileName; + plugin.File = pluginFileName; // id is already consitent so make sure the file name is ok } else { - manifestModel.CopilotExtensions.Plugins.Add(new Plugin(Configuration.ClientClassName, pluginFileName)); + // Add a new plugin entry + manifestModel.CopilotExtensions.Plugins.Add(new Plugin + { + File = pluginFileName, + Id = Configuration.ClientClassName + }); } return manifestModel; diff --git a/tests/Kiota.Builder.Tests/Plugins/PluginsGenerationServiceTests.cs b/tests/Kiota.Builder.Tests/Plugins/PluginsGenerationServiceTests.cs index 39df9954b4..4451fd9b4a 100644 --- a/tests/Kiota.Builder.Tests/Plugins/PluginsGenerationServiceTests.cs +++ b/tests/Kiota.Builder.Tests/Plugins/PluginsGenerationServiceTests.cs @@ -490,7 +490,7 @@ public async Task GeneratesManifestAndUpdatesExistingAppManifest() Assert.Null(originalAppManifestModelObject.PackageName);// package wasn't present Assert.Equal("Name of your app", originalAppManifestModelObject.Name.ShortName); // app name is same Assert.Equal("Publisher Name", originalAppManifestModelObject.Developer.Name); // app name is same - Assert.Empty(originalAppManifestModelObject.CopilotExtensions.Plugins); // no plugins present + Assert.Null(originalAppManifestModelObject.CopilotExtensions?.Plugins); // no plugins present // Run the plugin generation var pluginsGenerationService = new PluginsGenerationService(openApiDocument, urlTreeNode, generationConfiguration, workingDirectory); From 9180f60b0e29f61fe346f622082d45170de9d8ec Mon Sep 17 00:00:00 2001 From: Andrew Omondi Date: Mon, 1 Jul 2024 12:50:10 +0300 Subject: [PATCH 6/8] Improve coverage --- .../Plugins/PluginsGenerationServiceTests.cs | 109 ++++++++++++++++++ 1 file changed, 109 insertions(+) diff --git a/tests/Kiota.Builder.Tests/Plugins/PluginsGenerationServiceTests.cs b/tests/Kiota.Builder.Tests/Plugins/PluginsGenerationServiceTests.cs index 4451fd9b4a..ba633893c8 100644 --- a/tests/Kiota.Builder.Tests/Plugins/PluginsGenerationServiceTests.cs +++ b/tests/Kiota.Builder.Tests/Plugins/PluginsGenerationServiceTests.cs @@ -130,6 +130,7 @@ public async Task GeneratesManifestAndUpdatesExistingAppManifest() info: title: test version: 1.0 + description: A sample test api servers: - url: http://localhost/ description: There's no place like home @@ -515,6 +516,114 @@ public async Task GeneratesManifestAndUpdatesExistingAppManifest() Assert.True(rootJsonElement.TryGetProperty("devicePermissions", out _));// no loss of information Assert.True(rootJsonElement.TryGetProperty("composeExtensions", out _));// no loss of information } + + [Fact] + public async Task GeneratesManifestAndUpdatesExistingAppManifestWithExistingPlugins() + { + var simpleDescriptionContent = @"openapi: 3.0.0 +info: + version: 1.0 +servers: + - url: http://localhost/ + description: There's no place like home +paths: + /test/{id}: + get: + description: description for test path with id + operationId: test.WithId + parameters: + - name: id + in: path + required: true + description: The id of the test + schema: + type: integer + format: int32 + 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 mockLogger = new Mock>(); + var openAPIDocumentDS = new OpenApiDocumentDownloadService(_httpClient, mockLogger.Object); + var outputDirectory = Path.Combine(workingDirectory, "output"); + Directory.CreateDirectory(outputDirectory); + var preExistingManifestContents = @"{ + ""$schema"": ""https://developer.microsoft.com/json-schemas/teams/vDevPreview/MicrosoftTeams.schema.json"", + ""manifestVersion"": ""devPreview"", + ""version"": ""1.0.0"", + ""id"": """", + ""developer"": { + ""name"": ""Test Name"", + ""websiteUrl"": """", + ""privacyUrl"": """", + ""termsOfUseUrl"": """" + }, + ""packageName"": ""com.microsoft.kiota.plugin.client"", + ""name"": { + ""short"": ""client"", + ""full"": ""API Plugin for "" + }, + ""description"": { + ""short"": ""API Plugin for . If the description is not available, it defaults to `API Plugin for `"", + ""full"": ""API Plugin for . If the description is not available, it defaults to `API Plugin for `"" + }, + ""icons"": { + ""color"": ""color.png"", + ""outline"": ""outline.png"" + }, + ""accentColor"": ""#FFFFFF"", + ""copilotExtensions"": { + ""plugins"": [ + { + ""id"": ""client"", + ""file"": ""dummyFile.json"" + } + ] + } +}"; + var preExistingManifestPath = Path.Combine(outputDirectory, "manifest.json"); + await File.WriteAllTextAsync(preExistingManifestPath, preExistingManifestContents); + var generationConfiguration = new GenerationConfiguration + { + OutputPath = outputDirectory, + OpenAPIFilePath = "openapiPath", + PluginTypes = [PluginType.APIPlugin], + ClientClassName = "client", + ApiRootUrl = "http://localhost/", //Kiota builder would set this for us + }; + 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); + + // Assert manifest exists before generation and is parsable + Assert.True(File.Exists(Path.Combine(outputDirectory, "manifest.json"))); + var originalManifestFile = await File.ReadAllTextAsync(Path.Combine(outputDirectory, AppManifestFileName)); + var originalAppManifestModelObject = JsonSerializer.Deserialize(originalManifestFile, PluginsGenerationService.AppManifestModelGenerationContext.AppManifestModel); + Assert.Equal("com.microsoft.kiota.plugin.client", originalAppManifestModelObject.PackageName);// package was present + Assert.NotNull(originalAppManifestModelObject.CopilotExtensions); + Assert.Single(originalAppManifestModelObject.CopilotExtensions.Plugins);//one plugin present + Assert.Equal("dummyFile.json", originalAppManifestModelObject.CopilotExtensions.Plugins[0].File); // no plugins present + + // Run the plugin generation + 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, OpenApiFileName))); + Assert.True(File.Exists(Path.Combine(outputDirectory, "manifest.json")));// Assert manifest exists after generation + + // 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", originalAppManifestModelObject.PackageName);// package was present + Assert.Equal("client", appManifestModelObject.Name.ShortName); // app name is same + Assert.Equal("Test Name", originalAppManifestModelObject.Developer.Name); // developer name is same + Assert.Equal("client", appManifestModelObject.CopilotExtensions.Plugins[0].Id); + Assert.Equal(ManifestFileName, appManifestModelObject.CopilotExtensions.Plugins[0].File);// file name is updated + } [Fact] public async Task GeneratesManifestAndCleansUpInputDescription() { From d62d7bf096409b5d343c7cd94d5699bdfff50bec Mon Sep 17 00:00:00 2001 From: Andrew Omondi Date: Mon, 1 Jul 2024 12:57:09 +0300 Subject: [PATCH 7/8] Add more details --- .../Plugins/PluginsGenerationServiceTests.cs | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/tests/Kiota.Builder.Tests/Plugins/PluginsGenerationServiceTests.cs b/tests/Kiota.Builder.Tests/Plugins/PluginsGenerationServiceTests.cs index ba633893c8..42c33deb50 100644 --- a/tests/Kiota.Builder.Tests/Plugins/PluginsGenerationServiceTests.cs +++ b/tests/Kiota.Builder.Tests/Plugins/PluginsGenerationServiceTests.cs @@ -522,7 +522,11 @@ public async Task GeneratesManifestAndUpdatesExistingAppManifestWithExistingPlug { var simpleDescriptionContent = @"openapi: 3.0.0 info: - version: 1.0 + termsOfService: http://example.com/terms/ + contact: + name: API Support + email: support@example.com + url: http://example.com/support servers: - url: http://localhost/ description: There's no place like home From 892d6bff4d13c23b2dc9e1a3540be97c56b032b3 Mon Sep 17 00:00:00 2001 From: Andrew Omondi Date: Tue, 2 Jul 2024 16:58:44 +0300 Subject: [PATCH 8/8] PR review feedback --- .../Plugins/Models/AppManifestModel.cs | 32 +++++++++++++------ .../Plugins/PluginsGenerationService.cs | 11 ++----- .../Plugins/PluginsGenerationServiceTests.cs | 2 ++ 3 files changed, 26 insertions(+), 19 deletions(-) diff --git a/src/Kiota.Builder/Plugins/Models/AppManifestModel.cs b/src/Kiota.Builder/Plugins/Models/AppManifestModel.cs index 74c6cf981c..5ef0fae47a 100644 --- a/src/Kiota.Builder/Plugins/Models/AppManifestModel.cs +++ b/src/Kiota.Builder/Plugins/Models/AppManifestModel.cs @@ -7,19 +7,28 @@ namespace Kiota.Builder.Plugins.Models; internal class AppManifestModel { + private const string DefaultSchema = "https://developer.microsoft.com/json-schemas/teams/vDevPreview/MicrosoftTeams.schema.json"; + private const string DefaultManifestVersion = "devPreview"; + private const string DefaultVersion = "1.0.0"; + [JsonPropertyName("$schema")] public string? Schema { - get; set; - } + get; + set; + } = DefaultSchema; + public string? ManifestVersion { - get; set; - } + get; + set; + } = DefaultManifestVersion; + public string? Version { - get; set; - } + get; + set; + } = DefaultVersion; public string? Id { get; set; @@ -122,12 +131,15 @@ internal class Icons { public string? Color { - get; set; - } + get; + set; + } = "color.png"; + public string? Outline { - get; set; - } + get; + set; + } = "outline.png"; } internal class CopilotExtensions diff --git a/src/Kiota.Builder/Plugins/PluginsGenerationService.cs b/src/Kiota.Builder/Plugins/PluginsGenerationService.cs index b86bf1c63a..0e38aa92b9 100644 --- a/src/Kiota.Builder/Plugins/PluginsGenerationService.cs +++ b/src/Kiota.Builder/Plugins/PluginsGenerationService.cs @@ -117,13 +117,10 @@ private async Task GetAppManifestModelAsync(string pluginFileN // create default model var manifestModel = new AppManifestModel { - Schema = "https://developer.microsoft.com/json-schemas/teams/vDevPreview/MicrosoftTeams.schema.json", - ManifestVersion = "devPreview", - Version = "1.0.0", Id = Guid.NewGuid().ToString(), Developer = new Developer { - Name = !string.IsNullOrEmpty(OAIDocument.Info?.Contact?.Name) ? OAIDocument.Info?.Contact?.Name : "Kiota Generator, Inc.", + Name = !string.IsNullOrEmpty(OAIDocument.Info?.Contact?.Name) ? OAIDocument.Info?.Contact?.Name : "Microsoft Kiota.", WebsiteUrl = !string.IsNullOrEmpty(OAIDocument.Info?.Contact?.Url?.OriginalString) ? OAIDocument.Info?.Contact?.Url?.OriginalString : "https://www.example.com/contact/", PrivacyUrl = !string.IsNullOrEmpty(manifestInfo.PrivacyUrl) ? manifestInfo.PrivacyUrl : "https://www.example.com/privacy/", TermsOfUseUrl = !string.IsNullOrEmpty(OAIDocument.Info?.TermsOfService?.OriginalString) ? OAIDocument.Info?.TermsOfService?.OriginalString : "https://www.example.com/terms/", @@ -139,11 +136,7 @@ private async Task GetAppManifestModelAsync(string pluginFileN ShortName = !string.IsNullOrEmpty(OAIDocument.Info?.Description.CleanupXMLString()) ? $"API Plugin for {OAIDocument.Info?.Description.CleanupXMLString()}." : OAIDocument.Info?.Title.CleanupXMLString() ?? "OpenApi Document", FullName = !string.IsNullOrEmpty(OAIDocument.Info?.Description.CleanupXMLString()) ? $"API Plugin for {OAIDocument.Info?.Description.CleanupXMLString()}." : OAIDocument.Info?.Title.CleanupXMLString() ?? "OpenApi Document" }, - Icons = new Icons - { - Color = "color.png", - Outline = "outline.png" - }, + Icons = new Icons(), AccentColor = "#FFFFFF" }; diff --git a/tests/Kiota.Builder.Tests/Plugins/PluginsGenerationServiceTests.cs b/tests/Kiota.Builder.Tests/Plugins/PluginsGenerationServiceTests.cs index 42c33deb50..77469edd74 100644 --- a/tests/Kiota.Builder.Tests/Plugins/PluginsGenerationServiceTests.cs +++ b/tests/Kiota.Builder.Tests/Plugins/PluginsGenerationServiceTests.cs @@ -115,6 +115,8 @@ public async Task GeneratesManifest() 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("Microsoft Kiota.", appManifestModelObject.Developer.Name); + Assert.Equal("color.png", appManifestModelObject.Icons.Color); Assert.Equal("client", appManifestModelObject.CopilotExtensions.Plugins[0].Id); Assert.Equal(ManifestFileName, appManifestModelObject.CopilotExtensions.Plugins[0].File); }