diff --git a/src/Kiota.Builder/KiotaBuilder.cs b/src/Kiota.Builder/KiotaBuilder.cs index 193f336e99..fd4e0887c2 100644 --- a/src/Kiota.Builder/KiotaBuilder.cs +++ b/src/Kiota.Builder/KiotaBuilder.cs @@ -243,7 +243,7 @@ public async Task GeneratePluginAsync(CancellationToken cancellationToken) 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, Directory.GetCurrentDirectory()); + var pluginsService = new PluginsGenerationService(openApiDocument, openApiTree, config, Directory.GetCurrentDirectory(), logger); await pluginsService.GenerateManifestAsync(cancellationToken).ConfigureAwait(false); StopLogAndReset(sw, $"step {++stepId} - generate plugin - took"); return stepId; diff --git a/src/Kiota.Builder/Plugins/PluginsGenerationService.cs b/src/Kiota.Builder/Plugins/PluginsGenerationService.cs index ef638601fa..6a1e73c972 100644 --- a/src/Kiota.Builder/Plugins/PluginsGenerationService.cs +++ b/src/Kiota.Builder/Plugins/PluginsGenerationService.cs @@ -9,6 +9,7 @@ using Kiota.Builder.Configuration; using Kiota.Builder.Extensions; using Kiota.Builder.OpenApiExtensions; +using Microsoft.Extensions.Logging; using Microsoft.OpenApi.ApiManifest; using Microsoft.OpenApi.Models; using Microsoft.OpenApi.Services; @@ -23,9 +24,10 @@ public partial class PluginsGenerationService private readonly OpenApiUrlTreeNode TreeNode; private readonly GenerationConfiguration Configuration; private readonly string WorkingDirectory; + private readonly ILogger Logger; public PluginsGenerationService(OpenApiDocument document, OpenApiUrlTreeNode openApiUrlTreeNode, - GenerationConfiguration configuration, string workingDirectory) + GenerationConfiguration configuration, string workingDirectory, ILogger logger) { ArgumentNullException.ThrowIfNull(document); ArgumentNullException.ThrowIfNull(openApiUrlTreeNode); @@ -35,6 +37,7 @@ public PluginsGenerationService(OpenApiDocument document, OpenApiUrlTreeNode ope TreeNode = openApiUrlTreeNode; Configuration = configuration; WorkingDirectory = workingDirectory; + Logger = logger; } private static readonly OpenAPIRuntimeComparer _openAPIRuntimeComparer = new(); @@ -258,7 +261,7 @@ private OpenApiDocument GetDocumentWithTrimmedComponentsAndResponses(OpenApiDocu private PluginManifestDocument GetManifestDocument(string openApiDocumentPath) { - var (runtimes, functions, conversationStarters) = GetRuntimesFunctionsAndConversationStartersFromTree(OAIDocument, Configuration.PluginAuthInformation, TreeNode, openApiDocumentPath); + var (runtimes, functions, conversationStarters) = GetRuntimesFunctionsAndConversationStartersFromTree(OAIDocument, Configuration.PluginAuthInformation, TreeNode, openApiDocumentPath, Logger); var descriptionForHuman = OAIDocument.Info?.Description is string d && !string.IsNullOrEmpty(d) ? d : $"Description for {OAIDocument.Info?.Title}"; var manifestInfo = ExtractInfoFromDocument(OAIDocument.Info); var pluginManifestDocument = new PluginManifestDocument @@ -338,7 +341,7 @@ private sealed record OpenApiManifestInfo( string ContactEmail = DefaultContactEmail); private static (OpenApiRuntime[], Function[], ConversationStarter[]) GetRuntimesFunctionsAndConversationStartersFromTree(OpenApiDocument document, PluginAuthConfiguration? authInformation, OpenApiUrlTreeNode currentNode, - string openApiDocumentPath) + string openApiDocumentPath, ILogger logger) { var runtimes = new List(); var functions = new List(); @@ -348,10 +351,21 @@ private static (OpenApiRuntime[], Function[], ConversationStarter[]) GetRuntimes { foreach (var operation in pathItem.Operations.Values.Where(static x => !string.IsNullOrEmpty(x.OperationId))) { + var auth = configAuth; + try + { + auth = configAuth ?? GetAuth(operation.Security ?? document.SecurityRequirements); + } + catch (UnsupportedSecuritySchemeException e) + { + auth = new AnonymousAuth(); + logger.LogWarning("Authentication warning: {OperationId} - {Message}", operation.OperationId, e.Message); + } + runtimes.Add(new OpenApiRuntime { // Configuration overrides document information - Auth = configAuth ?? GetAuth(operation.Security ?? document.SecurityRequirements), + Auth = auth, Spec = new OpenApiRuntimeSpec { Url = openApiDocumentPath }, RunForFunctions = [operation.OperationId] }); @@ -376,7 +390,7 @@ private static (OpenApiRuntime[], Function[], ConversationStarter[]) GetRuntimes foreach (var node in currentNode.Children) { - var (childRuntimes, childFunctions, childConversationStarters) = GetRuntimesFunctionsAndConversationStartersFromTree(document, authInformation, node.Value, openApiDocumentPath); + var (childRuntimes, childFunctions, childConversationStarters) = GetRuntimesFunctionsAndConversationStartersFromTree(document, authInformation, node.Value, openApiDocumentPath, logger); runtimes.AddRange(childRuntimes); functions.AddRange(childFunctions); conversationStarters.AddRange(childConversationStarters); @@ -391,7 +405,7 @@ private static Auth GetAuth(IList securityRequiremen const string tooManySchemesError = "Multiple security requirements are not supported. Operations can only list one security requirement."; if (securityRequirements.Count > 1 || securityRequirements.FirstOrDefault()?.Keys.Count > 1) { - throw new InvalidOperationException(tooManySchemesError); + throw new UnsupportedSecuritySchemeException(tooManySchemesError); } var security = securityRequirements.FirstOrDefault(); var opSecurity = security?.Keys.FirstOrDefault(); diff --git a/tests/Kiota.Builder.Tests/Plugins/PluginsGenerationServiceTests.cs b/tests/Kiota.Builder.Tests/Plugins/PluginsGenerationServiceTests.cs index 3f74656037..cd5af624ad 100644 --- a/tests/Kiota.Builder.Tests/Plugins/PluginsGenerationServiceTests.cs +++ b/tests/Kiota.Builder.Tests/Plugins/PluginsGenerationServiceTests.cs @@ -19,14 +19,14 @@ namespace Kiota.Builder.Tests.Plugins; public sealed class PluginsGenerationServiceTests : IDisposable { private readonly HttpClient _httpClient = new(); - + private readonly ILogger _logger = new Mock>().Object; [Fact] public void Defensive() { - Assert.Throws(() => new PluginsGenerationService(null, OpenApiUrlTreeNode.Create(), new(), "foo")); - Assert.Throws(() => new PluginsGenerationService(new(), null, new(), "foo")); - Assert.Throws(() => new PluginsGenerationService(new(), OpenApiUrlTreeNode.Create(), null, "foo")); - Assert.Throws(() => new PluginsGenerationService(new(), OpenApiUrlTreeNode.Create(), new(), string.Empty)); + Assert.Throws(() => new PluginsGenerationService(null, OpenApiUrlTreeNode.Create(), new(), "foo", _logger)); + Assert.Throws(() => new PluginsGenerationService(new(), null, new(), "foo", _logger)); + Assert.Throws(() => new PluginsGenerationService(new(), OpenApiUrlTreeNode.Create(), null, "foo", _logger)); + Assert.Throws(() => new PluginsGenerationService(new(), OpenApiUrlTreeNode.Create(), new(), string.Empty, _logger)); } public void Dispose() @@ -76,7 +76,7 @@ public async Task GeneratesManifestAsync(string inputPluginName, string expected var simpleDescriptionPath = Path.Combine(workingDirectory) + "description.yaml"; await File.WriteAllTextAsync(simpleDescriptionPath, simpleDescriptionContent); var mockLogger = new Mock>(); - var openAPIDocumentDS = new OpenApiDocumentDownloadService(_httpClient, mockLogger.Object); + var openAPIDocumentDS = new OpenApiDocumentDownloadService(_httpClient, _logger); var outputDirectory = Path.Combine(workingDirectory, "output"); var generationConfiguration = new GenerationConfiguration { @@ -91,7 +91,7 @@ public async Task GeneratesManifestAsync(string inputPluginName, string expected KiotaBuilder.CleanupOperationIdForPlugins(openApiDocument); var urlTreeNode = OpenApiUrlTreeNode.Create(openApiDocument, Constants.DefaultOpenApiLabel); - var pluginsGenerationService = new PluginsGenerationService(openApiDocument, urlTreeNode, generationConfiguration, workingDirectory); + var pluginsGenerationService = new PluginsGenerationService(openApiDocument, urlTreeNode, generationConfiguration, workingDirectory, _logger); await pluginsGenerationService.GenerateManifestAsync(); Assert.True(File.Exists(Path.Combine(outputDirectory, $"{expectedPluginName.ToLower()}-apiplugin.json"))); @@ -211,7 +211,7 @@ public async Task GeneratesManifestAndCleansUpInputDescriptionAsync() var simpleDescriptionPath = Path.Combine(workingDirectory) + "description.yaml"; await File.WriteAllTextAsync(simpleDescriptionPath, simpleDescriptionContent); var mockLogger = new Mock>(); - var openAPIDocumentDS = new OpenApiDocumentDownloadService(_httpClient, mockLogger.Object); + var openAPIDocumentDS = new OpenApiDocumentDownloadService(_httpClient, _logger); var outputDirectory = Path.Combine(workingDirectory, "output"); var generationConfiguration = new GenerationConfiguration { @@ -226,7 +226,7 @@ public async Task GeneratesManifestAndCleansUpInputDescriptionAsync() KiotaBuilder.CleanupOperationIdForPlugins(openApiDocument); var urlTreeNode = OpenApiUrlTreeNode.Create(openApiDocument, Constants.DefaultOpenApiLabel); - var pluginsGenerationService = new PluginsGenerationService(openApiDocument, urlTreeNode, generationConfiguration, workingDirectory); + var pluginsGenerationService = new PluginsGenerationService(openApiDocument, urlTreeNode, generationConfiguration, workingDirectory, _logger); await pluginsGenerationService.GenerateManifestAsync(); Assert.True(File.Exists(Path.Combine(outputDirectory, ManifestFileName))); @@ -408,7 +408,33 @@ public static TheoryData(auth0); } + }, + // multiple security schemes in operation object + { + "{securitySchemes: {apiKey0: {type: apiKey, name: x-api-key0, in: header}, apiKey1: {type: apiKey, name: x-api-key1, in: header}}}", + string.Empty, "security: [apiKey0: [], apiKey1: []]", null, resultingManifest => + { + Assert.NotNull(resultingManifest.Document); + Assert.Empty(resultingManifest.Problems); + Assert.NotEmpty(resultingManifest.Document.Runtimes); + var auth0 = resultingManifest.Document.Runtimes[0].Auth; + Assert.IsType(auth0); + } + }, + // Unsupported security scheme (http basic) + { + "{securitySchemes: {httpBasic0: {type: http, scheme: basic}}}", + string.Empty, "security: [httpBasic0: []]", null, resultingManifest => + { + Assert.NotNull(resultingManifest.Document); + Assert.Empty(resultingManifest.Problems); + Assert.NotEmpty(resultingManifest.Document.Runtimes); + var auth0 = resultingManifest.Document.Runtimes[0].Auth; + Assert.IsType(auth0); + } } + + }; } @@ -437,7 +463,7 @@ public async Task GeneratesManifestWithAuthAsync(string securitySchemesComponent var simpleDescriptionPath = Path.Combine(workingDirectory) + "description.yaml"; await File.WriteAllTextAsync(simpleDescriptionPath, apiDescription); var mockLogger = new Mock>(); - var openApiDocumentDs = new OpenApiDocumentDownloadService(_httpClient, mockLogger.Object); + var openApiDocumentDs = new OpenApiDocumentDownloadService(_httpClient, _logger); var outputDirectory = Path.Combine(workingDirectory, "output"); var generationConfiguration = new GenerationConfiguration { @@ -457,7 +483,7 @@ public async Task GeneratesManifestWithAuthAsync(string securitySchemesComponent var urlTreeNode = OpenApiUrlTreeNode.Create(openApiDocument, Constants.DefaultOpenApiLabel); var pluginsGenerationService = - new PluginsGenerationService(openApiDocument, urlTreeNode, generationConfiguration, workingDirectory); + new PluginsGenerationService(openApiDocument, urlTreeNode, generationConfiguration, workingDirectory, _logger); await pluginsGenerationService.GenerateManifestAsync(); Assert.True(File.Exists(Path.Combine(outputDirectory, ManifestFileName))); @@ -480,98 +506,6 @@ public async Task GeneratesManifestWithAuthAsync(string securitySchemesComponent } } - public static TheoryData, Task>> - SecurityInformationFail() - { - return new TheoryData, Task>> - { - // multiple security schemes in operation object - { - "{securitySchemes: {apiKey0: {type: apiKey, name: x-api-key0, in: header}, apiKey1: {type: apiKey, name: x-api-key1, in: header}}}", - string.Empty, "security: [apiKey0: [], apiKey1: []]", null, async (action) => - { - await Assert.ThrowsAsync(async () => - { - await action(); - }); - } - }, - // Unsupported security scheme (http basic) - { - "{securitySchemes: {httpBasic0: {type: http, scheme: basic}}}", - string.Empty, "security: [httpBasic0: []]", null, async (action) => - { - await Assert.ThrowsAsync(async () => - { - await action(); - }); - } - }, - }; - } - - [Theory] - [MemberData(nameof(SecurityInformationFail))] - public async Task FailsToGeneratesManifestWithInvalidAuthAsync(string securitySchemesComponent, string rootSecurity, - string operationSecurity, PluginAuthConfiguration pluginAuthConfiguration, Func, Task> assertions) - { - var apiDescription = $""" - openapi: 3.0.0 - info: - title: test - version: "1.0" - paths: - /test: - get: - description: description for test path - responses: - '200': - description: test - {operationSecurity} - {rootSecurity} - components: {securitySchemesComponent} - """; - var workingDirectory = Path.Combine(Path.GetTempPath(), Path.GetRandomFileName()); - var simpleDescriptionPath = Path.Combine(workingDirectory) + "description.yaml"; - await File.WriteAllTextAsync(simpleDescriptionPath, apiDescription); - var mockLogger = new Mock>(); - var openApiDocumentDs = new OpenApiDocumentDownloadService(_httpClient, mockLogger.Object); - var outputDirectory = Path.Combine(workingDirectory, "output"); - var generationConfiguration = new GenerationConfiguration - { - OutputPath = outputDirectory, - OpenAPIFilePath = "openapiPath", - PluginTypes = [PluginType.APIPlugin], - ClientClassName = "client", - ApiRootUrl = "http://localhost/", //Kiota builder would set this for us - PluginAuthInformation = pluginAuthConfiguration, - }; - var (openApiDocumentStream, _) = - await openApiDocumentDs.LoadStreamAsync(simpleDescriptionPath, generationConfiguration, null, false); - var openApiDocument = - await openApiDocumentDs.GetDocumentFromStreamAsync(openApiDocumentStream, generationConfiguration); - Assert.NotNull(openApiDocument); - KiotaBuilder.CleanupOperationIdForPlugins(openApiDocument); - var urlTreeNode = OpenApiUrlTreeNode.Create(openApiDocument, Constants.DefaultOpenApiLabel); - - var pluginsGenerationService = - new PluginsGenerationService(openApiDocument, urlTreeNode, generationConfiguration, workingDirectory); - - await assertions(async () => - { - await pluginsGenerationService.GenerateManifestAsync(); - }); - // cleanup - try - { - Directory.Delete(outputDirectory); - } - catch (Exception) - { - // ignored - } - } - [Fact] public async Task GeneratesManifestWithMultipleSecuritySchemesAsync() { @@ -608,7 +542,7 @@ public async Task GeneratesManifestWithMultipleSecuritySchemesAsync() var simpleDescriptionPath = Path.Combine(workingDirectory) + "description.yaml"; await File.WriteAllTextAsync(simpleDescriptionPath, apiDescription); var mockLogger = new Mock>(); - var openApiDocumentDs = new OpenApiDocumentDownloadService(_httpClient, mockLogger.Object); + var openApiDocumentDs = new OpenApiDocumentDownloadService(_httpClient, _logger); var outputDirectory = Path.Combine(workingDirectory, "output"); var generationConfiguration = new GenerationConfiguration { @@ -627,7 +561,7 @@ public async Task GeneratesManifestWithMultipleSecuritySchemesAsync() var urlTreeNode = OpenApiUrlTreeNode.Create(openApiDocument, Constants.DefaultOpenApiLabel); var pluginsGenerationService = - new PluginsGenerationService(openApiDocument, urlTreeNode, generationConfiguration, workingDirectory); + new PluginsGenerationService(openApiDocument, urlTreeNode, generationConfiguration, workingDirectory, _logger); await pluginsGenerationService.GenerateManifestAsync(); Assert.True(File.Exists(Path.Combine(outputDirectory, ManifestFileName))); @@ -814,7 +748,7 @@ public async Task MergesAllOfRequestBodyAsync(string content, Action>(); - var openAPIDocumentDS = new OpenApiDocumentDownloadService(_httpClient, mockLogger.Object); + var openAPIDocumentDS = new OpenApiDocumentDownloadService(_httpClient, _logger); var outputDirectory = Path.Combine(workingDirectory, "output"); var generationConfiguration = new GenerationConfiguration { @@ -829,7 +763,7 @@ public async Task MergesAllOfRequestBodyAsync(string content, Action) => void; constructor( - openApiTreeProvider: OpenApiTreeProvider, - context: vscode.ExtensionContext, - dependenciesViewProvider: DependenciesViewProvider, - setWorkspaceGenerationContext: (params: Partial) => void + private _openApiTreeProvider: OpenApiTreeProvider, + private _context: vscode.ExtensionContext, + private _dependenciesViewProvider: DependenciesViewProvider, + private _setWorkspaceGenerationContext: (params: Partial) => void, + private _kiotaOutputChannel: vscode.LogOutputChannel ) { super(); - this._openApiTreeProvider = openApiTreeProvider; - this._context = context; - this._dependenciesViewProvider = dependenciesViewProvider; - this._setWorkspaceGenerationContext = setWorkspaceGenerationContext; } public getName(): string { @@ -127,6 +120,23 @@ export class GenerateClientCommand extends Command { ); return; } + + const authenticationWarnings = getLogEntriesForLevel(result ?? [], LogLevel.warning).filter(entry => entry.message.startsWith('Authentication warning')); + if (authenticationWarnings.length > 0) { + authenticationWarnings.forEach(entry => logFromLogLevel(entry, this._kiotaOutputChannel)); + + const showLogs = vscode.l10n.t("Show logs"); + const response = await vscode.window.showWarningMessage( + vscode.l10n.t( + "Incompatible security schemes for Copilot usage detected in the selected endpoints."), + showLogs, + vscode.l10n.t("Cancel") + ); + if (response === showLogs) { + this._kiotaOutputChannel.show(); + } + } + if (result && getLogEntriesForLevel(result, LogLevel.critical, LogLevel.error).length === 0) { // Save state before opening the new window const outputState = { @@ -206,7 +216,7 @@ export class GenerateClientCommand extends Command { if (result) { const isSuccess = await checkForSuccess(result); if (!isSuccess) { - await exportLogsAndShowErrors(result); + await exportLogsAndShowErrors(result, this._kiotaOutputChannel); } void vscode.window.showInformationMessage(vscode.l10n.t('Generation completed successfully.')); } @@ -250,7 +260,7 @@ export class GenerateClientCommand extends Command { if (result) { const isSuccess = await checkForSuccess(result); if (!isSuccess) { - await exportLogsAndShowErrors(result); + await exportLogsAndShowErrors(result, this._kiotaOutputChannel); } const deepLinkParams = getDeepLinkParams(); const isttkIntegration = deepLinkParams.source?.toLowerCase() === 'ttk'; @@ -320,7 +330,7 @@ export class GenerateClientCommand extends Command { if (result) { const isSuccess = await checkForSuccess(result); if (!isSuccess) { - await exportLogsAndShowErrors(result); + await exportLogsAndShowErrors(result, this._kiotaOutputChannel); } void vscode.window.showInformationMessage(vscode.l10n.t('Generation completed successfully.')); } diff --git a/vscode/microsoft-kiota/src/commands/generate/generation-util.ts b/vscode/microsoft-kiota/src/commands/generate/generation-util.ts index a8782a072a..b3b407a823 100644 --- a/vscode/microsoft-kiota/src/commands/generate/generation-util.ts +++ b/vscode/microsoft-kiota/src/commands/generate/generation-util.ts @@ -1,22 +1,10 @@ import * as vscode from "vscode"; import { treeViewId } from "../../constants"; -import { KiotaLogEntry } from "../../kiotaInterop"; import { OpenApiTreeProvider } from "../../providers/openApiTreeProvider"; import { getWorkspaceJsonPath, updateTreeViewIcons } from "../../util"; import { loadWorkspaceFile } from "../../utilities/file"; -export async function checkForSuccess(results: KiotaLogEntry[]) { - for (const result of results) { - if (result && result.message) { - if (result.message.includes("Generation completed successfully")) { - return true; - } - } - } - return false; -} - export async function displayGenerationResults(openApiTreeProvider: OpenApiTreeProvider, config: any) { const clientNameOrPluginName = config.clientClassName || config.pluginName; openApiTreeProvider.refreshView(); diff --git a/vscode/microsoft-kiota/src/commands/regenerate/regenerate.service.ts b/vscode/microsoft-kiota/src/commands/regenerate/regenerate.service.ts index 139425d8ff..ba72a5b966 100644 --- a/vscode/microsoft-kiota/src/commands/regenerate/regenerate.service.ts +++ b/vscode/microsoft-kiota/src/commands/regenerate/regenerate.service.ts @@ -9,23 +9,14 @@ import { OpenApiTreeProvider } from "../../providers/openApiTreeProvider"; import { KiotaGenerationLanguage, KiotaPluginType } from "../../types/enums"; import { ExtensionSettings } from "../../types/extensionSettings"; import { parseGenerationLanguage, parsePluginType } from "../../util"; -import { exportLogsAndShowErrors } from "../../utilities/logging"; +import { checkForSuccess, exportLogsAndShowErrors } from "../../utilities/logging"; import { generateClient } from "../generate/generateClient"; import { generatePlugin } from "../generate/generatePlugin"; -import { checkForSuccess } from "../generate/generation-util"; export class RegenerateService { - private _context: ExtensionContext; - private _openApiTreeProvider: OpenApiTreeProvider; - private _clientKey: string; - private _clientObject: ClientOrPluginProperties; - public constructor(context: ExtensionContext, openApiTreeProvider: OpenApiTreeProvider, - clientKey: string, clientObject: ClientOrPluginProperties) { - this._context = context; - this._openApiTreeProvider = openApiTreeProvider; - this._clientKey = clientKey; - this._clientObject = clientObject; + public constructor(private _context: ExtensionContext, private _openApiTreeProvider: OpenApiTreeProvider, + private _clientKey: string, private _clientObject: ClientOrPluginProperties, private _kiotaOutputChannel: vscode.LogOutputChannel) { } async regenerateClient(settings: ExtensionSettings, selectedPaths?: string[]): Promise { @@ -62,7 +53,7 @@ export class RegenerateService { if (result) { const isSuccess = await checkForSuccess(result); if (!isSuccess) { - await exportLogsAndShowErrors(result); + await exportLogsAndShowErrors(result, this._kiotaOutputChannel); } void vscode.window.showInformationMessage(`Client ${this._clientKey} re-generated successfully.`); } @@ -107,7 +98,7 @@ export class RegenerateService { if (result) { const isSuccess = await checkForSuccess(result); if (!isSuccess) { - await exportLogsAndShowErrors(result); + await exportLogsAndShowErrors(result, this._kiotaOutputChannel); } void vscode.window.showInformationMessage(vscode.l10n.t(`Plugin ${this._clientKey} re-generated successfully.`)); } diff --git a/vscode/microsoft-kiota/src/commands/regenerate/regenerateButtonCommand.ts b/vscode/microsoft-kiota/src/commands/regenerate/regenerateButtonCommand.ts index abfcdffdac..44e07ad3ff 100644 --- a/vscode/microsoft-kiota/src/commands/regenerate/regenerateButtonCommand.ts +++ b/vscode/microsoft-kiota/src/commands/regenerate/regenerateButtonCommand.ts @@ -12,13 +12,9 @@ import { Command } from "../Command"; import { RegenerateService } from "./regenerate.service"; export class RegenerateButtonCommand extends Command { - private _context: ExtensionContext; - private _openApiTreeProvider: OpenApiTreeProvider; - constructor(context: ExtensionContext, openApiTreeProvider: OpenApiTreeProvider) { + constructor(private _context: ExtensionContext, private _openApiTreeProvider: OpenApiTreeProvider, private _kiotaOutputChannel: vscode.LogOutputChannel) { super(); - this._context = context; - this._openApiTreeProvider = openApiTreeProvider; } public getName(): string { @@ -53,7 +49,7 @@ export class RegenerateButtonCommand extends Command { } const configObject = clientOrPluginObject || configuration; - const regenerateService = new RegenerateService(this._context, this._openApiTreeProvider, clientOrPluginKey, configObject); + const regenerateService = new RegenerateService(this._context, this._openApiTreeProvider, clientOrPluginKey, configObject, this._kiotaOutputChannel); if (isClientType(generationType)) { await regenerateService.regenerateClient(settings, selectedPaths); diff --git a/vscode/microsoft-kiota/src/commands/regenerate/regenerateCommand.ts b/vscode/microsoft-kiota/src/commands/regenerate/regenerateCommand.ts index f121131698..d9647fe9b6 100644 --- a/vscode/microsoft-kiota/src/commands/regenerate/regenerateCommand.ts +++ b/vscode/microsoft-kiota/src/commands/regenerate/regenerateCommand.ts @@ -11,13 +11,9 @@ import { Command } from "../Command"; import { RegenerateService } from "./regenerate.service"; export class RegenerateCommand extends Command { - private _context: ExtensionContext; - private _openApiTreeProvider: OpenApiTreeProvider; - constructor(context: ExtensionContext, openApiTreeProvider: OpenApiTreeProvider) { + constructor(private _context: ExtensionContext, private _openApiTreeProvider: OpenApiTreeProvider, private _kiotaOutputChannel: vscode.LogOutputChannel) { super(); - this._context = context; - this._openApiTreeProvider = openApiTreeProvider; } public getName(): string { @@ -40,7 +36,7 @@ export class RegenerateCommand extends Command { return; } - const regenerateService = new RegenerateService(this._context, this._openApiTreeProvider, clientOrPluginKey, clientOrPluginObject); + const regenerateService = new RegenerateService(this._context, this._openApiTreeProvider, clientOrPluginKey, clientOrPluginObject, this._kiotaOutputChannel); if (isClientType(generationType)) { await regenerateService.regenerateClient(settings); } diff --git a/vscode/microsoft-kiota/src/commands/updateClients/updateClientsCommand.ts b/vscode/microsoft-kiota/src/commands/updateClients/updateClientsCommand.ts index 2e05bd7692..7b517c15f6 100644 --- a/vscode/microsoft-kiota/src/commands/updateClients/updateClientsCommand.ts +++ b/vscode/microsoft-kiota/src/commands/updateClients/updateClientsCommand.ts @@ -9,13 +9,12 @@ import { Command } from "../Command"; import { updateClients } from './updateClients'; interface UpdateClientsCommandProps { - kiotaOutputChannel: vscode.LogOutputChannel; kiotaStatusBarItem: vscode.StatusBarItem; } export class UpdateClientsCommand extends Command { - constructor(private context: vscode.ExtensionContext) { + constructor(private context: vscode.ExtensionContext, private kiotaOutputChannel: vscode.LogOutputChannel) { super(); } @@ -23,7 +22,7 @@ export class UpdateClientsCommand extends Command { return `${extensionId}.updateClients`; } - public async execute({ kiotaOutputChannel, kiotaStatusBarItem }: UpdateClientsCommandProps): Promise { + public async execute({ kiotaStatusBarItem }: UpdateClientsCommandProps): Promise { if ( !vscode.workspace.workspaceFolders || vscode.workspace.workspaceFolders.length === 0 @@ -37,11 +36,11 @@ export class UpdateClientsCommand extends Command { if (existingApiManifestFileUris.length > 0) { await Promise.all(existingApiManifestFileUris.map(uri => showUpgradeWarningMessage(uri, null, null, this.context))); } - await updateStatusBarItem(this.context, kiotaOutputChannel, kiotaStatusBarItem); + await updateStatusBarItem(this.context, this.kiotaOutputChannel, kiotaStatusBarItem); try { - kiotaOutputChannel.clear(); - kiotaOutputChannel.show(); - kiotaOutputChannel.info( + this.kiotaOutputChannel.clear(); + this.kiotaOutputChannel.show(); + this.kiotaOutputChannel.info( vscode.l10n.t("updating client with path {path}", { path: vscode.workspace.workspaceFolders[0].uri.fsPath, }) @@ -55,10 +54,10 @@ export class UpdateClientsCommand extends Command { return updateClients(this.context, settings.cleanOutput, settings.clearCache); }); if (res) { - await exportLogsAndShowErrors(res); + await exportLogsAndShowErrors(res, this.kiotaOutputChannel); } } catch (error) { - kiotaOutputChannel.error( + this.kiotaOutputChannel.error( vscode.l10n.t(`error updating the clients {error}`), error ); diff --git a/vscode/microsoft-kiota/src/extension.ts b/vscode/microsoft-kiota/src/extension.ts index 5728e823c7..d681521038 100644 --- a/vscode/microsoft-kiota/src/extension.ts +++ b/vscode/microsoft-kiota/src/extension.ts @@ -70,13 +70,13 @@ export async function activate( const openDocumentationPageCommand = new OpenDocumentationPageCommand(); const editPathsCommand = new EditPathsCommand(openApiTreeProvider); const searchOrOpenApiDescriptionCommand = new SearchOrOpenApiDescriptionCommand(openApiTreeProvider, context); - const generateClientCommand = new GenerateClientCommand(openApiTreeProvider, context, dependenciesInfoProvider, setWorkspaceGenerationContext); - const regenerateCommand = new RegenerateCommand(context, openApiTreeProvider); - const regenerateButtonCommand = new RegenerateButtonCommand(context, openApiTreeProvider); + const generateClientCommand = new GenerateClientCommand(openApiTreeProvider, context, dependenciesInfoProvider, setWorkspaceGenerationContext, kiotaOutputChannel); + const regenerateCommand = new RegenerateCommand(context, openApiTreeProvider, kiotaOutputChannel); + const regenerateButtonCommand = new RegenerateButtonCommand(context, openApiTreeProvider, kiotaOutputChannel); const closeDescriptionCommand = new CloseDescriptionCommand(openApiTreeProvider); const statusCommand = new StatusCommand(); const selectLockCommand = new SelectLockCommand(openApiTreeProvider); - const updateClientsCommand = new UpdateClientsCommand(context); + const updateClientsCommand = new UpdateClientsCommand(context, kiotaOutputChannel); await loadTreeView(context); await checkForLockFileAndPrompt(context); @@ -137,7 +137,7 @@ export async function activate( // update status bar item once at start await updateStatusBarItem(context, kiotaOutputChannel, kiotaStatusBarItem); - context.subscriptions.push(vscode.commands.registerCommand(updateClientsCommand.getName(), async () => await updateClientsCommand.execute({ kiotaOutputChannel, kiotaStatusBarItem }))); + context.subscriptions.push(vscode.commands.registerCommand(updateClientsCommand.getName(), async () => await updateClientsCommand.execute({ kiotaStatusBarItem }))); } function registerCommandWithTelemetry(reporter: TelemetryReporter, command: string, callback: (...args: any[]) => any, thisArg?: any): vscode.Disposable { diff --git a/vscode/microsoft-kiota/src/test/suite/commands/generateClientCommand.test.ts b/vscode/microsoft-kiota/src/test/suite/commands/generateClientCommand.test.ts index 448448c9d3..5f7b4de9ee 100644 --- a/vscode/microsoft-kiota/src/test/suite/commands/generateClientCommand.test.ts +++ b/vscode/microsoft-kiota/src/test/suite/commands/generateClientCommand.test.ts @@ -75,7 +75,9 @@ const setWorkspaceGenerationContext = (params: Partial { const sanbox = sinon.createSandbox(); - + let myOutputChannel = vscode.window.createOutputChannel("Kiota", { + log: true, + }); teardown(() => { sanbox.restore(); }); @@ -83,7 +85,7 @@ suite('GenerateClientCommand Test Suite', () => { test('test function getName of GenerateClientCommand', () => { var treeProvider = sinon.createStubInstance(treeModule.OpenApiTreeProvider); var viewProvider = sinon.createStubInstance(dependenciesModule.DependenciesViewProvider); - const generateClientCommand = new generateModule.GenerateClientCommand(treeProvider, context, viewProvider, setWorkspaceGenerationContext); + const generateClientCommand = new generateModule.GenerateClientCommand(treeProvider, context, viewProvider, setWorkspaceGenerationContext, myOutputChannel); assert.strictEqual("kiota.openApiExplorer.generateClient", generateClientCommand.getName()); }); @@ -92,7 +94,7 @@ suite('GenerateClientCommand Test Suite', () => { treeProvider.getSelectedPaths.returns([]); var viewProvider = sinon.createStubInstance(dependenciesModule.DependenciesViewProvider); const vscodeWindowSpy = sinon.stub(vscode.window, "showErrorMessage"); - const generateClientCommand = new generateModule.GenerateClientCommand(treeProvider, context, viewProvider, setWorkspaceGenerationContext); + const generateClientCommand = new generateModule.GenerateClientCommand(treeProvider, context, viewProvider, setWorkspaceGenerationContext, myOutputChannel); await generateClientCommand.execute(); assert.strictEqual((treeProvider.getSelectedPaths()).length, 0); sinon.assert.calledOnceWithMatch(vscodeWindowSpy, vscode.l10n.t("No endpoints selected, select endpoints first")); @@ -114,7 +116,7 @@ suite('GenerateClientCommand Test Suite', () => { const generateStepsFn = sinon.stub(generateStepsModule, "generateSteps"); generateStepsFn.resolves(config); const showUpgradeWarningMessageStub = sinon.stub(msgUtilitiesModule, "showUpgradeWarningMessage"); - const generateClientCommand = new generateModule.GenerateClientCommand(treeProvider, context, viewProvider, setWorkspaceGenerationContext); + const generateClientCommand = new generateModule.GenerateClientCommand(treeProvider, context, viewProvider, setWorkspaceGenerationContext, myOutputChannel); await generateClientCommand.execute(); assert.strictEqual((treeProvider.getSelectedPaths()).length, 1); vscodeWindowSpy.verify(); @@ -162,7 +164,7 @@ suite('GenerateClientCommand Test Suite', () => { deepLinkParamsHandler.setDeepLinkParams(pluginParams); //stub and call generateCommand - const generateClientCommand = new generateModule.GenerateClientCommand(treeProvider, context, viewProvider, setWorkspaceGenerationContext); + const generateClientCommand = new generateModule.GenerateClientCommand(treeProvider, context, viewProvider, setWorkspaceGenerationContext, myOutputChannel); const generatePluginAndRefreshUIExpectation = sinon.mock(generateClientCommand).expects( "generatePluginAndRefreshUI").once().withArgs( config, extensionSettings, "path/to/temp/folder", ["repairs"] @@ -216,7 +218,7 @@ suite('GenerateClientCommand Test Suite', () => { deepLinkParamsHandler.setDeepLinkParams(pluginParams); //stub and call generateCommand - const generateClientCommand = new generateModule.GenerateClientCommand(treeProvider, context, viewProvider, setWorkspaceGenerationContext); + const generateClientCommand = new generateModule.GenerateClientCommand(treeProvider, context, viewProvider, setWorkspaceGenerationContext, myOutputChannel); var outputPath = path.join("path", "to", "temp", "folder", "appPackage"); //make it os agnostic const generateManifestAndRefreshUIExpectation = sinon.mock(generateClientCommand).expects( "generateManifestAndRefreshUI").twice().withArgs( diff --git a/vscode/microsoft-kiota/src/utilities/logging.ts b/vscode/microsoft-kiota/src/utilities/logging.ts index 616e822965..64228de614 100644 --- a/vscode/microsoft-kiota/src/utilities/logging.ts +++ b/vscode/microsoft-kiota/src/utilities/logging.ts @@ -1,18 +1,14 @@ import * as vscode from 'vscode'; +import { LogOutputChannel } from 'vscode'; import { getLogEntriesForLevel, KiotaLogEntry, LogLevel } from '../kiotaInterop'; -let kiotaOutputChannel: vscode.LogOutputChannel; -kiotaOutputChannel = vscode.window.createOutputChannel("Kiota", { - log: true, -}); - -export async function exportLogsAndShowErrors(result: KiotaLogEntry[]): Promise { +export async function exportLogsAndShowErrors(result: KiotaLogEntry[], kiotaOutputChannel: LogOutputChannel): Promise { const errorMessages = result ? getLogEntriesForLevel(result, LogLevel.critical, LogLevel.error) : []; result.forEach((element) => { - logFromLogLevel(element); + logFromLogLevel(element, kiotaOutputChannel); }); if (errorMessages.length > 0) { await Promise.all(errorMessages.map((element) => { @@ -21,7 +17,7 @@ export async function exportLogsAndShowErrors(result: KiotaLogEntry[]): Promise< } } -function logFromLogLevel(entry: KiotaLogEntry): void { +export function logFromLogLevel(entry: KiotaLogEntry, kiotaOutputChannel: LogOutputChannel): void { switch (entry.level) { case LogLevel.critical: case LogLevel.error: @@ -41,3 +37,18 @@ function logFromLogLevel(entry: KiotaLogEntry): void { break; } } + +export async function checkForSuccess(results: KiotaLogEntry[]) { + for (const result of results) { + if (result && result.message) { + if (result.message.includes("Generation completed successfully")) { + return true; + } + } + } + return false; +} + +export function showLogs(kiotaOutputChannel: LogOutputChannel): void { + kiotaOutputChannel.show(); +}