diff --git a/src/Kiota.Builder/Configuration/ConsumerOperation.cs b/src/Kiota.Builder/Configuration/ConsumerOperation.cs index 0973975f29..ffd7ddf7b1 100644 --- a/src/Kiota.Builder/Configuration/ConsumerOperation.cs +++ b/src/Kiota.Builder/Configuration/ConsumerOperation.cs @@ -5,4 +5,5 @@ public enum ConsumerOperation Edit, Remove, Generate, + GenerateHttpSnippet } diff --git a/src/Kiota.Builder/HTTP/HttpSnippetGenerationService.cs b/src/Kiota.Builder/HTTP/HttpSnippetGenerationService.cs new file mode 100644 index 0000000000..6ba8c37d93 --- /dev/null +++ b/src/Kiota.Builder/HTTP/HttpSnippetGenerationService.cs @@ -0,0 +1,69 @@ +using System; +using System.IO; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using Kiota.Builder.Configuration; +using Kiota.Builder.Writers.http; +using Microsoft.OpenApi.Models; + +namespace Kiota.Builder.http +{ + public partial class HttpSnippetGenerationService + { + private readonly OpenApiDocument OAIDocument; + private readonly GenerationConfiguration Configuration; + + public HttpSnippetGenerationService(OpenApiDocument document, GenerationConfiguration configuration) + { + ArgumentNullException.ThrowIfNull(document); + ArgumentNullException.ThrowIfNull(configuration); + OAIDocument = document; + Configuration = configuration; + } + + public async Task GenerateHttpSnippetAsync(CancellationToken cancellationToken = default) + { + // Create a http snippet file for each uri path segment + // Get all the paths with at least one operation + var tasks = OAIDocument.Paths + .Where(x => x.Value.Operations.Any()) + .Select(x => new { Path = x.Key, PathItem = x.Value }) + .Select(x => GenerateSnippetForPathAsync(x.Path, x.PathItem, cancellationToken)); // Create tasks for each path + + await Task.WhenAll(tasks).ConfigureAwait(false); // Wait for all tasks to complete + } + + private async Task GenerateSnippetForPathAsync(string path, OpenApiPathItem pathItem, CancellationToken cancellationToken) + { + var descriptionFullPath = Path.Combine(Configuration.OutputPath, SanitizePathSegment(path)); + 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}.http", 4096); + await using var fileWriter = new StreamWriter(descriptionStream); + var serverUrl = ExtractServerUrl(OAIDocument); + await fileWriter.WriteLineAsync($"# Http snippet for {serverUrl}"); + await fileWriter.WriteLineAsync($"@url = {serverUrl}"); + await fileWriter.WriteLineAsync(); + var httpSnippetWriter = new HttpSnippetWriter(fileWriter); + httpSnippetWriter.WriteOpenApiPathItem(pathItem, path); + httpSnippetWriter.Flush(); + await fileWriter.FlushAsync(cancellationToken); +#pragma warning restore CA2007 // Consider calling ConfigureAwait on the awaited task + } + + private static string? ExtractServerUrl(OpenApiDocument document) + { + return document.Servers?.FirstOrDefault()?.Url; + } + + private static string SanitizePathSegment(string pathSegment) + { + // remove the leading '/' + return pathSegment.TrimStart('/'); + } + } +} diff --git a/src/Kiota.Builder/Kiota.Builder.csproj b/src/Kiota.Builder/Kiota.Builder.csproj index 96912deaca..ea985e8af8 100644 --- a/src/Kiota.Builder/Kiota.Builder.csproj +++ b/src/Kiota.Builder/Kiota.Builder.csproj @@ -53,6 +53,7 @@ runtime; build; native; contentfiles; analyzers; buildtransitive all + diff --git a/src/Kiota.Builder/KiotaBuilder.cs b/src/Kiota.Builder/KiotaBuilder.cs index d575562412..01438963fc 100644 --- a/src/Kiota.Builder/KiotaBuilder.cs +++ b/src/Kiota.Builder/KiotaBuilder.cs @@ -21,6 +21,7 @@ using Kiota.Builder.Exceptions; using Kiota.Builder.Export; using Kiota.Builder.Extensions; +using Kiota.Builder.http; using Kiota.Builder.Logging; using Kiota.Builder.Manifest; using Kiota.Builder.OpenApiExtensions; @@ -71,14 +72,26 @@ private async Task CleanOutputDirectoryAsync(CancellationToken cancellationToken { if (config.CleanOutput && Directory.Exists(config.OutputPath)) { - logger.LogInformation("Cleaning output directory {Path}", config.OutputPath); - // not using Directory.Delete on the main directory because it's locked when mapped in a container - foreach (var subDir in Directory.EnumerateDirectories(config.OutputPath)) - Directory.Delete(subDir, true); - await workspaceManagementService.BackupStateAsync(config.OutputPath, cancellationToken).ConfigureAwait(false); - foreach (var subFile in Directory.EnumerateFiles(config.OutputPath) - .Where(x => !x.EndsWith(FileLogLogger.LogFileName, StringComparison.OrdinalIgnoreCase))) - File.Delete(subFile); + if (IsHttpSnippetGeneration(config)) + { + // Delete all files ending in .http in the current folder and all subdirectories + foreach (var file in Directory.EnumerateFiles(config.OutputPath, "*.http", SearchOption.AllDirectories)) + { + File.Delete(file); + } + } + else + { + logger.LogInformation("Cleaning output directory {Path}", config.OutputPath); + // not using Directory.Delete on the main directory because it's locked when mapped in a container + foreach (var subDir in Directory.EnumerateDirectories(config.OutputPath)) + Directory.Delete(subDir, true); + await workspaceManagementService.BackupStateAsync(config.OutputPath, cancellationToken).ConfigureAwait(false); + foreach (var subFile in Directory.EnumerateFiles(config.OutputPath) + .Where(x => !x.EndsWith(FileLogLogger.LogFileName, StringComparison.OrdinalIgnoreCase))) + File.Delete(subFile); + } + } } public async Task GetUrlTreeNodeAsync(CancellationToken cancellationToken) @@ -250,6 +263,22 @@ public async Task GeneratePluginAsync(CancellationToken cancellationToken) }, cancellationToken).ConfigureAwait(false); } + public async Task GenerateHttpSnippetAsync(CancellationToken cancellationToken) + { + return await GenerateConsumerAsync(async (sw, stepId, openApiTree, CancellationToken) => + { + if (openApiDocument is null) + throw new InvalidOperationException("The OpenAPI document and the URL tree must be loaded before generating the http snippet"); + // generate http snippets + sw.Start(); + var service = new HttpSnippetGenerationService(openApiDocument, config); + await service.GenerateHttpSnippetAsync(cancellationToken).ConfigureAwait(false); + logger.LogInformation("http snippets generated successfully"); + StopLogAndReset(sw, $"step {++stepId} - generate http snippet - took"); + return stepId; + }, cancellationToken).ConfigureAwait(false); + } + /// /// Generates the code from the OpenAPI document /// @@ -311,7 +340,7 @@ private async Task GenerateConsumerAsync(Func GenerateConsumerAsync(Func config.Operation == ConsumerOperation.GenerateHttpSnippet; private async Task FinalizeWorkspaceAsync(Stopwatch sw, int stepId, OpenApiUrlTreeNode? openApiTree, string inputPath, CancellationToken cancellationToken) { // Write lock file diff --git a/src/Kiota.Builder/Writers/HTTP/HttpSnippetWriter.cs b/src/Kiota.Builder/Writers/HTTP/HttpSnippetWriter.cs new file mode 100644 index 0000000000..cd2eb10741 --- /dev/null +++ b/src/Kiota.Builder/Writers/HTTP/HttpSnippetWriter.cs @@ -0,0 +1,165 @@ +using System; +using System.Collections.Generic; +using System.IO; +using Microsoft.OpenApi.Interfaces; +using Microsoft.OpenApi.Models; +using Microsoft.OpenApi.Services; +using Microsoft.OpenApi.Writers; +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; + +namespace Kiota.Builder.Writers.http +{ + internal class HttpSnippetWriter(TextWriter writer) + { + /// + /// The text writer. + /// + protected TextWriter Writer + { + get; + } = writer; + + // Create OpenApiWriterSettings with InlineReferencedSchemas set to true + private static readonly OpenApiWriterSettings _settings = new() + { + InlineLocalReferences = true, + InlineExternalReferences = true, + }; + + /// + /// Writes the given OpenAPI URL tree node to the writer. + /// This includes writing all path items and their children. + /// + /// The OpenAPI URL tree node to write. + public void Write(OpenApiUrlTreeNode node) + { + WritePathItems(node); + WriteChildren(node); + } + + /// + /// Writes all the path items for the given OpenAPI URL tree node to the writer. + /// Each path item is processed by calling the method. + /// + /// The OpenAPI URL tree node containing the path items to write. + private void WritePathItems(OpenApiUrlTreeNode node) + { + // Write all the path items + foreach (var item in node.PathItems) + { + WriteOpenApiPathItem(item.Value, node.Path); + } + } + + /// + /// Writes the children of the given OpenAPI URL tree node to the writer. + /// Each child node is processed by calling the method. + /// + /// The OpenAPI URL tree node whose children are to be written. + private void WriteChildren(OpenApiUrlTreeNode node) + { + foreach (var item in node.Children) + { + Write(item.Value); + } + } + + /// + /// Writes the operations for the given OpenAPI path item to the writer. + /// Each operation includes the HTTP method, sanitized path, parameters, and a formatted HTTP request line. + /// + /// The OpenAPI path item containing the operations to write. + public void WriteOpenApiPathItem(OpenApiPathItem pathItem, string path) + { + // Sanitize the path element + path = SanitizePath(path); + + // Write the operation + foreach (var item in pathItem.Operations) + { + var operation = item.Key.ToString().ToUpperInvariant(); + + // write the comment which also acts as the sections delimiter + Writer.WriteLine($"### {operation} {path}"); + + // write the parameters + WriteParameters(item.Value.Parameters); + + // write the http request operation + Writer.WriteLine($"{operation} {{{{url}}}}{path} HTTP/1.1"); + + // Write the request body if any + WriteRequestBody(item.Value.RequestBody); + + Writer.WriteLine(); + } + } + + private void WriteRequestBody(OpenApiRequestBody requestBody) + { + if (requestBody == null) return; + + foreach (var content in requestBody.Content) + { + // Write content type + Writer.WriteLine("Content-Type: " + content.Key); + + var schema = content.Value.Schema; + if (schema == null) return; + + var json = ConvertToJson(schema); + JObject jsonSchema = JsonHelper.StripJsonDownToRequestObject(json); + Writer.WriteLine(jsonSchema.ToString(Formatting.Indented)); + } + } + + /// + /// Sanitizes the given path by replacing '\\' with '/' and '\' with '/'. + /// Also converts '{foo}' into '{{foo}}' so that they can be used as variables in the HTTP snippet. + /// + /// The path to sanitize. + /// The sanitized path. + private static string SanitizePath(string path) + { + return path.Replace("\\\\", "/", StringComparison.OrdinalIgnoreCase) + .Replace("\\", "/", StringComparison.OrdinalIgnoreCase) + .Replace("{", "{{", StringComparison.OrdinalIgnoreCase) + .Replace("}", "}}", StringComparison.OrdinalIgnoreCase); + } + + /// + /// Writes the given list of OpenAPI parameters to the writer. + /// Each parameter's description and example value are written as comments and variable assignments, respectively. + /// + /// The list of OpenAPI parameters to write. + private void WriteParameters(IList parameters) + { + foreach (var parameter in parameters) + { + var parameterJsonObject = ConvertToJson(parameter); + var name = parameterJsonObject["name"]?.ToString(); + if (string.IsNullOrWhiteSpace(name)) continue; + Writer.WriteLine($"# {parameterJsonObject["description"]?.ToString()}"); + Writer.WriteLine($"@{name} = {parameterJsonObject["example"]?.ToString()}"); + } + } + + /// + /// Flush the writer. + /// + public void Flush() + { + Writer.Flush(); + } + + private static JObject ConvertToJson(IOpenApiReferenceable schema) + { + using var stringWriter = new StringWriter(); + var jsonWriter = new OpenApiJsonWriter(stringWriter, _settings); + schema.SerializeAsV3WithoutReference(jsonWriter); + // Return the resulting JSON + return JObject.Parse(stringWriter.ToString()); + } + } +} diff --git a/src/Kiota.Builder/Writers/HTTP/JsonHelper.cs b/src/Kiota.Builder/Writers/HTTP/JsonHelper.cs new file mode 100644 index 0000000000..eaa49e433d --- /dev/null +++ b/src/Kiota.Builder/Writers/HTTP/JsonHelper.cs @@ -0,0 +1,143 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using Newtonsoft.Json.Linq; + +namespace Kiota.Builder.Writers.http +{ + internal class JsonHelper + { + /// + /// Recursively processes a JSON schema to remove schema-specific fields (`type`, `allOf`, `oneOf`, `required`, `properties`), + /// and replaces them with placeholders based on the type (e.g., "string" for strings, true for booleans, and first value for enums). + /// If an `example` field is present, it returns the `example` value. + /// In case of `oneOf`, only the first object is used. + /// In case of `allOf`, objects are merged into a single one. + /// + /// The JSON object (JObject) representing a schema. + /// A new JObject with schema fields stripped, or the `example` values if present. + public static JObject StripJsonDownToRequestObject(JObject obj) + { + // Check if the schema contains a top-level "example" field, return it if found. + if (obj["example"] is JObject example) + { + return example; + } + + // Handle oneOf: take the first object + if (obj.ContainsKey("oneOf") && obj["oneOf"] is JArray oneOfArray && oneOfArray.First is JObject firstSchema) + { + return StripJsonDownToRequestObject(firstSchema); + } + + // Handle allOf: merge objects + if (obj.ContainsKey("allOf") && obj["allOf"] is JArray allOfArray) + { + return MergeAllOfSchemas(allOfArray); + } + + // Process properties if present + if (obj.ContainsKey("properties")) + { + if (obj["properties"] is not JObject propertiesObject) return new JObject(); + + return ProcessProperties(propertiesObject); + } + + return obj; + } + + /// + /// Merges the objects in an allOf array. + /// + /// The array of objects (JArray) to merge. + /// A new JObject representing the merged object. + private static JObject MergeAllOfSchemas(JArray allOfArray) + { + var mergedObject = new JObject(); + + foreach (var schema in allOfArray) + { + if (schema is JObject schemaObject) + { + var processedSchema = StripJsonDownToRequestObject(schemaObject); + mergedObject.Merge(processedSchema, new JsonMergeSettings + { + MergeArrayHandling = MergeArrayHandling.Union + }); + } + } + + return mergedObject; + } + + /// + /// Processes the properties of a JSON schema object, replacing with values based on type. + /// + /// The properties object (JObject) to process. + /// A new JObject with processed properties. + private static JObject ProcessProperties(JObject properties) + { + var newProperties = new JObject(); + + foreach (var property in properties) + { + var propertyName = property.Key; + + if (property.Value is not JObject propertyValue) continue; + + if (propertyValue.ContainsKey("example")) + { + // Use the example value instead of the property name as the placeholder + newProperties[propertyName] = propertyValue["example"]; + } + else if (propertyValue.ContainsKey("enum")) + { + // Use the first value from the enum array + if (propertyValue["enum"] is JArray enumArray && enumArray.Count > 0) + { + newProperties[propertyName] = enumArray.First(); + } + } + else if (propertyValue.ContainsKey("type")) + { + newProperties[propertyName] = GetPlaceholderForType(propertyValue); + } + else if (propertyValue.ContainsKey("oneOf")) + { + // Use only the first object from oneOf + if (propertyValue["oneOf"] is JArray oneOfArray && oneOfArray.First is JObject firstSchema) + { + newProperties[propertyName] = StripJsonDownToRequestObject(firstSchema); + } + } + } + + return newProperties; + } + + /// + /// Processes a property with a "type" field and returns an appropriate placeholder. + /// + /// The value of the property (JObject). + /// A JToken representing the placeholder based on the type. + private static JToken? GetPlaceholderForType(JObject propertyValue) + { + var type = propertyValue["type"]?.ToString(); + + // Return appropriate placeholder based on the type + return type switch + { + "string" => "string", // Placeholder for strings + "integer" => 0, // Placeholder for integers + "number" => 0.0, // Placeholder for numbers + "boolean" => true, // Placeholder for booleans + "array" => new JArray(),// Placeholder for arrays + "object" => StripJsonDownToRequestObject(propertyValue), // Recursively process objects + _ => null // If type is not recognized, return null + }; + } + } +} diff --git a/src/kiota/Rpc/IServer.cs b/src/kiota/Rpc/IServer.cs index de7a284c0a..933c7b55c9 100644 --- a/src/kiota/Rpc/IServer.cs +++ b/src/kiota/Rpc/IServer.cs @@ -14,4 +14,5 @@ internal interface IServer Task InfoForDescriptionAsync(string descriptionPath, bool clearCache, CancellationToken cancellationToken); Task> GeneratePluginAsync(string openAPIFilePath, string outputPath, PluginType[] pluginTypes, string[] includePatterns, string[] excludePatterns, string clientClassName, bool cleanOutput, bool clearCache, string[] disabledValidationRules, ConsumerOperation operation, CancellationToken cancellationToken); Task> MigrateFromLockFileAsync(string lockDirectoryPath, CancellationToken cancellationToken); + Task> GenerateHttpSnippetAsync(string openAPIFilePath, string outputPath, string[] includePatterns, string[] excludePatterns, bool cleanOutput, bool clearCache, bool excludeBackwardCompatible, string[] disabledValidationRules, string[] structuredMimeTypes, CancellationToken cancellationToken); } diff --git a/src/kiota/Rpc/Server.cs b/src/kiota/Rpc/Server.cs index e866eab942..ee6b53542c 100644 --- a/src/kiota/Rpc/Server.cs +++ b/src/kiota/Rpc/Server.cs @@ -222,6 +222,41 @@ public async Task> GeneratePluginAsync(string openAPIFilePath, st } return globalLogger.LogEntries; } + + public async Task> GenerateHttpSnippetAsync(string openAPIFilePath, string outputPath, string[] includePatterns, string[] excludePatterns, bool cleanOutput, bool clearCache, bool excludeBackwardCompatible, string[] disabledValidationRules, string[] structuredMimeTypes, CancellationToken cancellationToken) + { + var globalLogger = new ForwardedLogger(); + var configuration = Configuration.Generation; + configuration.OpenAPIFilePath = GetAbsolutePath(openAPIFilePath); + configuration.OutputPath = GetAbsolutePath(outputPath); + configuration.CleanOutput = cleanOutput; + configuration.ClearCache = clearCache; + configuration.Operation = ConsumerOperation.GenerateHttpSnippet; + if (disabledValidationRules is { Length: > 0 }) + configuration.DisabledValidationRules = disabledValidationRules.ToHashSet(StringComparer.OrdinalIgnoreCase); + if (includePatterns is { Length: > 0 }) + configuration.IncludePatterns = includePatterns.Select(static x => x.TrimQuotes()).ToHashSet(StringComparer.OrdinalIgnoreCase); + if (excludePatterns is { Length: > 0 }) + configuration.ExcludePatterns = excludePatterns.Select(static x => x.TrimQuotes()).ToHashSet(StringComparer.OrdinalIgnoreCase); + configuration.OpenAPIFilePath = GetAbsolutePath(configuration.OpenAPIFilePath); + configuration.OutputPath = NormalizeSlashesInPath(GetAbsolutePath(configuration.OutputPath)); + try + { + using var fileLogger = new FileLogLogger(configuration.OutputPath, LogLevel.Warning); + var logger = new AggregateLogger(globalLogger, fileLogger); + var result = await new KiotaBuilder(logger, configuration, httpClient, IsConfigPreviewEnabled.Value).GenerateHttpSnippetAsync(cancellationToken); + if (result) + logger.LogInformation("Generation completed successfully"); + else + logger.LogInformation("Http snippet generation failed"); + } + catch (Exception ex) + { + globalLogger.LogCritical(ex, "error adding the client: {exceptionMessage}", ex.Message); + } + return globalLogger.LogEntries; + } + public LanguagesInformation Info() { return Configuration.Languages; diff --git a/tests/Kiota.Builder.Tests/HTTP/HttpSnippetGenerationServiceTests.cs b/tests/Kiota.Builder.Tests/HTTP/HttpSnippetGenerationServiceTests.cs new file mode 100644 index 0000000000..b857edfb87 --- /dev/null +++ b/tests/Kiota.Builder.Tests/HTTP/HttpSnippetGenerationServiceTests.cs @@ -0,0 +1,78 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Net.Http; +using System.Text; +using System.Text.Json; +using System.Threading; +using System.Threading.Tasks; +using Kiota.Builder.Configuration; +using Kiota.Builder.http; +using Kiota.Builder.Tests.OpenApiSampleFiles; +using Microsoft.Extensions.Logging; +using Moq; +using Xunit; + +namespace Kiota.Builder.Tests.http; + +public sealed class HttpSnippetGenerationServiceTests : IDisposable +{ + private readonly HttpClient _httpClient = new(); + + public void Dispose() + { + _httpClient.Dispose(); + } + + + [Fact] + public void Defensive() + { + Assert.Throws(() => new HttpSnippetGenerationService(null, new())); + Assert.Throws(() => new HttpSnippetGenerationService(new(), null)); + } + + [Fact] + public async Task GeneratesHttpSnippetAsync() + { + var workingDirectory = Path.Combine(Path.GetTempPath(), Path.GetRandomFileName()); + var simpleDescriptionPath = Path.Combine(workingDirectory) + "description.yaml"; + await File.WriteAllTextAsync(simpleDescriptionPath, Posts.OpenApiYaml); + 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", + }; + var (openAPIDocumentStream, _) = await openAPIDocumentDS.LoadStreamAsync(simpleDescriptionPath, generationConfiguration, null, false); + var openApiDocument = await openAPIDocumentDS.GetDocumentFromStreamAsync(openAPIDocumentStream, generationConfiguration); + + var httpSnippetGenerationService = new HttpSnippetGenerationService(openApiDocument, generationConfiguration); + await httpSnippetGenerationService.GenerateHttpSnippetAsync(); + + var fileNames = openApiDocument.Paths + .Where(x => x.Value.Operations.Any()) + .Select(x => x.Key) + .Select(x => Path.Combine(outputDirectory, x.TrimStart('/') + ".http")) + .ToList(); + + foreach (var file in fileNames) + { + Assert.True(File.Exists(file)); + } + + var httpSnipetContent = await File.ReadAllTextAsync(Path.Combine(outputDirectory, "posts.http")); + + Assert.Contains("GET {{url}}/posts HTTP/1.1", httpSnipetContent); + Assert.Contains("POST {{url}}/posts HTTP/1.1", httpSnipetContent); + Assert.Contains("Content-Type: application/json", httpSnipetContent); + Assert.Contains("\"userId\": 0", httpSnipetContent); + Assert.Contains("\"id\": 0", httpSnipetContent); + Assert.Contains("\"title\": \"string\",", httpSnipetContent); + Assert.Contains("\"body\": \"string\"", httpSnipetContent); + Assert.Contains("}", httpSnipetContent); + } +} diff --git a/tests/Kiota.Builder.Tests/KiotaBuilderTests.cs b/tests/Kiota.Builder.Tests/KiotaBuilderTests.cs index 6b17ad738e..d5b5f250a4 100644 --- a/tests/Kiota.Builder.Tests/KiotaBuilderTests.cs +++ b/tests/Kiota.Builder.Tests/KiotaBuilderTests.cs @@ -11,7 +11,7 @@ using Kiota.Builder.CodeDOM; using Kiota.Builder.Configuration; using Kiota.Builder.Extensions; - +using Kiota.Builder.Tests.OpenApiSampleFiles; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging.Abstractions; using Microsoft.OpenApi.Any; @@ -9533,6 +9533,25 @@ public void CleansUpOperationIdChangesOperationId() Assert.Equal("PostAdministrativeUnits_With201_response", operations[1].Value.OperationId); Assert.Equal("directory_adminstativeunits_item_get", operations[2].Value.OperationId); } + + [Fact] + public async Task GeneratesHttpSnippetsAsync() + { + var workingDirectory = Path.Combine(Path.GetTempPath(), Path.GetRandomFileName()); + var simpleDescriptionPath = Path.Combine(workingDirectory) + "description.yaml"; + await File.WriteAllTextAsync(simpleDescriptionPath, Posts.OpenApiYaml); + var outputDirectory = Path.Combine(workingDirectory, "output"); + var generationConfiguration = new GenerationConfiguration + { + OutputPath = outputDirectory, + OpenAPIFilePath = simpleDescriptionPath, + Operation = ConsumerOperation.GenerateHttpSnippet + }; + var kiotaBuilder = new KiotaBuilder(new Mock>().Object, generationConfiguration, _httpClient, true); + var result = await kiotaBuilder.GenerateHttpSnippetAsync(CancellationToken.None); + Assert.True(result); + } + [GeneratedRegex(@"^[a-zA-Z0-9_]*$", RegexOptions.IgnoreCase | RegexOptions.Singleline, 2000)] private static partial Regex OperationIdValidationRegex(); } diff --git a/tests/Kiota.Builder.Tests/OpenApiSampleFiles/Posts.cs b/tests/Kiota.Builder.Tests/OpenApiSampleFiles/Posts.cs new file mode 100644 index 0000000000..fc29818888 --- /dev/null +++ b/tests/Kiota.Builder.Tests/OpenApiSampleFiles/Posts.cs @@ -0,0 +1,122 @@ +namespace Kiota.Builder.Tests.OpenApiSampleFiles; + + +public static class Posts +{ + /** + * An OpenAPI 3.0.1 sample document with a union of objects, comprising a union of Cats and Dogs. + */ + public static readonly string OpenApiYaml = @"openapi: '3.0.2' +info: + title: JSONPlaceholder + version: '1.0' +servers: + - url: https://jsonplaceholder.typicode.com/ + +components: + schemas: + post: + type: object + properties: + userId: + type: integer + id: + type: integer + title: + type: string + body: + type: string + parameters: + post-id: + name: post-id + in: path + description: 'key: id of post' + required: true + style: simple + schema: + type: integer + +paths: + /posts: + get: + description: Get posts + operationId: list-posts + parameters: + - name: userId + in: query + description: Filter results by user ID + required: false + style: form + schema: + type: integer + maxItems: 1 + - name: title + in: query + description: Filter results by title + required: false + style: form + schema: + type: string + maxItems: 1 + responses: + '200': + description: OK + content: + application/json: + schema: + type: array + items: + $ref: '#/components/schemas/post' + post: + description: 'Create post' + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/post' + responses: + '201': + description: Created + content: + application/json: + schema: + $ref: '#/components/schemas/post' + /posts/{post-id}: + get: + description: 'Get post by ID' + parameters: + - $ref: '#/components/parameters/post-id' + responses: + '200': + description: OK + content: + application/json: + schema: + $ref: '#/components/schemas/post' + patch: + description: 'Update post' + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/post' + parameters: + - $ref: '#/components/parameters/post-id' + responses: + '200': + description: OK + content: + application/json: + schema: + $ref: '#/components/schemas/post' + delete: + description: 'Delete post' + parameters: + - $ref: '#/components/parameters/post-id' + responses: + '200': + description: OK +"; +} diff --git a/tests/Kiota.Builder.Tests/Writers/HTTP/HttpSnippetWriterTests.cs b/tests/Kiota.Builder.Tests/Writers/HTTP/HttpSnippetWriterTests.cs new file mode 100644 index 0000000000..44bef4aa46 --- /dev/null +++ b/tests/Kiota.Builder.Tests/Writers/HTTP/HttpSnippetWriterTests.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.http; +using Kiota.Builder.Tests.OpenApiSampleFiles; +using Kiota.Builder.Writers.http; +using Microsoft.Extensions.Logging; +using Microsoft.OpenApi.Services; +using Moq; +using Xunit; + +namespace Kiota.Builder.Tests.Writers.http; +public sealed class HttpSnippetWriterTests : IDisposable +{ + private readonly StringWriter writer; + private readonly HttpClient _httpClient = new(); + + public HttpSnippetWriterTests() + { + writer = new StringWriter(); + } + + public void Dispose() + { + _httpClient.Dispose(); + writer?.Dispose(); + GC.SuppressFinalize(this); + } + + [Fact] + public async Task WritesHttpSnippetAsync() + { + var workingDirectory = Path.Combine(Path.GetTempPath(), Path.GetRandomFileName()); + var simpleDescriptionPath = Path.Combine(workingDirectory) + "description.yaml"; + await File.WriteAllTextAsync(simpleDescriptionPath, Posts.OpenApiYaml); + 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" + }; + var (openAPIDocumentStream, _) = await openAPIDocumentDS.LoadStreamAsync(simpleDescriptionPath, generationConfiguration, null, false); + var openApiDocument = await openAPIDocumentDS.GetDocumentFromStreamAsync(openAPIDocumentStream, generationConfiguration); + var urlTreeNode = OpenApiUrlTreeNode.Create(openApiDocument, Constants.DefaultOpenApiLabel); + var httpSnippetWriter = new HttpSnippetWriter(writer); + httpSnippetWriter.Write(urlTreeNode); + + var result = writer.ToString(); + + Assert.Contains("GET {{url}}/posts/{{post-id}} HTTP/1.1", result); + Assert.Contains("PATCH {{url}}/posts/{{post-id}} HTTP/1.1", result); + Assert.Contains("Content-Type: application/json", result); + Assert.Contains("\"userId\": 0", result); + Assert.Contains("\"id\": 0", result); + Assert.Contains("\"title\": \"string\",", result); + Assert.Contains("\"body\": \"string\"", result); + Assert.Contains("}", result); + } + +} diff --git a/tests/Kiota.Builder.Tests/Writers/HTTP/JsonHelperTests.cs b/tests/Kiota.Builder.Tests/Writers/HTTP/JsonHelperTests.cs new file mode 100644 index 0000000000..13c9f6e2cd --- /dev/null +++ b/tests/Kiota.Builder.Tests/Writers/HTTP/JsonHelperTests.cs @@ -0,0 +1,202 @@ +using Kiota.Builder.Writers.http; +using Newtonsoft.Json.Linq; +using Xunit; + +namespace Kiota.Builder.Tests.Writers.http; +public class JsonHelperTests +{ + [Fact] + public void StripJsonDownToRequestObject_ExampleAtTopLevel_ReturnsExample() + { + // Arrange + var json = JObject.Parse(@"{ + 'example': { + 'field': 'value' + } + }"); + + // Act + var result = JsonHelper.StripJsonDownToRequestObject(json); + + // Assert + Assert.Equal("value", result["field"].ToString()); + } + + [Fact] + public void StripJsonDownToRequestObject_OneOf_ReturnsFirstSchema() + { + // Arrange + var json = JObject.Parse(@"{ + 'oneOf': [ + { 'type': 'object', 'properties': { 'field1': { 'type': 'string' } } }, + { 'type': 'object', 'properties': { 'field2': { 'type': 'integer' } } } + ] + }"); + + // Act + var result = JsonHelper.StripJsonDownToRequestObject(json); + + // Assert + Assert.Equal("string", result["field1"].ToString()); + } + + [Fact] + public void StripJsonDownToRequestObject_AllOf_MergesSchemas() + { + // Arrange + var json = JObject.Parse(@"{ + 'allOf': [ + { 'type': 'object', 'properties': { 'field1': { 'type': 'string' } } }, + { 'type': 'object', 'properties': { 'field2': { 'type': 'integer' } } } + ] + }"); + + // Act + var result = JsonHelper.StripJsonDownToRequestObject(json); + + // Assert + Assert.Equal("string", result["field1"].ToString()); + Assert.Equal(0, result["field2"].ToObject()); + } + + [Fact] + public void StripJsonDownToRequestObject_Enum_UsesFirstEnumValue() + { + // Arrange + var json = JObject.Parse(@"{ + 'type': 'object', + 'properties': { + 'field': { + 'type': 'string', + 'enum': ['First', 'Second'] + } + } + }"); + + // Act + var result = JsonHelper.StripJsonDownToRequestObject(json); + + // Assert + Assert.Equal("First", result["field"].ToString()); + } + + [Fact] + public void StripJsonDownToRequestObject_Array_UsesEmptyArrayPlaceholder() + { + // Arrange + var json = JObject.Parse(@"{ + 'type': 'object', + 'properties': { + 'field': { + 'type': 'array', + 'items': { + 'type': 'string' + } + } + } + }"); + + // Act + var result = JsonHelper.StripJsonDownToRequestObject(json); + + // Assert + Assert.True(result["field"] is JArray); + } + + [Fact] + public void StripJsonDownToRequestObject_ObjectProperties_ProcessedCorrectly() + { + // Arrange + var json = JObject.Parse(@"{ + 'type': 'object', + 'properties': { + 'field1': { 'type': 'string' }, + 'field2': { 'type': 'integer' } + } + }"); + + // Act + var result = JsonHelper.StripJsonDownToRequestObject(json); + + // Assert + Assert.Equal("string", result["field1"].ToString()); + Assert.Equal(0, result["field2"].ToObject()); + } + + [Fact] + public void StripJsonDownToRequestObject_OneOf_WithComplexObject_ReturnsFirstObject() + { + // Arrange + var json = JObject.Parse(@"{ + 'oneOf': [ + { 'type': 'object', 'properties': { 'field1': { 'type': 'string' } } }, + { 'type': 'object', 'properties': { 'field2': { 'type': 'integer' } } } + ] + }"); + + // Act + var result = JsonHelper.StripJsonDownToRequestObject(json); + + // Assert + Assert.Equal("string", result["field1"].ToString()); + } + + [Fact] + public void StripJsonDownToRequestObject_AllOf_WithComplexObject_MergesObjects() + { + // Arrange + var json = JObject.Parse(@"{ + 'allOf': [ + { 'type': 'object', 'properties': { 'field1': { 'type': 'string' } } }, + { 'type': 'object', 'properties': { 'field2': { 'type': 'boolean' } } } + ] + }"); + + // Act + var result = JsonHelper.StripJsonDownToRequestObject(json); + + // Assert + Assert.Equal("string", result["field1"].ToString()); + Assert.True(result["field2"].ToObject()); + } + + [Fact] + public void StripJsonDownToRequestObject_PrimitiveTypes_ProcessedCorrectly() + { + // Arrange + var json = JObject.Parse(@"{ + 'type': 'object', + 'properties': { + 'stringField': { 'type': 'string' }, + 'integerField': { 'type': 'integer' }, + 'numberField': { 'type': 'number' }, + 'booleanField': { 'type': 'boolean' }, + 'nullField': { 'type': 'null' } + } + }"); + + // Act + var result = JsonHelper.StripJsonDownToRequestObject(json); + + // Assert + Assert.Equal("string", result["stringField"].ToString()); + Assert.Equal(0, result["integerField"].ToObject()); + Assert.Equal(0.0, result["numberField"].ToObject()); + Assert.True(result["booleanField"].ToObject()); + } + + [Fact] + public void StripJsonDownToRequestObject_NoProperties_ReturnsInput() + { + // Arrange + var json = JObject.Parse(@"{ + 'type': 'object' + }"); + + // Act + var result = JsonHelper.StripJsonDownToRequestObject(json); + + // Assert + Assert.Equal(json, result); + } +} diff --git a/vscode/microsoft-kiota/package.json b/vscode/microsoft-kiota/package.json index 9c6283c730..134df89a02 100644 --- a/vscode/microsoft-kiota/package.json +++ b/vscode/microsoft-kiota/package.json @@ -301,6 +301,11 @@ "command": "kiota.openApiExplorer.removeAllFromSelectedEndpoints", "when": "view == kiota.openApiExplorer && viewItem != clientNameOrPluginName", "group": "inline@5" + }, + { + "command": "kiota.openApiExplorer.generateHttpSnippet", + "when": "view == kiota.openApiExplorer", + "group": "inline@6" } ], "commandPalette": [ @@ -437,6 +442,12 @@ { "command": "kiota.migrateFromLockFile", "title": "%kiota.migrateClients.title%" + }, + { + "command": "kiota.openApiExplorer.generateHttpSnippet", + "category": "Kiota", + "title": "Generate http snippets", + "icon": "$(debug-alt)" } ], "languages": [ diff --git a/vscode/microsoft-kiota/src/enums.ts b/vscode/microsoft-kiota/src/enums.ts index e3340f0b2c..49c774f75b 100644 --- a/vscode/microsoft-kiota/src/enums.ts +++ b/vscode/microsoft-kiota/src/enums.ts @@ -5,6 +5,8 @@ export enum GenerationType { Plugin = 1, // eslint-disable-next-line @typescript-eslint/naming-convention ApiManifest = 2, + // eslint-disable-next-line @typescript-eslint/naming-convention + HttpSnippet = 3, }; export enum KiotaGenerationLanguage { diff --git a/vscode/microsoft-kiota/src/extension.ts b/vscode/microsoft-kiota/src/extension.ts index fe73196422..df379a92dd 100644 --- a/vscode/microsoft-kiota/src/extension.ts +++ b/vscode/microsoft-kiota/src/extension.ts @@ -30,7 +30,7 @@ import { import { checkForLockFileAndPrompt } from "./migrateFromLockFile"; import { OpenApiTreeNode, OpenApiTreeProvider } from "./openApiTreeProvider"; import { searchDescription } from "./searchDescription"; -import { GenerateState, filterSteps, generateSteps, searchSteps } from "./steps"; +import { GenerateState, filterSteps, generateSteps, searchSteps, generateHttpSnippetsSteps } from "./steps"; import { updateClients } from "./updateClients"; import { getSanitizedString, getWorkspaceJsonDirectory, getWorkspaceJsonPath, @@ -40,6 +40,7 @@ import { import { IntegrationParams, isDeeplinkEnabled, transformToGenerationConfig, validateDeepLinkQueryParams } from './utilities/deep-linking'; import { confirmOverride } from './utilities/regeneration'; import { loadTreeView } from "./workspaceTreeProvider"; +import { generateHttpSnippet } from './generateHttpSnippet'; let kiotaStatusBarItem: vscode.StatusBarItem; let kiotaOutputChannel: vscode.LogOutputChannel; @@ -191,6 +192,9 @@ export async function activate( case GenerationType.ApiManifest: result = await generateManifestAndRefreshUI(config, settings, outputPath, selectedPaths); break; + case GenerationType.HttpSnippet: + result = await generateHttpSnippetAndRefreshUI(config, settings, outputPath, selectedPaths); + break; default: await vscode.window.showErrorMessage( vscode.l10n.t("Invalid generation type") @@ -367,6 +371,75 @@ export async function activate( } }), registerCommandWithTelemetry(reporter, migrateFromLockFileCommand.getName(), async (uri: vscode.Uri) => await migrateFromLockFileCommand.execute(uri)), + registerCommandWithTelemetry(reporter, + `${treeViewId}.generateHttpSnippet`, + async () => { + const selectedPaths = openApiTreeProvider.getSelectedPaths(); + if (selectedPaths.length === 0) { + await vscode.window.showErrorMessage( + vscode.l10n.t("No endpoints selected, select endpoints first") + ); + return; + } + if ( + !vscode.workspace.workspaceFolders || + vscode.workspace.workspaceFolders.length === 0 + ) { + await vscode.window.showErrorMessage( + vscode.l10n.t("No workspace folder found, open a folder first") + ); + return; + } + const config = await generateHttpSnippetsSteps( + { + outputPath: openApiTreeProvider.outputPath, + } + ); + const outputPath = typeof config.outputPath === "string" + ? config.outputPath + : "./output"; + if (!openApiTreeProvider.descriptionUrl) { + await vscode.window.showErrorMessage( + vscode.l10n.t("No description found, select a description first") + ); + return; + } + const settings = getExtensionSettings(extensionId); + const result = await vscode.window.withProgress({ + location: vscode.ProgressLocation.Notification, + cancellable: false, + title: vscode.l10n.t("Generating http snippet...") + }, async (progress, _) => { + const start = performance.now(); + const result = await generateHttpSnippet( + context, + openApiTreeProvider.descriptionUrl, + outputPath, + selectedPaths, + [], + settings.clearCache, + settings.cleanOutput, + settings.excludeBackwardCompatible, + settings.disableValidationRules, + settings.structuredMimeTypes + ); + const duration = performance.now() - start; + const errorsCount = result ? getLogEntriesForLevel(result, LogLevel.critical, LogLevel.error).length : 0; + reporter.sendRawTelemetryEvent(`${extensionId}.generateHttpSnippet.completed`, { + "errorsCount": errorsCount.toString(), + }, { + "duration": duration, + }); + + // open the http snippet file + if (result && getLogEntriesForLevel(result, LogLevel.critical, LogLevel.error).length === 0) { + const httpSnippetFilePath = path.join(outputPath, "index.http"); + await openFile(httpSnippetFilePath); + } + return result; + }); + } + ) ); async function generateManifestAndRefreshUI(config: Partial, settings: ExtensionSettings, outputPath: string, selectedPaths: string[]): Promise { @@ -524,6 +597,48 @@ export async function activate( return result; } + async function generateHttpSnippetAndRefreshUI(config: Partial, settings: ExtensionSettings, outputPath: string, selectedPaths: string[]): Promise { + const result = await vscode.window.withProgress({ + location: vscode.ProgressLocation.Notification, + cancellable: false, + title: vscode.l10n.t("Generating http snippet...") + }, async (progress, _) => { + const start = performance.now(); + const result = await generateHttpSnippet( + context, + openApiTreeProvider.descriptionUrl, + outputPath, + selectedPaths, + [], + settings.clearCache, + settings.cleanOutput, + settings.excludeBackwardCompatible, + settings.disableValidationRules, + settings.structuredMimeTypes + ); + const duration = performance.now() - start; + const errorsCount = result ? getLogEntriesForLevel(result, LogLevel.critical, LogLevel.error).length : 0; + reporter.sendRawTelemetryEvent(`${extensionId}.generateHttpSnippet.completed`, { + "errorsCount": errorsCount.toString(), + }, { + "duration": duration, + }); + return result; + }); + if (result) { + const isSuccess = await checkForSuccess(result); + if (!isSuccess) { + await exportLogsAndShowErrors(result); + } else { + // open the http snippet file + const httpSnippetFilePath = path.join(outputPath, "index.http"); + await openFile(httpSnippetFilePath); + } + void vscode.window.showInformationMessage(vscode.l10n.t('Generation completed successfully.')); + } + return result; + } + async function displayGenerationResults(context: vscode.ExtensionContext, openApiTreeProvider: OpenApiTreeProvider, config: any) { const clientNameOrPluginName = config.clientClassName || config.pluginName; openApiTreeProvider.refreshView(); @@ -798,6 +913,11 @@ async function checkForSuccess(results: KiotaLogEntry[]) { return false; } +async function openFile(uri: string) { + if (fs.existsSync(uri)) { + await vscode.window.showTextDocument(vscode.Uri.file(uri)); + } +} // This method is called when your extension is deactivated export function deactivate() { } diff --git a/vscode/microsoft-kiota/src/generateHttpSnippet.ts b/vscode/microsoft-kiota/src/generateHttpSnippet.ts new file mode 100644 index 0000000000..8543034667 --- /dev/null +++ b/vscode/microsoft-kiota/src/generateHttpSnippet.ts @@ -0,0 +1,34 @@ +import { connectToKiota, HttpGenerationConfiguration, KiotaLogEntry } from "./kiotaInterop"; +import * as rpc from "vscode-jsonrpc/node"; +import * as vscode from "vscode"; + +export function generateHttpSnippet(context: vscode.ExtensionContext, + descriptionPath: string, + output: string, + includeFilters: string[], + excludeFilters: string[], + clearCache: boolean, + cleanOutput: boolean, + excludeBackwardCompatible: boolean, + disableValidationRules: string[], + structuredMimeTypes: string[]): Promise { + return connectToKiota(context, async (connection) => { + const request = new rpc.RequestType1( + "GenerateHttpSnippet" + ); + return await connection.sendRequest( + request, + { + cleanOutput: cleanOutput, + clearCache: clearCache, + disabledValidationRules: disableValidationRules, + excludeBackwardCompatible: excludeBackwardCompatible, + excludePatterns: excludeFilters, + includePatterns: includeFilters, + openAPIFilePath: descriptionPath, + outputPath: output, + structuredMimeTypes: structuredMimeTypes, + } as HttpGenerationConfiguration, + ); + }); +}; diff --git a/vscode/microsoft-kiota/src/kiotaInterop.ts b/vscode/microsoft-kiota/src/kiotaInterop.ts index 433b720c00..56f49d296f 100644 --- a/vscode/microsoft-kiota/src/kiotaInterop.ts +++ b/vscode/microsoft-kiota/src/kiotaInterop.ts @@ -271,3 +271,5 @@ export interface PluginObjectProperties extends WorkspaceObjectProperties { } export type ClientOrPluginProperties = ClientObjectProperties | PluginObjectProperties; + +export type HttpGenerationConfiguration = Omit; diff --git a/vscode/microsoft-kiota/src/steps.ts b/vscode/microsoft-kiota/src/steps.ts index f427c447bc..449aba4c0f 100644 --- a/vscode/microsoft-kiota/src/steps.ts +++ b/vscode/microsoft-kiota/src/steps.ts @@ -163,15 +163,55 @@ export async function generateSteps(existingConfiguration: Partial) { + while (true) { + const selectedOption = await input.showQuickPick({ + title: `${l10n.t('Generate HTTP snippet')} - ${l10n.t('output directory')}`, + step: 3, + totalSteps: 3, + placeholder: l10n.t('Enter an output path relative to the root of the project'), + items: inputOptions, + shouldResume: shouldResume + }); + if (selectedOption) { + if (selectedOption?.label === folderSelectionOption) { + const folderUri = await input.showOpenDialog({ + canSelectMany: false, + openLabel: 'Select', + canSelectFolders: true, + canSelectFiles: false + }); + + if (folderUri && folderUri[0]) { + state.outputPath = folderUri[0].fsPath; + } else { + continue; + } + } else { + state.outputPath = selectedOption.description; + if (workspaceOpen) { + state.workingDirectory = vscode.workspace.workspaceFolders![0].uri.fsPath; + } else { + state.workingDirectory = path.dirname(selectedOption.description!); + } + } + } + state.outputPath = state.outputPath === '' ? 'output' : state.outputPath; + break; + } + } async function inputGenerationType(input: MultiStepInput, state: Partial) { if (!isDeepLinkGenerationTypeProvided) { const items = [ l10n.t('Client'), l10n.t('Copilot plugin'), + l10n.t('HTTP snippet'), l10n.t('Other') ]; const option = await input.showQuickPick({ @@ -189,6 +229,9 @@ export async function generateSteps(existingConfiguration: Partial) { + const state = {...existingConfiguration} as Partial; + const title = l10n.t('Generate HTTP snippet'); + let step = 1; + let totalSteps = 1; + + async function inputOutputPath(input: MultiStepInput, state: Partial) { + const options: vscode.OpenDialogOptions = { + canSelectMany: false, + openLabel: 'Select', + canSelectFiles: false, + canSelectFolders: true, + }; + + const folderUri = await vscode.window.showOpenDialog(options); + if (folderUri && folderUri[0]) { + state.outputPath = folderUri[0].fsPath; + } else { + throw new Error('No folder selected'); + } + } + + await MultiStepInput.run(input => inputOutputPath(input, state), () => step -= 2); + return state; +} + function validateIsNotEmpty(value: string) { return Promise.resolve(value.length > 0 ? undefined : l10n.t('Required')); } @@ -469,6 +538,10 @@ export interface GenerateState extends BaseStepsState { workingDirectory: string; } +interface GenerateHttpSnippetState extends BaseStepsState { + outputPath: QuickPickItem | string; +} + class InputFlowAction { static back = new InputFlowAction(); static cancel = new InputFlowAction(); diff --git a/vscode/microsoft-kiota/src/util.ts b/vscode/microsoft-kiota/src/util.ts index 4f9367e686..74c9736116 100644 --- a/vscode/microsoft-kiota/src/util.ts +++ b/vscode/microsoft-kiota/src/util.ts @@ -112,6 +112,8 @@ export function parseGenerationType(generationType: string | QuickPickItem | und return GenerationType.Plugin; case "apimanifest": return GenerationType.ApiManifest; + case "httpSnippet": + return GenerationType.HttpSnippet; default: throw new Error(`Unknown generation type ${generationType}`); }