diff --git a/.vscode/launch.json b/.vscode/launch.json index 2638c90765..b780e8e5c7 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -404,6 +404,25 @@ "type": "coreclr", "request": "attach", "processId": "${command:pickProcess}" + }, + { + "name": "Launch Http", + "type": "coreclr", + "request": "launch", + "preLaunchTask": "build", + "program": "${workspaceFolder}/src/kiota/bin/Debug/net8.0/kiota.dll", + "args": [ + "generate", + "--openapi", + "https://raw.githubusercontent.com/microsoftgraph/msgraph-sdk-powershell/dev/openApiDocs/v1.0/Mail.yml", + "--language", + "http", + "--output", + "${workspaceFolder}/samples/msgraph-mail/http", + ], + "cwd": "${workspaceFolder}/src/kiota", + "stopAtEntry": false, + "console": "internalConsole" } ] } diff --git a/src/Kiota.Builder/Constants.cs b/src/Kiota.Builder/Constants.cs index 1968837aa6..c0589d425a 100644 --- a/src/Kiota.Builder/Constants.cs +++ b/src/Kiota.Builder/Constants.cs @@ -4,3 +4,11 @@ public static class Constants public const string DefaultOpenApiLabel = "default"; public const string TempDirectoryName = "kiota"; } + +public enum Authentication +{ + Basic, + Bearer, + OAuthV2, + APIKey, +} diff --git a/src/Kiota.Builder/GenerationLanguage.cs b/src/Kiota.Builder/GenerationLanguage.cs index 278bdcd8b2..99dfd831f2 100644 --- a/src/Kiota.Builder/GenerationLanguage.cs +++ b/src/Kiota.Builder/GenerationLanguage.cs @@ -11,4 +11,5 @@ public enum GenerationLanguage Ruby, CLI, Dart, + HTTP } diff --git a/src/Kiota.Builder/KiotaBuilder.cs b/src/Kiota.Builder/KiotaBuilder.cs index 450e392103..418d8b20dd 100644 --- a/src/Kiota.Builder/KiotaBuilder.cs +++ b/src/Kiota.Builder/KiotaBuilder.cs @@ -26,6 +26,7 @@ using Kiota.Builder.OpenApiExtensions; using Kiota.Builder.Plugins; using Kiota.Builder.Refiners; +using Kiota.Builder.Settings; using Kiota.Builder.WorkspaceManagement; using Kiota.Builder.Writers; using Microsoft.Extensions.Logging; @@ -46,6 +47,7 @@ public partial class KiotaBuilder private readonly ParallelOptions parallelOptions; private readonly HttpClient httpClient; private OpenApiDocument? openApiDocument; + private readonly SettingsFileManagementService settingsFileManagementService = new(); internal void SetOpenApiDocument(OpenApiDocument document) => openApiDocument = document ?? throw new ArgumentNullException(nameof(document)); public KiotaBuilder(ILogger logger, GenerationConfiguration config, HttpClient client, bool useKiotaConfig = false) @@ -285,6 +287,11 @@ public async Task GenerateClientAsync(CancellationToken cancellationToken) sw.Start(); await CreateLanguageSourceFilesAsync(config.Language, generatedCode, cancellationToken).ConfigureAwait(false); StopLogAndReset(sw, $"step {++stepId} - writing files - took"); + + if (config.Language == GenerationLanguage.HTTP && openApiDocument is not null) + { + await settingsFileManagementService.WriteSettingsFileAsync(config.OutputPath, openApiDocument, cancellationToken).ConfigureAwait(false); + } return stepId; }, cancellationToken).ConfigureAwait(false); } @@ -554,6 +561,78 @@ public CodeNamespace CreateSourceModel(OpenApiUrlTreeNode? root) return rootNamespace; } + private void AddOperationSecurityRequirementToDOM(OpenApiOperation operation, CodeClass codeClass) + { + if (openApiDocument is null) + { + logger.LogWarning("OpenAPI document is null"); + return; + } + + if (operation.Security == null || !operation.Security.Any()) + return; + + var securitySchemes = openApiDocument.Components.SecuritySchemes; + foreach (var securityRequirement in operation.Security) + { + foreach (var scheme in securityRequirement.Keys) + { + var securityScheme = securitySchemes[scheme.Reference.Id]; + switch (securityScheme.Type) + { + case SecuritySchemeType.Http: + AddHttpSecurity(codeClass, securityScheme); + break; + case SecuritySchemeType.ApiKey: + AddApiKeySecurity(codeClass, securityScheme); + break; + case SecuritySchemeType.OAuth2: + AddOAuth2Security(codeClass, securityScheme); + break; + default: + logger.LogWarning("Unsupported security scheme type: {Type}", securityScheme.Type); + break; + } + } + } + } + + private void AddHttpSecurity(CodeClass codeClass, OpenApiSecurityScheme securityScheme) + { + codeClass.AddProperty( + new CodeProperty + { + Type = new CodeType { Name = Authentication.Basic.ToString(), IsExternal = true }, + Kind = CodePropertyKind.Headers, + DefaultValue = "basicAuth" + } + ); + } + + private void AddApiKeySecurity(CodeClass codeClass, OpenApiSecurityScheme securityScheme) + { + codeClass.AddProperty( + new CodeProperty + { + Type = new CodeType { Name = Authentication.APIKey.ToString(), IsExternal = true }, + Kind = CodePropertyKind.Headers, + DefaultValue = "apiKeyAuth" + } + ); + } + + private void AddOAuth2Security(CodeClass codeClass, OpenApiSecurityScheme securityScheme) + { + codeClass.AddProperty( + new CodeProperty + { + Type = new CodeType { Name = Authentication.OAuthV2.ToString(), IsExternal = true }, + Kind = CodePropertyKind.Headers, + DefaultValue = "bearerAuth" + } + ); + } + /// /// Manipulate CodeDOM for language specific issues /// @@ -671,7 +750,14 @@ private void CreateRequestBuilderClass(CodeNamespace currentNamespace, OpenApiUr foreach (var operation in currentNode .PathItems[Constants.DefaultOpenApiLabel] .Operations) + { + CreateOperationMethods(currentNode, operation.Key, operation.Value, codeClass); + if (config.Language == GenerationLanguage.HTTP) + { + AddOperationSecurityRequirementToDOM(operation.Value, codeClass); + } + } } if (rootNamespace != null) diff --git a/src/Kiota.Builder/PathSegmenters/HttpPathSegmenter.cs b/src/Kiota.Builder/PathSegmenters/HttpPathSegmenter.cs new file mode 100644 index 0000000000..052da6654f --- /dev/null +++ b/src/Kiota.Builder/PathSegmenters/HttpPathSegmenter.cs @@ -0,0 +1,13 @@ +using Kiota.Builder.CodeDOM; +using Kiota.Builder.Extensions; + +namespace Kiota.Builder.PathSegmenters; +public class HttpPathSegmenter(string rootPath, string clientNamespaceName) : CommonPathSegmenter(rootPath, clientNamespaceName) +{ + public override string FileSuffix => ".http"; + public override string NormalizeNamespaceSegment(string segmentName) => segmentName.ToFirstCharacterUpperCase(); + public override string NormalizeFileName(CodeElement currentElement) + { + return GetLastFileNameSegment(currentElement).ToFirstCharacterUpperCase(); + } +} diff --git a/src/Kiota.Builder/Refiners/HttpRefiner.cs b/src/Kiota.Builder/Refiners/HttpRefiner.cs new file mode 100644 index 0000000000..4c22b02778 --- /dev/null +++ b/src/Kiota.Builder/Refiners/HttpRefiner.cs @@ -0,0 +1,150 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using Kiota.Builder.CodeDOM; +using Kiota.Builder.Configuration; +using Kiota.Builder.Extensions; + +namespace Kiota.Builder.Refiners; +public class HttpRefiner(GenerationConfiguration configuration) : CommonLanguageRefiner(configuration) +{ + public override Task RefineAsync(CodeNamespace generatedCode, CancellationToken cancellationToken) + { + return Task.Run(() => + { + cancellationToken.ThrowIfCancellationRequested(); + CapitalizeNamespacesFirstLetters(generatedCode); + ReplaceIndexersByMethodsWithParameter( + generatedCode, + false, + static x => $"By{x.ToFirstCharacterUpperCase()}", + static x => x.ToFirstCharacterUpperCase(), + GenerationLanguage.HTTP); + cancellationToken.ThrowIfCancellationRequested(); + ReplaceReservedNames( + generatedCode, + new HttpReservedNamesProvider(), + x => $"{x}_escaped"); + RemoveCancellationParameter(generatedCode); + ConvertUnionTypesToWrapper( + generatedCode, + _configuration.UsesBackingStore, + static s => s + ); + cancellationToken.ThrowIfCancellationRequested(); + SetBaseUrlForRequestBuilderMethods(generatedCode, GetBaseUrl(generatedCode)); + AddPathParameters(generatedCode); + // Remove unused code from the DOM e.g Models, BarrelInitializers, e.t.c + RemoveUnusedCodeElements(generatedCode); + }, cancellationToken); + } + + private string? GetBaseUrl(CodeElement element) + { + return element.GetImmediateParentOfType() + .GetRootNamespace()? + .FindChildByName(_configuration.ClientClassName)? + .Methods? + .FirstOrDefault(static x => x.IsOfKind(CodeMethodKind.ClientConstructor))? + .BaseUrl; + } + + private static void CapitalizeNamespacesFirstLetters(CodeElement current) + { + if (current is CodeNamespace currentNamespace) + currentNamespace.Name = currentNamespace.Name.Split('.').Select(static x => x.ToFirstCharacterUpperCase()).Aggregate(static (x, y) => $"{x}.{y}"); + CrawlTree(current, CapitalizeNamespacesFirstLetters); + } + + private static void SetBaseUrlForRequestBuilderMethods(CodeElement current, string? baseUrl) + { + if (baseUrl is not null && current is CodeClass codeClass && codeClass.IsOfKind(CodeClassKind.RequestBuilder)) + { + // Add a new property named BaseUrl and set its value to the baseUrl string + var baseUrlProperty = new CodeProperty + { + Name = "BaseUrl", + Kind = CodePropertyKind.Custom, + Access = AccessModifier.Private, + DefaultValue = baseUrl, + Type = new CodeType { Name = "string", IsExternal = true } + }; + codeClass.AddProperty(baseUrlProperty); + } + CrawlTree(current, (element) => SetBaseUrlForRequestBuilderMethods(element, baseUrl)); + } + + private void RemoveUnusedCodeElements(CodeElement element) + { + if (!IsRequestBuilderClass(element) || IsBaseRequestBuilder(element) || IsRequestBuilderClassWithoutAnyHttpOperations(element)) + { + var parentNameSpace = element.GetImmediateParentOfType(); + parentNameSpace?.RemoveChildElement(element); + } + CrawlTree(element, RemoveUnusedCodeElements); + } + + private void AddPathParameters(CodeElement element) + { + var parent = element.GetImmediateParentOfType().Parent; + while (parent is not null) + { + var codeIndexer = parent.GetChildElements(false) + .OfType() + .FirstOrDefault()? + .GetChildElements(false) + .OfType() + .FirstOrDefault(static x => x.IsOfKind(CodeMethodKind.IndexerBackwardCompatibility)); + + if (codeIndexer is not null) + { + // Retrieve all the parameters of kind CodeParameterKind.Custom + var customParameters = codeIndexer.Parameters + .Where(static param => param.IsOfKind(CodeParameterKind.Custom)) + .ToList(); + + // For each parameter: + foreach (var param in customParameters) + { + // Create a new property of kind CodePropertyKind.PathParameters using the parameter and add it to the codeClass + var pathParameterProperty = new CodeProperty + { + Name = param.Name, + Kind = CodePropertyKind.PathParameters, + Type = param.Type, + Access = AccessModifier.Public, + DefaultValue = param.DefaultValue, + SerializationName = param.SerializationName, + Documentation = param.Documentation + }; + + if (element is CodeClass codeClass) + codeClass.AddProperty(pathParameterProperty); + } + } + + parent = parent.Parent?.GetImmediateParentOfType(); + } + CrawlTree(element, AddPathParameters); + } + + private static bool IsRequestBuilderClass(CodeElement element) + { + return element is CodeClass code && code.IsOfKind(CodeClassKind.RequestBuilder); + } + + private bool IsBaseRequestBuilder(CodeElement element) + { + return element is CodeClass codeClass && + codeClass.Name.Equals(_configuration.ClientClassName, StringComparison.Ordinal); + } + + private static bool IsRequestBuilderClassWithoutAnyHttpOperations(CodeElement element) + { + return element is CodeClass codeClass && codeClass.IsOfKind(CodeClassKind.RequestBuilder) && + !codeClass.Methods.Any(static method => method.IsOfKind(CodeMethodKind.RequestExecutor)); + } +} diff --git a/src/Kiota.Builder/Refiners/HttpReservedNamesProvider.cs b/src/Kiota.Builder/Refiners/HttpReservedNamesProvider.cs new file mode 100644 index 0000000000..df60e5ee20 --- /dev/null +++ b/src/Kiota.Builder/Refiners/HttpReservedNamesProvider.cs @@ -0,0 +1,11 @@ +using System; +using System.Collections.Generic; + +namespace Kiota.Builder.Refiners; +public class HttpReservedNamesProvider : IReservedNamesProvider +{ + private readonly Lazy> _reservedNames = new(() => new HashSet(StringComparer.OrdinalIgnoreCase) + { + }); + public HashSet ReservedNames => _reservedNames.Value; +} diff --git a/src/Kiota.Builder/Refiners/ILanguageRefiner.cs b/src/Kiota.Builder/Refiners/ILanguageRefiner.cs index 37b42e51cb..3573e98671 100644 --- a/src/Kiota.Builder/Refiners/ILanguageRefiner.cs +++ b/src/Kiota.Builder/Refiners/ILanguageRefiner.cs @@ -37,6 +37,9 @@ public static async Task RefineAsync(GenerationConfiguration config, CodeNamespa case GenerationLanguage.Swift: await new SwiftRefiner(config).RefineAsync(generatedCode, cancellationToken).ConfigureAwait(false); break; + case GenerationLanguage.HTTP: + await new HttpRefiner(config).RefineAsync(generatedCode, cancellationToken).ConfigureAwait(false); + break; case GenerationLanguage.Python: await new PythonRefiner(config).RefineAsync(generatedCode, cancellationToken).ConfigureAwait(false); break; diff --git a/src/Kiota.Builder/Settings/ISettingsManagementService.cs b/src/Kiota.Builder/Settings/ISettingsManagementService.cs new file mode 100644 index 0000000000..a7b719240b --- /dev/null +++ b/src/Kiota.Builder/Settings/ISettingsManagementService.cs @@ -0,0 +1,28 @@ +using System.IO; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.OpenApi.Models; +using Microsoft.OpenApi.Services; + +namespace Kiota.Builder.Settings; +/// +/// A service that manages the settings file for http language snippets. +/// +public interface ISettingsManagementService +{ + /// + /// Gets the settings file for a Kiota project by crawling the directory tree. + /// + /// + /// + string? GetDirectoryContainingSettingsFile(string searchDirectory); + + /// + /// Writes the settings file to a directory. + /// + /// + /// OpenApi document + /// + /// + Task WriteSettingsFileAsync(string directoryPath, OpenApiDocument openApiDocument, CancellationToken cancellationToken); +} diff --git a/src/Kiota.Builder/Settings/SettingsFile.cs b/src/Kiota.Builder/Settings/SettingsFile.cs new file mode 100644 index 0000000000..735c0e1110 --- /dev/null +++ b/src/Kiota.Builder/Settings/SettingsFile.cs @@ -0,0 +1,85 @@ +using System.Text.Json.Serialization; +using Kiota.Builder.Configuration; + +namespace Kiota.Builder.Settings; +public class SettingsFile +{ + [JsonPropertyName("rest-client.environmentVariables")] + public EnvironmentVariables EnvironmentVariables + { + get; set; + } + + public SettingsFile() + { + EnvironmentVariables = new EnvironmentVariables(); + } +} + +public class EnvironmentVariables +{ + [JsonPropertyName("$shared")] + public SharedAuth Shared + { + get; set; + } + + [JsonPropertyName("remote")] + public AuthenticationSettings Remote + { + get; set; + } + + [JsonPropertyName("development")] + public AuthenticationSettings Development + { + get; set; + } + + public EnvironmentVariables() + { + Shared = new SharedAuth(); + Remote = new AuthenticationSettings(); + Development = new AuthenticationSettings(); + } +} + +public class SharedAuth +{ + +} + +public class AuthenticationSettings +{ + [JsonPropertyName("hostAddress")] + public string HostAddress + { + get; set; + } + + [JsonPropertyName("basicAuth")] + public string? BasicAuth + { + get; set; + } + + [JsonPropertyName("bearerAuth")] + public string? BearerAuth + { + get; set; + } + + [JsonPropertyName("apiKeyAuth")] + public string? ApiKey + { + get; set; + } + + public AuthenticationSettings() + { + HostAddress = ""; + BasicAuth = ""; + BearerAuth = ""; + ApiKey = ""; + } +} diff --git a/src/Kiota.Builder/Settings/SettingsFileGenerationContext.cs b/src/Kiota.Builder/Settings/SettingsFileGenerationContext.cs new file mode 100644 index 0000000000..8b4ea3ba01 --- /dev/null +++ b/src/Kiota.Builder/Settings/SettingsFileGenerationContext.cs @@ -0,0 +1,14 @@ +using System; +using System.Collections.Generic; +using System.Text.Json; +using System.Text.Json.Serialization; + +namespace Kiota.Builder.Settings; + +[JsonSerializable(typeof(SettingsFile))] +[JsonSourceGenerationOptions(WriteIndented = true)] +[JsonSerializable(typeof(Dictionary))] +[JsonSerializable(typeof(Dictionary))] +internal partial class SettingsFileGenerationContext : JsonSerializerContext +{ +} diff --git a/src/Kiota.Builder/Settings/SettingsFileManagementService.cs b/src/Kiota.Builder/Settings/SettingsFileManagementService.cs new file mode 100644 index 0000000000..94a1e9aae0 --- /dev/null +++ b/src/Kiota.Builder/Settings/SettingsFileManagementService.cs @@ -0,0 +1,100 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Runtime.Serialization.Formatters.Binary; +using System.Text.Json; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.OpenApi.Models; + +namespace Kiota.Builder.Settings; + +public class SettingsFileManagementService : ISettingsManagementService +{ + internal const string SettingsFileName = "settings.json"; + internal const string EnvironmentVariablesKey = "rest-client.environmentVariables"; + internal const string VsCodeDirectoryName = ".vscode"; + public string GetDirectoryContainingSettingsFile(string searchDirectory) + { + var currentDirectory = new DirectoryInfo(searchDirectory); + var vscodeDirectoryPath = Path.Combine(currentDirectory.FullName, VsCodeDirectoryName); + if (Directory.Exists(vscodeDirectoryPath)) + { + return vscodeDirectoryPath; + } + var pathToWrite = Path.Combine(searchDirectory, VsCodeDirectoryName); + return Directory.CreateDirectory(pathToWrite).FullName; + } + + public Task WriteSettingsFileAsync(string directoryPath, OpenApiDocument openApiDocument, CancellationToken cancellationToken) + { + ArgumentException.ThrowIfNullOrEmpty(directoryPath); + ArgumentNullException.ThrowIfNull(openApiDocument); + var settings = GenerateSettingsFile(openApiDocument); + return WriteSettingsFileInternalAsync(directoryPath, settings, cancellationToken); + } + + private static SettingsFile GenerateSettingsFile(OpenApiDocument openApiDocument) + { + var settings = new SettingsFile(); + settings.EnvironmentVariables.Development.HostAddress = openApiDocument.Servers[0].Url; + settings.EnvironmentVariables.Remote.HostAddress = openApiDocument.Servers[0].Url; + return settings; + } + + private async Task WriteSettingsFileInternalAsync(string directoryPath, SettingsFile settings, CancellationToken cancellationToken) + { + var parentDirectoryPath = Path.GetDirectoryName(directoryPath); + var vscodeDirectoryPath = GetDirectoryContainingSettingsFile(parentDirectoryPath!); + var settingsObjectString = JsonSerializer.Serialize(settings, SettingsFileGenerationContext.Default.SettingsFile); + VsCodeSettingsManager settingsManager = new(vscodeDirectoryPath, SettingsFileName); + await settingsManager.UpdateFileAsync(settingsObjectString, EnvironmentVariablesKey, cancellationToken).ConfigureAwait(false); + } +} + +public class VsCodeSettingsManager +{ + private readonly string _vscodePath; + private readonly string fileUpdatePath; + + public VsCodeSettingsManager(string basePath, string targetFilePath) + { + _vscodePath = basePath; + fileUpdatePath = Path.Combine(_vscodePath, targetFilePath); + } + + public async Task UpdateFileAsync(string fileUpdate, string fileUpdateKey, CancellationToken cancellationToken) + { + ArgumentException.ThrowIfNullOrEmpty(fileUpdate); + Dictionary settings; + + // Read existing settings or create new if file doesn't exist + if (File.Exists(fileUpdatePath)) + { + string jsonContent = await File.ReadAllTextAsync(fileUpdatePath, cancellationToken).ConfigureAwait(false); + try + { + settings = JsonSerializer.Deserialize( + jsonContent, + SettingsFileGenerationContext.Default.DictionaryStringObject) + ?? []; + } + catch (JsonException) + { + settings = []; + } + } + else + { + settings = []; + } + + var fileUpdateDictionary = JsonSerializer.Deserialize>(fileUpdate, SettingsFileGenerationContext.Default.DictionaryStringObject); + if (fileUpdateDictionary is not null) + settings[fileUpdateKey] = fileUpdateDictionary[fileUpdateKey]; + +#pragma warning disable CA2007 + await using var fileStream = File.Open(fileUpdatePath, FileMode.Create); + await JsonSerializer.SerializeAsync(fileStream, settings, SettingsFileGenerationContext.Default.DictionaryStringObject, cancellationToken).ConfigureAwait(false); + } +} diff --git a/src/Kiota.Builder/Writers/HTTP/CodeBlockEndWriter.cs b/src/Kiota.Builder/Writers/HTTP/CodeBlockEndWriter.cs new file mode 100644 index 0000000000..c59fd59d9c --- /dev/null +++ b/src/Kiota.Builder/Writers/HTTP/CodeBlockEndWriter.cs @@ -0,0 +1,12 @@ +using System; +using Kiota.Builder.CodeDOM; + +namespace Kiota.Builder.Writers.Http; +public class CodeBlockEndWriter : ICodeElementWriter +{ + public void WriteCodeElement(BlockEnd codeElement, LanguageWriter writer) + { + ArgumentNullException.ThrowIfNull(codeElement); + ArgumentNullException.ThrowIfNull(writer); + } +} diff --git a/src/Kiota.Builder/Writers/HTTP/CodeClassDeclarationWriter.cs b/src/Kiota.Builder/Writers/HTTP/CodeClassDeclarationWriter.cs new file mode 100644 index 0000000000..825ce73bbe --- /dev/null +++ b/src/Kiota.Builder/Writers/HTTP/CodeClassDeclarationWriter.cs @@ -0,0 +1,367 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using Kiota.Builder.CodeDOM; +using Kiota.Builder.Extensions; +using Microsoft.Kiota.Abstractions; + +namespace Kiota.Builder.Writers.Http; +public class CodeClassDeclarationWriter(HttpConventionService conventionService) : CodeProprietableBlockDeclarationWriter(conventionService) +{ + private const string BaseUrlPropertyName = "hostAddress"; + + protected override void WriteTypeDeclaration(ClassDeclaration codeElement, LanguageWriter writer) + { + ArgumentNullException.ThrowIfNull(codeElement); + ArgumentNullException.ThrowIfNull(writer); + + if (codeElement.Parent is CodeClass requestBuilderClass && requestBuilderClass.IsOfKind(CodeClassKind.RequestBuilder) && GetUrlTemplateProperty(requestBuilderClass) is CodeProperty urlTemplateProperty) + { + // Write short description + conventions.WriteShortDescription(requestBuilderClass, writer); + writer.WriteLine(); + + // Retrieve all query parameters + var queryParameters = GetAllQueryParameters(requestBuilderClass); + + // Retrieve all path parameters + var pathParameters = GetPathParameters(requestBuilderClass); + + var baseUrl = GetBaseUrl(requestBuilderClass); + + // Write path parameters + WritePathParameters(pathParameters, writer); + + // Write all query parameter variables + WriteQueryParameters(queryParameters, writer); + + // Write all HTTP methods GET, POST, PUT, DELETE e.t.c + WriteHttpMethods(requestBuilderClass, writer, queryParameters, pathParameters, urlTemplateProperty, baseUrl); + } + } + + /// + /// Retrieves all the query parameters for the given request builder class. + /// + /// The request builder class containing the query parameters. + /// A list of all query parameters. + private static List GetAllQueryParameters(CodeClass requestBuilderClass) + { + var queryParameters = new List(); + + // Retrieve all the query parameter classes + var queryParameterClasses = requestBuilderClass + .GetChildElements(true) + .OfType() + .Where(static element => element.IsOfKind(CodeClassKind.QueryParameters)) + .ToList(); + + // Collect all query parameter properties into the aggregated list + queryParameterClasses?.ForEach(paramCodeClass => + { + var queryParams = paramCodeClass + .Properties + .Where(static property => property.IsOfKind(CodePropertyKind.QueryParameter)) + .ToList(); + + queryParameters.AddRange(queryParams); + }); + + return queryParameters; + } + + /// + /// Retrieves all the path parameters for the given request builder class. + /// + /// The request builder class containing the path parameters. + /// A list of all path parameters, or an empty list if none are found. + private static List GetPathParameters(CodeClass requestBuilderClass) + { + // Retrieve all the path variables except the generic path parameter named "pathParameters" + var pathParameters = requestBuilderClass + .GetChildElements(true) + .OfType() + .Where(property => property.IsOfKind(CodePropertyKind.PathParameters) && !property.Name.Equals("pathParameters", StringComparison.OrdinalIgnoreCase)) + .ToList(); + + return pathParameters; + } + + /// + /// Writes the base URL for the given request builder class to the writer. + /// + /// The request builder class containing the base URL property. + /// The language writer to write the base URL to. + private static void WriteBaseUrl(string? baseUrl, LanguageWriter writer) + { + // Write the base URL variable to the writer + writer.WriteLine($"# Base url for the server/host"); + writer.WriteLine($"@{BaseUrlPropertyName} = {{hostAddress}}"); + writer.WriteLine(); + } + + /// + /// Retrieves the base URL for the given request builder class. + /// + /// The request builder class containing the base URL property. + /// The base URL as a string, or null if not found. + private static string? GetBaseUrl(CodeClass requestBuilderClass) + { + // Retrieve the base URL property from the request builder class + return requestBuilderClass.Properties + .FirstOrDefault(property => property.Name.Equals("BaseUrl", StringComparison.OrdinalIgnoreCase))?.DefaultValue; + } + + /// + /// Retrieves the URL template property for the given request builder class. + /// + /// The request builder class containing the URL template property. + /// The URL template property, or null if not found. + private static CodeProperty? GetUrlTemplateProperty(CodeClass requestBuilderClass) + { + // Retrieve the URL template property from the request builder class + return requestBuilderClass + .GetChildElements(true) + .OfType() + .FirstOrDefault(static property => property.IsOfKind(CodePropertyKind.UrlTemplate)); + } + + /// + /// Writes the URL template for the given URL template property to the writer. + /// + /// The URL template property containing the URL template. + /// The language writer to write the URL template to. + private static void WriteUrlTemplate(CodeProperty urlTemplateProperty, LanguageWriter writer) + { + // Write the URL template documentation as a comment + writer.WriteLine($"# {urlTemplateProperty.Documentation?.DescriptionTemplate}"); + + // Write the URL template value + writer.WriteLine($"# {urlTemplateProperty.DefaultValue}"); + + // Write an empty line for separation + writer.WriteLine(); + } + + /// + /// Writes the path parameters for the given request builder class to the writer. + /// + /// The list of path parameters to write. + /// The language writer to write the path parameters to. + private static void WritePathParameters(List pathParameters, LanguageWriter writer) + { + // Write each path parameter property + pathParameters?.ForEach(prop => WriteHttpParameterProperty(prop, writer)); + } + + /// + /// Writes the query parameters for the given request builder class to the writer. + /// + /// The list of query parameters to write. + /// The language writer to write the query parameters to. + private static void WriteQueryParameters(List queryParameters, LanguageWriter writer) + { + // Write each query parameter property + queryParameters.ForEach(prop => WriteHttpParameterProperty(prop, writer)); + } + + /// + /// Writes the HTTP parameter property to the writer. + /// + /// The property to write. + /// The language writer to write the property to. + private static void WriteHttpParameterProperty(CodeProperty property, LanguageWriter writer) + { + if (!string.IsNullOrEmpty(property.Name)) + { + // Write the property documentation as a comment + writer.WriteLine($"# {property.Documentation.DescriptionTemplate}"); + + // Write the property name and an assignment placeholder + writer.WriteLine($"@{property.Name.ToFirstCharacterLowerCase()} = "); + + // Write an empty line for separation + writer.WriteLine(); + } + } + + /// + /// Writes the HTTP methods (GET, POST, PATCH, DELETE, etc.) for the given request builder class to the writer. + /// + /// The request builder class containing the HTTP methods. + /// The language writer to write the HTTP methods to. + /// The list of query parameters. + /// The list of path parameters. + /// The URL template property containing the URL template. + /// The base URL. + private static void WriteHttpMethods( + CodeClass requestBuilderClass, + LanguageWriter writer, + List queryParameters, + List pathParameters, + CodeProperty urlTemplateProperty, + string? baseUrl) + { + // Retrieve all the HTTP methods of kind RequestExecutor + var httpMethods = GetHttpMethods(requestBuilderClass); + + var methodCount = httpMethods.Count; + var currentIndex = 0; + + foreach (var method in httpMethods) + { + // Write the method documentation as a comment + writer.WriteLine($"# {method.Documentation.DescriptionTemplate}"); + + // Build the actual URL string and replace all required fields (path and query) with placeholder variables + var url = BuildUrlStringFromTemplate( + urlTemplateProperty.DefaultValue, + queryParameters, + pathParameters, + baseUrl + ); + + // Write the HTTP operation (e.g., GET, POST, PATCH, etc.) + writer.WriteLine($"{method.Name.ToUpperInvariant()} {url} HTTP/1.1"); + + var authenticationMethod = requestBuilderClass + .Properties + .FirstOrDefault(static prop => prop.IsOfKind(CodePropertyKind.Headers)); + + if (authenticationMethod != null && Enum.IsDefined(typeof(Authentication), authenticationMethod.Type.Name)) + { + writer.WriteLine($"Authorization: {{{{{authenticationMethod.DefaultValue}}}}}"); + } + + // Write the request body if present + WriteRequestBody(method, writer); + + // Write a separator if there are more items that follow + if (++currentIndex < methodCount) + { + writer.WriteLine(); + writer.WriteLine("###"); + writer.WriteLine(); + } + } + } + + /// + /// Retrieves all the HTTP methods of kind RequestExecutor for the given request builder class. + /// + /// The request builder class containing the HTTP methods. + /// A list of HTTP methods of kind RequestExecutor. + private static List GetHttpMethods(CodeClass requestBuilderClass) + { + return requestBuilderClass + .GetChildElements(true) + .OfType() + .Where(static element => element.IsOfKind(CodeMethodKind.RequestExecutor)) + .ToList(); + } + + /// + /// Writes the request body for the given method to the writer. + /// + /// The method containing the request body. + /// The language writer to write the request body to. + private static void WriteRequestBody(CodeMethod method, LanguageWriter writer) + { + // If there is a request body, write it + var requestBody = method.Parameters.FirstOrDefault(static param => param.IsOfKind(CodeParameterKind.RequestBody)); + if (requestBody is null) return; + + writer.WriteLine($"Content-Type: {method.RequestBodyContentType}"); + + // Empty line before body content + writer.WriteLine(); + + // Loop through the properties of the request body and write a JSON object + if (requestBody.Type is CodeType ct && ct.TypeDefinition is CodeClass requestBodyClass) + { + writer.StartBlock(); + WriteProperties(requestBodyClass, writer); + writer.CloseBlock(); + } + } + + /// + /// Writes the properties of the given request body class to the writer. + /// + /// The request body class containing the properties. + /// The language writer to write the properties to. + private static void WriteProperties(CodeClass requestBodyClass, LanguageWriter writer, HashSet? processedClasses = null, int depth = 0) + { + + if (processedClasses == null) + { + processedClasses = new HashSet(); + } + + // Add the current class to the set of processed classes + if (!processedClasses.Add(requestBodyClass)) + { + // If the class is already processed, write its properties again up to a certain depth + if (depth >= 3) + { + return; + } + } + + var properties = requestBodyClass.Properties + .Where(static prop => prop.IsOfKind(CodePropertyKind.Custom)) + .ToArray(); + + var propertyCount = properties.Length; + + foreach (var prop in properties) + { + // Add a trailing comma if there are more properties to be written + var separator = ","; + var propName = $"\"{prop.Name}\""; + writer.Write($"{propName}: "); + + if (prop.Type is CodeType propType && propType.TypeDefinition is CodeClass propClass) + { + // If the property is an object, write a JSON representation recursively + writer.WriteLine("{", includeIndent: false); + writer.IncreaseIndent(); + WriteProperties(propClass, writer, processedClasses, depth + 1); + writer.CloseBlock($"}}{separator}"); + } + else + { + writer.WriteLine($"{HttpConventionService.GetDefaultValueForProperty(prop)}{separator}", includeIndent: false); + } + } + + // Remove the current class from the set of processed classes after processing + processedClasses.Remove(requestBodyClass); + + // If the class extends another class, write properties of the base class + if (requestBodyClass.StartBlock.Inherits?.TypeDefinition is CodeClass baseClass) + { + WriteProperties(baseClass, writer, processedClasses, depth + 1); + } + } + + private static string BuildUrlStringFromTemplate(string urlTemplateString, List queryParameters, List pathParameters, string? baseUrl) + { + // Use the provided baseUrl or default to "http://localhost/" + baseUrl ??= "http://localhost/"; + + // unquote the urlTemplate string and replace the {+baseurl} with the actual base url string + urlTemplateString = urlTemplateString.Trim('"').Replace("{+baseurl}", baseUrl, StringComparison.InvariantCultureIgnoreCase); + + // Build RequestInformation using the URL + var requestInformation = new RequestInformation() + { + UrlTemplate = urlTemplateString, + QueryParameters = queryParameters.ToDictionary(item => item.WireName, item => $"{{{{{item.Name.ToFirstCharacterLowerCase()}}}}}" as object), + PathParameters = pathParameters.ToDictionary(item => item.WireName, item => $"{{{{{item.Name.ToFirstCharacterLowerCase()}}}}}" as object), + }; + + // Erase baseUrl and use the placeholder variable {baseUrl} already defined in the snippet + return requestInformation.URI.ToString().Replace(baseUrl, $"{{{{{BaseUrlPropertyName}}}}}", StringComparison.InvariantCultureIgnoreCase); + } +} diff --git a/src/Kiota.Builder/Writers/HTTP/CodeEnumWriter.cs b/src/Kiota.Builder/Writers/HTTP/CodeEnumWriter.cs new file mode 100644 index 0000000000..2c0a14d506 --- /dev/null +++ b/src/Kiota.Builder/Writers/HTTP/CodeEnumWriter.cs @@ -0,0 +1,15 @@ +using System; +using System.Linq; + +using Kiota.Builder.CodeDOM; +using Kiota.Builder.Extensions; + +namespace Kiota.Builder.Writers.Http; +public class CodeEnumWriter(HttpConventionService conventionService) : BaseElementWriter(conventionService) +{ + public override void WriteCodeElement(CodeEnum codeElement, LanguageWriter writer) + { + ArgumentNullException.ThrowIfNull(codeElement); + ArgumentNullException.ThrowIfNull(writer); + } +} diff --git a/src/Kiota.Builder/Writers/HTTP/CodeMethodWriter.cs b/src/Kiota.Builder/Writers/HTTP/CodeMethodWriter.cs new file mode 100644 index 0000000000..4098a5cbb5 --- /dev/null +++ b/src/Kiota.Builder/Writers/HTTP/CodeMethodWriter.cs @@ -0,0 +1,13 @@ +using System; +using System.Linq; +using Kiota.Builder.CodeDOM; + +namespace Kiota.Builder.Writers.Http; +public class CodeMethodWriter(HttpConventionService conventionService) : BaseElementWriter(conventionService) +{ + public override void WriteCodeElement(CodeMethod codeElement, LanguageWriter writer) + { + ArgumentNullException.ThrowIfNull(codeElement); + ArgumentNullException.ThrowIfNull(writer); + } +} diff --git a/src/Kiota.Builder/Writers/HTTP/CodeNamespaceWriter.cs b/src/Kiota.Builder/Writers/HTTP/CodeNamespaceWriter.cs new file mode 100644 index 0000000000..47b679b029 --- /dev/null +++ b/src/Kiota.Builder/Writers/HTTP/CodeNamespaceWriter.cs @@ -0,0 +1,14 @@ +using System; +using System.Linq; + +using Kiota.Builder.CodeDOM; + +namespace Kiota.Builder.Writers.Http; +public class CodeNamespaceWriter(HttpConventionService conventionService) : BaseElementWriter(conventionService) +{ + public override void WriteCodeElement(CodeNamespace codeElement, LanguageWriter writer) + { + ArgumentNullException.ThrowIfNull(codeElement); + ArgumentNullException.ThrowIfNull(writer); + } +} diff --git a/src/Kiota.Builder/Writers/HTTP/CodePropertyWriter.cs b/src/Kiota.Builder/Writers/HTTP/CodePropertyWriter.cs new file mode 100644 index 0000000000..09cfd734ba --- /dev/null +++ b/src/Kiota.Builder/Writers/HTTP/CodePropertyWriter.cs @@ -0,0 +1,14 @@ +using System; +using Kiota.Builder.CodeDOM; +using Kiota.Builder.Extensions; + +namespace Kiota.Builder.Writers.Http; +public class CodePropertyWriter(HttpConventionService conventionService) : BaseElementWriter(conventionService) +{ + public override void WriteCodeElement(CodeProperty codeElement, LanguageWriter writer) + { + ArgumentNullException.ThrowIfNull(codeElement); + ArgumentNullException.ThrowIfNull(writer); + if (codeElement.Parent is null) throw new InvalidOperationException("The parent of a property should be a class"); + } +} diff --git a/src/Kiota.Builder/Writers/HTTP/CodeProprietableBlockDeclarationWriter.cs b/src/Kiota.Builder/Writers/HTTP/CodeProprietableBlockDeclarationWriter.cs new file mode 100644 index 0000000000..39dfd0d731 --- /dev/null +++ b/src/Kiota.Builder/Writers/HTTP/CodeProprietableBlockDeclarationWriter.cs @@ -0,0 +1,18 @@ +using System; +using System.Linq; + +using Kiota.Builder.CodeDOM; + +namespace Kiota.Builder.Writers.Http; + +public abstract class CodeProprietableBlockDeclarationWriter(HttpConventionService conventionService) : BaseElementWriter(conventionService) + where T : ProprietableBlockDeclaration +{ + public override void WriteCodeElement(T codeElement, LanguageWriter writer) + { + ArgumentNullException.ThrowIfNull(codeElement); + ArgumentNullException.ThrowIfNull(writer); + WriteTypeDeclaration(codeElement, writer); + } + protected abstract void WriteTypeDeclaration(T codeElement, LanguageWriter writer); +} diff --git a/src/Kiota.Builder/Writers/HTTP/HttpConventionService.cs b/src/Kiota.Builder/Writers/HTTP/HttpConventionService.cs new file mode 100644 index 0000000000..72e1c43fbe --- /dev/null +++ b/src/Kiota.Builder/Writers/HTTP/HttpConventionService.cs @@ -0,0 +1,104 @@ +using System; +using System.Linq; +using Kiota.Builder.CodeDOM; +using Kiota.Builder.Extensions; + +namespace Kiota.Builder.Writers.Http; +public class HttpConventionService : CommonLanguageConventionService +{ + public HttpConventionService() + { + } + public override string StreamTypeName => "stream"; + public override string VoidTypeName => "void"; + public override string DocCommentPrefix => "###"; + public static string NullableMarkerAsString => "?"; + public override string ParseNodeInterfaceName => "ParseNode"; + public override bool WriteShortDescription(IDocumentedElement element, LanguageWriter writer, string prefix = "", string suffix = "") + { + ArgumentNullException.ThrowIfNull(writer); + ArgumentNullException.ThrowIfNull(element); + if (!element.Documentation.DescriptionAvailable) return false; + if (element is not CodeElement codeElement) return false; + + var description = element.Documentation.GetDescription(type => GetTypeString(type, codeElement)); + writer.WriteLine($"{DocCommentPrefix} {prefix}{description}{prefix}"); + + return true; + } + public override string GetAccessModifier(AccessModifier access) + { + return access switch + { + AccessModifier.Public => "public", + AccessModifier.Protected => "internal", + _ => "private", + }; + } + public override string TempDictionaryVarName => "urlTplParams"; + public override string GetTypeString(CodeTypeBase code, CodeElement targetElement, bool includeCollectionInformation = true, LanguageWriter? writer = null) + { + if (code is CodeType currentType) + { + var typeName = TranslateType(currentType); + var nullableSuffix = code.IsNullable ? NullableMarkerAsString : string.Empty; + var collectionPrefix = currentType.IsCollection && includeCollectionInformation ? "[" : string.Empty; + var collectionSuffix = currentType.IsCollection && includeCollectionInformation ? $"]{nullableSuffix}" : string.Empty; + if (currentType.IsCollection && !string.IsNullOrEmpty(nullableSuffix)) + nullableSuffix = string.Empty; + + if (currentType.ActionOf) + return $"({collectionPrefix}{typeName}{nullableSuffix}{collectionSuffix}>) -> Void"; + return $"{collectionPrefix}{typeName}{nullableSuffix}{collectionSuffix}"; + } + + throw new InvalidOperationException($"type of type {code?.GetType()} is unknown"); + } + public override string TranslateType(CodeType type) + { + return type?.Name switch + { + "integer" => "Int32", + "boolean" => "Bool", + "float" => "Float32", + "long" => "Int64", + "double" or "decimal" => "Float64", + "guid" => "UUID", + "void" or "uint8" or "int8" or "int32" or "int64" or "float32" or "float64" or "string" => type.Name.ToFirstCharacterUpperCase(), + "binary" or "base64" or "base64url" => "[UInt8]", + "DateTimeOffset" => "Date", + null => "object", + _ => type.Name.ToFirstCharacterUpperCase() is string typeName && !string.IsNullOrEmpty(typeName) ? typeName : "object", + }; + } + public override string GetParameterSignature(CodeParameter parameter, CodeElement targetElement, LanguageWriter? writer = null) + { + ArgumentNullException.ThrowIfNull(parameter); + var parameterType = GetTypeString(parameter.Type, targetElement); + var defaultValue = parameter switch + { + _ when !string.IsNullOrEmpty(parameter.DefaultValue) => $" = {parameter.DefaultValue}", + _ when parameter.Optional => " = default", + _ => string.Empty, + }; + return $"{parameter.Name.ToFirstCharacterLowerCase()} : {parameterType}{defaultValue}"; + } + + /// + /// Gets the default value for the given property. + /// + /// The property to get the default value for. + /// The default value as a string. + public static string GetDefaultValueForProperty(CodeProperty codeProperty) + { + return codeProperty?.Type.Name switch + { + "int" or "integer" => "0", + "string" => "\"string\"", + "bool" or "boolean" => "false", + _ when codeProperty?.Type is CodeType enumType && enumType.TypeDefinition is CodeEnum enumDefinition => + enumDefinition.Options.FirstOrDefault()?.Name is string enumName ? $"\"{enumName}\"" : "null", + _ => "null" + }; + } +} diff --git a/src/Kiota.Builder/Writers/HTTP/HttpWriter.cs b/src/Kiota.Builder/Writers/HTTP/HttpWriter.cs new file mode 100644 index 0000000000..e0aafbbc8a --- /dev/null +++ b/src/Kiota.Builder/Writers/HTTP/HttpWriter.cs @@ -0,0 +1,15 @@ +using Kiota.Builder.PathSegmenters; + +namespace Kiota.Builder.Writers.Http; + +public class HttpWriter : LanguageWriter +{ + public HttpWriter(string rootPath, string clientNamespaceName) + { + PathSegmenter = new HttpPathSegmenter(rootPath, clientNamespaceName); + var conventionService = new HttpConventionService(); + AddOrReplaceCodeElementWriter(new CodeClassDeclarationWriter(conventionService)); + AddOrReplaceCodeElementWriter(new CodePropertyWriter(conventionService)); + AddOrReplaceCodeElementWriter(new CodeMethodWriter(conventionService)); + } +} diff --git a/src/Kiota.Builder/Writers/LanguageWriter.cs b/src/Kiota.Builder/Writers/LanguageWriter.cs index 9ccf0652ab..d181eb5ba1 100644 --- a/src/Kiota.Builder/Writers/LanguageWriter.cs +++ b/src/Kiota.Builder/Writers/LanguageWriter.cs @@ -3,13 +3,13 @@ using System.ComponentModel; using System.IO; using System.Linq; - using Kiota.Builder.CodeDOM; using Kiota.Builder.PathSegmenters; using Kiota.Builder.Writers.Cli; using Kiota.Builder.Writers.CSharp; using Kiota.Builder.Writers.Dart; using Kiota.Builder.Writers.Go; +using Kiota.Builder.Writers.Http; using Kiota.Builder.Writers.Java; using Kiota.Builder.Writers.Php; using Kiota.Builder.Writers.Python; @@ -194,6 +194,7 @@ public static LanguageWriter GetLanguageWriter(GenerationLanguage language, stri GenerationLanguage.CLI => new CliWriter(outputPath, clientNamespaceName), GenerationLanguage.Swift => new SwiftWriter(outputPath, clientNamespaceName), GenerationLanguage.Dart => new DartWriter(outputPath, clientNamespaceName), + GenerationLanguage.HTTP => new HttpWriter(outputPath, clientNamespaceName), _ => throw new InvalidEnumArgumentException($"{language} language currently not supported."), }; } diff --git a/tests/Kiota.Builder.Tests/PathSegmenters/HttpPathSegmenterTests.cs b/tests/Kiota.Builder.Tests/PathSegmenters/HttpPathSegmenterTests.cs new file mode 100644 index 0000000000..915ec3c01a --- /dev/null +++ b/tests/Kiota.Builder.Tests/PathSegmenters/HttpPathSegmenterTests.cs @@ -0,0 +1,36 @@ +using System.IO; +using Kiota.Builder.CodeDOM; +using Kiota.Builder.PathSegmenters; +using Xunit; + +namespace Kiota.Builder.Tests.PathSegmenters +{ + public class HttpPathSegmenterTests + { + private readonly HttpPathSegmenter segmenter; + + public HttpPathSegmenterTests() + { + var tempFilePath = Path.Combine(Path.GetTempPath(), Path.GetTempFileName()); + segmenter = new HttpPathSegmenter(tempFilePath, "client"); + } + + [Fact] + public void HttpPathSegmenterGeneratesCorrectFileName() + { + var fileName = segmenter.NormalizeFileName(new CodeClass + { + Name = "testClass" + }); + Assert.Equal("TestClass", fileName);// the file name should be Proper case + } + + [Fact] + public void HttpPathSegmenterGeneratesNamespaceFolderName() + { + var namespaceName = "microsoft.Graph"; + var normalizedNamespace = segmenter.NormalizeNamespaceSegment(namespaceName); + Assert.Equal("Microsoft.Graph", normalizedNamespace);// the first character is upper case + } + } +} diff --git a/tests/Kiota.Builder.Tests/Settings/SettingsManagementTests.cs b/tests/Kiota.Builder.Tests/Settings/SettingsManagementTests.cs new file mode 100644 index 0000000000..a6e779915a --- /dev/null +++ b/tests/Kiota.Builder.Tests/Settings/SettingsManagementTests.cs @@ -0,0 +1,86 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.OpenApi.Models; +using Xunit; + +namespace Kiota.Builder.Settings.Tests +{ + public class SettingsFileManagementServiceTest + { + [Fact] + public void GetDirectoryContainingSettingsFile_ShouldCreateTheDirectory_If_It_Doesnt_Exist() + { + // Arrange + var service = new SettingsFileManagementService(); + var tempDirectory = Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString()); + Directory.CreateDirectory(tempDirectory); + + // Act + var result = service.GetDirectoryContainingSettingsFile(tempDirectory); + tempDirectory = Path.Combine(tempDirectory, ".vscode"); + // Assert + Assert.Equal(tempDirectory, result); + + // Cleanup + try + { + Directory.Delete(tempDirectory, true); + } + catch (IOException) + { + // Handle the case where the directory is not empty + Directory.Delete(tempDirectory, true); + } + } + + [Fact] + public void GetDirectoryContainingSettingsFile_ShouldReturnVscodeDirectory_WhenExists() + { + // Arrange + var service = new SettingsFileManagementService(); + var tempDirectory = Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString()); + var vscodeDirectory = Path.Combine(tempDirectory, ".vscode"); + Directory.CreateDirectory(vscodeDirectory); + + // Act + var result = service.GetDirectoryContainingSettingsFile(tempDirectory); + + // Assert + Assert.Equal(vscodeDirectory, result); + + // Cleanup + Directory.Delete(tempDirectory, true); + } + + [Fact] + public async Task WriteSettingsFileAsync_ShouldThrowArgumentException_WhenDirectoryPathIsNullOrEmpty() + { + // Arrange + var service = new SettingsFileManagementService(); + var openApiDocument = new OpenApiDocument(); + var cancellationToken = CancellationToken.None; + + // Act & Assert + await Assert.ThrowsAsync(() => service.WriteSettingsFileAsync(string.Empty, openApiDocument, cancellationToken)); + } + + [Fact] + public async Task WriteSettingsFileAsync_ShouldThrowArgumentNullException_WhenOpenApiDocumentIsNull() + { + // Arrange + var service = new SettingsFileManagementService(); + var tempDirectory = Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString()); + Directory.CreateDirectory(tempDirectory); + var cancellationToken = CancellationToken.None; + + // Act & Assert + await Assert.ThrowsAsync(() => service.WriteSettingsFileAsync(tempDirectory, null, cancellationToken)); + + // Cleanup + Directory.Delete(tempDirectory); + } + } +} diff --git a/tests/Kiota.Builder.Tests/Writers/HTTP/CodeClassDeclarationWriterTests.cs b/tests/Kiota.Builder.Tests/Writers/HTTP/CodeClassDeclarationWriterTests.cs new file mode 100644 index 0000000000..5e4dbd3457 --- /dev/null +++ b/tests/Kiota.Builder.Tests/Writers/HTTP/CodeClassDeclarationWriterTests.cs @@ -0,0 +1,174 @@ +using System; +using System.Collections.Generic; +using System.Globalization; +using System.IO; +using System.Linq; +using System.Net.Http; +using System.Threading.Tasks; +using Kiota.Builder.CodeDOM; +using Kiota.Builder.Configuration; +using Kiota.Builder.Extensions; +using Kiota.Builder.Refiners; +using Kiota.Builder.Tests.OpenApiSampleFiles; +using Kiota.Builder.Writers; +using Microsoft.Extensions.Logging; +using Moq; +using Xunit; +using static Kiota.Builder.Refiners.HttpRefiner; + +namespace Kiota.Builder.Tests.Writers.Http; +public sealed class CodeClassDeclarationWriterTests : IDisposable +{ + private const string DefaultPath = "./"; + private const string DefaultName = "name"; + private readonly StringWriter tw; + private readonly LanguageWriter writer; + private readonly CodeNamespace root; + + public CodeClassDeclarationWriterTests() + { + writer = LanguageWriter.GetLanguageWriter(GenerationLanguage.HTTP, DefaultPath, DefaultName); + tw = new StringWriter(); + writer.SetTextWriter(tw); + root = CodeNamespace.InitRootNamespace(); + } + public void Dispose() + { + tw?.Dispose(); + GC.SuppressFinalize(this); + } + + [Fact] + public async Task WritesRequestExecutorMethods() + { + var codeClass = new CodeClass + { + Name = "TestClass", + Kind = CodeClassKind.RequestBuilder + }; + var urlTemplateProperty = new CodeProperty + { + Name = "urlTemplate", + Kind = CodePropertyKind.UrlTemplate, + DefaultValue = "\"{+baseurl}/posts\"", + Type = new CodeType + { + Name = "string", + IsExternal = true + }, + Documentation = new CodeDocumentation + { + DescriptionTemplate = "The URL template for the request." + } + }; + codeClass.AddProperty(urlTemplateProperty); + + // Add base url property + var baseUrlProperty = new CodeProperty + { + Name = "BaseUrl", + Kind = CodePropertyKind.Custom, + Access = AccessModifier.Private, + DefaultValue = "https://example.com", + Type = new CodeType { Name = "string", IsExternal = true } + }; + codeClass.AddProperty(baseUrlProperty); + + var method = new CodeMethod + { + Name = "get", + Kind = CodeMethodKind.RequestExecutor, + Documentation = new CodeDocumentation { DescriptionTemplate = "GET method" }, + ReturnType = new CodeType { Name = "void" } + }; + codeClass.AddMethod(method); + + var postMethod = new CodeMethod + { + Name = "post", + Kind = CodeMethodKind.RequestExecutor, + Documentation = new CodeDocumentation { DescriptionTemplate = "Post method" }, + ReturnType = new CodeType { Name = "void" }, + RequestBodyContentType = "application/json" + }; + + + var typeDefinition = new CodeClass + { + Name = "PostParameter", + }; + + var properties = new List + { + new() { + Name = "body", + Kind = CodePropertyKind.Custom, + Type = new CodeType { Name = "string", IsExternal = true } + }, + new() { + Name = "id", + Kind = CodePropertyKind.Custom, + Type = new CodeType { Name = "int", IsExternal = true } + }, + new() { + Name = "title", + Kind = CodePropertyKind.Custom, + Type = new CodeType { Name = "string", IsExternal = true } + }, + new() { + Name = "userId", + Kind = CodePropertyKind.Custom, + Type = new CodeType { Name = "int", IsExternal = true } + } + }; + + typeDefinition.AddProperty(properties.ToArray()); + + // Define the parameter with the specified properties + var postParameter = new CodeParameter + { + Name = "postParameter", + Kind = CodeParameterKind.RequestBody, + Type = new CodeType + { + Name = "PostParameter", + TypeDefinition = typeDefinition + } + }; + + // Add the parameter to the post method + postMethod.AddParameter(postParameter); + + codeClass.AddMethod(postMethod); + + var patchMethod = new CodeMethod + { + Name = "patch", + Kind = CodeMethodKind.RequestExecutor, + Documentation = new CodeDocumentation { DescriptionTemplate = "Patch method" }, + ReturnType = new CodeType { Name = "void" } + }; + codeClass.AddMethod(patchMethod); + + root.AddClass(codeClass); + + await ILanguageRefiner.RefineAsync(new GenerationConfiguration { Language = GenerationLanguage.HTTP }, root); + + writer.Write(codeClass.StartBlock); + var result = tw.ToString(); + + // Check HTTP operations + Assert.Contains("GET {{hostAddress}}/posts HTTP/1.1", result); + Assert.Contains("PATCH {{hostAddress}}/posts HTTP/1.1", result); + Assert.Contains("POST {{hostAddress}}/posts HTTP/1.1", result); + + // Check content type + Assert.Contains("Content-Type: application/json", result); + + // check the request body + Assert.Contains("\"body\": \"string\"", result); + Assert.Contains("\"id\": 0", result); + Assert.Contains("\"title\": \"string\"", result); + Assert.Contains("\"userId\": 0", result); + } +} diff --git a/tests/Kiota.Builder.Tests/Writers/HTTP/CodeEnumWriterTests.cs b/tests/Kiota.Builder.Tests/Writers/HTTP/CodeEnumWriterTests.cs new file mode 100644 index 0000000000..3fb970fb89 --- /dev/null +++ b/tests/Kiota.Builder.Tests/Writers/HTTP/CodeEnumWriterTests.cs @@ -0,0 +1,62 @@ +using System; +using System.IO; +using System.Linq; + +using Kiota.Builder.CodeDOM; +using Kiota.Builder.Writers; +using Kiota.Builder.Writers.Http; +using Xunit; + +namespace Kiota.Builder.Tests.Writers.Http; +public sealed class CodeEnumWriterTests : IDisposable +{ + private const string DefaultPath = "./"; + private const string DefaultName = "name"; + private readonly StringWriter tw; + private readonly LanguageWriter writer; + private readonly CodeEnum currentEnum; + private const string EnumName = "someEnum"; + private readonly CodeEnumWriter codeEnumWriter; + public CodeEnumWriterTests() + { + writer = LanguageWriter.GetLanguageWriter(GenerationLanguage.HTTP, DefaultPath, DefaultName); + codeEnumWriter = new CodeEnumWriter(new HttpConventionService()); + tw = new StringWriter(); + writer.SetTextWriter(tw); + var root = CodeNamespace.InitRootNamespace(); + currentEnum = root.AddEnum(new CodeEnum + { + Name = EnumName, + }).First(); + if (CodeConstant.FromCodeEnum(currentEnum) is CodeConstant constant) + { + currentEnum.CodeEnumObject = constant; + root.AddConstant(constant); + } + } + public void Dispose() + { + tw?.Dispose(); + GC.SuppressFinalize(this); + } + [Fact] + public void WriteCodeElement_ThrowsException_WhenCodeElementIsNull() + { + Assert.Throws(() => codeEnumWriter.WriteCodeElement(null, writer)); + } + [Fact] + public void WriteCodeElement_ThrowsException_WhenWriterIsNull() + { + var codeElement = new CodeEnum(); + Assert.Throws(() => codeEnumWriter.WriteCodeElement(codeElement, null)); + } + [Fact] + public void SkipsEnum() + { + const string optionName = "option1"; + currentEnum.AddOption(new CodeEnumOption { Name = optionName }); + codeEnumWriter.WriteCodeElement(currentEnum, writer); + var result = tw.ToString(); + Assert.True(string.IsNullOrEmpty(result)); + } +} diff --git a/tests/Kiota.Builder.Tests/Writers/HTTP/HttpConventionServiceTests.cs b/tests/Kiota.Builder.Tests/Writers/HTTP/HttpConventionServiceTests.cs new file mode 100644 index 0000000000..23efdcfd4f --- /dev/null +++ b/tests/Kiota.Builder.Tests/Writers/HTTP/HttpConventionServiceTests.cs @@ -0,0 +1,78 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; + +using Kiota.Builder.CodeDOM; +using Kiota.Builder.Writers; +using Kiota.Builder.Writers.Http; +using Xunit; + +namespace Kiota.Builder.Tests.Writers.Http; +public sealed class HttpConventionServiceTest +{ + + [Fact] + public void TestGetDefaultValueForProperty_Int() + { + // Arrange + var codeProperty = new CodeProperty + { + Type = new CodeType { Name = "int" } + }; + + // Act + var result = HttpConventionService.GetDefaultValueForProperty(codeProperty); + + // Assert + Assert.Equal("0", result); + } + + [Fact] + public void TestGetDefaultValueForProperty_String() + { + // Arrange + var codeProperty = new CodeProperty + { + Type = new CodeType { Name = "string" } + }; + + // Act + var result = HttpConventionService.GetDefaultValueForProperty(codeProperty); + + // Assert + Assert.Equal("\"string\"", result); + } + + [Fact] + public void TestGetDefaultValueForProperty_Bool() + { + // Arrange + var codeProperty = new CodeProperty + { + Type = new CodeType { Name = "bool" } + }; + + // Act + var result = HttpConventionService.GetDefaultValueForProperty(codeProperty); + + // Assert + Assert.Equal("false", result); + } + + [Fact] + public void TestGetDefaultValueForProperty_Null() + { + // Arrange + var codeProperty = new CodeProperty + { + Type = new CodeType { Name = "unknown" } + }; + + // Act + var result = HttpConventionService.GetDefaultValueForProperty(codeProperty); + + // Assert + Assert.Equal("null", result); + } +}