From 908fb2e0a57fb26d81ea6dada9e8c7ffc58ece62 Mon Sep 17 00:00:00 2001 From: Vincent Biret Date: Mon, 25 Mar 2024 11:24:13 -0400 Subject: [PATCH 01/40] - adds basic command infrastructure for plugins Signed-off-by: Vincent Biret --- src/Kiota.Builder/PluginType.cs | 7 ++ src/kiota/Handlers/Plugin/AddHandler.cs | 73 ++++++++++++++ src/kiota/KiotaClientCommands.cs | 4 +- src/kiota/KiotaHost.cs | 5 +- src/kiota/KiotaPluginCommands.cs | 127 ++++++++++++++++++++++++ 5 files changed, 212 insertions(+), 4 deletions(-) create mode 100644 src/Kiota.Builder/PluginType.cs create mode 100644 src/kiota/Handlers/Plugin/AddHandler.cs create mode 100644 src/kiota/KiotaPluginCommands.cs diff --git a/src/Kiota.Builder/PluginType.cs b/src/Kiota.Builder/PluginType.cs new file mode 100644 index 0000000000..b0285d4747 --- /dev/null +++ b/src/Kiota.Builder/PluginType.cs @@ -0,0 +1,7 @@ +namespace Kiota.Builder; + +public enum PluginType +{ + OpenAI, + APIManifest +} diff --git a/src/kiota/Handlers/Plugin/AddHandler.cs b/src/kiota/Handlers/Plugin/AddHandler.cs new file mode 100644 index 0000000000..dbaa5db1b7 --- /dev/null +++ b/src/kiota/Handlers/Plugin/AddHandler.cs @@ -0,0 +1,73 @@ +using System.CommandLine; +using System.CommandLine.Invocation; +using System.Text.Json; +using Kiota.Builder; +using Kiota.Builder.Configuration; +using Kiota.Builder.Extensions; +using Kiota.Builder.WorkspaceManagement; +using Microsoft.Extensions.Logging; + +namespace kiota.Handlers.Plugin; + +internal class AddHandler : BaseKiotaCommandHandler +{ + public required Option ClassOption + { + get; init; + } + public required Option OutputOption + { + get; init; + } + public required Option PluginTypeOption + { + get; init; + } + public required Option DescriptionOption + { + get; init; + } + public required Option> IncludePatternsOption + { + get; init; + } + public required Option> ExcludePatternsOption + { + get; init; + } + public required Option SkipGenerationOption + { + get; init; + } + public override async Task InvokeAsync(InvocationContext context) + { + string output = context.ParseResult.GetValueForOption(OutputOption) ?? string.Empty; + PluginType pluginType = context.ParseResult.GetValueForOption(PluginTypeOption); + string openapi = context.ParseResult.GetValueForOption(DescriptionOption) ?? string.Empty; + bool skipGeneration = context.ParseResult.GetValueForOption(SkipGenerationOption); + string className = context.ParseResult.GetValueForOption(ClassOption) ?? string.Empty; + List includePatterns = context.ParseResult.GetValueForOption(IncludePatternsOption) ?? []; + List excludePatterns = context.ParseResult.GetValueForOption(ExcludePatternsOption) ?? []; + CancellationToken cancellationToken = context.BindingContext.GetService(typeof(CancellationToken)) is CancellationToken token ? token : CancellationToken.None; + AssignIfNotNullOrEmpty(output, (c, s) => c.OutputPath = s); + AssignIfNotNullOrEmpty(openapi, (c, s) => c.OpenAPIFilePath = s); + AssignIfNotNullOrEmpty(className, (c, s) => c.ClientClassName = s); + Configuration.Generation.SkipGeneration = skipGeneration; + Configuration.Generation.Operation = ClientOperation.Add; + //TODO do something with the plugin type + if (includePatterns.Count != 0) + Configuration.Generation.IncludePatterns = includePatterns.Select(static x => x.TrimQuotes()).ToHashSet(StringComparer.OrdinalIgnoreCase); + if (excludePatterns.Count != 0) + Configuration.Generation.ExcludePatterns = excludePatterns.Select(static x => x.TrimQuotes()).ToHashSet(StringComparer.OrdinalIgnoreCase); + Configuration.Generation.OpenAPIFilePath = GetAbsolutePath(Configuration.Generation.OpenAPIFilePath); + Configuration.Generation.OutputPath = NormalizeSlashesInPath(GetAbsolutePath(Configuration.Generation.OutputPath)); + var (loggerFactory, logger) = GetLoggerAndFactory(context, Configuration.Generation.OutputPath); + using (loggerFactory) + { + await CheckForNewVersionAsync(logger, cancellationToken).ConfigureAwait(false); + logger.AppendInternalTracing(); + logger.LogTrace("configuration: {configuration}", JsonSerializer.Serialize(Configuration, KiotaConfigurationJsonContext.Default.KiotaConfiguration)); + } + throw new NotImplementedException(); + } +} diff --git a/src/kiota/KiotaClientCommands.cs b/src/kiota/KiotaClientCommands.cs index ba3b54fff9..8f08d20de4 100644 --- a/src/kiota/KiotaClientCommands.cs +++ b/src/kiota/KiotaClientCommands.cs @@ -14,7 +14,7 @@ public static Command GetClientNodeCommand() command.AddCommand(GetGenerateCommand()); return command; } - private static Option GetSkipGenerationOption() + internal static Option GetSkipGenerationOption() { var skipGeneration = new Option("--skip-generation", "Skips the generation of the client"); skipGeneration.AddAlias("--sg"); @@ -171,7 +171,7 @@ public static Command GetGenerateCommand() }; return command; } - private static Option GetRefreshOption() + internal static Option GetRefreshOption() { var refresh = new Option("--refresh", "Refreshes the client OpenAPI description before generating the client"); refresh.AddAlias("--r"); diff --git a/src/kiota/KiotaHost.cs b/src/kiota/KiotaHost.cs index 4323e97fcf..d1b8285d2d 100644 --- a/src/kiota/KiotaHost.cs +++ b/src/kiota/KiotaHost.cs @@ -30,6 +30,7 @@ public static RootCommand GetRootCommand() { rootCommand.AddCommand(KiotaWorkspaceCommands.GetWorkspaceNodeCommand()); rootCommand.AddCommand(KiotaClientCommands.GetClientNodeCommand()); + rootCommand.AddCommand(KiotaPluginCommands.GetPluginNodeCommand()); } return rootCommand; } @@ -567,14 +568,14 @@ private static void ValidateEnumValue(OptionResult input, string parameterNam input.ErrorMessage = $"{input.Tokens[0].Value} is not a supported generation {parameterName}, supported values are {validOptionsList}"; } } - private static void AddEnumValidator(Option option, string parameterName) where T : struct, Enum + internal static void AddEnumValidator(Option option, string parameterName) where T : struct, Enum { option.AddValidator(input => { ValidateEnumValue(input, parameterName); }); } - private static void AddEnumValidator(Option option, string parameterName) where T : struct, Enum + internal static void AddEnumValidator(Option option, string parameterName) where T : struct, Enum { option.AddValidator(input => { diff --git a/src/kiota/KiotaPluginCommands.cs b/src/kiota/KiotaPluginCommands.cs new file mode 100644 index 0000000000..e044eb90bb --- /dev/null +++ b/src/kiota/KiotaPluginCommands.cs @@ -0,0 +1,127 @@ +using System.CommandLine; +using kiota.Handlers.Plugin; +using Kiota.Builder; +using Kiota.Builder.Configuration; + +namespace kiota; +public static class KiotaPluginCommands +{ + public static Command GetPluginNodeCommand() + { + var command = new Command("plugin", "Manages the Kiota generated API plugins"); + command.AddCommand(GetAddCommand()); + command.AddCommand(GetRemoveCommand()); + command.AddCommand(GetEditCommand()); + command.AddCommand(GetGenerateCommand()); + return command; + } + internal static Option GetPluginNameOption(bool required = true) + { + var clientName = new Option("--plugin-name", "The name of the plugin to manage") + { + IsRequired = required, + }; + clientName.AddAlias("--pn"); + return clientName; + } + internal static Option GetPluginTypeOption() + { + var languageOption = new Option("--type", "The type of manifest to generate."); + languageOption.AddAlias("-t"); + languageOption.IsRequired = true; + KiotaHost.AddEnumValidator(languageOption, "language"); + return languageOption; + } + internal static Option GetPluginOptionalTypeOption() + { + var languageOption = new Option("--type", "The type of manifest to generate."); + languageOption.AddAlias("-t"); + KiotaHost.AddEnumValidator(languageOption, "language"); + return languageOption; + } + public static Command GetAddCommand() + { + var defaultConfiguration = new GenerationConfiguration(); + var outputOption = KiotaHost.GetOutputPathOption(defaultConfiguration.OutputPath); + var descriptionOption = KiotaHost.GetDescriptionOption(defaultConfiguration.OpenAPIFilePath, true); + var (includePatterns, excludePatterns) = KiotaHost.GetIncludeAndExcludeOptions(defaultConfiguration.IncludePatterns, defaultConfiguration.ExcludePatterns); + var logLevelOption = KiotaHost.GetLogLevelOption(); + var skipGenerationOption = KiotaClientCommands.GetSkipGenerationOption(); + var pluginNameOption = GetPluginNameOption(); + var pluginType = GetPluginTypeOption(); + var command = new Command("add", "Adds a new plugin to the Kiota configuration"){ + descriptionOption, + includePatterns, + excludePatterns, + logLevelOption, + skipGenerationOption, + outputOption, + pluginNameOption, + pluginType, + //TODO overlay when we have support for it in OAI.net + }; + command.Handler = new AddHandler + { + ClassOption = pluginNameOption, + OutputOption = outputOption, + PluginTypeOption = pluginType, + DescriptionOption = descriptionOption, + IncludePatternsOption = includePatterns, + ExcludePatternsOption = excludePatterns, + SkipGenerationOption = skipGenerationOption, + LogLevelOption = logLevelOption, + }; + return command; + } + public static Command GetEditCommand() + { + var outputOption = KiotaHost.GetOutputPathOption(string.Empty); + var descriptionOption = KiotaHost.GetDescriptionOption(string.Empty); + var (includePatterns, excludePatterns) = KiotaHost.GetIncludeAndExcludeOptions([], []); + var logLevelOption = KiotaHost.GetLogLevelOption(); + var skipGenerationOption = KiotaClientCommands.GetSkipGenerationOption(); + var pluginNameOption = GetPluginNameOption(); + var pluginType = GetPluginOptionalTypeOption(); + var command = new Command("edit", "Edits a plugin configuration and updates the Kiota configuration"){ + descriptionOption, + includePatterns, + excludePatterns, + logLevelOption, + skipGenerationOption, + outputOption, + pluginNameOption, + pluginType, + //TODO overlay when we have support for it in OAI.net + }; + //TODO map handler + return command; + } + public static Command GetRemoveCommand() + { + var pluginNameOption = GetPluginNameOption(); + var cleanOutputOption = KiotaHost.GetCleanOutputOption(false); + var logLevelOption = KiotaHost.GetLogLevelOption(); + var command = new Command("remove", "Removes a plugin from the Kiota configuration") + { + pluginNameOption, + cleanOutputOption, + logLevelOption, + }; + //TODO map handler + return command; + } + public static Command GetGenerateCommand() + { + var pluginNameOption = GetPluginNameOption(); + var logLevelOption = KiotaHost.GetLogLevelOption(); + var refreshOption = KiotaClientCommands.GetRefreshOption(); + var command = new Command("generate", "Generates one or all plugin from the Kiota configuration") + { + pluginNameOption, + logLevelOption, + refreshOption, + }; + //TODO map handler + return command; + } +} From b1a484b907a52447faa34e4117ecde555e65bb56 Mon Sep 17 00:00:00 2001 From: Vincent Biret Date: Mon, 25 Mar 2024 13:15:11 -0400 Subject: [PATCH 02/40] - code linting Signed-off-by: Vincent Biret --- src/kiota/Handlers/Client/GenerateHandler.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/src/kiota/Handlers/Client/GenerateHandler.cs b/src/kiota/Handlers/Client/GenerateHandler.cs index d4787f744c..f7b997109a 100644 --- a/src/kiota/Handlers/Client/GenerateHandler.cs +++ b/src/kiota/Handlers/Client/GenerateHandler.cs @@ -84,6 +84,5 @@ public override async Task InvokeAsync(InvocationContext context) #endif } } - throw new System.NotImplementedException(); } } From 33e780ffa5d6020cb04fea091838b0c0fdab4689 Mon Sep 17 00:00:00 2001 From: Vincent Biret Date: Mon, 25 Mar 2024 13:15:55 -0400 Subject: [PATCH 03/40] - maps plugin remove command Signed-off-by: Vincent Biret --- .../ApiClientConfiguration.cs | 34 ++----- .../ApiPluginConfiguration.cs | 27 ++++++ .../BaseApiConsumerConfiguration.cs | 48 ++++++++++ .../WorkspaceConfiguration.cs | 12 ++- .../WorkspaceManagementService.cs | 33 ++++++- src/kiota/Handlers/Plugin/AddHandler.cs | 5 +- src/kiota/Handlers/Plugin/GenerateHandler.cs | 91 +++++++++++++++++++ src/kiota/Handlers/Plugin/RemoveHandler.cs | 46 ++++++++++ src/kiota/KiotaHost.cs | 6 +- src/kiota/KiotaPluginCommands.cs | 44 +++++---- 10 files changed, 289 insertions(+), 57 deletions(-) create mode 100644 src/Kiota.Builder/WorkspaceManagement/ApiPluginConfiguration.cs create mode 100644 src/Kiota.Builder/WorkspaceManagement/BaseApiConsumerConfiguration.cs create mode 100644 src/kiota/Handlers/Plugin/GenerateHandler.cs create mode 100644 src/kiota/Handlers/Plugin/RemoveHandler.cs diff --git a/src/Kiota.Builder/WorkspaceManagement/ApiClientConfiguration.cs b/src/Kiota.Builder/WorkspaceManagement/ApiClientConfiguration.cs index 6ef6800a7c..ff0c9ca923 100644 --- a/src/Kiota.Builder/WorkspaceManagement/ApiClientConfiguration.cs +++ b/src/Kiota.Builder/WorkspaceManagement/ApiClientConfiguration.cs @@ -8,12 +8,8 @@ namespace Kiota.Builder.WorkspaceManagement; #pragma warning disable CA2227 // Collection properties should be read only -public class ApiClientConfiguration : ICloneable +public class ApiClientConfiguration : BaseApiConsumerConfiguration, ICloneable { - /// - /// The location of the OpenAPI description file. - /// - public string DescriptionLocation { get; set; } = string.Empty; /// /// The language for this client. /// @@ -24,18 +20,6 @@ public class ApiClientConfiguration : ICloneable #pragma warning disable CA1002 public List StructuredMimeTypes { get; set; } = new(); #pragma warning restore CA1002 - /// - /// The path patterns for API endpoints to include for this client. - /// - public HashSet IncludePatterns { get; set; } = new(); - /// - /// The path patterns for API endpoints to exclude for this client. - /// - public HashSet ExcludePatterns { get; set; } = new(); - /// - /// The output path for the generated code, related to the configuration file. - /// - public string OutputPath { get; set; } = string.Empty; /// /// The main namespace for this client. /// @@ -68,7 +52,7 @@ public bool ExcludeBackwardCompatible /// /// Initializes a new instance of the class. /// - public ApiClientConfiguration() + public ApiClientConfiguration() : base() { } @@ -76,7 +60,7 @@ public ApiClientConfiguration() /// Initializes a new instance of the class from an existing . /// /// The configuration to use to initialize the client configuration - public ApiClientConfiguration(GenerationConfiguration config) + public ApiClientConfiguration(GenerationConfiguration config) : base(config) { ArgumentNullException.ThrowIfNull(config); Language = config.Language.ToString(); @@ -85,11 +69,7 @@ public ApiClientConfiguration(GenerationConfiguration config) ExcludeBackwardCompatible = config.ExcludeBackwardCompatible; IncludeAdditionalData = config.IncludeAdditionalData; StructuredMimeTypes = config.StructuredMimeTypes.ToList(); - IncludePatterns = config.IncludePatterns; - ExcludePatterns = config.ExcludePatterns; - DescriptionLocation = config.OpenAPIFilePath; DisabledValidationRules = config.DisabledValidationRules; - OutputPath = config.OutputPath; } /// /// Updates the passed configuration with the values from the config file. @@ -126,20 +106,18 @@ public void UpdateGenerationConfigurationFromApiClientConfiguration(GenerationCo public object Clone() { - return new ApiClientConfiguration + var result = new ApiClientConfiguration { - DescriptionLocation = DescriptionLocation, Language = Language, StructuredMimeTypes = [.. StructuredMimeTypes], - IncludePatterns = new(IncludePatterns, StringComparer.OrdinalIgnoreCase), - ExcludePatterns = new(ExcludePatterns, StringComparer.OrdinalIgnoreCase), - OutputPath = OutputPath, ClientNamespaceName = ClientNamespaceName, UsesBackingStore = UsesBackingStore, IncludeAdditionalData = IncludeAdditionalData, ExcludeBackwardCompatible = ExcludeBackwardCompatible, DisabledValidationRules = new(DisabledValidationRules, StringComparer.OrdinalIgnoreCase), }; + CloneBase(result); + return result; } public void NormalizePaths(string targetDirectory) { diff --git a/src/Kiota.Builder/WorkspaceManagement/ApiPluginConfiguration.cs b/src/Kiota.Builder/WorkspaceManagement/ApiPluginConfiguration.cs new file mode 100644 index 0000000000..77478a03e2 --- /dev/null +++ b/src/Kiota.Builder/WorkspaceManagement/ApiPluginConfiguration.cs @@ -0,0 +1,27 @@ +using System; +using System.Collections.Generic; + +namespace Kiota.Builder.WorkspaceManagement; + +#pragma warning disable CA2227 // Collection properties should be read only +public class ApiPluginConfiguration : BaseApiConsumerConfiguration, ICloneable +{ + /// + /// Initializes a new instance of the class. + /// + public ApiPluginConfiguration() : base() + { + + } + public HashSet Types { get; set; } = new(StringComparer.OrdinalIgnoreCase); + public object Clone() + { + var result = new ApiPluginConfiguration() + { + Types = new HashSet(Types, StringComparer.OrdinalIgnoreCase) + }; + CloneBase(result); + return result; + } +} +#pragma warning restore CA2227 // Collection properties should be read only diff --git a/src/Kiota.Builder/WorkspaceManagement/BaseApiConsumerConfiguration.cs b/src/Kiota.Builder/WorkspaceManagement/BaseApiConsumerConfiguration.cs new file mode 100644 index 0000000000..076ac772e2 --- /dev/null +++ b/src/Kiota.Builder/WorkspaceManagement/BaseApiConsumerConfiguration.cs @@ -0,0 +1,48 @@ +using System; +using System.Collections.Generic; +using Kiota.Builder.Configuration; + +namespace Kiota.Builder.WorkspaceManagement; + +#pragma warning disable CA2227 // Collection properties should be read only +public abstract class BaseApiConsumerConfiguration +{ + internal BaseApiConsumerConfiguration() + { + + } + internal BaseApiConsumerConfiguration(GenerationConfiguration config) + { + ArgumentNullException.ThrowIfNull(config); + DescriptionLocation = config.OpenAPIFilePath; + IncludePatterns = new HashSet(config.IncludePatterns); + ExcludePatterns = new HashSet(config.ExcludePatterns); + OutputPath = config.OutputPath; + } + /// + /// The location of the OpenAPI description file. + /// + public string DescriptionLocation { get; set; } = string.Empty; + /// + /// The path patterns for API endpoints to include for this client. + /// + public HashSet IncludePatterns { get; set; } = new(StringComparer.OrdinalIgnoreCase); + /// + /// The path patterns for API endpoints to exclude for this client. + /// + public HashSet ExcludePatterns { get; set; } = new(StringComparer.OrdinalIgnoreCase); + /// + /// The output path for the generated code, related to the configuration file. + /// + public string OutputPath { get; set; } = string.Empty; + + protected void CloneBase(BaseApiConsumerConfiguration target) + { + ArgumentNullException.ThrowIfNull(target); + target.OutputPath = OutputPath; + target.DescriptionLocation = DescriptionLocation; + target.IncludePatterns = new HashSet(IncludePatterns, StringComparer.OrdinalIgnoreCase); + target.ExcludePatterns = new HashSet(ExcludePatterns, StringComparer.OrdinalIgnoreCase); + } +} +#pragma warning restore CA2227 // Collection properties should be read only diff --git a/src/Kiota.Builder/WorkspaceManagement/WorkspaceConfiguration.cs b/src/Kiota.Builder/WorkspaceManagement/WorkspaceConfiguration.cs index 29cbb638f3..57840f7f0f 100644 --- a/src/Kiota.Builder/WorkspaceManagement/WorkspaceConfiguration.cs +++ b/src/Kiota.Builder/WorkspaceManagement/WorkspaceConfiguration.cs @@ -15,13 +15,23 @@ public class WorkspaceConfiguration : ICloneable /// The clients to generate. /// public Dictionary Clients { get; set; } = new Dictionary(StringComparer.OrdinalIgnoreCase); + public Dictionary Plugins { get; set; } = new Dictionary(StringComparer.OrdinalIgnoreCase); #pragma warning restore CA2227 // Collection properties should be read only + public bool AreConsumersKeysUnique + { + get + { + return Clients.Keys.Concat(Plugins.Keys).GroupBy(static x => x, StringComparer.OrdinalIgnoreCase).All(static x => x.Count() == 1); + } + } + public bool AnyConsumerPresent => Clients.Count != 0 || Plugins.Count != 0; public object Clone() { return new WorkspaceConfiguration { Version = Version, - Clients = Clients.ToDictionary(static x => x.Key, static x => (ApiClientConfiguration)x.Value.Clone(), StringComparer.OrdinalIgnoreCase) + Clients = Clients.ToDictionary(static x => x.Key, static x => (ApiClientConfiguration)x.Value.Clone(), StringComparer.OrdinalIgnoreCase), + Plugins = Plugins.ToDictionary(static x => x.Key, static x => (ApiPluginConfiguration)x.Value.Clone(), StringComparer.OrdinalIgnoreCase) }; } } diff --git a/src/Kiota.Builder/WorkspaceManagement/WorkspaceManagementService.cs b/src/Kiota.Builder/WorkspaceManagement/WorkspaceManagementService.cs index a010b2ec2f..0acdf9ca4f 100644 --- a/src/Kiota.Builder/WorkspaceManagement/WorkspaceManagementService.cs +++ b/src/Kiota.Builder/WorkspaceManagement/WorkspaceManagementService.cs @@ -128,6 +128,32 @@ public async Task ShouldGenerateAsync(GenerationConfiguration inputConfig, return await descriptionStorageService.GetDescriptionAsync(clientName, new Uri(inputPath).GetFileExtension(), cancellationToken).ConfigureAwait(false); } public async Task RemoveClientAsync(string clientName, bool cleanOutput = false, CancellationToken cancellationToken = default) + { + await RemoveConsumerInternalAsync(clientName, + wsConfig => + { + if (cleanOutput && wsConfig.Clients.TryGetValue(clientName, out var clientConfig) && Directory.Exists(clientConfig.OutputPath)) + Directory.Delete(clientConfig.OutputPath, true); + + if (!wsConfig.Clients.Remove(clientName)) + throw new InvalidOperationException($"The client {clientName} was not found in the configuration"); + }, + cleanOutput, cancellationToken).ConfigureAwait(false); + } + public async Task RemovePluginAsync(string clientName, bool cleanOutput = false, CancellationToken cancellationToken = default) + { + await RemoveConsumerInternalAsync(clientName, + wsConfig => + { + if (cleanOutput && wsConfig.Plugins.TryGetValue(clientName, out var pluginConfig) && Directory.Exists(pluginConfig.OutputPath)) + Directory.Delete(pluginConfig.OutputPath, true); + + if (!wsConfig.Plugins.Remove(clientName)) + throw new InvalidOperationException($"The client {clientName} was not found in the configuration"); + }, + cleanOutput, cancellationToken).ConfigureAwait(false); + } + private async Task RemoveConsumerInternalAsync(string clientName, Action consumerRemoval, bool cleanOutput = false, CancellationToken cancellationToken = default) { if (!UseKiotaConfig) throw new InvalidOperationException("Cannot remove a client in lock mode"); @@ -135,15 +161,12 @@ public async Task RemoveClientAsync(string clientName, bool cleanOutput = false, if (wsConfig is null) throw new InvalidOperationException("Cannot remove a client without a configuration"); - if (cleanOutput && wsConfig.Clients.TryGetValue(clientName, out var clientConfig) && Directory.Exists(clientConfig.OutputPath)) - Directory.Delete(clientConfig.OutputPath, true); + consumerRemoval(wsConfig); - if (!wsConfig.Clients.Remove(clientName)) - throw new InvalidOperationException($"The client {clientName} was not found in the configuration"); manifest?.ApiDependencies.Remove(clientName); await workspaceConfigurationStorageService.UpdateWorkspaceConfigurationAsync(wsConfig, manifest, cancellationToken).ConfigureAwait(false); descriptionStorageService.RemoveDescription(clientName); - if (wsConfig.Clients.Count == 0) + if (wsConfig.AnyConsumerPresent) descriptionStorageService.Clean(); } private static readonly JsonSerializerOptions options = new() diff --git a/src/kiota/Handlers/Plugin/AddHandler.cs b/src/kiota/Handlers/Plugin/AddHandler.cs index dbaa5db1b7..ab5d8289ac 100644 --- a/src/kiota/Handlers/Plugin/AddHandler.cs +++ b/src/kiota/Handlers/Plugin/AddHandler.cs @@ -19,7 +19,7 @@ public required Option OutputOption { get; init; } - public required Option PluginTypeOption + public required Option> PluginTypesOption { get; init; } @@ -42,7 +42,7 @@ public required Option SkipGenerationOption public override async Task InvokeAsync(InvocationContext context) { string output = context.ParseResult.GetValueForOption(OutputOption) ?? string.Empty; - PluginType pluginType = context.ParseResult.GetValueForOption(PluginTypeOption); + List pluginTypes = context.ParseResult.GetValueForOption(PluginTypesOption) ?? []; string openapi = context.ParseResult.GetValueForOption(DescriptionOption) ?? string.Empty; bool skipGeneration = context.ParseResult.GetValueForOption(SkipGenerationOption); string className = context.ParseResult.GetValueForOption(ClassOption) ?? string.Empty; @@ -55,6 +55,7 @@ public override async Task InvokeAsync(InvocationContext context) Configuration.Generation.SkipGeneration = skipGeneration; Configuration.Generation.Operation = ClientOperation.Add; //TODO do something with the plugin type + // if (pluginTypes.Count != 0) if (includePatterns.Count != 0) Configuration.Generation.IncludePatterns = includePatterns.Select(static x => x.TrimQuotes()).ToHashSet(StringComparer.OrdinalIgnoreCase); if (excludePatterns.Count != 0) diff --git a/src/kiota/Handlers/Plugin/GenerateHandler.cs b/src/kiota/Handlers/Plugin/GenerateHandler.cs new file mode 100644 index 0000000000..b22f5d6f30 --- /dev/null +++ b/src/kiota/Handlers/Plugin/GenerateHandler.cs @@ -0,0 +1,91 @@ +using System.CommandLine; +using System.CommandLine.Invocation; +using System.Text.Json; +using Kiota.Builder; +using Kiota.Builder.Configuration; +using Kiota.Builder.WorkspaceManagement; +using Microsoft.Extensions.Logging; + +namespace kiota.Handlers.Plugin; + +internal class GenerateHandler : BaseKiotaCommandHandler +{ + public required Option ClassOption + { + get; init; + } + public required Option RefreshOption + { + get; init; + } + public override async Task InvokeAsync(InvocationContext context) + { + string className = context.ParseResult.GetValueForOption(ClassOption) ?? string.Empty; + bool refresh = context.ParseResult.GetValueForOption(RefreshOption); + CancellationToken cancellationToken = context.BindingContext.GetService(typeof(CancellationToken)) is CancellationToken token ? token : CancellationToken.None; + var (loggerFactory, logger) = GetLoggerAndFactory(context, Configuration.Generation.OutputPath); + using (loggerFactory) + { + await CheckForNewVersionAsync(logger, cancellationToken).ConfigureAwait(false); + logger.AppendInternalTracing(); + logger.LogTrace("configuration: {configuration}", JsonSerializer.Serialize(Configuration, KiotaConfigurationJsonContext.Default.KiotaConfiguration)); + try + { + var workspaceStorageService = new WorkspaceConfigurationStorageService(Directory.GetCurrentDirectory()); + var (config, manifest) = await workspaceStorageService.GetWorkspaceConfigurationAsync(cancellationToken).ConfigureAwait(false); + if (config == null) + { + DisplayError("The workspace configuration is missing, please run the init command first."); + return 1; + } + var clientNameWasNotProvided = string.IsNullOrEmpty(className); + var clientEntries = config + .Plugins + .Where(x => clientNameWasNotProvided || x.Key.Equals(className, StringComparison.OrdinalIgnoreCase)) + .ToArray(); + if (clientEntries.Length == 0 && !clientNameWasNotProvided) + { + DisplayError($"No client found with the provided name {className}"); + return 1; + } + foreach (var clientEntry in clientEntries) + { + var generationConfiguration = new GenerationConfiguration(); + var requests = !refresh && manifest is not null && manifest.ApiDependencies.TryGetValue(clientEntry.Key, out var value) ? value.Requests : []; + // clientEntry.Value.UpdateGenerationConfigurationFromApiClientConfiguration(generationConfiguration, clientEntry.Key, requests); + //TODO update to load configuration from plugin + DefaultSerializersAndDeserializers(generationConfiguration); + generationConfiguration.ClearCache = refresh; + generationConfiguration.CleanOutput = refresh; + generationConfiguration.Operation = ClientOperation.Generate; + var builder = new KiotaBuilder(logger, generationConfiguration, httpClient, true); + var result = await builder.GenerateClientAsync(cancellationToken).ConfigureAwait(false); + //TODO update to generate plugin instead + if (result) + { + DisplaySuccess($"Update of {clientEntry.Key} client completed"); + var manifestPath = $"{GetAbsolutePath(Path.Combine(WorkspaceConfigurationStorageService.KiotaDirectorySegment, WorkspaceConfigurationStorageService.ManifestFileName))}#{clientEntry.Key}"; + DisplayInfoHint(generationConfiguration.Language, string.Empty, manifestPath); + } + else + { + DisplayWarning($"Update of {clientEntry.Key} skipped, no changes detected"); + DisplayCleanHint("client generate", "--refresh"); + } + } + return 0; + } + catch (Exception ex) + { +#if DEBUG + logger.LogCritical(ex, "error adding the client: {exceptionMessage}", ex.Message); + throw; // so debug tools go straight to the source of the exception when attached +#else + logger.LogCritical("error adding the client: {exceptionMessage}", ex.Message); + return 1; +#endif + } + } + throw new NotImplementedException(); + } +} diff --git a/src/kiota/Handlers/Plugin/RemoveHandler.cs b/src/kiota/Handlers/Plugin/RemoveHandler.cs new file mode 100644 index 0000000000..0c0f3c7f5b --- /dev/null +++ b/src/kiota/Handlers/Plugin/RemoveHandler.cs @@ -0,0 +1,46 @@ +using System.CommandLine; +using System.CommandLine.Invocation; +using Kiota.Builder; +using Kiota.Builder.WorkspaceManagement; +using Microsoft.Extensions.Logging; + +namespace kiota.Handlers.Plugin; +internal class RemoveHandler : BaseKiotaCommandHandler +{ + public required Option ClassOption + { + get; init; + } + public required Option CleanOutputOption + { + get; init; + } + public override async Task InvokeAsync(InvocationContext context) + { + string className = context.ParseResult.GetValueForOption(ClassOption) ?? string.Empty; + bool cleanOutput = context.ParseResult.GetValueForOption(CleanOutputOption); + CancellationToken cancellationToken = context.BindingContext.GetService(typeof(CancellationToken)) is CancellationToken token ? token : CancellationToken.None; + var (loggerFactory, logger) = GetLoggerAndFactory(context, $"./{DescriptionStorageService.KiotaDirectorySegment}"); + using (loggerFactory) + { + try + { + await CheckForNewVersionAsync(logger, cancellationToken).ConfigureAwait(false); + var workspaceManagementService = new WorkspaceManagementService(logger, httpClient, true); + await workspaceManagementService.RemovePluginAsync(className, cleanOutput, cancellationToken).ConfigureAwait(false); + DisplaySuccess($"Plugin {className} removed successfully!"); + return 0; + } + catch (Exception ex) + { +#if DEBUG + logger.LogCritical(ex, "error removing the plugin: {exceptionMessage}", ex.Message); + throw; // so debug tools go straight to the source of the exception when attached +#else + logger.LogCritical("error removing the plugin: {exceptionMessage}", ex.Message); + return 1; +#endif + } + } + } +} diff --git a/src/kiota/KiotaHost.cs b/src/kiota/KiotaHost.cs index d1b8285d2d..5923772de7 100644 --- a/src/kiota/KiotaHost.cs +++ b/src/kiota/KiotaHost.cs @@ -551,7 +551,7 @@ private static void AddStringRegexValidator(Option option, Regex validat input.ErrorMessage = $"{value} is not a valid {parameterName} for the client, the {parameterName} must conform to {validator}"; }); } - private static void ValidateKnownValues(OptionResult input, string parameterName, IEnumerable knownValues) + internal static void ValidateKnownValues(OptionResult input, string parameterName, IEnumerable knownValues) { var knownValuesHash = new HashSet(knownValues, StringComparer.OrdinalIgnoreCase); if (input.Tokens.Any() && input.Tokens.Select(static x => x.Value).SelectMany(static x => x.Split(new[] { ',' }, StringSplitOptions.RemoveEmptyEntries)).FirstOrDefault(x => !knownValuesHash.Contains(x)) is string unknownValue) @@ -568,14 +568,14 @@ private static void ValidateEnumValue(OptionResult input, string parameterNam input.ErrorMessage = $"{input.Tokens[0].Value} is not a supported generation {parameterName}, supported values are {validOptionsList}"; } } - internal static void AddEnumValidator(Option option, string parameterName) where T : struct, Enum + private static void AddEnumValidator(Option option, string parameterName) where T : struct, Enum { option.AddValidator(input => { ValidateEnumValue(input, parameterName); }); } - internal static void AddEnumValidator(Option option, string parameterName) where T : struct, Enum + private static void AddEnumValidator(Option option, string parameterName) where T : struct, Enum { option.AddValidator(input => { diff --git a/src/kiota/KiotaPluginCommands.cs b/src/kiota/KiotaPluginCommands.cs index e044eb90bb..a8124129ba 100644 --- a/src/kiota/KiotaPluginCommands.cs +++ b/src/kiota/KiotaPluginCommands.cs @@ -24,20 +24,18 @@ internal static Option GetPluginNameOption(bool required = true) clientName.AddAlias("--pn"); return clientName; } - internal static Option GetPluginTypeOption() + internal static Option> GetPluginTypeOption(bool isRequired = true) { - var languageOption = new Option("--type", "The type of manifest to generate."); - languageOption.AddAlias("-t"); - languageOption.IsRequired = true; - KiotaHost.AddEnumValidator(languageOption, "language"); - return languageOption; - } - internal static Option GetPluginOptionalTypeOption() - { - var languageOption = new Option("--type", "The type of manifest to generate."); - languageOption.AddAlias("-t"); - KiotaHost.AddEnumValidator(languageOption, "language"); - return languageOption; + var typeOption = new Option>("--type", "The type of manifest to generate. Accepts multiple values."); + typeOption.AddAlias("-t"); + if (isRequired) + { + typeOption.IsRequired = true; + typeOption.Arity = ArgumentArity.OneOrMore; + typeOption.SetDefaultValue(new List { PluginType.OpenAI }); + } + typeOption.AddValidator(x => KiotaHost.ValidateKnownValues(x, "type", Enum.GetNames())); + return typeOption; } public static Command GetAddCommand() { @@ -64,7 +62,7 @@ public static Command GetAddCommand() { ClassOption = pluginNameOption, OutputOption = outputOption, - PluginTypeOption = pluginType, + PluginTypesOption = pluginType, DescriptionOption = descriptionOption, IncludePatternsOption = includePatterns, ExcludePatternsOption = excludePatterns, @@ -81,7 +79,7 @@ public static Command GetEditCommand() var logLevelOption = KiotaHost.GetLogLevelOption(); var skipGenerationOption = KiotaClientCommands.GetSkipGenerationOption(); var pluginNameOption = GetPluginNameOption(); - var pluginType = GetPluginOptionalTypeOption(); + var pluginTypes = GetPluginTypeOption(false); var command = new Command("edit", "Edits a plugin configuration and updates the Kiota configuration"){ descriptionOption, includePatterns, @@ -90,7 +88,7 @@ public static Command GetEditCommand() skipGenerationOption, outputOption, pluginNameOption, - pluginType, + pluginTypes, //TODO overlay when we have support for it in OAI.net }; //TODO map handler @@ -107,7 +105,12 @@ public static Command GetRemoveCommand() cleanOutputOption, logLevelOption, }; - //TODO map handler + command.Handler = new RemoveHandler + { + ClassOption = pluginNameOption, + CleanOutputOption = cleanOutputOption, + LogLevelOption = logLevelOption, + }; return command; } public static Command GetGenerateCommand() @@ -121,7 +124,12 @@ public static Command GetGenerateCommand() logLevelOption, refreshOption, }; - //TODO map handler + command.Handler = new GenerateHandler + { + ClassOption = pluginNameOption, + LogLevelOption = logLevelOption, + RefreshOption = refreshOption, + }; return command; } } From b020268d11cd3e91c9c06653b2075419a9d7bec8 Mon Sep 17 00:00:00 2001 From: Vincent Biret Date: Mon, 25 Mar 2024 14:00:02 -0400 Subject: [PATCH 04/40] - implements the plugin generate command Signed-off-by: Vincent Biret --- .../Configuration/GenerationConfiguration.cs | 4 +- src/Kiota.Builder/KiotaBuilder.cs | 51 ++++++++++++++----- .../ApiClientConfiguration.cs | 14 +---- .../ApiPluginConfiguration.cs | 16 ++++++ .../BaseApiConsumerConfiguration.cs | 20 ++++++++ src/kiota/Handlers/Plugin/GenerateHandler.cs | 6 +-- 6 files changed, 78 insertions(+), 33 deletions(-) diff --git a/src/Kiota.Builder/Configuration/GenerationConfiguration.cs b/src/Kiota.Builder/Configuration/GenerationConfiguration.cs index 0bb711394c..dc31b67f68 100644 --- a/src/Kiota.Builder/Configuration/GenerationConfiguration.cs +++ b/src/Kiota.Builder/Configuration/GenerationConfiguration.cs @@ -9,7 +9,6 @@ namespace Kiota.Builder.Configuration; #pragma warning disable CA2227 -#pragma warning disable CA1002 #pragma warning disable CA1056 public class GenerationConfiguration : ICloneable { @@ -46,6 +45,7 @@ public string ModelsNamespaceName get => $"{ClientNamespaceName}{NamespaceNameSeparator}{ModelsNamespaceSegmentName}"; } public GenerationLanguage Language { get; set; } = GenerationLanguage.CSharp; + public HashSet PluginTypes { get; set; } = []; public string? ApiRootUrl { get; set; @@ -150,6 +150,7 @@ public object Clone() SkipGeneration = SkipGeneration, Operation = Operation, PatternsOverride = new(PatternsOverride ?? Enumerable.Empty(), StringComparer.OrdinalIgnoreCase), + PluginTypes = new(PluginTypes ?? Enumerable.Empty()), }; } private static readonly StringIEnumerableDeepComparer comparer = new(); @@ -187,5 +188,4 @@ public ApiDependency ToApiDependency(string configurationHash, Dictionary + /// Generates the API plugins from the OpenAPI document + /// + /// The cancellation token + /// Whether the generated plugin was updated or not + public async Task GeneratePluginAsync(CancellationToken cancellationToken) + { + return await GenerateConsumerAsync(async (sw, stepId, openApiTree, CancellationToken) => + { + await Task.Delay(1, cancellationToken).ConfigureAwait(false); + logger.LogCritical("Plugins generation is not implemented yet"); + //TODO implement generation logic + return stepId; + }, cancellationToken).ConfigureAwait(false); + } /// /// Generates the code from the OpenAPI document @@ -228,6 +243,27 @@ private void UpdateConfigurationFromOpenApiDocument() /// The cancellation token /// Whether the generated code was updated or not public async Task GenerateClientAsync(CancellationToken cancellationToken) + { + return await GenerateConsumerAsync(async (sw, stepId, openApiTree, CancellationToken) => + { + // Create Source Model + sw.Start(); + var generatedCode = CreateSourceModel(openApiTree); + StopLogAndReset(sw, $"step {++stepId} - create source model - took"); + + // RefineByLanguage + sw.Start(); + await ApplyLanguageRefinement(config, generatedCode, cancellationToken).ConfigureAwait(false); + StopLogAndReset(sw, $"step {++stepId} - refine by language - took"); + + // Write language source + sw.Start(); + await CreateLanguageSourceFilesAsync(config.Language, generatedCode, cancellationToken).ConfigureAwait(false); + StopLogAndReset(sw, $"step {++stepId} - writing files - took"); + return stepId; + }, cancellationToken).ConfigureAwait(false); + } + private async Task GenerateConsumerAsync(Func> innerGenerationSteps, CancellationToken cancellationToken) { var sw = new Stopwatch(); // Read input stream @@ -252,20 +288,7 @@ public async Task GenerateClientAsync(CancellationToken cancellationToken) if (shouldGenerate) { - // Create Source Model - sw.Start(); - var generatedCode = CreateSourceModel(openApiTree); - StopLogAndReset(sw, $"step {++stepId} - create source model - took"); - - // RefineByLanguage - sw.Start(); - await ApplyLanguageRefinement(config, generatedCode, cancellationToken).ConfigureAwait(false); - StopLogAndReset(sw, $"step {++stepId} - refine by language - took"); - - // Write language source - sw.Start(); - await CreateLanguageSourceFilesAsync(config.Language, generatedCode, cancellationToken).ConfigureAwait(false); - StopLogAndReset(sw, $"step {++stepId} - writing files - took"); + stepId = await innerGenerationSteps(sw, stepId, openApiTree, cancellationToken).ConfigureAwait(false); await FinalizeWorkspaceAsync(sw, stepId, openApiTree, inputPath, cancellationToken).ConfigureAwait(false); } diff --git a/src/Kiota.Builder/WorkspaceManagement/ApiClientConfiguration.cs b/src/Kiota.Builder/WorkspaceManagement/ApiClientConfiguration.cs index ff0c9ca923..a90e128ff8 100644 --- a/src/Kiota.Builder/WorkspaceManagement/ApiClientConfiguration.cs +++ b/src/Kiota.Builder/WorkspaceManagement/ApiClientConfiguration.cs @@ -88,20 +88,8 @@ public void UpdateGenerationConfigurationFromApiClientConfiguration(GenerationCo config.ExcludeBackwardCompatible = ExcludeBackwardCompatible; config.IncludeAdditionalData = IncludeAdditionalData; config.StructuredMimeTypes = new(StructuredMimeTypes); - config.IncludePatterns = IncludePatterns; - config.ExcludePatterns = ExcludePatterns; - config.OpenAPIFilePath = DescriptionLocation; config.DisabledValidationRules = DisabledValidationRules; - config.OutputPath = OutputPath; - config.ClientClassName = clientName; - config.Serializers.Clear(); - config.Deserializers.Clear(); - if (requests is { Count: > 0 }) - { - config.PatternsOverride = requests.Where(static x => !x.Exclude && !string.IsNullOrEmpty(x.Method) && !string.IsNullOrEmpty(x.UriTemplate)) - .Select(static x => $"/{x.UriTemplate}#{x.Method!.ToUpperInvariant()}") - .ToHashSet(); - } + UpdateGenerationConfigurationFromBase(config, clientName, requests); } public object Clone() diff --git a/src/Kiota.Builder/WorkspaceManagement/ApiPluginConfiguration.cs b/src/Kiota.Builder/WorkspaceManagement/ApiPluginConfiguration.cs index 77478a03e2..cdc02424ce 100644 --- a/src/Kiota.Builder/WorkspaceManagement/ApiPluginConfiguration.cs +++ b/src/Kiota.Builder/WorkspaceManagement/ApiPluginConfiguration.cs @@ -1,5 +1,8 @@ using System; using System.Collections.Generic; +using System.Linq; +using Kiota.Builder.Configuration; +using Microsoft.OpenApi.ApiManifest; namespace Kiota.Builder.WorkspaceManagement; @@ -23,5 +26,18 @@ public object Clone() CloneBase(result); return result; } + /// + /// Updates the passed configuration with the values from the config file. + /// + /// Generation configuration to update. + /// Plugin name. + /// The requests to use when updating an existing client. + public void UpdateGenerationConfigurationFromApiPluginConfiguration(GenerationConfiguration config, string pluginName, IList? requests = default) + { + ArgumentNullException.ThrowIfNull(config); + ArgumentException.ThrowIfNullOrEmpty(pluginName); + config.PluginTypes = Types.Select(x => Enum.TryParse(x, out var result) ? result : (PluginType?)null).OfType().ToHashSet(); + UpdateGenerationConfigurationFromBase(config, pluginName, requests); + } } #pragma warning restore CA2227 // Collection properties should be read only diff --git a/src/Kiota.Builder/WorkspaceManagement/BaseApiConsumerConfiguration.cs b/src/Kiota.Builder/WorkspaceManagement/BaseApiConsumerConfiguration.cs index 076ac772e2..690ca000eb 100644 --- a/src/Kiota.Builder/WorkspaceManagement/BaseApiConsumerConfiguration.cs +++ b/src/Kiota.Builder/WorkspaceManagement/BaseApiConsumerConfiguration.cs @@ -1,6 +1,8 @@ using System; using System.Collections.Generic; +using System.Linq; using Kiota.Builder.Configuration; +using Microsoft.OpenApi.ApiManifest; namespace Kiota.Builder.WorkspaceManagement; @@ -44,5 +46,23 @@ protected void CloneBase(BaseApiConsumerConfiguration target) target.IncludePatterns = new HashSet(IncludePatterns, StringComparer.OrdinalIgnoreCase); target.ExcludePatterns = new HashSet(ExcludePatterns, StringComparer.OrdinalIgnoreCase); } + protected void UpdateGenerationConfigurationFromBase(GenerationConfiguration config, string clientName, IList? requests) + { + ArgumentNullException.ThrowIfNull(config); + ArgumentException.ThrowIfNullOrEmpty(clientName); + config.IncludePatterns = IncludePatterns; + config.ExcludePatterns = ExcludePatterns; + config.OpenAPIFilePath = DescriptionLocation; + config.OutputPath = OutputPath; + config.ClientClassName = clientName; + config.Serializers.Clear(); + config.Deserializers.Clear(); + if (requests is { Count: > 0 }) + { + config.PatternsOverride = requests.Where(static x => !x.Exclude && !string.IsNullOrEmpty(x.Method) && !string.IsNullOrEmpty(x.UriTemplate)) + .Select(static x => $"/{x.UriTemplate}#{x.Method!.ToUpperInvariant()}") + .ToHashSet(); + } + } } #pragma warning restore CA2227 // Collection properties should be read only diff --git a/src/kiota/Handlers/Plugin/GenerateHandler.cs b/src/kiota/Handlers/Plugin/GenerateHandler.cs index b22f5d6f30..7edb4ce9a7 100644 --- a/src/kiota/Handlers/Plugin/GenerateHandler.cs +++ b/src/kiota/Handlers/Plugin/GenerateHandler.cs @@ -52,15 +52,13 @@ public override async Task InvokeAsync(InvocationContext context) { var generationConfiguration = new GenerationConfiguration(); var requests = !refresh && manifest is not null && manifest.ApiDependencies.TryGetValue(clientEntry.Key, out var value) ? value.Requests : []; - // clientEntry.Value.UpdateGenerationConfigurationFromApiClientConfiguration(generationConfiguration, clientEntry.Key, requests); - //TODO update to load configuration from plugin + clientEntry.Value.UpdateGenerationConfigurationFromApiPluginConfiguration(generationConfiguration, clientEntry.Key, requests); DefaultSerializersAndDeserializers(generationConfiguration); generationConfiguration.ClearCache = refresh; generationConfiguration.CleanOutput = refresh; generationConfiguration.Operation = ClientOperation.Generate; var builder = new KiotaBuilder(logger, generationConfiguration, httpClient, true); - var result = await builder.GenerateClientAsync(cancellationToken).ConfigureAwait(false); - //TODO update to generate plugin instead + var result = await builder.GeneratePluginAsync(cancellationToken).ConfigureAwait(false); if (result) { DisplaySuccess($"Update of {clientEntry.Key} client completed"); From 9aa74826ab1e612e37497a6f97d5ed5ee6d45a63 Mon Sep 17 00:00:00 2001 From: Vincent Biret Date: Mon, 25 Mar 2024 15:00:31 -0400 Subject: [PATCH 05/40] - adds launch configuration for plugin add --- .vscode/launch.json | 25 +++++++++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/.vscode/launch.json b/.vscode/launch.json index 06fa4a05f2..34885c1e13 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -314,6 +314,31 @@ "KIOTA_CONFIG_PREVIEW": "true" } }, + { + "name": "Launch Plugin Add", + "type": "coreclr", + "request": "launch", + "preLaunchTask": "build", + "program": "${workspaceFolder}/src/kiota/bin/Debug/net8.0/kiota.dll", + "args": [ + "plugin", + "add", + "--plugin-name", + "MicrosoftGraph", + "-d", + "https://raw.githubusercontent.com/microsoftgraph/msgraph-sdk-powershell/dev/openApiDocs/v1.0/Mail.yml", + "-i", + "**/messages", + "--type", + "APIManifest" + ], + "cwd": "${workspaceFolder}/samples/msgraph-mail/dotnet", + "console": "internalConsole", + "stopAtEntry": false, + "env": { + "KIOTA_CONFIG_PREVIEW": "true" + } + }, { "name": "Launch Login (github - device)", "type": "coreclr", From 3300d1528050705ecfa1dfa71f3243ade3c9a81e Mon Sep 17 00:00:00 2001 From: Vincent Biret Date: Mon, 25 Mar 2024 15:25:19 -0400 Subject: [PATCH 06/40] - implements plugin add command handler body Signed-off-by: Vincent Biret --- ...lientOperation.cs => ConsumerOperation.cs} | 2 +- .../Configuration/GenerationConfiguration.cs | 3 +- src/Kiota.Builder/KiotaBuilder.cs | 4 +- .../OpenApiDocumentDownloadService.cs | 2 +- .../ApiClientConfiguration.cs | 5 - .../ApiPluginConfiguration.cs | 9 + .../BaseApiConsumerConfiguration.cs | 7 +- .../WorkspaceConfiguration.cs | 3 + .../WorkspaceManagementService.cs | 40 ++- src/kiota/Handlers/Client/AddHandler.cs | 2 +- src/kiota/Handlers/Client/EditHandler.cs | 2 +- src/kiota/Handlers/Client/GenerateHandler.cs | 2 +- src/kiota/Handlers/Plugin/AddHandler.cs | 31 +- src/kiota/Handlers/Plugin/GenerateHandler.cs | 2 +- .../WorkspaceManagementServiceTests.cs | 326 +++++++++--------- 15 files changed, 247 insertions(+), 193 deletions(-) rename src/Kiota.Builder/Configuration/{ClientOperation.cs => ConsumerOperation.cs} (75%) diff --git a/src/Kiota.Builder/Configuration/ClientOperation.cs b/src/Kiota.Builder/Configuration/ConsumerOperation.cs similarity index 75% rename from src/Kiota.Builder/Configuration/ClientOperation.cs rename to src/Kiota.Builder/Configuration/ConsumerOperation.cs index 3d7770a738..0973975f29 100644 --- a/src/Kiota.Builder/Configuration/ClientOperation.cs +++ b/src/Kiota.Builder/Configuration/ConsumerOperation.cs @@ -1,5 +1,5 @@ namespace Kiota.Builder.Configuration; -public enum ClientOperation +public enum ConsumerOperation { Add, Edit, diff --git a/src/Kiota.Builder/Configuration/GenerationConfiguration.cs b/src/Kiota.Builder/Configuration/GenerationConfiguration.cs index dc31b67f68..4a22c24b8a 100644 --- a/src/Kiota.Builder/Configuration/GenerationConfiguration.cs +++ b/src/Kiota.Builder/Configuration/GenerationConfiguration.cs @@ -29,7 +29,7 @@ public bool SkipGeneration { get; set; } - public ClientOperation? Operation + public ConsumerOperation? Operation { get; set; } @@ -186,6 +186,7 @@ public ApiDependency ToApiDependency(string configurationHash, Dictionary PluginTypes.Count != 0; } #pragma warning restore CA1056 #pragma warning restore CA2227 diff --git a/src/Kiota.Builder/KiotaBuilder.cs b/src/Kiota.Builder/KiotaBuilder.cs index e67b461c83..d9060ec596 100644 --- a/src/Kiota.Builder/KiotaBuilder.cs +++ b/src/Kiota.Builder/KiotaBuilder.cs @@ -269,7 +269,7 @@ private async Task GenerateConsumerAsync(Func GenerateConsumerAsync(Func + /// Initializes a new instance of the class from an existing . + /// + /// The configuration to use to initialize the client configuration + public ApiPluginConfiguration(GenerationConfiguration config) : base(config) + { + ArgumentNullException.ThrowIfNull(config); + Types = config.PluginTypes.Select(x => x.ToString()).ToHashSet(); } public HashSet Types { get; set; } = new(StringComparer.OrdinalIgnoreCase); public object Clone() diff --git a/src/Kiota.Builder/WorkspaceManagement/BaseApiConsumerConfiguration.cs b/src/Kiota.Builder/WorkspaceManagement/BaseApiConsumerConfiguration.cs index 690ca000eb..b619b95f6a 100644 --- a/src/Kiota.Builder/WorkspaceManagement/BaseApiConsumerConfiguration.cs +++ b/src/Kiota.Builder/WorkspaceManagement/BaseApiConsumerConfiguration.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.IO; using System.Linq; using Kiota.Builder.Configuration; using Microsoft.OpenApi.ApiManifest; @@ -37,7 +38,11 @@ internal BaseApiConsumerConfiguration(GenerationConfiguration config) /// The output path for the generated code, related to the configuration file. /// public string OutputPath { get; set; } = string.Empty; - + public void NormalizePaths(string targetDirectory) + { + if (Path.IsPathRooted(OutputPath)) + OutputPath = "./" + Path.GetRelativePath(targetDirectory, OutputPath); + } protected void CloneBase(BaseApiConsumerConfiguration target) { ArgumentNullException.ThrowIfNull(target); diff --git a/src/Kiota.Builder/WorkspaceManagement/WorkspaceConfiguration.cs b/src/Kiota.Builder/WorkspaceManagement/WorkspaceConfiguration.cs index 57840f7f0f..00bd947474 100644 --- a/src/Kiota.Builder/WorkspaceManagement/WorkspaceConfiguration.cs +++ b/src/Kiota.Builder/WorkspaceManagement/WorkspaceConfiguration.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Generic; using System.Linq; +using System.Text.Json.Serialization; namespace Kiota.Builder.WorkspaceManagement; @@ -17,6 +18,7 @@ public class WorkspaceConfiguration : ICloneable public Dictionary Clients { get; set; } = new Dictionary(StringComparer.OrdinalIgnoreCase); public Dictionary Plugins { get; set; } = new Dictionary(StringComparer.OrdinalIgnoreCase); #pragma warning restore CA2227 // Collection properties should be read only + [JsonIgnore] public bool AreConsumersKeysUnique { get @@ -24,6 +26,7 @@ public bool AreConsumersKeysUnique return Clients.Keys.Concat(Plugins.Keys).GroupBy(static x => x, StringComparer.OrdinalIgnoreCase).All(static x => x.Count() == 1); } } + [JsonIgnore] public bool AnyConsumerPresent => Clients.Count != 0 || Plugins.Count != 0; public object Clone() { diff --git a/src/Kiota.Builder/WorkspaceManagement/WorkspaceManagementService.cs b/src/Kiota.Builder/WorkspaceManagement/WorkspaceManagementService.cs index 0acdf9ca4f..d4ee1f3256 100644 --- a/src/Kiota.Builder/WorkspaceManagement/WorkspaceManagementService.cs +++ b/src/Kiota.Builder/WorkspaceManagement/WorkspaceManagementService.cs @@ -9,7 +9,6 @@ using System.Text.Json; using System.Threading; using System.Threading.Tasks; -using AsyncKeyedLock; using Kiota.Builder.Configuration; using Kiota.Builder.Extensions; using Kiota.Builder.Lock; @@ -41,11 +40,28 @@ public WorkspaceManagementService(ILogger logger, HttpClient httpClient, bool us private readonly LockManagementService lockManagementService = new(); private readonly WorkspaceConfigurationStorageService workspaceConfigurationStorageService; private readonly DescriptionStorageService descriptionStorageService; - public async Task IsClientPresent(string clientName, CancellationToken cancellationToken = default) + public async Task IsConsumerPresent(string clientName, CancellationToken cancellationToken = default) { if (!UseKiotaConfig) return false; var (wsConfig, _) = await workspaceConfigurationStorageService.GetWorkspaceConfigurationAsync(cancellationToken).ConfigureAwait(false); - return wsConfig?.Clients.ContainsKey(clientName) ?? false; + return wsConfig is not null && (wsConfig.Clients.ContainsKey(clientName) || wsConfig.Plugins.ContainsKey(clientName)); + } + private BaseApiConsumerConfiguration UpdateConsumerConfiguration(GenerationConfiguration generationConfiguration, WorkspaceConfiguration wsConfig) + { + if (generationConfiguration.IsPluginConfiguration) + { + var generationPluginConfig = new ApiPluginConfiguration(generationConfiguration); + generationPluginConfig.NormalizePaths(WorkingDirectory); + wsConfig.Plugins.AddOrReplace(generationConfiguration.ClientClassName, generationPluginConfig); + return generationPluginConfig; + } + else + { + var generationClientConfig = new ApiClientConfiguration(generationConfiguration); + generationClientConfig.NormalizePaths(WorkingDirectory); + wsConfig.Clients.AddOrReplace(generationConfiguration.ClientClassName, generationClientConfig); + return generationClientConfig; + } } public async Task UpdateStateFromConfigurationAsync(GenerationConfiguration generationConfiguration, string descriptionHash, Dictionary> templatesWithOperations, Stream descriptionStream, CancellationToken cancellationToken = default) { @@ -53,10 +69,8 @@ public async Task UpdateStateFromConfigurationAsync(GenerationConfiguration gene if (UseKiotaConfig) { var (wsConfig, manifest) = await LoadConfigurationAndManifestAsync(cancellationToken).ConfigureAwait(false); - var generationClientConfig = new ApiClientConfiguration(generationConfiguration); - generationClientConfig.NormalizePaths(WorkingDirectory); - wsConfig.Clients.AddOrReplace(generationConfiguration.ClientClassName, generationClientConfig); - var inputConfigurationHash = await GetConfigurationHashAsync(generationClientConfig, descriptionHash).ConfigureAwait(false); + var generationClientConfig = UpdateConsumerConfiguration(generationConfiguration, wsConfig); + var inputConfigurationHash = await GetConsumerConfigurationHashAsync(generationClientConfig, descriptionHash).ConfigureAwait(false); manifest.ApiDependencies.AddOrReplace(generationConfiguration.ClientClassName, generationConfiguration.ToApiDependency(inputConfigurationHash, templatesWithOperations)); await workspaceConfigurationStorageService.UpdateWorkspaceConfigurationAsync(wsConfig, manifest, cancellationToken).ConfigureAwait(false); if (descriptionStream != Stream.Null) @@ -100,7 +114,7 @@ public async Task ShouldGenerateAsync(GenerationConfiguration inputConfig, { var inputClientConfig = new ApiClientConfiguration(inputConfig); inputClientConfig.NormalizePaths(WorkingDirectory); - var inputConfigurationHash = await GetConfigurationHashAsync(inputClientConfig, descriptionHash).ConfigureAwait(false); + var inputConfigurationHash = await GetConsumerConfigurationHashAsync(inputClientConfig, descriptionHash).ConfigureAwait(false); return !clientConfigurationComparer.Equals(existingClientConfig, inputClientConfig) || !apiDependencyComparer.Equals(inputConfig.ToApiDependency(inputConfigurationHash, []), existingApiManifest); } @@ -177,11 +191,13 @@ private async Task RemoveConsumerInternalAsync(string clientName, Action HashAlgorithm = new(SHA256.Create); private readonly string WorkingDirectory; - - private async Task GetConfigurationHashAsync(ApiClientConfiguration apiClientConfiguration, string descriptionHash) + private async Task GetConsumerConfigurationHashAsync(T apiClientConfiguration, string descriptionHash) where T : BaseApiConsumerConfiguration { using var stream = new MemoryStream(); - await JsonSerializer.SerializeAsync(stream, apiClientConfiguration, context.ApiClientConfiguration).ConfigureAwait(false); + if (apiClientConfiguration is ApiClientConfiguration) + await JsonSerializer.SerializeAsync(stream, apiClientConfiguration, context.ApiClientConfiguration).ConfigureAwait(false); + else + await JsonSerializer.SerializeAsync(stream, apiClientConfiguration, context.ApiPluginConfiguration).ConfigureAwait(false); await stream.WriteAsync(Encoding.UTF8.GetBytes(descriptionHash)).ConfigureAwait(false); stream.Position = 0; if (HashAlgorithm.Value is null) @@ -269,7 +285,7 @@ public async Task> MigrateFromLockFileAsync(string clientNam var clientConfiguration = new ApiClientConfiguration(generationConfiguration); clientConfiguration.NormalizePaths(WorkingDirectory); wsConfig.Clients.Add(generationConfiguration.ClientClassName, clientConfiguration); - var inputConfigurationHash = await GetConfigurationHashAsync(clientConfiguration, "migrated-pending-generate").ConfigureAwait(false); + var inputConfigurationHash = await GetConsumerConfigurationHashAsync(clientConfiguration, "migrated-pending-generate").ConfigureAwait(false); // because it's a migration, we don't want to calculate the exact hash since the description might have changed since the initial generation that created the lock file apiManifest.ApiDependencies.Add(generationConfiguration.ClientClassName, generationConfiguration.ToApiDependency(inputConfigurationHash, [])); lockManagementService.DeleteLockFile(Path.Combine(WorkingDirectory, clientConfiguration.OutputPath)); diff --git a/src/kiota/Handlers/Client/AddHandler.cs b/src/kiota/Handlers/Client/AddHandler.cs index 9808f3835a..e4d2237f89 100644 --- a/src/kiota/Handlers/Client/AddHandler.cs +++ b/src/kiota/Handlers/Client/AddHandler.cs @@ -91,7 +91,7 @@ public override async Task InvokeAsync(InvocationContext context) Configuration.Generation.Language = language; WarnUsingPreviewLanguage(language); Configuration.Generation.SkipGeneration = skipGeneration; - Configuration.Generation.Operation = ClientOperation.Add; + Configuration.Generation.Operation = ConsumerOperation.Add; if (includePatterns.Count != 0) Configuration.Generation.IncludePatterns = includePatterns.Select(static x => x.TrimQuotes()).ToHashSet(StringComparer.OrdinalIgnoreCase); if (excludePatterns.Count != 0) diff --git a/src/kiota/Handlers/Client/EditHandler.cs b/src/kiota/Handlers/Client/EditHandler.cs index 09bd603f2e..6b65b92759 100644 --- a/src/kiota/Handlers/Client/EditHandler.cs +++ b/src/kiota/Handlers/Client/EditHandler.cs @@ -83,7 +83,7 @@ public override async Task InvokeAsync(InvocationContext context) CancellationToken cancellationToken = context.BindingContext.GetService(typeof(CancellationToken)) is CancellationToken token ? token : CancellationToken.None; Configuration.Generation.SkipGeneration = skipGeneration; - Configuration.Generation.Operation = ClientOperation.Edit; + Configuration.Generation.Operation = ConsumerOperation.Edit; var (loggerFactory, logger) = GetLoggerAndFactory(context, Configuration.Generation.OutputPath); using (loggerFactory) diff --git a/src/kiota/Handlers/Client/GenerateHandler.cs b/src/kiota/Handlers/Client/GenerateHandler.cs index f7b997109a..27e90907a3 100644 --- a/src/kiota/Handlers/Client/GenerateHandler.cs +++ b/src/kiota/Handlers/Client/GenerateHandler.cs @@ -56,7 +56,7 @@ public override async Task InvokeAsync(InvocationContext context) DefaultSerializersAndDeserializers(generationConfiguration); generationConfiguration.ClearCache = refresh; generationConfiguration.CleanOutput = refresh; - generationConfiguration.Operation = ClientOperation.Generate; + generationConfiguration.Operation = ConsumerOperation.Generate; var builder = new KiotaBuilder(logger, generationConfiguration, httpClient, true); var result = await builder.GenerateClientAsync(cancellationToken).ConfigureAwait(false); if (result) diff --git a/src/kiota/Handlers/Plugin/AddHandler.cs b/src/kiota/Handlers/Plugin/AddHandler.cs index ab5d8289ac..2f150fcd82 100644 --- a/src/kiota/Handlers/Plugin/AddHandler.cs +++ b/src/kiota/Handlers/Plugin/AddHandler.cs @@ -53,9 +53,9 @@ public override async Task InvokeAsync(InvocationContext context) AssignIfNotNullOrEmpty(openapi, (c, s) => c.OpenAPIFilePath = s); AssignIfNotNullOrEmpty(className, (c, s) => c.ClientClassName = s); Configuration.Generation.SkipGeneration = skipGeneration; - Configuration.Generation.Operation = ClientOperation.Add; - //TODO do something with the plugin type - // if (pluginTypes.Count != 0) + Configuration.Generation.Operation = ConsumerOperation.Add; + if (pluginTypes.Count != 0) + Configuration.Generation.PluginTypes = pluginTypes.ToHashSet(); if (includePatterns.Count != 0) Configuration.Generation.IncludePatterns = includePatterns.Select(static x => x.TrimQuotes()).ToHashSet(StringComparer.OrdinalIgnoreCase); if (excludePatterns.Count != 0) @@ -68,6 +68,31 @@ public override async Task InvokeAsync(InvocationContext context) await CheckForNewVersionAsync(logger, cancellationToken).ConfigureAwait(false); logger.AppendInternalTracing(); logger.LogTrace("configuration: {configuration}", JsonSerializer.Serialize(Configuration, KiotaConfigurationJsonContext.Default.KiotaConfiguration)); + try + { + var builder = new KiotaBuilder(logger, Configuration.Generation, httpClient, true); + var result = await builder.GeneratePluginAsync(cancellationToken).ConfigureAwait(false); + if (result) + DisplaySuccess("Generation completed successfully"); + else if (skipGeneration) + { + DisplaySuccess("Generation skipped as --skip-generation was passed"); + DisplayGenerateCommandHint(); + } // else we get an error because we're adding a client that already exists + var manifestPath = $"{GetAbsolutePath(Path.Combine(WorkspaceConfigurationStorageService.KiotaDirectorySegment, WorkspaceConfigurationStorageService.ManifestFileName))}#{Configuration.Generation.ClientClassName}"; + DisplayGenerateAdvancedHint(includePatterns, excludePatterns, string.Empty, manifestPath, "plugin add"); + return 0; + } + catch (Exception ex) + { +#if DEBUG + logger.LogCritical(ex, "error adding the client: {exceptionMessage}", ex.Message); + throw; // so debug tools go straight to the source of the exception when attached +#else + logger.LogCritical("error adding the client: {exceptionMessage}", ex.Message); + return 1; +#endif + } } throw new NotImplementedException(); } diff --git a/src/kiota/Handlers/Plugin/GenerateHandler.cs b/src/kiota/Handlers/Plugin/GenerateHandler.cs index 7edb4ce9a7..db85404d61 100644 --- a/src/kiota/Handlers/Plugin/GenerateHandler.cs +++ b/src/kiota/Handlers/Plugin/GenerateHandler.cs @@ -56,7 +56,7 @@ public override async Task InvokeAsync(InvocationContext context) DefaultSerializersAndDeserializers(generationConfiguration); generationConfiguration.ClearCache = refresh; generationConfiguration.CleanOutput = refresh; - generationConfiguration.Operation = ClientOperation.Generate; + generationConfiguration.Operation = ConsumerOperation.Generate; var builder = new KiotaBuilder(logger, generationConfiguration, httpClient, true); var result = await builder.GeneratePluginAsync(cancellationToken).ConfigureAwait(false); if (result) diff --git a/tests/Kiota.Builder.Tests/WorkspaceManagement/WorkspaceManagementServiceTests.cs b/tests/Kiota.Builder.Tests/WorkspaceManagement/WorkspaceManagementServiceTests.cs index f862bd24a7..965adb0b7d 100644 --- a/tests/Kiota.Builder.Tests/WorkspaceManagement/WorkspaceManagementServiceTests.cs +++ b/tests/Kiota.Builder.Tests/WorkspaceManagement/WorkspaceManagementServiceTests.cs @@ -15,136 +15,136 @@ namespace Kiota.Builder.Tests.WorkspaceManagement; public sealed class WorkspaceManagementServiceTests : IDisposable { - private readonly string tempPath = Path.Combine(Path.GetTempPath(), Path.GetRandomFileName()); - private readonly HttpClient httpClient = new(); - [Fact] - public void Defensive() + private readonly string tempPath = Path.Combine(Path.GetTempPath(), Path.GetRandomFileName()); + private readonly HttpClient httpClient = new(); + [Fact] + public void Defensive() + { + Assert.Throws(() => new WorkspaceManagementService(null, httpClient)); + Assert.Throws(() => new WorkspaceManagementService(Mock.Of(), null)); + } + [InlineData(true)] + [InlineData(false)] + [Theory] + public async Task IsClientPresentReturnsFalseOnNoClient(bool usesConfig) + { + var mockLogger = Mock.Of(); + Directory.CreateDirectory(tempPath); + var service = new WorkspaceManagementService(mockLogger, httpClient, usesConfig, tempPath); + var result = await service.IsConsumerPresent("clientName"); + Assert.False(result); + } + [InlineData(true, true)] + [InlineData(true, false)] + [InlineData(false, true)] + [InlineData(false, false)] + [Theory] + public async Task ShouldGenerateReturnsTrue(bool usesConfig, bool cleanOutput) + { + var mockLogger = Mock.Of(); + Directory.CreateDirectory(tempPath); + var service = new WorkspaceManagementService(mockLogger, httpClient, usesConfig, tempPath); + var configuration = new GenerationConfiguration { - Assert.Throws(() => new WorkspaceManagementService(null, httpClient)); - Assert.Throws(() => new WorkspaceManagementService(Mock.Of(), null)); - } - [InlineData(true)] - [InlineData(false)] - [Theory] - public async Task IsClientPresentReturnsFalseOnNoClient(bool usesConfig) + ClientClassName = "clientName", + OutputPath = tempPath, + OpenAPIFilePath = Path.Combine(tempPath, "openapi.yaml"), + CleanOutput = cleanOutput, + }; + var result = await service.ShouldGenerateAsync(configuration, "foo"); + Assert.True(result); + } + [InlineData(true)] + [InlineData(false)] + [Theory] + public async Task ShouldGenerateReturnsFalse(bool usesConfig) + { + var mockLogger = Mock.Of(); + Directory.CreateDirectory(tempPath); + var service = new WorkspaceManagementService(mockLogger, httpClient, usesConfig, tempPath); + var configuration = new GenerationConfiguration { - var mockLogger = Mock.Of(); - Directory.CreateDirectory(tempPath); - var service = new WorkspaceManagementService(mockLogger, httpClient, usesConfig, tempPath); - var result = await service.IsClientPresent("clientName"); - Assert.False(result); - } - [InlineData(true, true)] - [InlineData(true, false)] - [InlineData(false, true)] - [InlineData(false, false)] - [Theory] - public async Task ShouldGenerateReturnsTrue(bool usesConfig, bool cleanOutput) + ClientClassName = "clientName", + OutputPath = tempPath, + OpenAPIFilePath = Path.Combine(tempPath, "openapi.yaml"), + ApiRootUrl = "https://graph.microsoft.com", + }; + Directory.CreateDirectory(tempPath); + await service.UpdateStateFromConfigurationAsync(configuration, "foo", [], Stream.Null); + var result = await service.ShouldGenerateAsync(configuration, "foo"); + Assert.False(result); + } + [Fact] + public async Task RemovesAClient() + { + var mockLogger = Mock.Of(); + Directory.CreateDirectory(tempPath); + var service = new WorkspaceManagementService(mockLogger, httpClient, true, tempPath); + var configuration = new GenerationConfiguration { - var mockLogger = Mock.Of(); - Directory.CreateDirectory(tempPath); - var service = new WorkspaceManagementService(mockLogger, httpClient, usesConfig, tempPath); - var configuration = new GenerationConfiguration - { - ClientClassName = "clientName", - OutputPath = tempPath, - OpenAPIFilePath = Path.Combine(tempPath, "openapi.yaml"), - CleanOutput = cleanOutput, - }; - var result = await service.ShouldGenerateAsync(configuration, "foo"); - Assert.True(result); - } - [InlineData(true)] - [InlineData(false)] - [Theory] - public async Task ShouldGenerateReturnsFalse(bool usesConfig) + ClientClassName = "clientName", + OutputPath = tempPath, + OpenAPIFilePath = Path.Combine(tempPath, "openapi.yaml"), + ApiRootUrl = "https://graph.microsoft.com", + }; + Directory.CreateDirectory(tempPath); + await service.UpdateStateFromConfigurationAsync(configuration, "foo", [], Stream.Null); + await service.RemoveClientAsync("clientName"); + var result = await service.IsConsumerPresent("clientName"); + Assert.False(result); + } + [Fact] + public async Task FailsOnMigrateWithoutKiotaConfigMode() + { + var mockLogger = Mock.Of(); + Directory.CreateDirectory(tempPath); + var service = new WorkspaceManagementService(mockLogger, httpClient, false, tempPath); + await Assert.ThrowsAsync(() => service.MigrateFromLockFileAsync(string.Empty, tempPath)); + } + [Fact] + public async Task FailsWhenTargetLockDirectoryIsNotSubDirectory() + { + var mockLogger = Mock.Of(); + Directory.CreateDirectory(tempPath); + var service = new WorkspaceManagementService(mockLogger, httpClient, true, tempPath); + await Assert.ThrowsAsync(() => service.MigrateFromLockFileAsync(string.Empty, Path.Combine(Path.GetTempPath(), Path.GetRandomFileName()))); + } + [Fact] + public async Task FailsWhenNoLockFilesAreFound() + { + var mockLogger = Mock.Of(); + Directory.CreateDirectory(tempPath); + var service = new WorkspaceManagementService(mockLogger, httpClient, true, tempPath); + await Assert.ThrowsAsync(() => service.MigrateFromLockFileAsync(string.Empty, tempPath)); + } + [Fact] + public async Task FailsOnMultipleLockFilesAndClientName() + { + var mockLogger = Mock.Of(); + Directory.CreateDirectory(tempPath); + var service = new WorkspaceManagementService(mockLogger, httpClient, true, tempPath); + Directory.CreateDirectory(Path.Combine(tempPath, "client1")); + Directory.CreateDirectory(Path.Combine(tempPath, "client2")); + File.WriteAllText(Path.Combine(tempPath, "client1", LockManagementService.LockFileName), "foo"); + File.WriteAllText(Path.Combine(tempPath, "client2", LockManagementService.LockFileName), "foo"); + await Assert.ThrowsAsync(() => service.MigrateFromLockFileAsync("bar", tempPath)); + } + [Fact] + public async Task MigratesAClient() + { + var mockLogger = Mock.Of(); + Directory.CreateDirectory(tempPath); + var service = new WorkspaceManagementService(mockLogger, httpClient, true, tempPath); + var descriptionPath = Path.Combine(tempPath, "description.yml"); + var generationConfiguration = new GenerationConfiguration { - var mockLogger = Mock.Of(); - Directory.CreateDirectory(tempPath); - var service = new WorkspaceManagementService(mockLogger, httpClient, usesConfig, tempPath); - var configuration = new GenerationConfiguration - { - ClientClassName = "clientName", - OutputPath = tempPath, - OpenAPIFilePath = Path.Combine(tempPath, "openapi.yaml"), - ApiRootUrl = "https://graph.microsoft.com", - }; - Directory.CreateDirectory(tempPath); - await service.UpdateStateFromConfigurationAsync(configuration, "foo", [], Stream.Null); - var result = await service.ShouldGenerateAsync(configuration, "foo"); - Assert.False(result); - } - [Fact] - public async Task RemovesAClient() - { - var mockLogger = Mock.Of(); - Directory.CreateDirectory(tempPath); - var service = new WorkspaceManagementService(mockLogger, httpClient, true, tempPath); - var configuration = new GenerationConfiguration - { - ClientClassName = "clientName", - OutputPath = tempPath, - OpenAPIFilePath = Path.Combine(tempPath, "openapi.yaml"), - ApiRootUrl = "https://graph.microsoft.com", - }; - Directory.CreateDirectory(tempPath); - await service.UpdateStateFromConfigurationAsync(configuration, "foo", [], Stream.Null); - await service.RemoveClientAsync("clientName"); - var result = await service.IsClientPresent("clientName"); - Assert.False(result); - } - [Fact] - public async Task FailsOnMigrateWithoutKiotaConfigMode() - { - var mockLogger = Mock.Of(); - Directory.CreateDirectory(tempPath); - var service = new WorkspaceManagementService(mockLogger, httpClient, false, tempPath); - await Assert.ThrowsAsync(() => service.MigrateFromLockFileAsync(string.Empty, tempPath)); - } - [Fact] - public async Task FailsWhenTargetLockDirectoryIsNotSubDirectory() - { - var mockLogger = Mock.Of(); - Directory.CreateDirectory(tempPath); - var service = new WorkspaceManagementService(mockLogger, httpClient, true, tempPath); - await Assert.ThrowsAsync(() => service.MigrateFromLockFileAsync(string.Empty, Path.Combine(Path.GetTempPath(), Path.GetRandomFileName()))); - } - [Fact] - public async Task FailsWhenNoLockFilesAreFound() - { - var mockLogger = Mock.Of(); - Directory.CreateDirectory(tempPath); - var service = new WorkspaceManagementService(mockLogger, httpClient, true, tempPath); - await Assert.ThrowsAsync(() => service.MigrateFromLockFileAsync(string.Empty, tempPath)); - } - [Fact] - public async Task FailsOnMultipleLockFilesAndClientName() - { - var mockLogger = Mock.Of(); - Directory.CreateDirectory(tempPath); - var service = new WorkspaceManagementService(mockLogger, httpClient, true, tempPath); - Directory.CreateDirectory(Path.Combine(tempPath, "client1")); - Directory.CreateDirectory(Path.Combine(tempPath, "client2")); - File.WriteAllText(Path.Combine(tempPath, "client1", LockManagementService.LockFileName), "foo"); - File.WriteAllText(Path.Combine(tempPath, "client2", LockManagementService.LockFileName), "foo"); - await Assert.ThrowsAsync(() => service.MigrateFromLockFileAsync("bar", tempPath)); - } - [Fact] - public async Task MigratesAClient() - { - var mockLogger = Mock.Of(); - Directory.CreateDirectory(tempPath); - var service = new WorkspaceManagementService(mockLogger, httpClient, true, tempPath); - var descriptionPath = Path.Combine(tempPath, "description.yml"); - var generationConfiguration = new GenerationConfiguration - { - ClientClassName = "clientName", - OutputPath = Path.Combine(tempPath, "client"), - OpenAPIFilePath = descriptionPath, - ApiRootUrl = "https://graph.microsoft.com", - }; - Directory.CreateDirectory(generationConfiguration.OutputPath); - await File.WriteAllTextAsync(descriptionPath, @$"openapi: 3.0.1 + ClientClassName = "clientName", + OutputPath = Path.Combine(tempPath, "client"), + OpenAPIFilePath = descriptionPath, + ApiRootUrl = "https://graph.microsoft.com", + }; + Directory.CreateDirectory(generationConfiguration.OutputPath); + await File.WriteAllTextAsync(descriptionPath, @$"openapi: 3.0.1 info: title: OData Service for namespace microsoft.graph description: This OData service is located at https://graph.microsoft.com/v1.0 @@ -166,31 +166,31 @@ await File.WriteAllTextAsync(descriptionPath, @$"openapi: 3.0.1 properties: foo: type: string"); - var classicService = new WorkspaceManagementService(mockLogger, httpClient, false, tempPath); - await classicService.UpdateStateFromConfigurationAsync(generationConfiguration, "foo", [], Stream.Null); - var clientNames = await service.MigrateFromLockFileAsync("clientName", tempPath); - Assert.Single(clientNames); - Assert.Equal("clientName", clientNames.First()); - Assert.False(File.Exists(Path.Combine(tempPath, LockManagementService.LockFileName))); - Assert.True(File.Exists(Path.Combine(tempPath, WorkspaceConfigurationStorageService.KiotaDirectorySegment, WorkspaceConfigurationStorageService.ConfigurationFileName))); - Assert.True(File.Exists(Path.Combine(tempPath, WorkspaceConfigurationStorageService.KiotaDirectorySegment, WorkspaceConfigurationStorageService.ManifestFileName))); - Assert.True(File.Exists(Path.Combine(tempPath, DescriptionStorageService.DescriptionsSubDirectoryRelativePath, "clientName", "description.yml"))); - } - [InlineData(true, true)] - [InlineData(true, false)] - [InlineData(false, true)] - [InlineData(false, false)] - [Theory] - public async Task GetsADescription(bool usesConfig, bool cleanOutput) - { - var mockLogger = Mock.Of(); - Directory.CreateDirectory(tempPath); - var service = new WorkspaceManagementService(mockLogger, httpClient, usesConfig, tempPath); - var descriptionPath = Path.Combine(tempPath, $"{DescriptionStorageService.DescriptionsSubDirectoryRelativePath}/clientName/description.yml"); - var outputPath = Path.Combine(tempPath, "client"); - Directory.CreateDirectory(outputPath); - Directory.CreateDirectory(Path.GetDirectoryName(descriptionPath)); - await File.WriteAllTextAsync(descriptionPath, @$"openapi: 3.0.1 + var classicService = new WorkspaceManagementService(mockLogger, httpClient, false, tempPath); + await classicService.UpdateStateFromConfigurationAsync(generationConfiguration, "foo", [], Stream.Null); + var clientNames = await service.MigrateFromLockFileAsync("clientName", tempPath); + Assert.Single(clientNames); + Assert.Equal("clientName", clientNames.First()); + Assert.False(File.Exists(Path.Combine(tempPath, LockManagementService.LockFileName))); + Assert.True(File.Exists(Path.Combine(tempPath, WorkspaceConfigurationStorageService.KiotaDirectorySegment, WorkspaceConfigurationStorageService.ConfigurationFileName))); + Assert.True(File.Exists(Path.Combine(tempPath, WorkspaceConfigurationStorageService.KiotaDirectorySegment, WorkspaceConfigurationStorageService.ManifestFileName))); + Assert.True(File.Exists(Path.Combine(tempPath, DescriptionStorageService.DescriptionsSubDirectoryRelativePath, "clientName", "description.yml"))); + } + [InlineData(true, true)] + [InlineData(true, false)] + [InlineData(false, true)] + [InlineData(false, false)] + [Theory] + public async Task GetsADescription(bool usesConfig, bool cleanOutput) + { + var mockLogger = Mock.Of(); + Directory.CreateDirectory(tempPath); + var service = new WorkspaceManagementService(mockLogger, httpClient, usesConfig, tempPath); + var descriptionPath = Path.Combine(tempPath, $"{DescriptionStorageService.DescriptionsSubDirectoryRelativePath}/clientName/description.yml"); + var outputPath = Path.Combine(tempPath, "client"); + Directory.CreateDirectory(outputPath); + Directory.CreateDirectory(Path.GetDirectoryName(descriptionPath)); + await File.WriteAllTextAsync(descriptionPath, @$"openapi: 3.0.1 info: title: OData Service for namespace microsoft.graph description: This OData service is located at https://graph.microsoft.com/v1.0 @@ -212,17 +212,17 @@ await File.WriteAllTextAsync(descriptionPath, @$"openapi: 3.0.1 properties: foo: type: string"); - var descriptionCopy = await service.GetDescriptionCopyAsync("clientName", descriptionPath, cleanOutput); - if (!usesConfig || cleanOutput) - Assert.Null(descriptionCopy); - else - Assert.NotNull(descriptionCopy); - } + var descriptionCopy = await service.GetDescriptionCopyAsync("clientName", descriptionPath, cleanOutput); + if (!usesConfig || cleanOutput) + Assert.Null(descriptionCopy); + else + Assert.NotNull(descriptionCopy); + } - public void Dispose() - { - if (Directory.Exists(tempPath)) - Directory.Delete(tempPath, true); - httpClient.Dispose(); - } + public void Dispose() + { + if (Directory.Exists(tempPath)) + Directory.Delete(tempPath, true); + httpClient.Dispose(); + } } From bba96d6f34e9507dc5cd294811a7d311fa369f78 Mon Sep 17 00:00:00 2001 From: Vincent Biret Date: Mon, 25 Mar 2024 15:31:36 -0400 Subject: [PATCH 07/40] - fixes should generate implementation for plugins Signed-off-by: Vincent Biret --- .../ApiPluginConfigurationComparer.cs | 30 +++++++++++++++++++ .../WorkspaceManagementService.cs | 16 ++++++++-- 2 files changed, 44 insertions(+), 2 deletions(-) create mode 100644 src/Kiota.Builder/WorkspaceManagement/ApiPluginConfigurationComparer.cs diff --git a/src/Kiota.Builder/WorkspaceManagement/ApiPluginConfigurationComparer.cs b/src/Kiota.Builder/WorkspaceManagement/ApiPluginConfigurationComparer.cs new file mode 100644 index 0000000000..359ea1a3fe --- /dev/null +++ b/src/Kiota.Builder/WorkspaceManagement/ApiPluginConfigurationComparer.cs @@ -0,0 +1,30 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; +using System.Linq; +using Kiota.Builder.Lock; + +namespace Kiota.Builder.WorkspaceManagement; +/// +/// Compares two instances. +/// +public class ApiPluginConfigurationComparer : IEqualityComparer +{ + private static readonly StringIEnumerableDeepComparer _stringIEnumerableDeepComparer = new(); + /// + public bool Equals(ApiPluginConfiguration? x, ApiPluginConfiguration? y) + { + return x == null && y == null || x != null && y != null && GetHashCode(x) == GetHashCode(y); + } + /// + public int GetHashCode([DisallowNull] ApiPluginConfiguration obj) + { + if (obj == null) return 0; + return + (string.IsNullOrEmpty(obj.DescriptionLocation) ? 0 : obj.DescriptionLocation.GetHashCode(StringComparison.OrdinalIgnoreCase)) * 11 + + (string.IsNullOrEmpty(obj.OutputPath) ? 0 : obj.OutputPath.GetHashCode(StringComparison.OrdinalIgnoreCase)) * 7 + + _stringIEnumerableDeepComparer.GetHashCode(obj.Types?.Order(StringComparer.OrdinalIgnoreCase) ?? Enumerable.Empty()) * 5 + + _stringIEnumerableDeepComparer.GetHashCode(obj.IncludePatterns?.Order(StringComparer.OrdinalIgnoreCase) ?? Enumerable.Empty()) * 3 + + _stringIEnumerableDeepComparer.GetHashCode(obj.ExcludePatterns?.Order(StringComparer.OrdinalIgnoreCase) ?? Enumerable.Empty()) * 2; + } +} diff --git a/src/Kiota.Builder/WorkspaceManagement/WorkspaceManagementService.cs b/src/Kiota.Builder/WorkspaceManagement/WorkspaceManagementService.cs index d4ee1f3256..98b51724c2 100644 --- a/src/Kiota.Builder/WorkspaceManagement/WorkspaceManagementService.cs +++ b/src/Kiota.Builder/WorkspaceManagement/WorkspaceManagementService.cs @@ -101,6 +101,7 @@ public async Task BackupStateAsync(string outputPath, CancellationToken cancella } private static readonly KiotaLockComparer lockComparer = new(); private static readonly ApiClientConfigurationComparer clientConfigurationComparer = new(); + private static readonly ApiPluginConfigurationComparer pluginConfigurationComparer = new(); private static readonly ApiDependencyComparer apiDependencyComparer = new(); public async Task ShouldGenerateAsync(GenerationConfiguration inputConfig, string descriptionHash, CancellationToken cancellationToken = default) { @@ -109,8 +110,10 @@ public async Task ShouldGenerateAsync(GenerationConfiguration inputConfig, if (UseKiotaConfig) { var (wsConfig, apiManifest) = await workspaceConfigurationStorageService.GetWorkspaceConfigurationAsync(cancellationToken).ConfigureAwait(false); - if ((wsConfig?.Clients.TryGetValue(inputConfig.ClientClassName, out var existingClientConfig) ?? false) && - (apiManifest?.ApiDependencies.TryGetValue(inputConfig.ClientClassName, out var existingApiManifest) ?? false)) + if (wsConfig is null || apiManifest is null) + return true; + if (wsConfig.Clients.TryGetValue(inputConfig.ClientClassName, out var existingClientConfig) && + apiManifest.ApiDependencies.TryGetValue(inputConfig.ClientClassName, out var existingApiManifest)) { var inputClientConfig = new ApiClientConfiguration(inputConfig); inputClientConfig.NormalizePaths(WorkingDirectory); @@ -118,6 +121,15 @@ public async Task ShouldGenerateAsync(GenerationConfiguration inputConfig, return !clientConfigurationComparer.Equals(existingClientConfig, inputClientConfig) || !apiDependencyComparer.Equals(inputConfig.ToApiDependency(inputConfigurationHash, []), existingApiManifest); } + if (wsConfig.Plugins.TryGetValue(inputConfig.ClientClassName, out var existingPluginConfig) && + apiManifest.ApiDependencies.TryGetValue(inputConfig.ClientClassName, out var existingPluginApiManifest)) + { + var inputClientConfig = new ApiPluginConfiguration(inputConfig); + inputClientConfig.NormalizePaths(WorkingDirectory); + var inputConfigurationHash = await GetConsumerConfigurationHashAsync(inputClientConfig, descriptionHash).ConfigureAwait(false); + return !pluginConfigurationComparer.Equals(existingPluginConfig, inputClientConfig) || + !apiDependencyComparer.Equals(inputConfig.ToApiDependency(inputConfigurationHash, []), existingPluginApiManifest); + } return true; } else From 967ed848f4fb0e1e486043139c8caa16e8a0dcf9 Mon Sep 17 00:00:00 2001 From: Vincent Biret Date: Tue, 26 Mar 2024 11:14:52 -0400 Subject: [PATCH 08/40] - fixes formatting Signed-off-by: Vincent Biret --- .../WorkspaceManagementServiceTests.cs | 326 +++++++++--------- 1 file changed, 163 insertions(+), 163 deletions(-) diff --git a/tests/Kiota.Builder.Tests/WorkspaceManagement/WorkspaceManagementServiceTests.cs b/tests/Kiota.Builder.Tests/WorkspaceManagement/WorkspaceManagementServiceTests.cs index 965adb0b7d..e00e61df84 100644 --- a/tests/Kiota.Builder.Tests/WorkspaceManagement/WorkspaceManagementServiceTests.cs +++ b/tests/Kiota.Builder.Tests/WorkspaceManagement/WorkspaceManagementServiceTests.cs @@ -15,136 +15,136 @@ namespace Kiota.Builder.Tests.WorkspaceManagement; public sealed class WorkspaceManagementServiceTests : IDisposable { - private readonly string tempPath = Path.Combine(Path.GetTempPath(), Path.GetRandomFileName()); - private readonly HttpClient httpClient = new(); - [Fact] - public void Defensive() - { - Assert.Throws(() => new WorkspaceManagementService(null, httpClient)); - Assert.Throws(() => new WorkspaceManagementService(Mock.Of(), null)); - } - [InlineData(true)] - [InlineData(false)] - [Theory] - public async Task IsClientPresentReturnsFalseOnNoClient(bool usesConfig) - { - var mockLogger = Mock.Of(); - Directory.CreateDirectory(tempPath); - var service = new WorkspaceManagementService(mockLogger, httpClient, usesConfig, tempPath); - var result = await service.IsConsumerPresent("clientName"); - Assert.False(result); - } - [InlineData(true, true)] - [InlineData(true, false)] - [InlineData(false, true)] - [InlineData(false, false)] - [Theory] - public async Task ShouldGenerateReturnsTrue(bool usesConfig, bool cleanOutput) - { - var mockLogger = Mock.Of(); - Directory.CreateDirectory(tempPath); - var service = new WorkspaceManagementService(mockLogger, httpClient, usesConfig, tempPath); - var configuration = new GenerationConfiguration + private readonly string tempPath = Path.Combine(Path.GetTempPath(), Path.GetRandomFileName()); + private readonly HttpClient httpClient = new(); + [Fact] + public void Defensive() { - ClientClassName = "clientName", - OutputPath = tempPath, - OpenAPIFilePath = Path.Combine(tempPath, "openapi.yaml"), - CleanOutput = cleanOutput, - }; - var result = await service.ShouldGenerateAsync(configuration, "foo"); - Assert.True(result); - } - [InlineData(true)] - [InlineData(false)] - [Theory] - public async Task ShouldGenerateReturnsFalse(bool usesConfig) - { - var mockLogger = Mock.Of(); - Directory.CreateDirectory(tempPath); - var service = new WorkspaceManagementService(mockLogger, httpClient, usesConfig, tempPath); - var configuration = new GenerationConfiguration + Assert.Throws(() => new WorkspaceManagementService(null, httpClient)); + Assert.Throws(() => new WorkspaceManagementService(Mock.Of(), null)); + } + [InlineData(true)] + [InlineData(false)] + [Theory] + public async Task IsClientPresentReturnsFalseOnNoClient(bool usesConfig) { - ClientClassName = "clientName", - OutputPath = tempPath, - OpenAPIFilePath = Path.Combine(tempPath, "openapi.yaml"), - ApiRootUrl = "https://graph.microsoft.com", - }; - Directory.CreateDirectory(tempPath); - await service.UpdateStateFromConfigurationAsync(configuration, "foo", [], Stream.Null); - var result = await service.ShouldGenerateAsync(configuration, "foo"); - Assert.False(result); - } - [Fact] - public async Task RemovesAClient() - { - var mockLogger = Mock.Of(); - Directory.CreateDirectory(tempPath); - var service = new WorkspaceManagementService(mockLogger, httpClient, true, tempPath); - var configuration = new GenerationConfiguration + var mockLogger = Mock.Of(); + Directory.CreateDirectory(tempPath); + var service = new WorkspaceManagementService(mockLogger, httpClient, usesConfig, tempPath); + var result = await service.IsConsumerPresent("clientName"); + Assert.False(result); + } + [InlineData(true, true)] + [InlineData(true, false)] + [InlineData(false, true)] + [InlineData(false, false)] + [Theory] + public async Task ShouldGenerateReturnsTrue(bool usesConfig, bool cleanOutput) { - ClientClassName = "clientName", - OutputPath = tempPath, - OpenAPIFilePath = Path.Combine(tempPath, "openapi.yaml"), - ApiRootUrl = "https://graph.microsoft.com", - }; - Directory.CreateDirectory(tempPath); - await service.UpdateStateFromConfigurationAsync(configuration, "foo", [], Stream.Null); - await service.RemoveClientAsync("clientName"); - var result = await service.IsConsumerPresent("clientName"); - Assert.False(result); - } - [Fact] - public async Task FailsOnMigrateWithoutKiotaConfigMode() - { - var mockLogger = Mock.Of(); - Directory.CreateDirectory(tempPath); - var service = new WorkspaceManagementService(mockLogger, httpClient, false, tempPath); - await Assert.ThrowsAsync(() => service.MigrateFromLockFileAsync(string.Empty, tempPath)); - } - [Fact] - public async Task FailsWhenTargetLockDirectoryIsNotSubDirectory() - { - var mockLogger = Mock.Of(); - Directory.CreateDirectory(tempPath); - var service = new WorkspaceManagementService(mockLogger, httpClient, true, tempPath); - await Assert.ThrowsAsync(() => service.MigrateFromLockFileAsync(string.Empty, Path.Combine(Path.GetTempPath(), Path.GetRandomFileName()))); - } - [Fact] - public async Task FailsWhenNoLockFilesAreFound() - { - var mockLogger = Mock.Of(); - Directory.CreateDirectory(tempPath); - var service = new WorkspaceManagementService(mockLogger, httpClient, true, tempPath); - await Assert.ThrowsAsync(() => service.MigrateFromLockFileAsync(string.Empty, tempPath)); - } - [Fact] - public async Task FailsOnMultipleLockFilesAndClientName() - { - var mockLogger = Mock.Of(); - Directory.CreateDirectory(tempPath); - var service = new WorkspaceManagementService(mockLogger, httpClient, true, tempPath); - Directory.CreateDirectory(Path.Combine(tempPath, "client1")); - Directory.CreateDirectory(Path.Combine(tempPath, "client2")); - File.WriteAllText(Path.Combine(tempPath, "client1", LockManagementService.LockFileName), "foo"); - File.WriteAllText(Path.Combine(tempPath, "client2", LockManagementService.LockFileName), "foo"); - await Assert.ThrowsAsync(() => service.MigrateFromLockFileAsync("bar", tempPath)); - } - [Fact] - public async Task MigratesAClient() - { - var mockLogger = Mock.Of(); - Directory.CreateDirectory(tempPath); - var service = new WorkspaceManagementService(mockLogger, httpClient, true, tempPath); - var descriptionPath = Path.Combine(tempPath, "description.yml"); - var generationConfiguration = new GenerationConfiguration + var mockLogger = Mock.Of(); + Directory.CreateDirectory(tempPath); + var service = new WorkspaceManagementService(mockLogger, httpClient, usesConfig, tempPath); + var configuration = new GenerationConfiguration + { + ClientClassName = "clientName", + OutputPath = tempPath, + OpenAPIFilePath = Path.Combine(tempPath, "openapi.yaml"), + CleanOutput = cleanOutput, + }; + var result = await service.ShouldGenerateAsync(configuration, "foo"); + Assert.True(result); + } + [InlineData(true)] + [InlineData(false)] + [Theory] + public async Task ShouldGenerateReturnsFalse(bool usesConfig) { - ClientClassName = "clientName", - OutputPath = Path.Combine(tempPath, "client"), - OpenAPIFilePath = descriptionPath, - ApiRootUrl = "https://graph.microsoft.com", - }; - Directory.CreateDirectory(generationConfiguration.OutputPath); - await File.WriteAllTextAsync(descriptionPath, @$"openapi: 3.0.1 + var mockLogger = Mock.Of(); + Directory.CreateDirectory(tempPath); + var service = new WorkspaceManagementService(mockLogger, httpClient, usesConfig, tempPath); + var configuration = new GenerationConfiguration + { + ClientClassName = "clientName", + OutputPath = tempPath, + OpenAPIFilePath = Path.Combine(tempPath, "openapi.yaml"), + ApiRootUrl = "https://graph.microsoft.com", + }; + Directory.CreateDirectory(tempPath); + await service.UpdateStateFromConfigurationAsync(configuration, "foo", [], Stream.Null); + var result = await service.ShouldGenerateAsync(configuration, "foo"); + Assert.False(result); + } + [Fact] + public async Task RemovesAClient() + { + var mockLogger = Mock.Of(); + Directory.CreateDirectory(tempPath); + var service = new WorkspaceManagementService(mockLogger, httpClient, true, tempPath); + var configuration = new GenerationConfiguration + { + ClientClassName = "clientName", + OutputPath = tempPath, + OpenAPIFilePath = Path.Combine(tempPath, "openapi.yaml"), + ApiRootUrl = "https://graph.microsoft.com", + }; + Directory.CreateDirectory(tempPath); + await service.UpdateStateFromConfigurationAsync(configuration, "foo", [], Stream.Null); + await service.RemoveClientAsync("clientName"); + var result = await service.IsConsumerPresent("clientName"); + Assert.False(result); + } + [Fact] + public async Task FailsOnMigrateWithoutKiotaConfigMode() + { + var mockLogger = Mock.Of(); + Directory.CreateDirectory(tempPath); + var service = new WorkspaceManagementService(mockLogger, httpClient, false, tempPath); + await Assert.ThrowsAsync(() => service.MigrateFromLockFileAsync(string.Empty, tempPath)); + } + [Fact] + public async Task FailsWhenTargetLockDirectoryIsNotSubDirectory() + { + var mockLogger = Mock.Of(); + Directory.CreateDirectory(tempPath); + var service = new WorkspaceManagementService(mockLogger, httpClient, true, tempPath); + await Assert.ThrowsAsync(() => service.MigrateFromLockFileAsync(string.Empty, Path.Combine(Path.GetTempPath(), Path.GetRandomFileName()))); + } + [Fact] + public async Task FailsWhenNoLockFilesAreFound() + { + var mockLogger = Mock.Of(); + Directory.CreateDirectory(tempPath); + var service = new WorkspaceManagementService(mockLogger, httpClient, true, tempPath); + await Assert.ThrowsAsync(() => service.MigrateFromLockFileAsync(string.Empty, tempPath)); + } + [Fact] + public async Task FailsOnMultipleLockFilesAndClientName() + { + var mockLogger = Mock.Of(); + Directory.CreateDirectory(tempPath); + var service = new WorkspaceManagementService(mockLogger, httpClient, true, tempPath); + Directory.CreateDirectory(Path.Combine(tempPath, "client1")); + Directory.CreateDirectory(Path.Combine(tempPath, "client2")); + File.WriteAllText(Path.Combine(tempPath, "client1", LockManagementService.LockFileName), "foo"); + File.WriteAllText(Path.Combine(tempPath, "client2", LockManagementService.LockFileName), "foo"); + await Assert.ThrowsAsync(() => service.MigrateFromLockFileAsync("bar", tempPath)); + } + [Fact] + public async Task MigratesAClient() + { + var mockLogger = Mock.Of(); + Directory.CreateDirectory(tempPath); + var service = new WorkspaceManagementService(mockLogger, httpClient, true, tempPath); + var descriptionPath = Path.Combine(tempPath, "description.yml"); + var generationConfiguration = new GenerationConfiguration + { + ClientClassName = "clientName", + OutputPath = Path.Combine(tempPath, "client"), + OpenAPIFilePath = descriptionPath, + ApiRootUrl = "https://graph.microsoft.com", + }; + Directory.CreateDirectory(generationConfiguration.OutputPath); + await File.WriteAllTextAsync(descriptionPath, @$"openapi: 3.0.1 info: title: OData Service for namespace microsoft.graph description: This OData service is located at https://graph.microsoft.com/v1.0 @@ -166,31 +166,31 @@ await File.WriteAllTextAsync(descriptionPath, @$"openapi: 3.0.1 properties: foo: type: string"); - var classicService = new WorkspaceManagementService(mockLogger, httpClient, false, tempPath); - await classicService.UpdateStateFromConfigurationAsync(generationConfiguration, "foo", [], Stream.Null); - var clientNames = await service.MigrateFromLockFileAsync("clientName", tempPath); - Assert.Single(clientNames); - Assert.Equal("clientName", clientNames.First()); - Assert.False(File.Exists(Path.Combine(tempPath, LockManagementService.LockFileName))); - Assert.True(File.Exists(Path.Combine(tempPath, WorkspaceConfigurationStorageService.KiotaDirectorySegment, WorkspaceConfigurationStorageService.ConfigurationFileName))); - Assert.True(File.Exists(Path.Combine(tempPath, WorkspaceConfigurationStorageService.KiotaDirectorySegment, WorkspaceConfigurationStorageService.ManifestFileName))); - Assert.True(File.Exists(Path.Combine(tempPath, DescriptionStorageService.DescriptionsSubDirectoryRelativePath, "clientName", "description.yml"))); - } - [InlineData(true, true)] - [InlineData(true, false)] - [InlineData(false, true)] - [InlineData(false, false)] - [Theory] - public async Task GetsADescription(bool usesConfig, bool cleanOutput) - { - var mockLogger = Mock.Of(); - Directory.CreateDirectory(tempPath); - var service = new WorkspaceManagementService(mockLogger, httpClient, usesConfig, tempPath); - var descriptionPath = Path.Combine(tempPath, $"{DescriptionStorageService.DescriptionsSubDirectoryRelativePath}/clientName/description.yml"); - var outputPath = Path.Combine(tempPath, "client"); - Directory.CreateDirectory(outputPath); - Directory.CreateDirectory(Path.GetDirectoryName(descriptionPath)); - await File.WriteAllTextAsync(descriptionPath, @$"openapi: 3.0.1 + var classicService = new WorkspaceManagementService(mockLogger, httpClient, false, tempPath); + await classicService.UpdateStateFromConfigurationAsync(generationConfiguration, "foo", [], Stream.Null); + var clientNames = await service.MigrateFromLockFileAsync("clientName", tempPath); + Assert.Single(clientNames); + Assert.Equal("clientName", clientNames.First()); + Assert.False(File.Exists(Path.Combine(tempPath, LockManagementService.LockFileName))); + Assert.True(File.Exists(Path.Combine(tempPath, WorkspaceConfigurationStorageService.KiotaDirectorySegment, WorkspaceConfigurationStorageService.ConfigurationFileName))); + Assert.True(File.Exists(Path.Combine(tempPath, WorkspaceConfigurationStorageService.KiotaDirectorySegment, WorkspaceConfigurationStorageService.ManifestFileName))); + Assert.True(File.Exists(Path.Combine(tempPath, DescriptionStorageService.DescriptionsSubDirectoryRelativePath, "clientName", "description.yml"))); + } + [InlineData(true, true)] + [InlineData(true, false)] + [InlineData(false, true)] + [InlineData(false, false)] + [Theory] + public async Task GetsADescription(bool usesConfig, bool cleanOutput) + { + var mockLogger = Mock.Of(); + Directory.CreateDirectory(tempPath); + var service = new WorkspaceManagementService(mockLogger, httpClient, usesConfig, tempPath); + var descriptionPath = Path.Combine(tempPath, $"{DescriptionStorageService.DescriptionsSubDirectoryRelativePath}/clientName/description.yml"); + var outputPath = Path.Combine(tempPath, "client"); + Directory.CreateDirectory(outputPath); + Directory.CreateDirectory(Path.GetDirectoryName(descriptionPath)); + await File.WriteAllTextAsync(descriptionPath, @$"openapi: 3.0.1 info: title: OData Service for namespace microsoft.graph description: This OData service is located at https://graph.microsoft.com/v1.0 @@ -212,17 +212,17 @@ await File.WriteAllTextAsync(descriptionPath, @$"openapi: 3.0.1 properties: foo: type: string"); - var descriptionCopy = await service.GetDescriptionCopyAsync("clientName", descriptionPath, cleanOutput); - if (!usesConfig || cleanOutput) - Assert.Null(descriptionCopy); - else - Assert.NotNull(descriptionCopy); - } + var descriptionCopy = await service.GetDescriptionCopyAsync("clientName", descriptionPath, cleanOutput); + if (!usesConfig || cleanOutput) + Assert.Null(descriptionCopy); + else + Assert.NotNull(descriptionCopy); + } - public void Dispose() - { - if (Directory.Exists(tempPath)) - Directory.Delete(tempPath, true); - httpClient.Dispose(); - } + public void Dispose() + { + if (Directory.Exists(tempPath)) + Directory.Delete(tempPath, true); + httpClient.Dispose(); + } } From 66e7d079961f4c4079706e88e419f282581bd2ed Mon Sep 17 00:00:00 2001 From: Vincent Biret Date: Tue, 26 Mar 2024 14:25:38 -0400 Subject: [PATCH 09/40] - initial plugin generation Signed-off-by: Vincent Biret --- src/Kiota.Builder/KiotaBuilder.cs | 11 ++- .../Plugins/PluginsGenerationService.cs | 91 +++++++++++++++++++ 2 files changed, 99 insertions(+), 3 deletions(-) create mode 100644 src/Kiota.Builder/Plugins/PluginsGenerationService.cs diff --git a/src/Kiota.Builder/KiotaBuilder.cs b/src/Kiota.Builder/KiotaBuilder.cs index d9060ec596..671c5c1713 100644 --- a/src/Kiota.Builder/KiotaBuilder.cs +++ b/src/Kiota.Builder/KiotaBuilder.cs @@ -22,6 +22,7 @@ using Kiota.Builder.Logging; using Kiota.Builder.Manifest; using Kiota.Builder.OpenApiExtensions; +using Kiota.Builder.Plugins; using Kiota.Builder.Refiners; using Kiota.Builder.WorkspaceManagement; using Kiota.Builder.Writers; @@ -230,9 +231,13 @@ public async Task GeneratePluginAsync(CancellationToken cancellationToken) { return await GenerateConsumerAsync(async (sw, stepId, openApiTree, CancellationToken) => { - await Task.Delay(1, cancellationToken).ConfigureAwait(false); - logger.LogCritical("Plugins generation is not implemented yet"); - //TODO implement generation logic + if (openApiDocument is null || openApiTree is null) + throw new InvalidOperationException("The OpenAPI document and the URL tree must be loaded before generating the plugins"); + // generate plugin + sw.Start(); + var pluginsService = new PluginsGenerationService(openApiDocument, openApiTree, config); + await pluginsService.GenerateManifestAsync(cancellationToken).ConfigureAwait(false); + StopLogAndReset(sw, $"step {++stepId} - generate plugin - took"); return stepId; }, cancellationToken).ConfigureAwait(false); } diff --git a/src/Kiota.Builder/Plugins/PluginsGenerationService.cs b/src/Kiota.Builder/Plugins/PluginsGenerationService.cs new file mode 100644 index 0000000000..61e7b44c32 --- /dev/null +++ b/src/Kiota.Builder/Plugins/PluginsGenerationService.cs @@ -0,0 +1,91 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Text.Json; +using System.Threading; +using System.Threading.Tasks; +using Kiota.Builder.Configuration; +using Kiota.Builder.Extensions; +using Microsoft.OpenApi.Models; +using Microsoft.OpenApi.Services; +using Microsoft.Plugins.Manifest; + +namespace Kiota.Builder.Plugins; +public class PluginsGenerationService +{ + private readonly OpenApiDocument OAIDocument; + private readonly OpenApiUrlTreeNode TreeNode; + private readonly GenerationConfiguration Configuration; + + public PluginsGenerationService(OpenApiDocument document, OpenApiUrlTreeNode openApiUrlTreeNode, GenerationConfiguration configuration) + { + ArgumentNullException.ThrowIfNull(document); + ArgumentNullException.ThrowIfNull(openApiUrlTreeNode); + ArgumentNullException.ThrowIfNull(configuration); + OAIDocument = document; + TreeNode = openApiUrlTreeNode; + Configuration = configuration; + } + public async Task GenerateManifestAsync(CancellationToken cancellationToken = default) + { + var (runtimes, functions) = GetRuntimesAndFunctionsFromTree(TreeNode); + var pluginDocument = new ManifestDocument + { + SchemaVersion = "v2", + //TODO name for human + //TODO description for human + //TODO contact email + //TODO namespace + Runtimes = [.. runtimes.OrderBy(static x => x.RunForFunctions[0], StringComparer.OrdinalIgnoreCase)], + Functions = [.. functions.OrderBy(static x => x.Name, StringComparer.OrdinalIgnoreCase)] + }; + var outputPath = Path.Combine(Configuration.OutputPath, "manifest.json"); + + var directory = Path.GetDirectoryName(outputPath); + if (!string.IsNullOrEmpty(directory) && !Directory.Exists(directory)) + Directory.CreateDirectory(directory); +#pragma warning disable CA2007 // Consider calling ConfigureAwait on the awaited task + await using var fileStream = File.Create(outputPath, 4096); + await using var writer = new Utf8JsonWriter(fileStream, new JsonWriterOptions { Indented = true }); +#pragma warning restore CA2007 // Consider calling ConfigureAwait on the awaited task + pluginDocument.Write(writer); + await writer.FlushAsync(cancellationToken).ConfigureAwait(false); + } + private (Runtime[], Function[]) GetRuntimesAndFunctionsFromTree(OpenApiUrlTreeNode currentNode) + { + var runtimes = new List(); + var functions = new List(); + if (currentNode.PathItems.TryGetValue(Constants.DefaultOpenApiLabel, out var pathItem)) + { + foreach (var operation in pathItem.Operations.Values.Where(static x => !string.IsNullOrEmpty(x.OperationId))) + { + runtimes.Add(new OpenAPIRuntime + { + Auth = new Auth("none"), + Spec = new Dictionary { { "url", "./openapi.yaml" } }, //TODO update from context once the slice copy is implemented + RunForFunctions = [operation.OperationId] + }); + functions.Add(new Function + { + Name = operation.OperationId, + //TODO map parameters + Description = operation.Summary.CleanupXMLString() is string summary && !string.IsNullOrEmpty(summary) ? summary : operation.Description.CleanupXMLString(), + // Parameters = operation.Parameters.Select(static x => new Parameter + // { + // Name = x.Name, + // Type = x.Schema.Type, + // Required = x.Required + // }).ToArray() + }); + } + } + foreach (var node in currentNode.Children) + { + var (childRuntimes, childFunctions) = GetRuntimesAndFunctionsFromTree(node.Value); + runtimes.AddRange(childRuntimes); + functions.AddRange(childFunctions); + } + return (runtimes.ToArray(), functions.ToArray()); + } +} From e1c17d236730e0b9f5b139a1a1f300637d556038 Mon Sep 17 00:00:00 2001 From: Vincent Biret Date: Tue, 26 Mar 2024 15:41:22 -0400 Subject: [PATCH 10/40] - adds parameters to the plugin Signed-off-by: Vincent Biret --- .../Plugins/PluginsGenerationService.cs | 27 ++++++++++++++----- 1 file changed, 20 insertions(+), 7 deletions(-) diff --git a/src/Kiota.Builder/Plugins/PluginsGenerationService.cs b/src/Kiota.Builder/Plugins/PluginsGenerationService.cs index 61e7b44c32..355efa0257 100644 --- a/src/Kiota.Builder/Plugins/PluginsGenerationService.cs +++ b/src/Kiota.Builder/Plugins/PluginsGenerationService.cs @@ -66,17 +66,28 @@ public async Task GenerateManifestAsync(CancellationToken cancellationToken = de Spec = new Dictionary { { "url", "./openapi.yaml" } }, //TODO update from context once the slice copy is implemented RunForFunctions = [operation.OperationId] }); + var oasParameters = operation.Parameters + .Union(pathItem.Parameters.Where(static x => x.In is ParameterLocation.Path)) + .Where(static x => x.Schema?.Type is not null && scalarTypes.Contains(x.Schema.Type)) + .ToArray(); + //TODO add request body + functions.Add(new Function { Name = operation.OperationId, - //TODO map parameters Description = operation.Summary.CleanupXMLString() is string summary && !string.IsNullOrEmpty(summary) ? summary : operation.Description.CleanupXMLString(), - // Parameters = operation.Parameters.Select(static x => new Parameter - // { - // Name = x.Name, - // Type = x.Schema.Type, - // Required = x.Required - // }).ToArray() + Parameters = oasParameters.Length == 0 ? null : + new Parameters( + "object", + new Properties(oasParameters.ToDictionary( + static x => x.Name, + static x => new Property( + x.Schema.Type ?? string.Empty, + x.Description.CleanupXMLString(), + x.Schema.Default?.ToString() ?? string.Empty, + null), //TODO enums + StringComparer.OrdinalIgnoreCase)), + oasParameters.Where(static x => x.Required).Select(static x => x.Name).ToList()), }); } } @@ -88,4 +99,6 @@ public async Task GenerateManifestAsync(CancellationToken cancellationToken = de } return (runtimes.ToArray(), functions.ToArray()); } + private static readonly HashSet scalarTypes = new(StringComparer.OrdinalIgnoreCase) { "string", "number", "integer", "boolean" }; + //TODO validate this is right, in OAS integer are under type number for the json schema, but integer is ok for query parameters } From bc2dce75b6057eb8d62f2d4081daae114e7a1cd3 Mon Sep 17 00:00:00 2001 From: Vincent Biret Date: Tue, 26 Mar 2024 15:59:09 -0400 Subject: [PATCH 11/40] - adds support for descriptive properties Signed-off-by: Vincent Biret --- src/Kiota.Builder/Plugins/PluginsGenerationService.cs | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/src/Kiota.Builder/Plugins/PluginsGenerationService.cs b/src/Kiota.Builder/Plugins/PluginsGenerationService.cs index 355efa0257..ead3baa991 100644 --- a/src/Kiota.Builder/Plugins/PluginsGenerationService.cs +++ b/src/Kiota.Builder/Plugins/PluginsGenerationService.cs @@ -33,10 +33,13 @@ public async Task GenerateManifestAsync(CancellationToken cancellationToken = de var pluginDocument = new ManifestDocument { SchemaVersion = "v2", - //TODO name for human - //TODO description for human - //TODO contact email + NameForHuman = OAIDocument.Info?.Title.CleanupXMLString(), + // TODO name for model + DescriptionForHuman = OAIDocument.Info?.Description.CleanupXMLString() is string d && !string.IsNullOrEmpty(d) ? d : $"Description for {OAIDocument.Info?.Title.CleanupXMLString()}", + DescriptionForModel = OAIDocument.Info?.Description.CleanupXMLString() is string e && !string.IsNullOrEmpty(e) ? e : $"Description for {OAIDocument.Info?.Title.CleanupXMLString()}", + ContactEmail = OAIDocument.Info?.Contact?.Email, //TODO namespace + //TODO logo Runtimes = [.. runtimes.OrderBy(static x => x.RunForFunctions[0], StringComparer.OrdinalIgnoreCase)], Functions = [.. functions.OrderBy(static x => x.Name, StringComparer.OrdinalIgnoreCase)] }; @@ -88,6 +91,7 @@ public async Task GenerateManifestAsync(CancellationToken cancellationToken = de null), //TODO enums StringComparer.OrdinalIgnoreCase)), oasParameters.Where(static x => x.Required).Select(static x => x.Name).ToList()), + //TODO states with reasoning and instructions from OAS extensions }); } } From 84ad261ad005f9e7cff15b2785485a91e8f9d3d4 Mon Sep 17 00:00:00 2001 From: Vincent Biret Date: Wed, 27 Mar 2024 09:22:23 -0400 Subject: [PATCH 12/40] - dedupes runtimes for plugin generation Signed-off-by: Vincent Biret --- src/Kiota.Builder/Plugins/AuthComparer.cs | 21 +++++++++++++ .../Plugins/OpenAPiRuntimeComparer.cs | 31 +++++++++++++++++++ .../Plugins/PluginsGenerationService.cs | 15 +++++++-- 3 files changed, 64 insertions(+), 3 deletions(-) create mode 100644 src/Kiota.Builder/Plugins/AuthComparer.cs create mode 100644 src/Kiota.Builder/Plugins/OpenAPiRuntimeComparer.cs diff --git a/src/Kiota.Builder/Plugins/AuthComparer.cs b/src/Kiota.Builder/Plugins/AuthComparer.cs new file mode 100644 index 0000000000..a38c781692 --- /dev/null +++ b/src/Kiota.Builder/Plugins/AuthComparer.cs @@ -0,0 +1,21 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; +using Microsoft.Plugins.Manifest; + +namespace Kiota.Builder.Plugins; + +internal class AuthComparer : IEqualityComparer +{ + /// + public bool Equals(Auth? x, Auth? y) + { + return x == null && y == null || x != null && y != null && GetHashCode(x) == GetHashCode(y); + } + /// + public int GetHashCode([DisallowNull] Auth obj) + { + if (obj == null) return 0; + return string.IsNullOrEmpty(obj.Type) ? 0 : StringComparer.OrdinalIgnoreCase.GetHashCode(obj.Type) * 3; + } +} diff --git a/src/Kiota.Builder/Plugins/OpenAPiRuntimeComparer.cs b/src/Kiota.Builder/Plugins/OpenAPiRuntimeComparer.cs new file mode 100644 index 0000000000..944cc78d39 --- /dev/null +++ b/src/Kiota.Builder/Plugins/OpenAPiRuntimeComparer.cs @@ -0,0 +1,31 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; +using System.Linq; +using Kiota.Builder.Lock; +using Microsoft.Plugins.Manifest; + +namespace Kiota.Builder.Plugins; + +internal class OpenAPIRuntimeComparer : IEqualityComparer +{ + public bool EvaluateFunctions + { + get; init; + } + private static readonly StringIEnumerableDeepComparer _stringIEnumerableDeepComparer = new(); + private static readonly AuthComparer _authComparer = new(); + /// + public bool Equals(OpenAPIRuntime? x, OpenAPIRuntime? y) + { + return x == null && y == null || x != null && y != null && GetHashCode(x) == GetHashCode(y); + } + /// + public int GetHashCode([DisallowNull] OpenAPIRuntime obj) + { + if (obj == null) return 0; + return (EvaluateFunctions ? _stringIEnumerableDeepComparer.GetHashCode(obj.RunForFunctions ?? Enumerable.Empty()) * 7 : 0) + + obj.Spec.Select(static x => StringComparer.Ordinal.GetHashCode($"{x.Key}:{x.Value}")).Aggregate(0, (acc, next) => acc + next) * 5 + + (obj.Auth is null ? 0 : _authComparer.GetHashCode(obj.Auth) * 3); + } +} diff --git a/src/Kiota.Builder/Plugins/PluginsGenerationService.cs b/src/Kiota.Builder/Plugins/PluginsGenerationService.cs index ead3baa991..5d70618cb6 100644 --- a/src/Kiota.Builder/Plugins/PluginsGenerationService.cs +++ b/src/Kiota.Builder/Plugins/PluginsGenerationService.cs @@ -27,6 +27,7 @@ public PluginsGenerationService(OpenApiDocument document, OpenApiUrlTreeNode ope TreeNode = openApiUrlTreeNode; Configuration = configuration; } + private static readonly OpenAPIRuntimeComparer _openAPIRuntimeComparer = new(); public async Task GenerateManifestAsync(CancellationToken cancellationToken = default) { var (runtimes, functions) = GetRuntimesAndFunctionsFromTree(TreeNode); @@ -40,7 +41,15 @@ public async Task GenerateManifestAsync(CancellationToken cancellationToken = de ContactEmail = OAIDocument.Info?.Contact?.Email, //TODO namespace //TODO logo - Runtimes = [.. runtimes.OrderBy(static x => x.RunForFunctions[0], StringComparer.OrdinalIgnoreCase)], + Runtimes = [.. runtimes + .GroupBy(static x => x, _openAPIRuntimeComparer) + .Select(static x => + { + var result = x.First(); + result.RunForFunctions = x.SelectMany(static y => y.RunForFunctions).Distinct(StringComparer.OrdinalIgnoreCase).ToList(); + return result; + }) + .OrderBy(static x => x.RunForFunctions[0], StringComparer.OrdinalIgnoreCase)], Functions = [.. functions.OrderBy(static x => x.Name, StringComparer.OrdinalIgnoreCase)] }; var outputPath = Path.Combine(Configuration.OutputPath, "manifest.json"); @@ -55,9 +64,9 @@ public async Task GenerateManifestAsync(CancellationToken cancellationToken = de pluginDocument.Write(writer); await writer.FlushAsync(cancellationToken).ConfigureAwait(false); } - private (Runtime[], Function[]) GetRuntimesAndFunctionsFromTree(OpenApiUrlTreeNode currentNode) + private (OpenAPIRuntime[], Function[]) GetRuntimesAndFunctionsFromTree(OpenApiUrlTreeNode currentNode) { - var runtimes = new List(); + var runtimes = new List(); var functions = new List(); if (currentNode.PathItems.TryGetValue(Constants.DefaultOpenApiLabel, out var pathItem)) { From 8ce7f7425315e4f413a59ff9d8e4c8d0cbcbabd2 Mon Sep 17 00:00:00 2001 From: Vincent Biret Date: Wed, 27 Mar 2024 13:48:23 -0400 Subject: [PATCH 13/40] - code linting Signed-off-by: Vincent Biret --- src/Kiota.Builder/OpenApiExtensions/OpenApiKiotaExtension.cs | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/Kiota.Builder/OpenApiExtensions/OpenApiKiotaExtension.cs b/src/Kiota.Builder/OpenApiExtensions/OpenApiKiotaExtension.cs index b6ef45f507..80a9149f2f 100644 --- a/src/Kiota.Builder/OpenApiExtensions/OpenApiKiotaExtension.cs +++ b/src/Kiota.Builder/OpenApiExtensions/OpenApiKiotaExtension.cs @@ -1,6 +1,4 @@ - - -using System; +using System; using System.Linq; using Kiota.Builder.Configuration; using Kiota.Builder.Extensions; From 59817d5e84cfec26f5544a29c9bee5d37726727e Mon Sep 17 00:00:00 2001 From: Vincent Biret Date: Wed, 27 Mar 2024 13:48:43 -0400 Subject: [PATCH 14/40] - adds AI extensions support Signed-off-by: Vincent Biret --- .../OpenApiDocumentDownloadService.cs | 3 + ...OpenApiAiReasoningInstructionsExtension.cs | 38 +++++++++++++ ...penApiAiRespondingInstructionsExtension.cs | 38 +++++++++++++ .../OpenApiDescriptionForModelExtension.cs | 32 +++++++++++ ...piAiReasoningInstructionsExtensionTests.cs | 56 +++++++++++++++++++ ...iAiRespondingInstructionsExtensionTests.cs | 56 +++++++++++++++++++ ...penApiDescriptionForModelExtensionTests.cs | 51 +++++++++++++++++ 7 files changed, 274 insertions(+) create mode 100644 src/Kiota.Builder/OpenApiExtensions/OpenApiAiReasoningInstructionsExtension.cs create mode 100644 src/Kiota.Builder/OpenApiExtensions/OpenApiAiRespondingInstructionsExtension.cs create mode 100644 src/Kiota.Builder/OpenApiExtensions/OpenApiDescriptionForModelExtension.cs create mode 100644 tests/Kiota.Builder.Tests/OpenApiExtensions/OpenApiAiReasoningInstructionsExtensionTests.cs create mode 100644 tests/Kiota.Builder.Tests/OpenApiExtensions/OpenApiAiRespondingInstructionsExtensionTests.cs create mode 100644 tests/Kiota.Builder.Tests/OpenApiExtensions/OpenApiDescriptionForModelExtensionTests.cs diff --git a/src/Kiota.Builder/OpenApiDocumentDownloadService.cs b/src/Kiota.Builder/OpenApiDocumentDownloadService.cs index 0bf3f6a8df..20ce471058 100644 --- a/src/Kiota.Builder/OpenApiDocumentDownloadService.cs +++ b/src/Kiota.Builder/OpenApiDocumentDownloadService.cs @@ -114,6 +114,9 @@ ex is SecurityException || }; settings.AddMicrosoftExtensionParsers(); settings.ExtensionParsers.TryAdd(OpenApiKiotaExtension.Name, static (i, _) => OpenApiKiotaExtension.Parse(i)); + settings.ExtensionParsers.TryAdd(OpenApiDescriptionForModelExtension.Name, static (i, _) => OpenApiDescriptionForModelExtension.Parse(i)); + settings.ExtensionParsers.TryAdd(OpenApiAiReasoningInstructionsExtension.Name, static (i, _) => OpenApiAiReasoningInstructionsExtension.Parse(i)); + settings.ExtensionParsers.TryAdd(OpenApiAiRespondingInstructionsExtension.Name, static (i, _) => OpenApiAiRespondingInstructionsExtension.Parse(i)); try { var rawUri = config.OpenAPIFilePath.TrimEnd(KiotaBuilder.ForwardSlash); diff --git a/src/Kiota.Builder/OpenApiExtensions/OpenApiAiReasoningInstructionsExtension.cs b/src/Kiota.Builder/OpenApiExtensions/OpenApiAiReasoningInstructionsExtension.cs new file mode 100644 index 0000000000..f690e1df81 --- /dev/null +++ b/src/Kiota.Builder/OpenApiExtensions/OpenApiAiReasoningInstructionsExtension.cs @@ -0,0 +1,38 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using Microsoft.OpenApi; +using Microsoft.OpenApi.Any; +using Microsoft.OpenApi.Interfaces; +using Microsoft.OpenApi.Writers; + +namespace Kiota.Builder.OpenApiExtensions; + +public class OpenApiAiReasoningInstructionsExtension : IOpenApiExtension +{ + public static string Name => "x-ai-reasoning-instructions"; +#pragma warning disable CA1002 // Do not expose generic lists + public List ReasoningInstructions { get; init; } = []; +#pragma warning restore CA1002 // Do not expose generic lists + public void Write(IOpenApiWriter writer, OpenApiSpecVersion specVersion) + { + ArgumentNullException.ThrowIfNull(writer); + if (ReasoningInstructions != null && + ReasoningInstructions.Count != 0) + { + writer.WriteStartArray(); + foreach (var instruction in ReasoningInstructions) + { + writer.WriteValue(instruction); + } + writer.WriteEndArray(); + } + } + public static OpenApiAiReasoningInstructionsExtension Parse(IOpenApiAny source) + { + if (source is not OpenApiArray rawArray) throw new ArgumentOutOfRangeException(nameof(source)); + var result = new OpenApiAiReasoningInstructionsExtension(); + result.ReasoningInstructions.AddRange(rawArray.OfType().Select(x => x.Value)); + return result; + } +} diff --git a/src/Kiota.Builder/OpenApiExtensions/OpenApiAiRespondingInstructionsExtension.cs b/src/Kiota.Builder/OpenApiExtensions/OpenApiAiRespondingInstructionsExtension.cs new file mode 100644 index 0000000000..b8137ec225 --- /dev/null +++ b/src/Kiota.Builder/OpenApiExtensions/OpenApiAiRespondingInstructionsExtension.cs @@ -0,0 +1,38 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using Microsoft.OpenApi; +using Microsoft.OpenApi.Any; +using Microsoft.OpenApi.Interfaces; +using Microsoft.OpenApi.Writers; + +namespace Kiota.Builder.OpenApiExtensions; + +public class OpenApiAiRespondingInstructionsExtension : IOpenApiExtension +{ + public static string Name => "x-ai-responding-instructions"; +#pragma warning disable CA1002 // Do not expose generic lists + public List RespondingInstructions { get; init; } = []; +#pragma warning restore CA1002 // Do not expose generic lists + public void Write(IOpenApiWriter writer, OpenApiSpecVersion specVersion) + { + ArgumentNullException.ThrowIfNull(writer); + if (RespondingInstructions != null && + RespondingInstructions.Count != 0) + { + writer.WriteStartArray(); + foreach (var instruction in RespondingInstructions) + { + writer.WriteValue(instruction); + } + writer.WriteEndArray(); + } + } + public static OpenApiAiRespondingInstructionsExtension Parse(IOpenApiAny source) + { + if (source is not OpenApiArray rawArray) throw new ArgumentOutOfRangeException(nameof(source)); + var result = new OpenApiAiRespondingInstructionsExtension(); + result.RespondingInstructions.AddRange(rawArray.OfType().Select(x => x.Value)); + return result; + } +} diff --git a/src/Kiota.Builder/OpenApiExtensions/OpenApiDescriptionForModelExtension.cs b/src/Kiota.Builder/OpenApiExtensions/OpenApiDescriptionForModelExtension.cs new file mode 100644 index 0000000000..3e3acfa5bf --- /dev/null +++ b/src/Kiota.Builder/OpenApiExtensions/OpenApiDescriptionForModelExtension.cs @@ -0,0 +1,32 @@ +using System; +using Microsoft.OpenApi; +using Microsoft.OpenApi.Any; +using Microsoft.OpenApi.Interfaces; +using Microsoft.OpenApi.Writers; + +namespace Kiota.Builder.OpenApiExtensions; + +public class OpenApiDescriptionForModelExtension : IOpenApiExtension +{ + public static string Name => "x-ai-description"; + public string? Description + { + get; set; + } + public void Write(IOpenApiWriter writer, OpenApiSpecVersion specVersion) + { + ArgumentNullException.ThrowIfNull(writer); + if (!string.IsNullOrWhiteSpace(Description)) + { + writer.WriteValue(Description); + } + } + public static OpenApiDescriptionForModelExtension Parse(IOpenApiAny source) + { + if (source is not OpenApiString rawString) throw new ArgumentOutOfRangeException(nameof(source)); + return new OpenApiDescriptionForModelExtension + { + Description = rawString.Value + }; + } +} diff --git a/tests/Kiota.Builder.Tests/OpenApiExtensions/OpenApiAiReasoningInstructionsExtensionTests.cs b/tests/Kiota.Builder.Tests/OpenApiExtensions/OpenApiAiReasoningInstructionsExtensionTests.cs new file mode 100644 index 0000000000..169c477a1d --- /dev/null +++ b/tests/Kiota.Builder.Tests/OpenApiExtensions/OpenApiAiReasoningInstructionsExtensionTests.cs @@ -0,0 +1,56 @@ +using System; +using System.IO; +using System.Net.Http; +using System.Threading.Tasks; +using Kiota.Builder.Configuration; +using Kiota.Builder.OpenApiExtensions; +using Microsoft.Extensions.Logging; +using Microsoft.OpenApi.Any; +using Moq; +using Xunit; + +namespace Kiota.Builder.Tests.OpenApiExtensions; +public sealed class OpenApiAiReasoningInstructionsExtensionTests : IDisposable +{ + private readonly HttpClient _httpClient = new(); + public void Dispose() + { + _httpClient.Dispose(); + } + [Fact] + public void Parses() + { + var oaiValue = new OpenApiArray { + new OpenApiString("This is a description"), + new OpenApiString("This is a description 2"), + }; + var value = OpenApiAiReasoningInstructionsExtension.Parse(oaiValue); + Assert.NotNull(value); + Assert.Equal("This is a description", value.ReasoningInstructions[0]); + } + private readonly string TempDirectory = Path.Combine(Path.GetTempPath(), Path.GetRandomFileName()); + [Fact] + public async Task ParsesInDocument() + { + var documentContent = @"openapi: 3.0.0 +info: + title: Test + version: 1.0.0 + x-ai-reasoning-instructions: + - This is a description + - This is a description 2"; + Directory.CreateDirectory(TempDirectory); + var documentPath = Path.Combine(TempDirectory, "document.yaml"); + await File.WriteAllTextAsync(documentPath, documentContent); + var mockLogger = new Mock>(); + var documentDownloadService = new OpenApiDocumentDownloadService(_httpClient, mockLogger.Object); + var generationConfig = new GenerationConfiguration { OutputPath = TempDirectory }; + var (openApiDocumentStream, _) = await documentDownloadService.LoadStreamAsync(documentPath, generationConfig); + var document = await documentDownloadService.GetDocumentFromStreamAsync(openApiDocumentStream, generationConfig); + Assert.NotNull(document); + Assert.NotNull(document.Info); + Assert.True(document.Info.Extensions.TryGetValue(OpenApiAiReasoningInstructionsExtension.Name, out var descriptionExtension)); + Assert.IsType(descriptionExtension); + Assert.Equal("This is a description", ((OpenApiAiReasoningInstructionsExtension)descriptionExtension).ReasoningInstructions[0]); + } +} diff --git a/tests/Kiota.Builder.Tests/OpenApiExtensions/OpenApiAiRespondingInstructionsExtensionTests.cs b/tests/Kiota.Builder.Tests/OpenApiExtensions/OpenApiAiRespondingInstructionsExtensionTests.cs new file mode 100644 index 0000000000..0402527c03 --- /dev/null +++ b/tests/Kiota.Builder.Tests/OpenApiExtensions/OpenApiAiRespondingInstructionsExtensionTests.cs @@ -0,0 +1,56 @@ +using System; +using System.IO; +using System.Net.Http; +using System.Threading.Tasks; +using Kiota.Builder.Configuration; +using Kiota.Builder.OpenApiExtensions; +using Microsoft.Extensions.Logging; +using Microsoft.OpenApi.Any; +using Moq; +using Xunit; + +namespace Kiota.Builder.Tests.OpenApiExtensions; +public sealed class OpenApiAiRespondingInstructionsExtensionTests : IDisposable +{ + private readonly HttpClient _httpClient = new(); + public void Dispose() + { + _httpClient.Dispose(); + } + [Fact] + public void Parses() + { + var oaiValue = new OpenApiArray { + new OpenApiString("This is a description"), + new OpenApiString("This is a description 2"), + }; + var value = OpenApiAiRespondingInstructionsExtension.Parse(oaiValue); + Assert.NotNull(value); + Assert.Equal("This is a description", value.RespondingInstructions[0]); + } + private readonly string TempDirectory = Path.Combine(Path.GetTempPath(), Path.GetRandomFileName()); + [Fact] + public async Task ParsesInDocument() + { + var documentContent = @"openapi: 3.0.0 +info: + title: Test + version: 1.0.0 + x-ai-responding-instructions: + - This is a description + - This is a description 2"; + Directory.CreateDirectory(TempDirectory); + var documentPath = Path.Combine(TempDirectory, "document.yaml"); + await File.WriteAllTextAsync(documentPath, documentContent); + var mockLogger = new Mock>(); + var documentDownloadService = new OpenApiDocumentDownloadService(_httpClient, mockLogger.Object); + var generationConfig = new GenerationConfiguration { OutputPath = TempDirectory }; + var (openApiDocumentStream, _) = await documentDownloadService.LoadStreamAsync(documentPath, generationConfig); + var document = await documentDownloadService.GetDocumentFromStreamAsync(openApiDocumentStream, generationConfig); + Assert.NotNull(document); + Assert.NotNull(document.Info); + Assert.True(document.Info.Extensions.TryGetValue(OpenApiAiRespondingInstructionsExtension.Name, out var descriptionExtension)); + Assert.IsType(descriptionExtension); + Assert.Equal("This is a description", ((OpenApiAiRespondingInstructionsExtension)descriptionExtension).RespondingInstructions[0]); + } +} diff --git a/tests/Kiota.Builder.Tests/OpenApiExtensions/OpenApiDescriptionForModelExtensionTests.cs b/tests/Kiota.Builder.Tests/OpenApiExtensions/OpenApiDescriptionForModelExtensionTests.cs new file mode 100644 index 0000000000..cd7341f9c6 --- /dev/null +++ b/tests/Kiota.Builder.Tests/OpenApiExtensions/OpenApiDescriptionForModelExtensionTests.cs @@ -0,0 +1,51 @@ +using System; +using System.IO; +using System.Net.Http; +using System.Threading.Tasks; +using Kiota.Builder.Configuration; +using Kiota.Builder.OpenApiExtensions; +using Microsoft.Extensions.Logging; +using Microsoft.OpenApi.Any; +using Moq; +using Xunit; + +namespace Kiota.Builder.Tests.OpenApiExtensions; +public sealed class OpenApiDescriptionForModelExtensionTests : IDisposable +{ + private readonly HttpClient _httpClient = new(); + public void Dispose() + { + _httpClient.Dispose(); + } + [Fact] + public void Parses() + { + var oaiValue = new OpenApiString("This is a description"); + var value = OpenApiDescriptionForModelExtension.Parse(oaiValue); + Assert.NotNull(value); + Assert.Equal("This is a description", value.Description); + } + private readonly string TempDirectory = Path.Combine(Path.GetTempPath(), Path.GetRandomFileName()); + [Fact] + public async Task ParsesInDocument() + { + var documentContent = @"openapi: 3.0.0 +info: + title: Test + version: 1.0.0 + x-ai-description: This is a description"; + Directory.CreateDirectory(TempDirectory); + var documentPath = Path.Combine(TempDirectory, "document.yaml"); + await File.WriteAllTextAsync(documentPath, documentContent); + var mockLogger = new Mock>(); + var documentDownloadService = new OpenApiDocumentDownloadService(_httpClient, mockLogger.Object); + var generationConfig = new GenerationConfiguration { OutputPath = TempDirectory }; + var (openApiDocumentStream, _) = await documentDownloadService.LoadStreamAsync(documentPath, generationConfig); + var document = await documentDownloadService.GetDocumentFromStreamAsync(openApiDocumentStream, generationConfig); + Assert.NotNull(document); + Assert.NotNull(document.Info); + Assert.True(document.Info.Extensions.TryGetValue(OpenApiDescriptionForModelExtension.Name, out var descriptionExtension)); + Assert.IsType(descriptionExtension); + Assert.Equal("This is a description", ((OpenApiDescriptionForModelExtension)descriptionExtension).Description); + } +} From 29fad06a6133992879e5166da74dd97eef218a68 Mon Sep 17 00:00:00 2001 From: Vincent Biret Date: Wed, 27 Mar 2024 13:49:32 -0400 Subject: [PATCH 15/40] - overrides description for model by equivalent extension when present Signed-off-by: Vincent Biret --- .../Plugins/PluginsGenerationService.cs | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/src/Kiota.Builder/Plugins/PluginsGenerationService.cs b/src/Kiota.Builder/Plugins/PluginsGenerationService.cs index 5d70618cb6..4e79a602cf 100644 --- a/src/Kiota.Builder/Plugins/PluginsGenerationService.cs +++ b/src/Kiota.Builder/Plugins/PluginsGenerationService.cs @@ -7,6 +7,7 @@ using System.Threading.Tasks; using Kiota.Builder.Configuration; using Kiota.Builder.Extensions; +using Kiota.Builder.OpenApiExtensions; using Microsoft.OpenApi.Models; using Microsoft.OpenApi.Services; using Microsoft.Plugins.Manifest; @@ -31,13 +32,20 @@ public PluginsGenerationService(OpenApiDocument document, OpenApiUrlTreeNode ope public async Task GenerateManifestAsync(CancellationToken cancellationToken = default) { var (runtimes, functions) = GetRuntimesAndFunctionsFromTree(TreeNode); + var descriptionForHuman = OAIDocument.Info?.Description.CleanupXMLString() is string d && !string.IsNullOrEmpty(d) ? d : $"Description for {OAIDocument.Info?.Title.CleanupXMLString()}"; + var descriptionForModel = descriptionForHuman; + if (OAIDocument.Info is not null && + OAIDocument.Info.Extensions.TryGetValue(OpenApiDescriptionForModelExtension.Name, out var descriptionExtension) && + descriptionExtension is OpenApiDescriptionForModelExtension extension && + !string.IsNullOrEmpty(extension.Description)) + descriptionForModel = extension.Description.CleanupXMLString(); var pluginDocument = new ManifestDocument { SchemaVersion = "v2", NameForHuman = OAIDocument.Info?.Title.CleanupXMLString(), // TODO name for model - DescriptionForHuman = OAIDocument.Info?.Description.CleanupXMLString() is string d && !string.IsNullOrEmpty(d) ? d : $"Description for {OAIDocument.Info?.Title.CleanupXMLString()}", - DescriptionForModel = OAIDocument.Info?.Description.CleanupXMLString() is string e && !string.IsNullOrEmpty(e) ? e : $"Description for {OAIDocument.Info?.Title.CleanupXMLString()}", + DescriptionForHuman = descriptionForHuman, + DescriptionForModel = descriptionForModel, ContactEmail = OAIDocument.Info?.Contact?.Email, //TODO namespace //TODO logo From 7910deb00f10ca925a3111eea54004f192d2b86c Mon Sep 17 00:00:00 2001 From: Vincent Biret Date: Wed, 27 Mar 2024 14:31:48 -0400 Subject: [PATCH 16/40] - adds support for instructions Signed-off-by: Vincent Biret --- .../Plugins/PluginsGenerationService.cs | 38 ++++++++++++++++++- 1 file changed, 36 insertions(+), 2 deletions(-) diff --git a/src/Kiota.Builder/Plugins/PluginsGenerationService.cs b/src/Kiota.Builder/Plugins/PluginsGenerationService.cs index 4e79a602cf..24b134e3e3 100644 --- a/src/Kiota.Builder/Plugins/PluginsGenerationService.cs +++ b/src/Kiota.Builder/Plugins/PluginsGenerationService.cs @@ -43,7 +43,7 @@ descriptionExtension is OpenApiDescriptionForModelExtension extension && { SchemaVersion = "v2", NameForHuman = OAIDocument.Info?.Title.CleanupXMLString(), - // TODO name for model + // TODO name for model ??? DescriptionForHuman = descriptionForHuman, DescriptionForModel = descriptionForModel, ContactEmail = OAIDocument.Info?.Contact?.Email, @@ -108,7 +108,7 @@ descriptionExtension is OpenApiDescriptionForModelExtension extension && null), //TODO enums StringComparer.OrdinalIgnoreCase)), oasParameters.Where(static x => x.Required).Select(static x => x.Name).ToList()), - //TODO states with reasoning and instructions from OAS extensions + States = GetStatesFromOperation(operation), }); } } @@ -120,6 +120,40 @@ descriptionExtension is OpenApiDescriptionForModelExtension extension && } return (runtimes.ToArray(), functions.ToArray()); } + private static States? GetStatesFromOperation(OpenApiOperation openApiOperation) + { + return (GetStateFromExtension(openApiOperation, OpenApiAiReasoningInstructionsExtension.Name, static x => x.ReasoningInstructions), + GetStateFromExtension(openApiOperation, OpenApiAiRespondingInstructionsExtension.Name, static x => x.RespondingInstructions)) switch + { + (State reasoning, State responding) => new States + { + Reasoning = reasoning, + Responding = responding + }, + (State reasoning, _) => new States + { + Reasoning = reasoning + }, + (_, State responding) => new States + { + Responding = responding + }, + _ => null + }; + } + private static State? GetStateFromExtension(OpenApiOperation openApiOperation, string extensionName, Func> instructionsExtractor) + { + if (openApiOperation.Extensions.TryGetValue(extensionName, out var rExtRaw) && + rExtRaw is T rExt && + instructionsExtractor(rExt).Exists(static x => !string.IsNullOrEmpty(x))) + { + return new State + { + Instructions = instructionsExtractor(rExt).Where(static x => !string.IsNullOrEmpty(x)).Select(static x => x.CleanupXMLString()).ToList() + }; + } + return null; + } private static readonly HashSet scalarTypes = new(StringComparer.OrdinalIgnoreCase) { "string", "number", "integer", "boolean" }; //TODO validate this is right, in OAS integer are under type number for the json schema, but integer is ok for query parameters } From f661f68ef2c9c8a00296455ac3f5fa874b1be5de Mon Sep 17 00:00:00 2001 From: Vincent Biret Date: Wed, 27 Mar 2024 14:47:30 -0400 Subject: [PATCH 17/40] - adds copy of sliced description Signed-off-by: Vincent Biret --- .../Plugins/PluginsGenerationService.cs | 48 ++++++++++++------- 1 file changed, 31 insertions(+), 17 deletions(-) diff --git a/src/Kiota.Builder/Plugins/PluginsGenerationService.cs b/src/Kiota.Builder/Plugins/PluginsGenerationService.cs index 24b134e3e3..e5ff9436d7 100644 --- a/src/Kiota.Builder/Plugins/PluginsGenerationService.cs +++ b/src/Kiota.Builder/Plugins/PluginsGenerationService.cs @@ -10,6 +10,7 @@ using Kiota.Builder.OpenApiExtensions; using Microsoft.OpenApi.Models; using Microsoft.OpenApi.Services; +using Microsoft.OpenApi.Writers; using Microsoft.Plugins.Manifest; namespace Kiota.Builder.Plugins; @@ -29,9 +30,33 @@ public PluginsGenerationService(OpenApiDocument document, OpenApiUrlTreeNode ope Configuration = configuration; } private static readonly OpenAPIRuntimeComparer _openAPIRuntimeComparer = new(); + private const string ManifestFileName = "manifest.json"; + private const string DescriptionRelativePath = "./openapi.yml"; public async Task GenerateManifestAsync(CancellationToken cancellationToken = default) { - var (runtimes, functions) = GetRuntimesAndFunctionsFromTree(TreeNode); + var manifestOutputPath = Path.Combine(Configuration.OutputPath, ManifestFileName); + var directory = Path.GetDirectoryName(manifestOutputPath); + if (!string.IsNullOrEmpty(directory) && !Directory.Exists(directory)) + Directory.CreateDirectory(directory); + + var descriptionFullPath = Path.Combine(Configuration.OutputPath, DescriptionRelativePath); +#pragma warning disable CA2007 // Consider calling ConfigureAwait on the awaited task + await using var descriptionStream = File.Create(descriptionFullPath, 4096); + await using var fileWriter = new StreamWriter(descriptionStream); + var descriptionWriter = new OpenApiYamlWriter(fileWriter); + OAIDocument.SerializeAsV3(descriptionWriter); + descriptionWriter.Flush(); + + var pluginDocument = GetManifestDocument(DescriptionRelativePath); + await using var fileStream = File.Create(manifestOutputPath, 4096); + await using var writer = new Utf8JsonWriter(fileStream, new JsonWriterOptions { Indented = true }); +#pragma warning restore CA2007 // Consider calling ConfigureAwait on the awaited task + pluginDocument.Write(writer); + await writer.FlushAsync(cancellationToken).ConfigureAwait(false); + } + private ManifestDocument GetManifestDocument(string openApiDocumentPath) + { + var (runtimes, functions) = GetRuntimesAndFunctionsFromTree(TreeNode, openApiDocumentPath); var descriptionForHuman = OAIDocument.Info?.Description.CleanupXMLString() is string d && !string.IsNullOrEmpty(d) ? d : $"Description for {OAIDocument.Info?.Title.CleanupXMLString()}"; var descriptionForModel = descriptionForHuman; if (OAIDocument.Info is not null && @@ -39,7 +64,7 @@ public async Task GenerateManifestAsync(CancellationToken cancellationToken = de descriptionExtension is OpenApiDescriptionForModelExtension extension && !string.IsNullOrEmpty(extension.Description)) descriptionForModel = extension.Description.CleanupXMLString(); - var pluginDocument = new ManifestDocument + return new ManifestDocument { SchemaVersion = "v2", NameForHuman = OAIDocument.Info?.Title.CleanupXMLString(), @@ -47,7 +72,7 @@ descriptionExtension is OpenApiDescriptionForModelExtension extension && DescriptionForHuman = descriptionForHuman, DescriptionForModel = descriptionForModel, ContactEmail = OAIDocument.Info?.Contact?.Email, - //TODO namespace + Namespace = Configuration.ClientClassName, //TODO logo Runtimes = [.. runtimes .GroupBy(static x => x, _openAPIRuntimeComparer) @@ -60,19 +85,8 @@ descriptionExtension is OpenApiDescriptionForModelExtension extension && .OrderBy(static x => x.RunForFunctions[0], StringComparer.OrdinalIgnoreCase)], Functions = [.. functions.OrderBy(static x => x.Name, StringComparer.OrdinalIgnoreCase)] }; - var outputPath = Path.Combine(Configuration.OutputPath, "manifest.json"); - - var directory = Path.GetDirectoryName(outputPath); - if (!string.IsNullOrEmpty(directory) && !Directory.Exists(directory)) - Directory.CreateDirectory(directory); -#pragma warning disable CA2007 // Consider calling ConfigureAwait on the awaited task - await using var fileStream = File.Create(outputPath, 4096); - await using var writer = new Utf8JsonWriter(fileStream, new JsonWriterOptions { Indented = true }); -#pragma warning restore CA2007 // Consider calling ConfigureAwait on the awaited task - pluginDocument.Write(writer); - await writer.FlushAsync(cancellationToken).ConfigureAwait(false); } - private (OpenAPIRuntime[], Function[]) GetRuntimesAndFunctionsFromTree(OpenApiUrlTreeNode currentNode) + private (OpenAPIRuntime[], Function[]) GetRuntimesAndFunctionsFromTree(OpenApiUrlTreeNode currentNode, string openApiDocumentPath) { var runtimes = new List(); var functions = new List(); @@ -83,7 +97,7 @@ descriptionExtension is OpenApiDescriptionForModelExtension extension && runtimes.Add(new OpenAPIRuntime { Auth = new Auth("none"), - Spec = new Dictionary { { "url", "./openapi.yaml" } }, //TODO update from context once the slice copy is implemented + Spec = new Dictionary { { "url", openApiDocumentPath } }, RunForFunctions = [operation.OperationId] }); var oasParameters = operation.Parameters @@ -114,7 +128,7 @@ descriptionExtension is OpenApiDescriptionForModelExtension extension && } foreach (var node in currentNode.Children) { - var (childRuntimes, childFunctions) = GetRuntimesAndFunctionsFromTree(node.Value); + var (childRuntimes, childFunctions) = GetRuntimesAndFunctionsFromTree(node.Value, openApiDocumentPath); runtimes.AddRange(childRuntimes); functions.AddRange(childFunctions); } From 5e4c4d09a7ef8116f1e9d023e0943ca063b9dd3d Mon Sep 17 00:00:00 2001 From: Vincent Biret Date: Wed, 27 Mar 2024 14:49:09 -0400 Subject: [PATCH 18/40] - updates output directory for descriptions copies Signed-off-by: Vincent Biret --- .../WorkspaceManagement/DescriptionStorageService.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Kiota.Builder/WorkspaceManagement/DescriptionStorageService.cs b/src/Kiota.Builder/WorkspaceManagement/DescriptionStorageService.cs index 170f1f2bd0..ed66d1e1dd 100644 --- a/src/Kiota.Builder/WorkspaceManagement/DescriptionStorageService.cs +++ b/src/Kiota.Builder/WorkspaceManagement/DescriptionStorageService.cs @@ -9,7 +9,7 @@ namespace Kiota.Builder.WorkspaceManagement; public class DescriptionStorageService { public const string KiotaDirectorySegment = ".kiota"; - internal const string DescriptionsSubDirectoryRelativePath = $"{KiotaDirectorySegment}/clients"; + internal const string DescriptionsSubDirectoryRelativePath = $"{KiotaDirectorySegment}/descriptions"; private readonly string TargetDirectory; public DescriptionStorageService(string targetDirectory) { From f63190cd0100d4c7c8f149f6f6b0d58260d2cf25 Mon Sep 17 00:00:00 2001 From: Vincent Biret Date: Thu, 28 Mar 2024 07:44:05 -0400 Subject: [PATCH 19/40] - replaces project reference by package --- src/Kiota.Builder/Kiota.Builder.csproj | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/Kiota.Builder/Kiota.Builder.csproj b/src/Kiota.Builder/Kiota.Builder.csproj index 5fec13e9b1..6fcdb878cc 100644 --- a/src/Kiota.Builder/Kiota.Builder.csproj +++ b/src/Kiota.Builder/Kiota.Builder.csproj @@ -47,9 +47,11 @@ + - + @@ -57,4 +59,4 @@ - + \ No newline at end of file From 39c092aa91246659ec852aa3c37b8960eb6b7ef2 Mon Sep 17 00:00:00 2001 From: Vincent Biret Date: Thu, 28 Mar 2024 08:00:15 -0400 Subject: [PATCH 20/40] - adds validation error for OpenAI plugins Signed-off-by: Vincent Biret --- src/Kiota.Builder/KiotaBuilder.cs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/Kiota.Builder/KiotaBuilder.cs b/src/Kiota.Builder/KiotaBuilder.cs index 671c5c1713..fef6de6b76 100644 --- a/src/Kiota.Builder/KiotaBuilder.cs +++ b/src/Kiota.Builder/KiotaBuilder.cs @@ -231,6 +231,8 @@ public async Task GeneratePluginAsync(CancellationToken cancellationToken) { return await GenerateConsumerAsync(async (sw, stepId, openApiTree, CancellationToken) => { + if (config.PluginTypes.Contains(PluginType.OpenAI)) + throw new NotImplementedException("The OpenAI plugin type is not supported for generation"); if (openApiDocument is null || openApiTree is null) throw new InvalidOperationException("The OpenAPI document and the URL tree must be loaded before generating the plugins"); // generate plugin From 74d62eae7c28b29991523dd563ddf527e35a1487 Mon Sep 17 00:00:00 2001 From: Vincent Biret Date: Thu, 28 Mar 2024 08:28:06 -0400 Subject: [PATCH 21/40] - implements plugin edit command --- src/kiota/Handlers/Plugin/EditHandler.cs | 121 +++++++++++++++++++++++ src/kiota/KiotaPluginCommands.cs | 12 ++- 2 files changed, 132 insertions(+), 1 deletion(-) create mode 100644 src/kiota/Handlers/Plugin/EditHandler.cs diff --git a/src/kiota/Handlers/Plugin/EditHandler.cs b/src/kiota/Handlers/Plugin/EditHandler.cs new file mode 100644 index 0000000000..ef78b19c29 --- /dev/null +++ b/src/kiota/Handlers/Plugin/EditHandler.cs @@ -0,0 +1,121 @@ +using System.CommandLine; +using System.CommandLine.Invocation; +using System.Text.Json; +using Kiota.Builder; +using Kiota.Builder.Configuration; +using Kiota.Builder.Extensions; +using Kiota.Builder.WorkspaceManagement; +using Microsoft.Extensions.Logging; + +namespace kiota.Handlers.Plugin; + +internal class EditHandler : BaseKiotaCommandHandler +{ + public required Option ClassOption + { + get; init; + } + public required Option OutputOption + { + get; init; + } + public required Option DescriptionOption + { + get; init; + } + public required Option> IncludePatternsOption + { + get; init; + } + public required Option> ExcludePatternsOption + { + get; init; + } + public required Option SkipGenerationOption + { + get; init; + } + public required Option> PluginTypesOption + { + get; init; + } + + public override async Task InvokeAsync(InvocationContext context) + { + string output = context.ParseResult.GetValueForOption(OutputOption) ?? string.Empty; + List? pluginTypes = context.ParseResult.GetValueForOption(PluginTypesOption); + string openapi = context.ParseResult.GetValueForOption(DescriptionOption) ?? string.Empty; + bool skipGeneration = context.ParseResult.GetValueForOption(SkipGenerationOption); + string className = context.ParseResult.GetValueForOption(ClassOption) ?? string.Empty; + List? includePatterns = context.ParseResult.GetValueForOption(IncludePatternsOption); + List? excludePatterns = context.ParseResult.GetValueForOption(ExcludePatternsOption); + CancellationToken cancellationToken = context.BindingContext.GetService(typeof(CancellationToken)) is CancellationToken token ? token : CancellationToken.None; + + Configuration.Generation.SkipGeneration = skipGeneration; + Configuration.Generation.Operation = ConsumerOperation.Edit; + + var (loggerFactory, logger) = GetLoggerAndFactory(context, Configuration.Generation.OutputPath); + using (loggerFactory) + { + await CheckForNewVersionAsync(logger, cancellationToken).ConfigureAwait(false); + logger.AppendInternalTracing(); + logger.LogTrace("configuration: {configuration}", JsonSerializer.Serialize(Configuration, KiotaConfigurationJsonContext.Default.KiotaConfiguration)); + + try + { + var workspaceStorageService = new WorkspaceConfigurationStorageService(Directory.GetCurrentDirectory()); + var (config, _) = await workspaceStorageService.GetWorkspaceConfigurationAsync(cancellationToken).ConfigureAwait(false); + if (config == null) + { + DisplayError("The workspace configuration is missing, please run the init command first."); + return 1; + } + if (!config.Plugins.TryGetValue(className, out var pluginConfiguration)) + { + DisplayError($"No plugin found with the provided name {className}"); + return 1; + } + pluginConfiguration.UpdateGenerationConfigurationFromApiPluginConfiguration(Configuration.Generation, className); + AssignIfNotNullOrEmpty(output, (c, s) => c.OutputPath = s); + AssignIfNotNullOrEmpty(openapi, (c, s) => c.OpenAPIFilePath = s); + AssignIfNotNullOrEmpty(className, (c, s) => c.ClientClassName = s); + if (includePatterns is { Count: > 0 }) + Configuration.Generation.IncludePatterns = includePatterns.Select(static x => x.TrimQuotes()).ToHashSet(StringComparer.OrdinalIgnoreCase); + if (excludePatterns is { Count: > 0 }) + Configuration.Generation.ExcludePatterns = excludePatterns.Select(static x => x.TrimQuotes()).ToHashSet(StringComparer.OrdinalIgnoreCase); + if (pluginTypes is { Count: > 0 }) + Configuration.Generation.PluginTypes = pluginTypes.ToHashSet(); + + DefaultSerializersAndDeserializers(Configuration.Generation); + var builder = new KiotaBuilder(logger, Configuration.Generation, httpClient, true); + var result = await builder.GeneratePluginAsync(cancellationToken).ConfigureAwait(false); + if (result) + DisplaySuccess("Generation completed successfully"); + else if (skipGeneration) + { + DisplaySuccess("Generation skipped as --skip-generation was passed"); + DisplayGenerateCommandHint(); + } + else + { + DisplayWarning("Generation skipped as no changes were detected"); + DisplayCleanHint("plugin generate", "--refresh"); + } + var manifestPath = $"{GetAbsolutePath(Path.Combine(WorkspaceConfigurationStorageService.KiotaDirectorySegment, WorkspaceConfigurationStorageService.ManifestFileName))}#{Configuration.Generation.ClientClassName}"; + DisplayInfoHint(Configuration.Generation.Language, string.Empty, manifestPath); + DisplayGenerateAdvancedHint(includePatterns ?? [], excludePatterns ?? [], string.Empty, manifestPath, "plugin edit"); + return 0; + } + catch (Exception ex) + { +#if DEBUG + logger.LogCritical(ex, "error adding the plugin: {exceptionMessage}", ex.Message); + throw; // so debug tools go straight to the source of the exception when attached +#else + logger.LogCritical("error adding the plugin: {exceptionMessage}", ex.Message); + return 1; +#endif + } + } + } +} diff --git a/src/kiota/KiotaPluginCommands.cs b/src/kiota/KiotaPluginCommands.cs index a8124129ba..2b15c34af9 100644 --- a/src/kiota/KiotaPluginCommands.cs +++ b/src/kiota/KiotaPluginCommands.cs @@ -91,7 +91,17 @@ public static Command GetEditCommand() pluginTypes, //TODO overlay when we have support for it in OAI.net }; - //TODO map handler + command.Handler = new EditHandler + { + ClassOption = pluginNameOption, + OutputOption = outputOption, + PluginTypesOption = pluginTypes, + DescriptionOption = descriptionOption, + IncludePatternsOption = includePatterns, + ExcludePatternsOption = excludePatterns, + SkipGenerationOption = skipGenerationOption, + LogLevelOption = logLevelOption, + }; return command; } public static Command GetRemoveCommand() From d564e4a08bdcad5bfe948f6285730a2cd2a0edf7 Mon Sep 17 00:00:00 2001 From: Vincent Biret Date: Thu, 28 Mar 2024 08:48:27 -0400 Subject: [PATCH 22/40] - adds missing unit tests for serialization of extensions Signed-off-by: Vincent Biret --- ...piAiReasoningInstructionsExtensionTests.cs | 20 +++++++++++++++++++ ...iAiRespondingInstructionsExtensionTests.cs | 20 +++++++++++++++++++ ...penApiDescriptionForModelExtensionTests.cs | 17 ++++++++++++++++ .../OpenApiKiotaExtensionTests.cs | 6 +++--- 4 files changed, 60 insertions(+), 3 deletions(-) diff --git a/tests/Kiota.Builder.Tests/OpenApiExtensions/OpenApiAiReasoningInstructionsExtensionTests.cs b/tests/Kiota.Builder.Tests/OpenApiExtensions/OpenApiAiReasoningInstructionsExtensionTests.cs index 169c477a1d..1a4d716b51 100644 --- a/tests/Kiota.Builder.Tests/OpenApiExtensions/OpenApiAiReasoningInstructionsExtensionTests.cs +++ b/tests/Kiota.Builder.Tests/OpenApiExtensions/OpenApiAiReasoningInstructionsExtensionTests.cs @@ -5,7 +5,9 @@ using Kiota.Builder.Configuration; using Kiota.Builder.OpenApiExtensions; using Microsoft.Extensions.Logging; +using Microsoft.OpenApi; using Microsoft.OpenApi.Any; +using Microsoft.OpenApi.Writers; using Moq; using Xunit; @@ -53,4 +55,22 @@ public async Task ParsesInDocument() Assert.IsType(descriptionExtension); Assert.Equal("This is a description", ((OpenApiAiReasoningInstructionsExtension)descriptionExtension).ReasoningInstructions[0]); } + [Fact] + public void Serializes() + { + var value = new OpenApiAiReasoningInstructionsExtension + { + ReasoningInstructions = [ + "This is a description", + "This is a description 2", + ] + }; + using var sWriter = new StringWriter(); + OpenApiJsonWriter writer = new(sWriter, new OpenApiJsonWriterSettings { Terse = true }); + + + value.Write(writer, OpenApiSpecVersion.OpenApi3_0); + var result = sWriter.ToString(); + Assert.Equal("[\"This is a description\",\"This is a description 2\"]", result); + } } diff --git a/tests/Kiota.Builder.Tests/OpenApiExtensions/OpenApiAiRespondingInstructionsExtensionTests.cs b/tests/Kiota.Builder.Tests/OpenApiExtensions/OpenApiAiRespondingInstructionsExtensionTests.cs index 0402527c03..ba82ff57e7 100644 --- a/tests/Kiota.Builder.Tests/OpenApiExtensions/OpenApiAiRespondingInstructionsExtensionTests.cs +++ b/tests/Kiota.Builder.Tests/OpenApiExtensions/OpenApiAiRespondingInstructionsExtensionTests.cs @@ -5,7 +5,9 @@ using Kiota.Builder.Configuration; using Kiota.Builder.OpenApiExtensions; using Microsoft.Extensions.Logging; +using Microsoft.OpenApi; using Microsoft.OpenApi.Any; +using Microsoft.OpenApi.Writers; using Moq; using Xunit; @@ -53,4 +55,22 @@ public async Task ParsesInDocument() Assert.IsType(descriptionExtension); Assert.Equal("This is a description", ((OpenApiAiRespondingInstructionsExtension)descriptionExtension).RespondingInstructions[0]); } + [Fact] + public void Serializes() + { + var value = new OpenApiAiRespondingInstructionsExtension + { + RespondingInstructions = [ + "This is a description", + "This is a description 2", + ] + }; + using var sWriter = new StringWriter(); + OpenApiJsonWriter writer = new(sWriter, new OpenApiJsonWriterSettings { Terse = true }); + + + value.Write(writer, OpenApiSpecVersion.OpenApi3_0); + var result = sWriter.ToString(); + Assert.Equal("[\"This is a description\",\"This is a description 2\"]", result); + } } diff --git a/tests/Kiota.Builder.Tests/OpenApiExtensions/OpenApiDescriptionForModelExtensionTests.cs b/tests/Kiota.Builder.Tests/OpenApiExtensions/OpenApiDescriptionForModelExtensionTests.cs index cd7341f9c6..f66cc5e28e 100644 --- a/tests/Kiota.Builder.Tests/OpenApiExtensions/OpenApiDescriptionForModelExtensionTests.cs +++ b/tests/Kiota.Builder.Tests/OpenApiExtensions/OpenApiDescriptionForModelExtensionTests.cs @@ -5,7 +5,9 @@ using Kiota.Builder.Configuration; using Kiota.Builder.OpenApiExtensions; using Microsoft.Extensions.Logging; +using Microsoft.OpenApi; using Microsoft.OpenApi.Any; +using Microsoft.OpenApi.Writers; using Moq; using Xunit; @@ -48,4 +50,19 @@ public async Task ParsesInDocument() Assert.IsType(descriptionExtension); Assert.Equal("This is a description", ((OpenApiDescriptionForModelExtension)descriptionExtension).Description); } + [Fact] + public void Serializes() + { + var value = new OpenApiDescriptionForModelExtension + { + Description = "This is a description", + }; + using var sWriter = new StringWriter(); + OpenApiJsonWriter writer = new(sWriter, new OpenApiJsonWriterSettings { Terse = true }); + + + value.Write(writer, OpenApiSpecVersion.OpenApi3_0); + var result = sWriter.ToString(); + Assert.Equal("\"This is a description\"", result); + } } diff --git a/tests/Kiota.Builder.Tests/OpenApiExtensions/OpenApiKiotaExtensionTests.cs b/tests/Kiota.Builder.Tests/OpenApiExtensions/OpenApiKiotaExtensionTests.cs index 56bac02da8..27096a9862 100644 --- a/tests/Kiota.Builder.Tests/OpenApiExtensions/OpenApiKiotaExtensionTests.cs +++ b/tests/Kiota.Builder.Tests/OpenApiExtensions/OpenApiKiotaExtensionTests.cs @@ -36,13 +36,13 @@ public void Serializes() } }, }; - using TextWriter sWriter = new StringWriter(); - OpenApiJsonWriter writer = new(sWriter); + using var sWriter = new StringWriter(); + OpenApiJsonWriter writer = new(sWriter, new OpenApiJsonWriterSettings { Terse = true }); value.Write(writer, OpenApiSpecVersion.OpenApi3_0); var result = sWriter.ToString(); - Assert.Equal("{\n \"languagesInformation\": {\n \"CSharp\": {\n \"maturityLevel\": \"Preview\",\n \"dependencyInstallCommand\": \"dotnet add package\",\n \"dependencies\": [\n {\n \"name\": \"Microsoft.Graph.Core\",\n \"version\": \"1.0.0\"\n }\n ],\n \"clientClassName\": \"GraphServiceClient\",\n \"clientNamespaceName\": \"Microsoft.Graph\",\n \"structuredMimeTypes\": [\n \"application/json\",\n \"application/xml\"\n ]\n }\n }\n}", result); + Assert.Equal("{\"languagesInformation\":{\"CSharp\":{\"maturityLevel\":\"Preview\",\"dependencyInstallCommand\":\"dotnet add package\",\"dependencies\":[{\"name\":\"Microsoft.Graph.Core\",\"version\":\"1.0.0\"}],\"clientClassName\":\"GraphServiceClient\",\"clientNamespaceName\":\"Microsoft.Graph\",\"structuredMimeTypes\":[\"application/json\",\"application/xml\"]}}}", result); } [Fact] public void Parses() From 61da72495788b3c57525eb11d43ae3cb86a04637 Mon Sep 17 00:00:00 2001 From: Vincent Biret Date: Thu, 28 Mar 2024 09:12:47 -0400 Subject: [PATCH 23/40] - adds unit tests for plugin removal Signed-off-by: Vincent Biret --- .../WorkspaceManagementService.cs | 55 +++++++++---------- .../WorkspaceManagementServiceTests.cs | 20 +++++++ 2 files changed, 46 insertions(+), 29 deletions(-) diff --git a/src/Kiota.Builder/WorkspaceManagement/WorkspaceManagementService.cs b/src/Kiota.Builder/WorkspaceManagement/WorkspaceManagementService.cs index 98b51724c2..6a78b05806 100644 --- a/src/Kiota.Builder/WorkspaceManagement/WorkspaceManagementService.cs +++ b/src/Kiota.Builder/WorkspaceManagement/WorkspaceManagementService.cs @@ -153,46 +153,43 @@ public async Task ShouldGenerateAsync(GenerationConfiguration inputConfig, return null; return await descriptionStorageService.GetDescriptionAsync(clientName, new Uri(inputPath).GetFileExtension(), cancellationToken).ConfigureAwait(false); } - public async Task RemoveClientAsync(string clientName, bool cleanOutput = false, CancellationToken cancellationToken = default) + public Task RemoveClientAsync(string clientName, bool cleanOutput = false, CancellationToken cancellationToken = default) { - await RemoveConsumerInternalAsync(clientName, - wsConfig => - { - if (cleanOutput && wsConfig.Clients.TryGetValue(clientName, out var clientConfig) && Directory.Exists(clientConfig.OutputPath)) - Directory.Delete(clientConfig.OutputPath, true); - - if (!wsConfig.Clients.Remove(clientName)) - throw new InvalidOperationException($"The client {clientName} was not found in the configuration"); - }, - cleanOutput, cancellationToken).ConfigureAwait(false); + return RemoveConsumerInternalAsync(clientName, + static wsConfig => wsConfig.Clients, + cleanOutput, + "client", + cancellationToken + ); } - public async Task RemovePluginAsync(string clientName, bool cleanOutput = false, CancellationToken cancellationToken = default) + public Task RemovePluginAsync(string clientName, bool cleanOutput = false, CancellationToken cancellationToken = default) { - await RemoveConsumerInternalAsync(clientName, - wsConfig => - { - if (cleanOutput && wsConfig.Plugins.TryGetValue(clientName, out var pluginConfig) && Directory.Exists(pluginConfig.OutputPath)) - Directory.Delete(pluginConfig.OutputPath, true); - - if (!wsConfig.Plugins.Remove(clientName)) - throw new InvalidOperationException($"The client {clientName} was not found in the configuration"); - }, - cleanOutput, cancellationToken).ConfigureAwait(false); + return RemoveConsumerInternalAsync(clientName, + static wsConfig => wsConfig.Plugins, + cleanOutput, + "plugin", + cancellationToken + ); } - private async Task RemoveConsumerInternalAsync(string clientName, Action consumerRemoval, bool cleanOutput = false, CancellationToken cancellationToken = default) + private async Task RemoveConsumerInternalAsync(string consumerName, Func> consumerRetrieval, bool cleanOutput, string consumerDisplayName, CancellationToken cancellationToken) where T : BaseApiConsumerConfiguration { if (!UseKiotaConfig) - throw new InvalidOperationException("Cannot remove a client in lock mode"); + throw new InvalidOperationException($"Cannot remove a {consumerDisplayName} in lock mode"); var (wsConfig, manifest) = await workspaceConfigurationStorageService.GetWorkspaceConfigurationAsync(cancellationToken).ConfigureAwait(false); if (wsConfig is null) - throw new InvalidOperationException("Cannot remove a client without a configuration"); + throw new InvalidOperationException($"Cannot remove a {consumerDisplayName} without a configuration"); + + var consumers = consumerRetrieval(wsConfig); + if (cleanOutput && consumers.TryGetValue(consumerName, out var consumerConfig) && Directory.Exists(consumerConfig.OutputPath)) + Directory.Delete(consumerConfig.OutputPath, true); - consumerRemoval(wsConfig); + if (!consumers.Remove(consumerName)) + throw new InvalidOperationException($"The {consumerDisplayName} {consumerName} was not found in the configuration"); - manifest?.ApiDependencies.Remove(clientName); + manifest?.ApiDependencies.Remove(consumerName); await workspaceConfigurationStorageService.UpdateWorkspaceConfigurationAsync(wsConfig, manifest, cancellationToken).ConfigureAwait(false); - descriptionStorageService.RemoveDescription(clientName); - if (wsConfig.AnyConsumerPresent) + descriptionStorageService.RemoveDescription(consumerName); + if (!wsConfig.AnyConsumerPresent) descriptionStorageService.Clean(); } private static readonly JsonSerializerOptions options = new() diff --git a/tests/Kiota.Builder.Tests/WorkspaceManagement/WorkspaceManagementServiceTests.cs b/tests/Kiota.Builder.Tests/WorkspaceManagement/WorkspaceManagementServiceTests.cs index e00e61df84..584bc57c06 100644 --- a/tests/Kiota.Builder.Tests/WorkspaceManagement/WorkspaceManagementServiceTests.cs +++ b/tests/Kiota.Builder.Tests/WorkspaceManagement/WorkspaceManagementServiceTests.cs @@ -94,6 +94,26 @@ public async Task RemovesAClient() Assert.False(result); } [Fact] + public async Task RemovesAPlugin() + { + var mockLogger = Mock.Of(); + Directory.CreateDirectory(tempPath); + var service = new WorkspaceManagementService(mockLogger, httpClient, true, tempPath); + var configuration = new GenerationConfiguration + { + ClientClassName = "clientName", + OutputPath = tempPath, + OpenAPIFilePath = Path.Combine(tempPath, "openapi.yaml"), + ApiRootUrl = "https://graph.microsoft.com", + PluginTypes = [PluginType.APIManifest], + }; + Directory.CreateDirectory(tempPath); + await service.UpdateStateFromConfigurationAsync(configuration, "foo", [], Stream.Null); + await service.RemovePluginAsync("clientName"); + var result = await service.IsConsumerPresent("clientName"); + Assert.False(result); + } + [Fact] public async Task FailsOnMigrateWithoutKiotaConfigMode() { var mockLogger = Mock.Of(); From 3aa5ef1048bc6dde5ff5618b34bb4630a93407e1 Mon Sep 17 00:00:00 2001 From: Vincent Biret Date: Thu, 28 Mar 2024 09:17:46 -0400 Subject: [PATCH 24/40] - code linting Signed-off-by: Vincent Biret --- .../WorkspaceManagement/BaseApiConsumerConfiguration.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Kiota.Builder/WorkspaceManagement/BaseApiConsumerConfiguration.cs b/src/Kiota.Builder/WorkspaceManagement/BaseApiConsumerConfiguration.cs index b619b95f6a..e2a9487f5e 100644 --- a/src/Kiota.Builder/WorkspaceManagement/BaseApiConsumerConfiguration.cs +++ b/src/Kiota.Builder/WorkspaceManagement/BaseApiConsumerConfiguration.cs @@ -10,11 +10,11 @@ namespace Kiota.Builder.WorkspaceManagement; #pragma warning disable CA2227 // Collection properties should be read only public abstract class BaseApiConsumerConfiguration { - internal BaseApiConsumerConfiguration() + private protected BaseApiConsumerConfiguration() { } - internal BaseApiConsumerConfiguration(GenerationConfiguration config) + private protected BaseApiConsumerConfiguration(GenerationConfiguration config) { ArgumentNullException.ThrowIfNull(config); DescriptionLocation = config.OpenAPIFilePath; From 1c06fd963cef6d80216cb79d423bdb106e8b349b Mon Sep 17 00:00:00 2001 From: Vincent Biret Date: Thu, 28 Mar 2024 09:55:21 -0400 Subject: [PATCH 25/40] - adds unit tests for plugin configuration --- .../ApiPluginConfigurationTests.cs | 48 +++++++++++++++++++ 1 file changed, 48 insertions(+) create mode 100644 tests/Kiota.Builder.Tests/WorkspaceManagement/ApiPluginConfigurationTests.cs diff --git a/tests/Kiota.Builder.Tests/WorkspaceManagement/ApiPluginConfigurationTests.cs b/tests/Kiota.Builder.Tests/WorkspaceManagement/ApiPluginConfigurationTests.cs new file mode 100644 index 0000000000..e9c51e6fa3 --- /dev/null +++ b/tests/Kiota.Builder.Tests/WorkspaceManagement/ApiPluginConfigurationTests.cs @@ -0,0 +1,48 @@ +using System; +using System.Linq; +using Kiota.Builder.Configuration; +using Kiota.Builder.WorkspaceManagement; +using Xunit; + +namespace Kiota.Builder.Tests.WorkspaceManagement; +public sealed class ApiPluginConfigurationTests +{ + [Fact] + public void Defensive() + { + Assert.Throws(() => new ApiPluginConfiguration(null)); + } + [Fact] + public void CopiesPluginTypesFromConfiguration() + { + var generationConfig = new GenerationConfiguration + { + PluginTypes = [PluginType.APIManifest] + }; + var apiPluginConfig = new ApiPluginConfiguration(generationConfig); + Assert.NotNull(apiPluginConfig); + Assert.Contains("APIManifest", apiPluginConfig.Types); + } + [Fact] + public void Clones() + { + var apiPluginConfig = new ApiPluginConfiguration + { + Types = ["APIManifest"] + }; + var cloned = (ApiPluginConfiguration)apiPluginConfig.Clone(); + Assert.NotNull(cloned); + Assert.Equal(apiPluginConfig.Types, cloned.Types); + } + [Fact] + public void UpdateGenerationConfigurationFromPluginConfiguration() + { + var generationConfig = new GenerationConfiguration(); + var apiPluginConfig = new ApiPluginConfiguration + { + Types = ["APIManifest"] + }; + apiPluginConfig.UpdateGenerationConfigurationFromApiPluginConfiguration(generationConfig, "Foo"); + Assert.Contains(PluginType.APIManifest, generationConfig.PluginTypes); + } +} From 6a71f06691a4f8a36c9798ba9934f3446eab29f0 Mon Sep 17 00:00:00 2001 From: Vincent Biret Date: Thu, 28 Mar 2024 10:19:59 -0400 Subject: [PATCH 26/40] - adds unit tests for plugin comparer --- .../ApiClientConfigurationComparer.cs | 27 +++++++------------ .../ApiPluginConfigurationComparer.cs | 17 +++--------- .../BaseApiConsumerConfigurationComparer.cs | 25 +++++++++++++++++ .../ApiClientConfigurationComparerTests.cs | 2 +- .../ApiPluginConfigurationComparerTests.cs | 14 ++++++++++ 5 files changed, 53 insertions(+), 32 deletions(-) create mode 100644 src/Kiota.Builder/WorkspaceManagement/BaseApiConsumerConfigurationComparer.cs create mode 100644 tests/Kiota.Builder.Tests/WorkspaceManagement/ApiPluginConfigurationComparerTests.cs diff --git a/src/Kiota.Builder/WorkspaceManagement/ApiClientConfigurationComparer.cs b/src/Kiota.Builder/WorkspaceManagement/ApiClientConfigurationComparer.cs index c1503a4ded..e70537639d 100644 --- a/src/Kiota.Builder/WorkspaceManagement/ApiClientConfigurationComparer.cs +++ b/src/Kiota.Builder/WorkspaceManagement/ApiClientConfigurationComparer.cs @@ -1,5 +1,4 @@ using System; -using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; using System.Linq; using Kiota.Builder.Lock; @@ -8,29 +7,21 @@ namespace Kiota.Builder.WorkspaceManagement; /// /// Compares two instances. /// -public class ApiClientConfigurationComparer : IEqualityComparer +public class ApiClientConfigurationComparer : BaseApiConsumerConfigurationComparer { private static readonly StringIEnumerableDeepComparer _stringIEnumerableDeepComparer = new(); /// - public bool Equals(ApiClientConfiguration? x, ApiClientConfiguration? y) - { - return x == null && y == null || x != null && y != null && GetHashCode(x) == GetHashCode(y); - } - /// - public int GetHashCode([DisallowNull] ApiClientConfiguration obj) + public override int GetHashCode([DisallowNull] ApiClientConfiguration obj) { if (obj == null) return 0; return _stringIEnumerableDeepComparer.GetHashCode(obj.DisabledValidationRules?.Order(StringComparer.OrdinalIgnoreCase) ?? Enumerable.Empty()) * 37 + - (string.IsNullOrEmpty(obj.DescriptionLocation) ? 0 : obj.DescriptionLocation.GetHashCode(StringComparison.OrdinalIgnoreCase)) * 31 + - (string.IsNullOrEmpty(obj.OutputPath) ? 0 : obj.OutputPath.GetHashCode(StringComparison.OrdinalIgnoreCase)) * 19 + - (string.IsNullOrEmpty(obj.ClientNamespaceName) ? 0 : obj.ClientNamespaceName.GetHashCode(StringComparison.OrdinalIgnoreCase)) * 23 + - (string.IsNullOrEmpty(obj.Language) ? 0 : obj.Language.GetHashCode(StringComparison.OrdinalIgnoreCase)) * 19 + - obj.ExcludeBackwardCompatible.GetHashCode() * 17 + - obj.UsesBackingStore.GetHashCode() * 13 + - obj.IncludeAdditionalData.GetHashCode() * 7 + - _stringIEnumerableDeepComparer.GetHashCode(obj.StructuredMimeTypes?.Order(StringComparer.OrdinalIgnoreCase) ?? Enumerable.Empty()) * 5 + - _stringIEnumerableDeepComparer.GetHashCode(obj.IncludePatterns?.Order(StringComparer.OrdinalIgnoreCase) ?? Enumerable.Empty()) * 3 + - _stringIEnumerableDeepComparer.GetHashCode(obj.ExcludePatterns?.Order(StringComparer.OrdinalIgnoreCase) ?? Enumerable.Empty()) * 2; + (string.IsNullOrEmpty(obj.ClientNamespaceName) ? 0 : obj.ClientNamespaceName.GetHashCode(StringComparison.OrdinalIgnoreCase)) * 31 + + (string.IsNullOrEmpty(obj.Language) ? 0 : obj.Language.GetHashCode(StringComparison.OrdinalIgnoreCase)) * 23 + + obj.ExcludeBackwardCompatible.GetHashCode() * 19 + + obj.UsesBackingStore.GetHashCode() * 17 + + obj.IncludeAdditionalData.GetHashCode() * 13 + + _stringIEnumerableDeepComparer.GetHashCode(obj.StructuredMimeTypes?.Order(StringComparer.OrdinalIgnoreCase) ?? Enumerable.Empty()) * 11 + + base.GetHashCode(obj); } } diff --git a/src/Kiota.Builder/WorkspaceManagement/ApiPluginConfigurationComparer.cs b/src/Kiota.Builder/WorkspaceManagement/ApiPluginConfigurationComparer.cs index 359ea1a3fe..44f1366325 100644 --- a/src/Kiota.Builder/WorkspaceManagement/ApiPluginConfigurationComparer.cs +++ b/src/Kiota.Builder/WorkspaceManagement/ApiPluginConfigurationComparer.cs @@ -1,5 +1,4 @@ using System; -using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; using System.Linq; using Kiota.Builder.Lock; @@ -8,23 +7,15 @@ namespace Kiota.Builder.WorkspaceManagement; /// /// Compares two instances. /// -public class ApiPluginConfigurationComparer : IEqualityComparer +public class ApiPluginConfigurationComparer : BaseApiConsumerConfigurationComparer { private static readonly StringIEnumerableDeepComparer _stringIEnumerableDeepComparer = new(); /// - public bool Equals(ApiPluginConfiguration? x, ApiPluginConfiguration? y) - { - return x == null && y == null || x != null && y != null && GetHashCode(x) == GetHashCode(y); - } - /// - public int GetHashCode([DisallowNull] ApiPluginConfiguration obj) + public override int GetHashCode([DisallowNull] ApiPluginConfiguration obj) { if (obj == null) return 0; return - (string.IsNullOrEmpty(obj.DescriptionLocation) ? 0 : obj.DescriptionLocation.GetHashCode(StringComparison.OrdinalIgnoreCase)) * 11 + - (string.IsNullOrEmpty(obj.OutputPath) ? 0 : obj.OutputPath.GetHashCode(StringComparison.OrdinalIgnoreCase)) * 7 + - _stringIEnumerableDeepComparer.GetHashCode(obj.Types?.Order(StringComparer.OrdinalIgnoreCase) ?? Enumerable.Empty()) * 5 + - _stringIEnumerableDeepComparer.GetHashCode(obj.IncludePatterns?.Order(StringComparer.OrdinalIgnoreCase) ?? Enumerable.Empty()) * 3 + - _stringIEnumerableDeepComparer.GetHashCode(obj.ExcludePatterns?.Order(StringComparer.OrdinalIgnoreCase) ?? Enumerable.Empty()) * 2; + _stringIEnumerableDeepComparer.GetHashCode(obj.Types?.Order(StringComparer.OrdinalIgnoreCase) ?? Enumerable.Empty()) * 11 + + base.GetHashCode(obj); } } diff --git a/src/Kiota.Builder/WorkspaceManagement/BaseApiConsumerConfigurationComparer.cs b/src/Kiota.Builder/WorkspaceManagement/BaseApiConsumerConfigurationComparer.cs new file mode 100644 index 0000000000..8a35a9a99a --- /dev/null +++ b/src/Kiota.Builder/WorkspaceManagement/BaseApiConsumerConfigurationComparer.cs @@ -0,0 +1,25 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; +using System.Linq; +using Kiota.Builder.Lock; + +namespace Kiota.Builder.WorkspaceManagement; +public abstract class BaseApiConsumerConfigurationComparer : IEqualityComparer where T : BaseApiConsumerConfiguration +{ + private static readonly StringIEnumerableDeepComparer _stringIEnumerableDeepComparer = new(); + /// + public virtual bool Equals(T? x, T? y) + { + return x == null && y == null || x != null && y != null && GetHashCode(x) == GetHashCode(y); + } + + public virtual int GetHashCode([DisallowNull] T obj) + { + if (obj == null) return 0; + return (string.IsNullOrEmpty(obj.DescriptionLocation) ? 0 : obj.DescriptionLocation.GetHashCode(StringComparison.OrdinalIgnoreCase)) * 7 + + (string.IsNullOrEmpty(obj.OutputPath) ? 0 : obj.OutputPath.GetHashCode(StringComparison.OrdinalIgnoreCase)) * 5 + + _stringIEnumerableDeepComparer.GetHashCode(obj.IncludePatterns?.Order(StringComparer.OrdinalIgnoreCase) ?? Enumerable.Empty()) * 3 + + _stringIEnumerableDeepComparer.GetHashCode(obj.ExcludePatterns?.Order(StringComparer.OrdinalIgnoreCase) ?? Enumerable.Empty()) * 2; + } +} diff --git a/tests/Kiota.Builder.Tests/WorkspaceManagement/ApiClientConfigurationComparerTests.cs b/tests/Kiota.Builder.Tests/WorkspaceManagement/ApiClientConfigurationComparerTests.cs index d7476f9923..013f0a4da7 100644 --- a/tests/Kiota.Builder.Tests/WorkspaceManagement/ApiClientConfigurationComparerTests.cs +++ b/tests/Kiota.Builder.Tests/WorkspaceManagement/ApiClientConfigurationComparerTests.cs @@ -16,6 +16,6 @@ public void Defensive() [Fact] public void GetsHashCode() { - Assert.Equal(13, _comparer.GetHashCode(new() { UsesBackingStore = true })); + Assert.Equal(17, _comparer.GetHashCode(new() { UsesBackingStore = true })); } } diff --git a/tests/Kiota.Builder.Tests/WorkspaceManagement/ApiPluginConfigurationComparerTests.cs b/tests/Kiota.Builder.Tests/WorkspaceManagement/ApiPluginConfigurationComparerTests.cs new file mode 100644 index 0000000000..78d7b2a9c3 --- /dev/null +++ b/tests/Kiota.Builder.Tests/WorkspaceManagement/ApiPluginConfigurationComparerTests.cs @@ -0,0 +1,14 @@ +using Kiota.Builder.WorkspaceManagement; +using Xunit; + +namespace Kiota.Builder.Tests.WorkspaceManagement; + +public class ApiPluginConfigurationComparerTests +{ + private readonly ApiPluginConfigurationComparer _comparer = new(); + [Fact] + public void GetsHashCode() + { + Assert.NotEqual(_comparer.GetHashCode(new() { Types = ["OpenAI"] }), _comparer.GetHashCode(new() { Types = ["APIManifest"] })); + } +} From 8bcc32fd175ff332d11d2d2129ce4838d278206e Mon Sep 17 00:00:00 2001 From: Vincent Biret Date: Thu, 28 Mar 2024 10:27:15 -0400 Subject: [PATCH 27/40] - adds unit tests for runtime comparer Signed-off-by: Vincent Biret --- .../Plugins/OpenAPIRuntimeComparerTests.cs | 24 +++++++++++++++++++ 1 file changed, 24 insertions(+) create mode 100644 tests/Kiota.Builder.Tests/Plugins/OpenAPIRuntimeComparerTests.cs diff --git a/tests/Kiota.Builder.Tests/Plugins/OpenAPIRuntimeComparerTests.cs b/tests/Kiota.Builder.Tests/Plugins/OpenAPIRuntimeComparerTests.cs new file mode 100644 index 0000000000..936f073ac3 --- /dev/null +++ b/tests/Kiota.Builder.Tests/Plugins/OpenAPIRuntimeComparerTests.cs @@ -0,0 +1,24 @@ +using Kiota.Builder.Plugins; +using Microsoft.Plugins.Manifest; +using Xunit; + +namespace Kiota.Builder.Tests.Plugins; +public class OpenAPIRuntimeComparerTests +{ + private readonly OpenAPIRuntimeComparer _comparer = new(); + [Fact] + public void Defensive() + { + Assert.Equal(0, _comparer.GetHashCode(null)); + Assert.True(_comparer.Equals(null, null)); + Assert.False(_comparer.Equals(new(), null)); + Assert.False(_comparer.Equals(null, new())); + } + [Fact] + public void GetsHashCode() + { + var runtime1 = new OpenAPIRuntime { Spec = new() { { "key1", "value1" } } }; + var runtime2 = new OpenAPIRuntime { Spec = new() { { "key2", "value2" } }, Auth = new() { Type = "type" } }; + Assert.NotEqual(_comparer.GetHashCode(runtime1), _comparer.GetHashCode(runtime2)); + } +} From 2f924eda902f3f2e6dac818ee42eaa2397184b80 Mon Sep 17 00:00:00 2001 From: Vincent Biret Date: Thu, 28 Mar 2024 11:04:18 -0400 Subject: [PATCH 28/40] - adds unit tests for plugin generation service Signed-off-by: Vincent Biret --- .../Plugins/PluginsGenerationServiceTests.cs | 64 +++++++++++++++++++ 1 file changed, 64 insertions(+) create mode 100644 tests/Kiota.Builder.Tests/Plugins/PluginsGenerationServiceTests.cs diff --git a/tests/Kiota.Builder.Tests/Plugins/PluginsGenerationServiceTests.cs b/tests/Kiota.Builder.Tests/Plugins/PluginsGenerationServiceTests.cs new file mode 100644 index 0000000000..74be5b809d --- /dev/null +++ b/tests/Kiota.Builder.Tests/Plugins/PluginsGenerationServiceTests.cs @@ -0,0 +1,64 @@ +using System; +using System.IO; +using System.Net.Http; +using System.Threading.Tasks; +using Kiota.Builder.Configuration; +using Kiota.Builder.Plugins; +using Microsoft.Extensions.Logging; +using Microsoft.OpenApi.Services; +using Moq; +using Xunit; + +namespace Kiota.Builder.Tests.Plugins; +public sealed class PluginsGenerationServiceTests : IDisposable +{ + private readonly HttpClient _httpClient = new(); + [Fact] + public void Defensive() + { + Assert.Throws(() => new PluginsGenerationService(null, OpenApiUrlTreeNode.Create(), new())); + Assert.Throws(() => new PluginsGenerationService(new(), null, new())); + Assert.Throws(() => new PluginsGenerationService(new(), OpenApiUrlTreeNode.Create(), null)); + } + + public void Dispose() + { + _httpClient.Dispose(); + } + + [Fact] + public async Task GeneratesManifest() + { + var simpleDescriptionContent = @"openapi: 3.0.0 +info: + title: test + version: 1.0 +paths: + /test: + get: + responses: + '200': + description: test"; + var simpleDescriptionPath = Path.Combine(Path.GetTempPath(), Path.GetRandomFileName()) + ".yaml"; + await File.WriteAllTextAsync(simpleDescriptionPath, simpleDescriptionContent); + var mockLogger = new Mock>(); + var openAPIDocumentDS = new OpenApiDocumentDownloadService(_httpClient, mockLogger.Object); + var outputDirectory = Path.Combine(Path.GetTempPath(), Path.GetRandomFileName()); + var generationConfiguration = new GenerationConfiguration + { + OutputPath = outputDirectory, + OpenAPIFilePath = "openapiPath", + PluginTypes = [PluginType.APIManifest], + ClientClassName = "client", + }; + var (openAPIDocumentStream, _) = await openAPIDocumentDS.LoadStreamAsync(simpleDescriptionPath, generationConfiguration, null, false); + var openApiDocument = await openAPIDocumentDS.GetDocumentFromStreamAsync(openAPIDocumentStream, generationConfiguration); + var urlTreeNode = OpenApiUrlTreeNode.Create(openApiDocument, Constants.DefaultOpenApiLabel); + + var pluginsGenerationService = new PluginsGenerationService(openApiDocument, urlTreeNode, generationConfiguration); + await pluginsGenerationService.GenerateManifestAsync(); + + Assert.True(File.Exists(Path.Combine(outputDirectory, "manifest.json"))); + Assert.True(File.Exists(Path.Combine(outputDirectory, "openapi.yml"))); + } +} From 9193228b44d8d01faba390fc5e97d83a131e9a64 Mon Sep 17 00:00:00 2001 From: Vincent Biret Date: Thu, 28 Mar 2024 11:11:17 -0400 Subject: [PATCH 29/40] - adds missing operation id 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 74be5b809d..83baa0ba06 100644 --- a/tests/Kiota.Builder.Tests/Plugins/PluginsGenerationServiceTests.cs +++ b/tests/Kiota.Builder.Tests/Plugins/PluginsGenerationServiceTests.cs @@ -36,6 +36,7 @@ public async Task GeneratesManifest() paths: /test: get: + operationId: test responses: '200': description: test"; From 643f9ee94a49c956d74044e139cac971ecbc6390 Mon Sep 17 00:00:00 2001 From: Vincent Biret Date: Thu, 28 Mar 2024 14:36:29 -0400 Subject: [PATCH 30/40] - adds support for logo, privacy, legal urls Signed-off-by: Vincent Biret --- .../OpenApiDocumentDownloadService.cs | 3 ++ .../OpenApiDescriptionForModelExtension.cs | 20 +++---------- .../OpenApiLegalInfoUrlExtension.cs | 20 +++++++++++++ .../OpenApiExtensions/OpenApiLogoExtension.cs | 20 +++++++++++++ .../OpenApiPrivacyPolicyUrlExtension.cs | 20 +++++++++++++ .../OpenApiSimpleStringExtension.cs | 28 +++++++++++++++++++ .../Plugins/PluginsGenerationService.cs | 22 ++++++++++++--- 7 files changed, 113 insertions(+), 20 deletions(-) create mode 100644 src/Kiota.Builder/OpenApiExtensions/OpenApiLegalInfoUrlExtension.cs create mode 100644 src/Kiota.Builder/OpenApiExtensions/OpenApiLogoExtension.cs create mode 100644 src/Kiota.Builder/OpenApiExtensions/OpenApiPrivacyPolicyUrlExtension.cs create mode 100644 src/Kiota.Builder/OpenApiExtensions/OpenApiSimpleStringExtension.cs diff --git a/src/Kiota.Builder/OpenApiDocumentDownloadService.cs b/src/Kiota.Builder/OpenApiDocumentDownloadService.cs index 20ce471058..b4dad81576 100644 --- a/src/Kiota.Builder/OpenApiDocumentDownloadService.cs +++ b/src/Kiota.Builder/OpenApiDocumentDownloadService.cs @@ -115,6 +115,9 @@ ex is SecurityException || settings.AddMicrosoftExtensionParsers(); settings.ExtensionParsers.TryAdd(OpenApiKiotaExtension.Name, static (i, _) => OpenApiKiotaExtension.Parse(i)); settings.ExtensionParsers.TryAdd(OpenApiDescriptionForModelExtension.Name, static (i, _) => OpenApiDescriptionForModelExtension.Parse(i)); + settings.ExtensionParsers.TryAdd(OpenApiLogoExtension.Name, static (i, _) => OpenApiLogoExtension.Parse(i)); + settings.ExtensionParsers.TryAdd(OpenApiPrivacyPolicyUrlExtension.Name, static (i, _) => OpenApiPrivacyPolicyUrlExtension.Parse(i)); + settings.ExtensionParsers.TryAdd(OpenApiLegalInfoUrlExtension.Name, static (i, _) => OpenApiLegalInfoUrlExtension.Parse(i)); settings.ExtensionParsers.TryAdd(OpenApiAiReasoningInstructionsExtension.Name, static (i, _) => OpenApiAiReasoningInstructionsExtension.Parse(i)); settings.ExtensionParsers.TryAdd(OpenApiAiRespondingInstructionsExtension.Name, static (i, _) => OpenApiAiRespondingInstructionsExtension.Parse(i)); try diff --git a/src/Kiota.Builder/OpenApiExtensions/OpenApiDescriptionForModelExtension.cs b/src/Kiota.Builder/OpenApiExtensions/OpenApiDescriptionForModelExtension.cs index 3e3acfa5bf..ba198df7c9 100644 --- a/src/Kiota.Builder/OpenApiExtensions/OpenApiDescriptionForModelExtension.cs +++ b/src/Kiota.Builder/OpenApiExtensions/OpenApiDescriptionForModelExtension.cs @@ -1,32 +1,20 @@ -using System; -using Microsoft.OpenApi; -using Microsoft.OpenApi.Any; -using Microsoft.OpenApi.Interfaces; -using Microsoft.OpenApi.Writers; +using Microsoft.OpenApi.Any; namespace Kiota.Builder.OpenApiExtensions; -public class OpenApiDescriptionForModelExtension : IOpenApiExtension +public class OpenApiDescriptionForModelExtension : OpenApiSimpleStringExtension { public static string Name => "x-ai-description"; public string? Description { get; set; } - public void Write(IOpenApiWriter writer, OpenApiSpecVersion specVersion) - { - ArgumentNullException.ThrowIfNull(writer); - if (!string.IsNullOrWhiteSpace(Description)) - { - writer.WriteValue(Description); - } - } + protected override string? ValueSelector => Description; public static OpenApiDescriptionForModelExtension Parse(IOpenApiAny source) { - if (source is not OpenApiString rawString) throw new ArgumentOutOfRangeException(nameof(source)); return new OpenApiDescriptionForModelExtension { - Description = rawString.Value + Description = ParseString(source) }; } } diff --git a/src/Kiota.Builder/OpenApiExtensions/OpenApiLegalInfoUrlExtension.cs b/src/Kiota.Builder/OpenApiExtensions/OpenApiLegalInfoUrlExtension.cs new file mode 100644 index 0000000000..3f042fc70c --- /dev/null +++ b/src/Kiota.Builder/OpenApiExtensions/OpenApiLegalInfoUrlExtension.cs @@ -0,0 +1,20 @@ +using Microsoft.OpenApi.Any; + +namespace Kiota.Builder.OpenApiExtensions; + +public class OpenApiLegalInfoUrlExtension : OpenApiSimpleStringExtension +{ + public static string Name => "x-legal-info-url"; + public string? Legal + { + get; set; + } + protected override string? ValueSelector => Legal; + public static OpenApiLegalInfoUrlExtension Parse(IOpenApiAny source) + { + return new OpenApiLegalInfoUrlExtension + { + Legal = ParseString(source) + }; + } +} diff --git a/src/Kiota.Builder/OpenApiExtensions/OpenApiLogoExtension.cs b/src/Kiota.Builder/OpenApiExtensions/OpenApiLogoExtension.cs new file mode 100644 index 0000000000..c987228fab --- /dev/null +++ b/src/Kiota.Builder/OpenApiExtensions/OpenApiLogoExtension.cs @@ -0,0 +1,20 @@ +using Microsoft.OpenApi.Any; + +namespace Kiota.Builder.OpenApiExtensions; + +public class OpenApiLogoExtension : OpenApiSimpleStringExtension +{ + public static string Name => "x-logo"; + public string? Logo + { + get; set; + } + protected override string? ValueSelector => Logo; + public static OpenApiLogoExtension Parse(IOpenApiAny source) + { + return new OpenApiLogoExtension + { + Logo = ParseString(source) + }; + } +} diff --git a/src/Kiota.Builder/OpenApiExtensions/OpenApiPrivacyPolicyUrlExtension.cs b/src/Kiota.Builder/OpenApiExtensions/OpenApiPrivacyPolicyUrlExtension.cs new file mode 100644 index 0000000000..4cbc47c72a --- /dev/null +++ b/src/Kiota.Builder/OpenApiExtensions/OpenApiPrivacyPolicyUrlExtension.cs @@ -0,0 +1,20 @@ +using Microsoft.OpenApi.Any; + +namespace Kiota.Builder.OpenApiExtensions; + +public class OpenApiPrivacyPolicyUrlExtension : OpenApiSimpleStringExtension +{ + public static string Name => "x-privacy-policy-url"; + public string? Privacy + { + get; set; + } + protected override string? ValueSelector => Privacy; + public static OpenApiPrivacyPolicyUrlExtension Parse(IOpenApiAny source) + { + return new OpenApiPrivacyPolicyUrlExtension + { + Privacy = ParseString(source) + }; + } +} diff --git a/src/Kiota.Builder/OpenApiExtensions/OpenApiSimpleStringExtension.cs b/src/Kiota.Builder/OpenApiExtensions/OpenApiSimpleStringExtension.cs new file mode 100644 index 0000000000..9978939205 --- /dev/null +++ b/src/Kiota.Builder/OpenApiExtensions/OpenApiSimpleStringExtension.cs @@ -0,0 +1,28 @@ +using System; +using Microsoft.OpenApi; +using Microsoft.OpenApi.Any; +using Microsoft.OpenApi.Interfaces; +using Microsoft.OpenApi.Writers; + +namespace Kiota.Builder.OpenApiExtensions; + +public abstract class OpenApiSimpleStringExtension : IOpenApiExtension +{ + protected abstract string? ValueSelector + { + get; + } + public void Write(IOpenApiWriter writer, OpenApiSpecVersion specVersion) + { + ArgumentNullException.ThrowIfNull(writer); + if (!string.IsNullOrWhiteSpace(ValueSelector)) + { + writer.WriteValue(ValueSelector); + } + } + public static string ParseString(IOpenApiAny source) + { + if (source is not OpenApiString rawString) throw new ArgumentOutOfRangeException(nameof(source)); + return rawString.Value; + } +} diff --git a/src/Kiota.Builder/Plugins/PluginsGenerationService.cs b/src/Kiota.Builder/Plugins/PluginsGenerationService.cs index e5ff9436d7..318ab007c7 100644 --- a/src/Kiota.Builder/Plugins/PluginsGenerationService.cs +++ b/src/Kiota.Builder/Plugins/PluginsGenerationService.cs @@ -59,11 +59,23 @@ private ManifestDocument GetManifestDocument(string openApiDocumentPath) var (runtimes, functions) = GetRuntimesAndFunctionsFromTree(TreeNode, openApiDocumentPath); var descriptionForHuman = OAIDocument.Info?.Description.CleanupXMLString() is string d && !string.IsNullOrEmpty(d) ? d : $"Description for {OAIDocument.Info?.Title.CleanupXMLString()}"; var descriptionForModel = descriptionForHuman; - if (OAIDocument.Info is not null && - OAIDocument.Info.Extensions.TryGetValue(OpenApiDescriptionForModelExtension.Name, out var descriptionExtension) && + string? legalUrl = null; + string? logoUrl = null; + string? privacyUrl = null; + if (OAIDocument.Info is not null) + { + + if (OAIDocument.Info.Extensions.TryGetValue(OpenApiDescriptionForModelExtension.Name, out var descriptionExtension) && descriptionExtension is OpenApiDescriptionForModelExtension extension && !string.IsNullOrEmpty(extension.Description)) - descriptionForModel = extension.Description.CleanupXMLString(); + descriptionForModel = extension.Description.CleanupXMLString(); + if (OAIDocument.Info.Extensions.TryGetValue(OpenApiLegalInfoUrlExtension.Name, out var legalExtension) && legalExtension is OpenApiLegalInfoUrlExtension legal) + legalUrl = legal.Legal; + if (OAIDocument.Info.Extensions.TryGetValue(OpenApiLogoExtension.Name, out var logoExtension) && logoExtension is OpenApiLogoExtension logo) + logoUrl = logo.Logo; + if (OAIDocument.Info.Extensions.TryGetValue(OpenApiPrivacyPolicyUrlExtension.Name, out var privacyExtension) && privacyExtension is OpenApiPrivacyPolicyUrlExtension privacy) + privacyUrl = privacy.Privacy; + } return new ManifestDocument { SchemaVersion = "v2", @@ -73,7 +85,9 @@ descriptionExtension is OpenApiDescriptionForModelExtension extension && DescriptionForModel = descriptionForModel, ContactEmail = OAIDocument.Info?.Contact?.Email, Namespace = Configuration.ClientClassName, - //TODO logo + LogoUrl = logoUrl, + LegalInfoUrl = legalUrl, + PrivacyPolicyUrl = privacyUrl, Runtimes = [.. runtimes .GroupBy(static x => x, _openAPIRuntimeComparer) .Select(static x => From cab61c920272cdd05960953de77e03cc0f6275db Mon Sep 17 00:00:00 2001 From: Andrew Omondi Date: Thu, 11 Apr 2024 12:23:16 +0300 Subject: [PATCH 31/40] No defaults for plugin types --- src/kiota/KiotaPluginCommands.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/src/kiota/KiotaPluginCommands.cs b/src/kiota/KiotaPluginCommands.cs index 2b15c34af9..4781021c12 100644 --- a/src/kiota/KiotaPluginCommands.cs +++ b/src/kiota/KiotaPluginCommands.cs @@ -32,7 +32,6 @@ internal static Option> GetPluginTypeOption(bool isRequired = t { typeOption.IsRequired = true; typeOption.Arity = ArgumentArity.OneOrMore; - typeOption.SetDefaultValue(new List { PluginType.OpenAI }); } typeOption.AddValidator(x => KiotaHost.ValidateKnownValues(x, "type", Enum.GetNames())); return typeOption; From 35e1b491a3c88bb2ec1a44aa23cfcaebdcbce0e7 Mon Sep 17 00:00:00 2001 From: Andrew Omondi Date: Thu, 11 Apr 2024 15:03:56 +0300 Subject: [PATCH 32/40] Update logo extension --- .../OpenApiExtensions/OpenApiLogoExtension.cs | 35 +++++++++++++++---- .../Plugins/PluginsGenerationService.cs | 2 +- 2 files changed, 29 insertions(+), 8 deletions(-) diff --git a/src/Kiota.Builder/OpenApiExtensions/OpenApiLogoExtension.cs b/src/Kiota.Builder/OpenApiExtensions/OpenApiLogoExtension.cs index c987228fab..edd5a2daf5 100644 --- a/src/Kiota.Builder/OpenApiExtensions/OpenApiLogoExtension.cs +++ b/src/Kiota.Builder/OpenApiExtensions/OpenApiLogoExtension.cs @@ -1,20 +1,41 @@ -using Microsoft.OpenApi.Any; +using System; +using Kiota.Builder.Extensions; +using Microsoft.OpenApi; +using Microsoft.OpenApi.Any; +using Microsoft.OpenApi.Interfaces; +using Microsoft.OpenApi.Writers; namespace Kiota.Builder.OpenApiExtensions; -public class OpenApiLogoExtension : OpenApiSimpleStringExtension +public class OpenApiLogoExtension : IOpenApiExtension { public static string Name => "x-logo"; - public string? Logo +#pragma warning disable CA1056 + public string? Url +#pragma warning restore CA1056 { get; set; } - protected override string? ValueSelector => Logo; public static OpenApiLogoExtension Parse(IOpenApiAny source) { - return new OpenApiLogoExtension + if (source is not OpenApiObject rawObject) throw new ArgumentOutOfRangeException(nameof(source)); + var extension = new OpenApiLogoExtension(); + if (rawObject.TryGetValue(nameof(Url).ToFirstCharacterLowerCase(), out var url) && url is OpenApiString urlValue) { - Logo = ParseString(source) - }; + extension.Url = urlValue.Value; + } + return extension; + } + + public void Write(IOpenApiWriter writer, OpenApiSpecVersion specVersion) + { + ArgumentNullException.ThrowIfNull(writer); + if (!string.IsNullOrEmpty(Url)) + { + writer.WriteStartObject(); + writer.WritePropertyName(nameof(Url).ToFirstCharacterLowerCase()); + writer.WriteValue(Url); + writer.WriteEndObject(); + } } } diff --git a/src/Kiota.Builder/Plugins/PluginsGenerationService.cs b/src/Kiota.Builder/Plugins/PluginsGenerationService.cs index 318ab007c7..bc0c3e4bf7 100644 --- a/src/Kiota.Builder/Plugins/PluginsGenerationService.cs +++ b/src/Kiota.Builder/Plugins/PluginsGenerationService.cs @@ -72,7 +72,7 @@ descriptionExtension is OpenApiDescriptionForModelExtension extension && if (OAIDocument.Info.Extensions.TryGetValue(OpenApiLegalInfoUrlExtension.Name, out var legalExtension) && legalExtension is OpenApiLegalInfoUrlExtension legal) legalUrl = legal.Legal; if (OAIDocument.Info.Extensions.TryGetValue(OpenApiLogoExtension.Name, out var logoExtension) && logoExtension is OpenApiLogoExtension logo) - logoUrl = logo.Logo; + logoUrl = logo.Url; if (OAIDocument.Info.Extensions.TryGetValue(OpenApiPrivacyPolicyUrlExtension.Name, out var privacyExtension) && privacyExtension is OpenApiPrivacyPolicyUrlExtension privacy) privacyUrl = privacy.Privacy; } From 68a35575334f09bede511a34ced3998fe834d6c3 Mon Sep 17 00:00:00 2001 From: Andrew Omondi Date: Tue, 16 Apr 2024 13:51:09 +0300 Subject: [PATCH 33/40] Fixes breaking changes from latest update from manifest lib --- .vscode/launch.json | 2 +- src/Kiota.Builder/Kiota.Builder.csproj | 2 +- src/Kiota.Builder/KiotaBuilder.cs | 4 +- src/Kiota.Builder/PluginType.cs | 3 +- src/Kiota.Builder/Plugins/AuthComparer.cs | 2 +- .../Plugins/OpenAPiRuntimeComparer.cs | 9 +- .../Plugins/OpenApiRuntimeSpecComparer.cs | 23 +++++ .../Plugins/PluginsGenerationService.cs | 91 +++++++++++-------- src/kiota/Handlers/Plugin/AddHandler.cs | 1 - .../Plugins/OpenAPIRuntimeComparerTests.cs | 4 +- 10 files changed, 90 insertions(+), 51 deletions(-) create mode 100644 src/Kiota.Builder/Plugins/OpenApiRuntimeSpecComparer.cs diff --git a/.vscode/launch.json b/.vscode/launch.json index 34885c1e13..9035ac11ff 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -330,7 +330,7 @@ "-i", "**/messages", "--type", - "APIManifest" + "microsoft" ], "cwd": "${workspaceFolder}/samples/msgraph-mail/dotnet", "console": "internalConsole", diff --git a/src/Kiota.Builder/Kiota.Builder.csproj b/src/Kiota.Builder/Kiota.Builder.csproj index faa7c7dd75..bee0e291c5 100644 --- a/src/Kiota.Builder/Kiota.Builder.csproj +++ b/src/Kiota.Builder/Kiota.Builder.csproj @@ -47,7 +47,7 @@ - + GeneratePluginAsync(CancellationToken cancellationToken) { return await GenerateConsumerAsync(async (sw, stepId, openApiTree, CancellationToken) => { - if (config.PluginTypes.Contains(PluginType.OpenAI)) - throw new NotImplementedException("The OpenAI plugin type is not supported for generation"); + if (config.PluginTypes.Any(static pluginType => pluginType != PluginType.Microsoft)) + throw new NotImplementedException("Only the Microsoft plugin type is not supported for generation"); if (openApiDocument is null || openApiTree is null) throw new InvalidOperationException("The OpenAPI document and the URL tree must be loaded before generating the plugins"); // generate plugin diff --git a/src/Kiota.Builder/PluginType.cs b/src/Kiota.Builder/PluginType.cs index b0285d4747..b985a129b0 100644 --- a/src/Kiota.Builder/PluginType.cs +++ b/src/Kiota.Builder/PluginType.cs @@ -3,5 +3,6 @@ public enum PluginType { OpenAI, - APIManifest + APIManifest, + Microsoft } diff --git a/src/Kiota.Builder/Plugins/AuthComparer.cs b/src/Kiota.Builder/Plugins/AuthComparer.cs index a38c781692..e5212d44de 100644 --- a/src/Kiota.Builder/Plugins/AuthComparer.cs +++ b/src/Kiota.Builder/Plugins/AuthComparer.cs @@ -16,6 +16,6 @@ public bool Equals(Auth? x, Auth? y) public int GetHashCode([DisallowNull] Auth obj) { if (obj == null) return 0; - return string.IsNullOrEmpty(obj.Type) ? 0 : StringComparer.OrdinalIgnoreCase.GetHashCode(obj.Type) * 3; + return obj.Type is null ? 0 : StringComparer.OrdinalIgnoreCase.GetHashCode(obj.Type.Value.ToString()) * 3; } } diff --git a/src/Kiota.Builder/Plugins/OpenAPiRuntimeComparer.cs b/src/Kiota.Builder/Plugins/OpenAPiRuntimeComparer.cs index 944cc78d39..fc699b070e 100644 --- a/src/Kiota.Builder/Plugins/OpenAPiRuntimeComparer.cs +++ b/src/Kiota.Builder/Plugins/OpenAPiRuntimeComparer.cs @@ -7,7 +7,7 @@ namespace Kiota.Builder.Plugins; -internal class OpenAPIRuntimeComparer : IEqualityComparer +internal class OpenAPIRuntimeComparer : IEqualityComparer { public bool EvaluateFunctions { @@ -15,17 +15,18 @@ public bool EvaluateFunctions } private static readonly StringIEnumerableDeepComparer _stringIEnumerableDeepComparer = new(); private static readonly AuthComparer _authComparer = new(); + private static readonly OpenApiRuntimeSpecComparer _openApiRuntimeSpecComparer = new(); /// - public bool Equals(OpenAPIRuntime? x, OpenAPIRuntime? y) + public bool Equals(OpenApiRuntime? x, OpenApiRuntime? y) { return x == null && y == null || x != null && y != null && GetHashCode(x) == GetHashCode(y); } /// - public int GetHashCode([DisallowNull] OpenAPIRuntime obj) + public int GetHashCode([DisallowNull] OpenApiRuntime obj) { if (obj == null) return 0; return (EvaluateFunctions ? _stringIEnumerableDeepComparer.GetHashCode(obj.RunForFunctions ?? Enumerable.Empty()) * 7 : 0) + - obj.Spec.Select(static x => StringComparer.Ordinal.GetHashCode($"{x.Key}:{x.Value}")).Aggregate(0, (acc, next) => acc + next) * 5 + + (obj.Spec is null ? 0 : _openApiRuntimeSpecComparer.GetHashCode(obj.Spec) * 5) + (obj.Auth is null ? 0 : _authComparer.GetHashCode(obj.Auth) * 3); } } diff --git a/src/Kiota.Builder/Plugins/OpenApiRuntimeSpecComparer.cs b/src/Kiota.Builder/Plugins/OpenApiRuntimeSpecComparer.cs new file mode 100644 index 0000000000..38de08d0cf --- /dev/null +++ b/src/Kiota.Builder/Plugins/OpenApiRuntimeSpecComparer.cs @@ -0,0 +1,23 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; +using System.Linq; +using Microsoft.Plugins.Manifest; + +namespace Kiota.Builder.Plugins; + +public class OpenApiRuntimeSpecComparer : IEqualityComparer +{ + /// + public bool Equals(OpenApiRuntimeSpec? x, OpenApiRuntimeSpec? y) + { + return x == null && y == null || x != null && y != null && GetHashCode(x) == GetHashCode(y); + } + /// + public int GetHashCode([DisallowNull] OpenApiRuntimeSpec obj) + { + if (obj == null) return 0; + return (string.IsNullOrEmpty(obj.Url) ? 0 : StringComparer.OrdinalIgnoreCase.GetHashCode(obj.Url) * 5) + + (string.IsNullOrEmpty(obj.ApiDescription) ? 0 : StringComparer.OrdinalIgnoreCase.GetHashCode(obj.ApiDescription) * 3); + } +} diff --git a/src/Kiota.Builder/Plugins/PluginsGenerationService.cs b/src/Kiota.Builder/Plugins/PluginsGenerationService.cs index bc0c3e4bf7..354e5bbe4a 100644 --- a/src/Kiota.Builder/Plugins/PluginsGenerationService.cs +++ b/src/Kiota.Builder/Plugins/PluginsGenerationService.cs @@ -30,31 +30,37 @@ public PluginsGenerationService(OpenApiDocument document, OpenApiUrlTreeNode ope Configuration = configuration; } private static readonly OpenAPIRuntimeComparer _openAPIRuntimeComparer = new(); - private const string ManifestFileName = "manifest.json"; + private const string ManifestFileNameSuffix = ".json"; private const string DescriptionRelativePath = "./openapi.yml"; public async Task GenerateManifestAsync(CancellationToken cancellationToken = default) { - var manifestOutputPath = Path.Combine(Configuration.OutputPath, ManifestFileName); - var directory = Path.GetDirectoryName(manifestOutputPath); - if (!string.IsNullOrEmpty(directory) && !Directory.Exists(directory)) - Directory.CreateDirectory(directory); + foreach (var pluginType in Configuration.PluginTypes) + { + if (pluginType != PluginType.Microsoft) + continue; //TODO add support for other plugin type generation + + var manifestOutputPath = Path.Combine(Configuration.OutputPath, $"{Configuration.ClientClassName.ToLowerInvariant()}-{pluginType.ToString().ToLowerInvariant()}{ManifestFileNameSuffix}"); + var directory = Path.GetDirectoryName(manifestOutputPath); + if (!string.IsNullOrEmpty(directory) && !Directory.Exists(directory)) + Directory.CreateDirectory(directory); - var descriptionFullPath = Path.Combine(Configuration.OutputPath, DescriptionRelativePath); + var descriptionFullPath = Path.Combine(Configuration.OutputPath, DescriptionRelativePath); #pragma warning disable CA2007 // Consider calling ConfigureAwait on the awaited task - await using var descriptionStream = File.Create(descriptionFullPath, 4096); - await using var fileWriter = new StreamWriter(descriptionStream); - var descriptionWriter = new OpenApiYamlWriter(fileWriter); - OAIDocument.SerializeAsV3(descriptionWriter); - descriptionWriter.Flush(); + await using var descriptionStream = File.Create(descriptionFullPath, 4096); + await using var fileWriter = new StreamWriter(descriptionStream); + var descriptionWriter = new OpenApiYamlWriter(fileWriter); + OAIDocument.SerializeAsV3(descriptionWriter); + descriptionWriter.Flush(); - var pluginDocument = GetManifestDocument(DescriptionRelativePath); - await using var fileStream = File.Create(manifestOutputPath, 4096); - await using var writer = new Utf8JsonWriter(fileStream, new JsonWriterOptions { Indented = true }); + var pluginDocument = GetManifestDocument(DescriptionRelativePath); + await using var fileStream = File.Create(manifestOutputPath, 4096); + await using var writer = new Utf8JsonWriter(fileStream, new JsonWriterOptions { Indented = true }); #pragma warning restore CA2007 // Consider calling ConfigureAwait on the awaited task - pluginDocument.Write(writer); - await writer.FlushAsync(cancellationToken).ConfigureAwait(false); + pluginDocument.Write(writer); + await writer.FlushAsync(cancellationToken).ConfigureAwait(false); + } } - private ManifestDocument GetManifestDocument(string openApiDocumentPath) + private PluginManifestDocument GetManifestDocument(string openApiDocumentPath) { var (runtimes, functions) = GetRuntimesAndFunctionsFromTree(TreeNode, openApiDocumentPath); var descriptionForHuman = OAIDocument.Info?.Description.CleanupXMLString() is string d && !string.IsNullOrEmpty(d) ? d : $"Description for {OAIDocument.Info?.Title.CleanupXMLString()}"; @@ -64,7 +70,6 @@ private ManifestDocument GetManifestDocument(string openApiDocumentPath) string? privacyUrl = null; if (OAIDocument.Info is not null) { - if (OAIDocument.Info.Extensions.TryGetValue(OpenApiDescriptionForModelExtension.Name, out var descriptionExtension) && descriptionExtension is OpenApiDescriptionForModelExtension extension && !string.IsNullOrEmpty(extension.Description)) @@ -76,7 +81,7 @@ descriptionExtension is OpenApiDescriptionForModelExtension extension && if (OAIDocument.Info.Extensions.TryGetValue(OpenApiPrivacyPolicyUrlExtension.Name, out var privacyExtension) && privacyExtension is OpenApiPrivacyPolicyUrlExtension privacy) privacyUrl = privacy.Privacy; } - return new ManifestDocument + return new PluginManifestDocument { SchemaVersion = "v2", NameForHuman = OAIDocument.Info?.Title.CleanupXMLString(), @@ -100,18 +105,21 @@ descriptionExtension is OpenApiDescriptionForModelExtension extension && Functions = [.. functions.OrderBy(static x => x.Name, StringComparer.OrdinalIgnoreCase)] }; } - private (OpenAPIRuntime[], Function[]) GetRuntimesAndFunctionsFromTree(OpenApiUrlTreeNode currentNode, string openApiDocumentPath) + private (OpenApiRuntime[], Function[]) GetRuntimesAndFunctionsFromTree(OpenApiUrlTreeNode currentNode, string openApiDocumentPath) { - var runtimes = new List(); + var runtimes = new List(); var functions = new List(); if (currentNode.PathItems.TryGetValue(Constants.DefaultOpenApiLabel, out var pathItem)) { foreach (var operation in pathItem.Operations.Values.Where(static x => !string.IsNullOrEmpty(x.OperationId))) { - runtimes.Add(new OpenAPIRuntime + runtimes.Add(new OpenApiRuntime { - Auth = new Auth("none"), - Spec = new Dictionary { { "url", openApiDocumentPath } }, + Auth = new AnonymousAuth(), + Spec = new OpenApiRuntimeSpec() + { + Url = openApiDocumentPath + }, RunForFunctions = [operation.OperationId] }); var oasParameters = operation.Parameters @@ -123,19 +131,26 @@ descriptionExtension is OpenApiDescriptionForModelExtension extension && functions.Add(new Function { Name = operation.OperationId, - Description = operation.Summary.CleanupXMLString() is string summary && !string.IsNullOrEmpty(summary) ? summary : operation.Description.CleanupXMLString(), - Parameters = oasParameters.Length == 0 ? null : - new Parameters( - "object", - new Properties(oasParameters.ToDictionary( - static x => x.Name, - static x => new Property( - x.Schema.Type ?? string.Empty, - x.Description.CleanupXMLString(), - x.Schema.Default?.ToString() ?? string.Empty, - null), //TODO enums - StringComparer.OrdinalIgnoreCase)), - oasParameters.Where(static x => x.Required).Select(static x => x.Name).ToList()), + Description = + operation.Summary.CleanupXMLString() is string summary && !string.IsNullOrEmpty(summary) + ? summary + : operation.Description.CleanupXMLString(), + Parameters = oasParameters.Length == 0 + ? null + : new Parameters + { + Type = "object", + Properties = new Properties(oasParameters.ToDictionary( + static x => x.Name, + static x => new FunctionParameter() + { + Type = x.Schema.Type ?? string.Empty, + Description = x.Description.CleanupXMLString(), + Default = x.Schema.Default?.ToString() ?? string.Empty, + //TODO enums + })), + Required = oasParameters.Where(static x => x.Required).Select(static x => x.Name).ToList() + }, States = GetStatesFromOperation(operation), }); } @@ -177,7 +192,7 @@ rExtRaw is T rExt && { return new State { - Instructions = instructionsExtractor(rExt).Where(static x => !string.IsNullOrEmpty(x)).Select(static x => x.CleanupXMLString()).ToList() + Instructions = new Instructions(instructionsExtractor(rExt).Where(static x => !string.IsNullOrEmpty(x)).Select(static x => x.CleanupXMLString()).ToList()) }; } return null; diff --git a/src/kiota/Handlers/Plugin/AddHandler.cs b/src/kiota/Handlers/Plugin/AddHandler.cs index 2f150fcd82..be662926cd 100644 --- a/src/kiota/Handlers/Plugin/AddHandler.cs +++ b/src/kiota/Handlers/Plugin/AddHandler.cs @@ -94,6 +94,5 @@ public override async Task InvokeAsync(InvocationContext context) #endif } } - throw new NotImplementedException(); } } diff --git a/tests/Kiota.Builder.Tests/Plugins/OpenAPIRuntimeComparerTests.cs b/tests/Kiota.Builder.Tests/Plugins/OpenAPIRuntimeComparerTests.cs index 936f073ac3..97808aa1c9 100644 --- a/tests/Kiota.Builder.Tests/Plugins/OpenAPIRuntimeComparerTests.cs +++ b/tests/Kiota.Builder.Tests/Plugins/OpenAPIRuntimeComparerTests.cs @@ -17,8 +17,8 @@ public void Defensive() [Fact] public void GetsHashCode() { - var runtime1 = new OpenAPIRuntime { Spec = new() { { "key1", "value1" } } }; - var runtime2 = new OpenAPIRuntime { Spec = new() { { "key2", "value2" } }, Auth = new() { Type = "type" } }; + var runtime1 = new OpenApiRuntime { Spec = new() { Url = "url", ApiDescription = "description" } }; + var runtime2 = new OpenApiRuntime { Spec = new() { Url = "url", ApiDescription = "description" }, Auth = new AnonymousAuth() }; Assert.NotEqual(_comparer.GetHashCode(runtime1), _comparer.GetHashCode(runtime2)); } } From 9f9d0bbd5d20a97e7165f052e3b1e2b331738b2f Mon Sep 17 00:00:00 2001 From: Andrew Omondi Date: Tue, 16 Apr 2024 14:51:43 +0300 Subject: [PATCH 34/40] Fix test --- .../Plugins/PluginsGenerationServiceTests.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/Kiota.Builder.Tests/Plugins/PluginsGenerationServiceTests.cs b/tests/Kiota.Builder.Tests/Plugins/PluginsGenerationServiceTests.cs index 83baa0ba06..5651aee580 100644 --- a/tests/Kiota.Builder.Tests/Plugins/PluginsGenerationServiceTests.cs +++ b/tests/Kiota.Builder.Tests/Plugins/PluginsGenerationServiceTests.cs @@ -49,7 +49,7 @@ public async Task GeneratesManifest() { OutputPath = outputDirectory, OpenAPIFilePath = "openapiPath", - PluginTypes = [PluginType.APIManifest], + PluginTypes = [PluginType.Microsoft], ClientClassName = "client", }; var (openAPIDocumentStream, _) = await openAPIDocumentDS.LoadStreamAsync(simpleDescriptionPath, generationConfiguration, null, false); @@ -59,7 +59,7 @@ public async Task GeneratesManifest() var pluginsGenerationService = new PluginsGenerationService(openApiDocument, urlTreeNode, generationConfiguration); await pluginsGenerationService.GenerateManifestAsync(); - Assert.True(File.Exists(Path.Combine(outputDirectory, "manifest.json"))); + Assert.True(File.Exists(Path.Combine(outputDirectory, "client-microsoft.json"))); Assert.True(File.Exists(Path.Combine(outputDirectory, "openapi.yml"))); } } From 0fe8875699fb78e3252b206fe41509ced9c3c4a9 Mon Sep 17 00:00:00 2001 From: Andrew Omondi Date: Wed, 17 Apr 2024 11:36:32 +0300 Subject: [PATCH 35/40] Bump manifest and plugins versions --- src/Kiota.Builder/Kiota.Builder.csproj | 2 +- src/kiota/kiota.csproj | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Kiota.Builder/Kiota.Builder.csproj b/src/Kiota.Builder/Kiota.Builder.csproj index bee0e291c5..e8aebabd07 100644 --- a/src/Kiota.Builder/Kiota.Builder.csproj +++ b/src/Kiota.Builder/Kiota.Builder.csproj @@ -47,7 +47,7 @@ - + - + From 691a5885024e4841418d681a3eb37a256a4d9ecf Mon Sep 17 00:00:00 2001 From: Andrew Omondi Date: Thu, 18 Apr 2024 10:10:16 +0300 Subject: [PATCH 36/40] Add support for api manifest generation --- .vscode/launch.json | 42 ++++++++++++++---- src/Kiota.Builder/KiotaBuilder.cs | 4 +- .../Plugins/PluginsGenerationService.cs | 43 +++++++++++++------ 3 files changed, 66 insertions(+), 23 deletions(-) diff --git a/.vscode/launch.json b/.vscode/launch.json index 9035ac11ff..cdb61f49af 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -196,7 +196,10 @@ "request": "launch", "preLaunchTask": "build", "program": "${workspaceFolder}/src/kiota/bin/Debug/net8.0/kiota.dll", - "args": ["search", "microsoft"], + "args": [ + "search", + "microsoft" + ], "cwd": "${workspaceFolder}/src/kiota", "console": "internalConsole", "stopAtEntry": false @@ -207,7 +210,10 @@ "request": "launch", "preLaunchTask": "build", "program": "${workspaceFolder}/src/kiota/bin/Debug/net8.0/kiota.dll", - "args": ["search", "test"], + "args": [ + "search", + "test" + ], "cwd": "${workspaceFolder}/src/kiota", "console": "internalConsole", "stopAtEntry": false @@ -249,7 +255,11 @@ "request": "launch", "preLaunchTask": "build", "program": "${workspaceFolder}/src/kiota/bin/Debug/net8.0/kiota.dll", - "args": ["info", "-l", "CSharp"], + "args": [ + "info", + "-l", + "CSharp" + ], "cwd": "${workspaceFolder}/src/kiota", "console": "internalConsole", "stopAtEntry": false @@ -260,7 +270,11 @@ "request": "launch", "preLaunchTask": "build", "program": "${workspaceFolder}/src/kiota/bin/Debug/net8.0/kiota.dll", - "args": ["update", "-o", "${workspaceFolder}/samples"], + "args": [ + "update", + "-o", + "${workspaceFolder}/samples" + ], "cwd": "${workspaceFolder}/src/kiota", "console": "internalConsole", "stopAtEntry": false @@ -271,7 +285,10 @@ "request": "launch", "preLaunchTask": "build", "program": "${workspaceFolder}/src/kiota/bin/Debug/net8.0/kiota.dll", - "args": ["workspace", "migrate"], + "args": [ + "workspace", + "migrate" + ], "cwd": "${workspaceFolder}/samples/msgraph-mail/dotnet", "console": "internalConsole", "stopAtEntry": false, @@ -285,7 +302,10 @@ "request": "launch", "preLaunchTask": "build", "program": "${workspaceFolder}/src/kiota/bin/Debug/net8.0/kiota.dll", - "args": ["client", "generate"], + "args": [ + "client", + "generate" + ], "cwd": "${workspaceFolder}/samples/msgraph-mail/dotnet", "console": "internalConsole", "stopAtEntry": false, @@ -330,6 +350,8 @@ "-i", "**/messages", "--type", + "ApiManifest", + "--type", "microsoft" ], "cwd": "${workspaceFolder}/samples/msgraph-mail/dotnet", @@ -345,7 +367,11 @@ "request": "launch", "preLaunchTask": "build", "program": "${workspaceFolder}/src/kiota/bin/Debug/net8.0/kiota.dll", - "args": ["login", "github", "device"], + "args": [ + "login", + "github", + "device" + ], "cwd": "${workspaceFolder}/src/kiota", "console": "internalConsole", "stopAtEntry": false @@ -357,4 +383,4 @@ "processId": "${command:pickProcess}" } ] -} +} \ No newline at end of file diff --git a/src/Kiota.Builder/KiotaBuilder.cs b/src/Kiota.Builder/KiotaBuilder.cs index 9111119604..5b7c2b6f0f 100644 --- a/src/Kiota.Builder/KiotaBuilder.cs +++ b/src/Kiota.Builder/KiotaBuilder.cs @@ -231,8 +231,8 @@ public async Task GeneratePluginAsync(CancellationToken cancellationToken) { return await GenerateConsumerAsync(async (sw, stepId, openApiTree, CancellationToken) => { - if (config.PluginTypes.Any(static pluginType => pluginType != PluginType.Microsoft)) - throw new NotImplementedException("Only the Microsoft plugin type is not supported for generation"); + if (config.PluginTypes.Contains(PluginType.OpenAI)) + throw new NotImplementedException("The OpenAI plugin type is not supported for generation"); if (openApiDocument is null || openApiTree is null) throw new InvalidOperationException("The OpenAPI document and the URL tree must be loaded before generating the plugins"); // generate plugin diff --git a/src/Kiota.Builder/Plugins/PluginsGenerationService.cs b/src/Kiota.Builder/Plugins/PluginsGenerationService.cs index 354e5bbe4a..63f1ad1c6e 100644 --- a/src/Kiota.Builder/Plugins/PluginsGenerationService.cs +++ b/src/Kiota.Builder/Plugins/PluginsGenerationService.cs @@ -8,6 +8,8 @@ using Kiota.Builder.Configuration; using Kiota.Builder.Extensions; using Kiota.Builder.OpenApiExtensions; +using Microsoft.Kiota.Abstractions.Extensions; +using Microsoft.OpenApi.ApiManifest; using Microsoft.OpenApi.Models; using Microsoft.OpenApi.Services; using Microsoft.OpenApi.Writers; @@ -34,29 +36,44 @@ public PluginsGenerationService(OpenApiDocument document, OpenApiUrlTreeNode ope private const string DescriptionRelativePath = "./openapi.yml"; public async Task GenerateManifestAsync(CancellationToken cancellationToken = default) { + // write the decription + var descriptionFullPath = Path.Combine(Configuration.OutputPath, DescriptionRelativePath); +#pragma warning disable CA2007 // Consider calling ConfigureAwait on the awaited task + await using var descriptionStream = File.Create(descriptionFullPath, 4096); + await using var fileWriter = new StreamWriter(descriptionStream); +#pragma warning restore CA2007 // Consider calling ConfigureAwait on the awaited task + var descriptionWriter = new OpenApiYamlWriter(fileWriter); + OAIDocument.SerializeAsV3(descriptionWriter); + descriptionWriter.Flush(); + + // write the plugins foreach (var pluginType in Configuration.PluginTypes) { - if (pluginType != PluginType.Microsoft) - continue; //TODO add support for other plugin type generation - var manifestOutputPath = Path.Combine(Configuration.OutputPath, $"{Configuration.ClientClassName.ToLowerInvariant()}-{pluginType.ToString().ToLowerInvariant()}{ManifestFileNameSuffix}"); var directory = Path.GetDirectoryName(manifestOutputPath); if (!string.IsNullOrEmpty(directory) && !Directory.Exists(directory)) Directory.CreateDirectory(directory); - - var descriptionFullPath = Path.Combine(Configuration.OutputPath, DescriptionRelativePath); #pragma warning disable CA2007 // Consider calling ConfigureAwait on the awaited task - await using var descriptionStream = File.Create(descriptionFullPath, 4096); - await using var fileWriter = new StreamWriter(descriptionStream); - var descriptionWriter = new OpenApiYamlWriter(fileWriter); - OAIDocument.SerializeAsV3(descriptionWriter); - descriptionWriter.Flush(); - - var pluginDocument = GetManifestDocument(DescriptionRelativePath); await using var fileStream = File.Create(manifestOutputPath, 4096); await using var writer = new Utf8JsonWriter(fileStream, new JsonWriterOptions { Indented = true }); #pragma warning restore CA2007 // Consider calling ConfigureAwait on the awaited task - pluginDocument.Write(writer); + + switch (pluginType) + { + case PluginType.Microsoft: + var pluginDocument = GetManifestDocument(DescriptionRelativePath); + pluginDocument.Write(writer); + break; + case PluginType.APIManifest: + var apiManifest = new ApiManifestDocument("application"); //TODO add application name + apiManifest.ApiDependencies.AddOrReplace(Configuration.ClientClassName, Configuration.ToApiDependency(OAIDocument.HashCode ?? string.Empty, TreeNode?.GetRequestInfo().ToDictionary(static x => x.Key, static x => x.Value) ?? [])); + apiManifest.Write(writer); + break; + case PluginType.OpenAI: + //TODO add support for OpenAI plugin type generation + default: + continue; + } await writer.FlushAsync(cancellationToken).ConfigureAwait(false); } } From a065bded3694287d4bdab6c5ed4468f2dc7d1d5d Mon Sep 17 00:00:00 2001 From: Andrew Omondi Date: Thu, 18 Apr 2024 10:35:37 +0300 Subject: [PATCH 37/40] Adds support for manifest generation --- src/Kiota.Builder/Plugins/PluginsGenerationService.cs | 6 +++--- .../Plugins/PluginsGenerationServiceTests.cs | 7 ++++++- 2 files changed, 9 insertions(+), 4 deletions(-) diff --git a/src/Kiota.Builder/Plugins/PluginsGenerationService.cs b/src/Kiota.Builder/Plugins/PluginsGenerationService.cs index 63f1ad1c6e..667020730d 100644 --- a/src/Kiota.Builder/Plugins/PluginsGenerationService.cs +++ b/src/Kiota.Builder/Plugins/PluginsGenerationService.cs @@ -38,6 +38,9 @@ public async Task GenerateManifestAsync(CancellationToken cancellationToken = de { // write the decription var descriptionFullPath = Path.Combine(Configuration.OutputPath, DescriptionRelativePath); + var directory = Path.GetDirectoryName(descriptionFullPath); + if (!string.IsNullOrEmpty(directory) && !Directory.Exists(directory)) + Directory.CreateDirectory(directory); #pragma warning disable CA2007 // Consider calling ConfigureAwait on the awaited task await using var descriptionStream = File.Create(descriptionFullPath, 4096); await using var fileWriter = new StreamWriter(descriptionStream); @@ -50,9 +53,6 @@ public async Task GenerateManifestAsync(CancellationToken cancellationToken = de foreach (var pluginType in Configuration.PluginTypes) { var manifestOutputPath = Path.Combine(Configuration.OutputPath, $"{Configuration.ClientClassName.ToLowerInvariant()}-{pluginType.ToString().ToLowerInvariant()}{ManifestFileNameSuffix}"); - var directory = Path.GetDirectoryName(manifestOutputPath); - if (!string.IsNullOrEmpty(directory) && !Directory.Exists(directory)) - Directory.CreateDirectory(directory); #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 }); diff --git a/tests/Kiota.Builder.Tests/Plugins/PluginsGenerationServiceTests.cs b/tests/Kiota.Builder.Tests/Plugins/PluginsGenerationServiceTests.cs index 5651aee580..77e2b4a638 100644 --- a/tests/Kiota.Builder.Tests/Plugins/PluginsGenerationServiceTests.cs +++ b/tests/Kiota.Builder.Tests/Plugins/PluginsGenerationServiceTests.cs @@ -33,6 +33,9 @@ public async Task GeneratesManifest() info: title: test version: 1.0 +servers: + - url: http://localhost/ + description: There's no place like home paths: /test: get: @@ -49,8 +52,9 @@ public async Task GeneratesManifest() { OutputPath = outputDirectory, OpenAPIFilePath = "openapiPath", - PluginTypes = [PluginType.Microsoft], + PluginTypes = [PluginType.Microsoft, PluginType.APIManifest], 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); @@ -60,6 +64,7 @@ public async Task GeneratesManifest() await pluginsGenerationService.GenerateManifestAsync(); Assert.True(File.Exists(Path.Combine(outputDirectory, "client-microsoft.json"))); + Assert.True(File.Exists(Path.Combine(outputDirectory, "client-apimanifest.json"))); Assert.True(File.Exists(Path.Combine(outputDirectory, "openapi.yml"))); } } From 6cbc4536861215e3a6432d4e62f1b2f844f7160e Mon Sep 17 00:00:00 2001 From: Andrew Omondi Date: Fri, 19 Apr 2024 15:45:54 +0300 Subject: [PATCH 38/40] Throw error for not supported case --- src/Kiota.Builder/Plugins/PluginsGenerationService.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/Kiota.Builder/Plugins/PluginsGenerationService.cs b/src/Kiota.Builder/Plugins/PluginsGenerationService.cs index 667020730d..ed8c865230 100644 --- a/src/Kiota.Builder/Plugins/PluginsGenerationService.cs +++ b/src/Kiota.Builder/Plugins/PluginsGenerationService.cs @@ -69,10 +69,10 @@ public async Task GenerateManifestAsync(CancellationToken cancellationToken = de apiManifest.ApiDependencies.AddOrReplace(Configuration.ClientClassName, Configuration.ToApiDependency(OAIDocument.HashCode ?? string.Empty, TreeNode?.GetRequestInfo().ToDictionary(static x => x.Key, static x => x.Value) ?? [])); apiManifest.Write(writer); break; - case PluginType.OpenAI: - //TODO add support for OpenAI plugin type generation + case PluginType.OpenAI://TODO add support for OpenAI plugin type generation + // intentional drop to the default case default: - continue; + throw new NotImplementedException($"The {pluginType} plugin is not implemented."); } await writer.FlushAsync(cancellationToken).ConfigureAwait(false); } From 3c5852a4e4519a4b26d6040df1cc0658e792c04a Mon Sep 17 00:00:00 2001 From: Andrew Omondi Date: Fri, 19 Apr 2024 15:55:49 +0300 Subject: [PATCH 39/40] Fix formatting --- src/Kiota.Builder/Plugins/PluginsGenerationService.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Kiota.Builder/Plugins/PluginsGenerationService.cs b/src/Kiota.Builder/Plugins/PluginsGenerationService.cs index ed8c865230..09f073b182 100644 --- a/src/Kiota.Builder/Plugins/PluginsGenerationService.cs +++ b/src/Kiota.Builder/Plugins/PluginsGenerationService.cs @@ -70,7 +70,7 @@ public async Task GenerateManifestAsync(CancellationToken cancellationToken = de apiManifest.Write(writer); break; case PluginType.OpenAI://TODO add support for OpenAI plugin type generation - // intentional drop to the default case + // intentional drop to the default case default: throw new NotImplementedException($"The {pluginType} plugin is not implemented."); } From c9aa347635c30bbc67c405d72232b1d5ad767778 Mon Sep 17 00:00:00 2001 From: Vincent Biret Date: Fri, 19 Apr 2024 09:33:06 -0400 Subject: [PATCH 40/40] Update src/Kiota.Builder/WorkspaceManagement/DescriptionStorageService.cs Co-authored-by: Eastman --- .../WorkspaceManagement/DescriptionStorageService.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Kiota.Builder/WorkspaceManagement/DescriptionStorageService.cs b/src/Kiota.Builder/WorkspaceManagement/DescriptionStorageService.cs index ed66d1e1dd..b9cb1bcf97 100644 --- a/src/Kiota.Builder/WorkspaceManagement/DescriptionStorageService.cs +++ b/src/Kiota.Builder/WorkspaceManagement/DescriptionStorageService.cs @@ -9,7 +9,7 @@ namespace Kiota.Builder.WorkspaceManagement; public class DescriptionStorageService { public const string KiotaDirectorySegment = ".kiota"; - internal const string DescriptionsSubDirectoryRelativePath = $"{KiotaDirectorySegment}/descriptions"; + internal const string DescriptionsSubDirectoryRelativePath = $"{KiotaDirectorySegment}/documents"; private readonly string TargetDirectory; public DescriptionStorageService(string targetDirectory) {