Skip to content

Commit

Permalink
Merge pull request #6046 from microsoft/feature/http-language-support-a
Browse files Browse the repository at this point in the history
Feature: http language support
  • Loading branch information
Onokaev authored Jan 22, 2025
2 parents 395d4f1 + 0b40ad4 commit 1b22a55
Show file tree
Hide file tree
Showing 23 changed files with 1,438 additions and 2 deletions.
19 changes: 19 additions & 0 deletions .vscode/launch.json
Original file line number Diff line number Diff line change
Expand Up @@ -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"
}
]
}
1 change: 1 addition & 0 deletions src/Kiota.Builder/GenerationLanguage.cs
Original file line number Diff line number Diff line change
Expand Up @@ -11,4 +11,5 @@ public enum GenerationLanguage
Ruby,
CLI,
Dart,
HTTP
}
54 changes: 53 additions & 1 deletion src/Kiota.Builder/KiotaBuilder.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -46,9 +47,10 @@ public partial class KiotaBuilder
private readonly ParallelOptions parallelOptions;
private readonly HttpClient httpClient;
private OpenApiDocument? openApiDocument;
private readonly ISettingsManagementService settingsFileManagementService;
internal void SetOpenApiDocument(OpenApiDocument document) => openApiDocument = document ?? throw new ArgumentNullException(nameof(document));

public KiotaBuilder(ILogger<KiotaBuilder> logger, GenerationConfiguration config, HttpClient client, bool useKiotaConfig = false)
public KiotaBuilder(ILogger<KiotaBuilder> logger, GenerationConfiguration config, HttpClient client, bool useKiotaConfig = false, ISettingsManagementService? settingsManagementService = null)
{
ArgumentNullException.ThrowIfNull(logger);
ArgumentNullException.ThrowIfNull(config);
Expand All @@ -64,6 +66,7 @@ public KiotaBuilder(ILogger<KiotaBuilder> logger, GenerationConfiguration config
workspaceManagementService = new WorkspaceManagementService(logger, client, useKiotaConfig, workingDirectory);
this.useKiotaConfig = useKiotaConfig;
openApiDocumentDownloadService = new OpenApiDocumentDownloadService(client, logger);
settingsFileManagementService = settingsManagementService ?? new SettingsFileManagementService();
}
private readonly OpenApiDocumentDownloadService openApiDocumentDownloadService;
private readonly bool useKiotaConfig;
Expand Down Expand Up @@ -285,6 +288,13 @@ public async Task<bool> 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)
{
sw.Start();
await settingsFileManagementService.WriteSettingsFileAsync(config.OutputPath, openApiDocument, cancellationToken).ConfigureAwait(false);
StopLogAndReset(sw, $"step {++stepId} - generating settings file for HTTP authentication - took");
}
return stepId;
}, cancellationToken).ConfigureAwait(false);
}
Expand Down Expand Up @@ -554,6 +564,41 @@ 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)
{
if (securitySchemes.TryGetValue(scheme.Reference.Id, out var securityScheme))
{
AddSecurity(codeClass, securityScheme);
}
}
}
}

private void AddSecurity(CodeClass codeClass, OpenApiSecurityScheme openApiSecurityScheme)
{
codeClass.AddProperty(
new CodeProperty
{
Type = new CodeType { Name = openApiSecurityScheme.Type.ToString(), IsExternal = true },
Kind = CodePropertyKind.Headers
}
);
}

/// <summary>
/// Manipulate CodeDOM for language specific issues
/// </summary>
Expand Down Expand Up @@ -671,7 +716,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)
Expand Down
13 changes: 13 additions & 0 deletions src/Kiota.Builder/PathSegmenters/HttpPathSegmenter.cs
Original file line number Diff line number Diff line change
@@ -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();
}
}
148 changes: 148 additions & 0 deletions src/Kiota.Builder/Refiners/HttpRefiner.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,148 @@
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)
{
private const string BaseUrl = "BaseUrl";
private const string BaseUrlName = "string";
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<CodeNamespace>()
.GetRootNamespace()?
.FindChildByName<CodeClass>(_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 = BaseUrlName, 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<CodeNamespace>();
parentNameSpace?.RemoveChildElement(element);
}
CrawlTree(element, RemoveUnusedCodeElements);
}

private void AddPathParameters(CodeElement element)
{
var parent = element.GetImmediateParentOfType<CodeNamespace>().Parent;
while (parent is not null)
{
var codeIndexer = parent.GetChildElements(false)
.OfType<CodeClass>()
.FirstOrDefault()?
.GetChildElements(false)
.OfType<CodeMethod>()
.FirstOrDefault(static x => x.IsOfKind(CodeMethodKind.IndexerBackwardCompatibility));

if (codeIndexer is not null && element is CodeClass codeClass)
{
// Retrieve all the parameters of kind CodeParameterKind.Custom
var customProperties = codeIndexer.Parameters
.Where(static x => x.IsOfKind(CodeParameterKind.Custom))
.Select(x => new CodeProperty
{
Name = x.Name,
Kind = CodePropertyKind.PathParameters,
Type = x.Type,
Access = AccessModifier.Public,
DefaultValue = x.DefaultValue,
SerializationName = x.SerializationName,
Documentation = x.Documentation
})
.ToArray();

if (customProperties.Length > 0)
{
codeClass.AddProperty(customProperties);
}
}

parent = parent.Parent?.GetImmediateParentOfType<CodeNamespace>();
}
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));
}
}
11 changes: 11 additions & 0 deletions src/Kiota.Builder/Refiners/HttpReservedNamesProvider.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
using System;
using System.Collections.Generic;

namespace Kiota.Builder.Refiners;
public class HttpReservedNamesProvider : IReservedNamesProvider
{
private readonly Lazy<HashSet<string>> _reservedNames = new(() => new HashSet<string>(StringComparer.OrdinalIgnoreCase)
{
});
public HashSet<string> ReservedNames => _reservedNames.Value;
}
3 changes: 3 additions & 0 deletions src/Kiota.Builder/Refiners/ILanguageRefiner.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
28 changes: 28 additions & 0 deletions src/Kiota.Builder/Settings/ISettingsManagementService.cs
Original file line number Diff line number Diff line change
@@ -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;
/// <summary>
/// A service that manages the settings file for http language snippets.
/// </summary>
public interface ISettingsManagementService
{
/// <summary>
/// Gets the settings file for a Kiota project by crawling the directory tree.
/// </summary>
/// <param name="searchDirectory"></param>
/// <returns></returns>
string? GetDirectoryContainingSettingsFile(string searchDirectory);

/// <summary>
/// Writes the settings file to a directory.
/// </summary>
/// <param name="directoryPath"></param>
/// <param name="openApiDocument">OpenApi document</param>
/// <param name="cancellationToken"></param>
/// <returns></returns>
Task WriteSettingsFileAsync(string directoryPath, OpenApiDocument openApiDocument, CancellationToken cancellationToken);
}
Loading

0 comments on commit 1b22a55

Please sign in to comment.