diff --git a/.vscode/launch.json b/.vscode/launch.json index 34885c1e13..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,7 +350,9 @@ "-i", "**/messages", "--type", - "APIManifest" + "ApiManifest", + "--type", + "microsoft" ], "cwd": "${workspaceFolder}/samples/msgraph-mail/dotnet", "console": "internalConsole", @@ -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/Kiota.Builder.csproj b/src/Kiota.Builder/Kiota.Builder.csproj index faa7c7dd75..e8aebabd07 100644 --- a/src/Kiota.Builder/Kiota.Builder.csproj +++ b/src/Kiota.Builder/Kiota.Builder.csproj @@ -47,7 +47,7 @@ - + +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..09f073b182 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; @@ -30,31 +32,52 @@ 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); + // 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); - - 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(); - var pluginDocument = GetManifestDocument(DescriptionRelativePath); - await using var fileStream = File.Create(manifestOutputPath, 4096); - await using var writer = new Utf8JsonWriter(fileStream, new JsonWriterOptions { Indented = true }); + // write the plugins + foreach (var pluginType in Configuration.PluginTypes) + { + var manifestOutputPath = Path.Combine(Configuration.OutputPath, $"{Configuration.ClientClassName.ToLowerInvariant()}-{pluginType.ToString().ToLowerInvariant()}{ManifestFileNameSuffix}"); +#pragma warning disable CA2007 // Consider calling ConfigureAwait on the awaited task + await using var fileStream = File.Create(manifestOutputPath, 4096); + await using var writer = new Utf8JsonWriter(fileStream, new JsonWriterOptions { Indented = true }); #pragma warning restore CA2007 // Consider calling ConfigureAwait on the awaited task - pluginDocument.Write(writer); - await writer.FlushAsync(cancellationToken).ConfigureAwait(false); + + 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 + // intentional drop to the default case + default: + throw new NotImplementedException($"The {pluginType} plugin is not implemented."); + } + 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 +87,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 +98,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 +122,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 +148,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 +209,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/src/kiota/kiota.csproj b/src/kiota/kiota.csproj index bd261b9694..68e3fec832 100644 --- a/src/kiota/kiota.csproj +++ b/src/kiota/kiota.csproj @@ -46,7 +46,7 @@ - + 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)); } } diff --git a/tests/Kiota.Builder.Tests/Plugins/PluginsGenerationServiceTests.cs b/tests/Kiota.Builder.Tests/Plugins/PluginsGenerationServiceTests.cs index 83baa0ba06..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.APIManifest], + 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); @@ -59,7 +63,8 @@ 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, "client-apimanifest.json"))); Assert.True(File.Exists(Path.Combine(outputDirectory, "openapi.yml"))); } }