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}`);
}