-
Notifications
You must be signed in to change notification settings - Fork 219
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Feature/http language support #5778
base: main
Are you sure you want to change the base?
Changes from 67 commits
8dd143d
c7e352a
c2c7443
3a25dab
9bcac7d
5154280
80d771c
bac834f
31c3c53
0ec5815
f033a45
f2f1c8e
ec4aea7
da3bf05
69ba9ac
3852185
7508ebd
0c7e0ee
41345d5
cfabf37
cfbbefe
ff4caed
d9f7584
69bac1f
fc39c89
6900e7e
2ade0bd
0343d2a
0fa1c0d
106cbae
4f2bda2
41dbd77
7d30713
4ccc263
d9da636
ef519d1
51c6e25
cfb3024
fdfabb7
407f61a
54c3d8f
c2d0635
7b9b16d
5738cb9
c505b92
6ff322a
5d42a53
78b2c1d
2daae83
a99c4a8
a96a7e8
958c5c3
2d54897
3155aa5
b531fc7
b97613a
2103872
37d2e6e
f316a5c
e6a542c
926759e
5b8c189
7693fcf
5c6db56
58a31e2
8713ba8
e89c51f
33d06ae
2441aee
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -11,4 +11,5 @@ public enum GenerationLanguage | |
Ruby, | ||
CLI, | ||
Dart, | ||
HTTP | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -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<KiotaBuilder> logger, GenerationConfiguration config, HttpClient client, bool useKiotaConfig = false) | ||
|
@@ -285,6 +287,11 @@ 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) | ||
{ | ||
await settingsFileManagementService.WriteSettingsFileAsync(config.OutputPath, openApiDocument, cancellationToken).ConfigureAwait(false); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. add logs to track the step like other steps before |
||
} | ||
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) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. opportunity to refactor to more concise code with the switch since all that's changing is the name |
||
{ | ||
codeClass.AddProperty( | ||
new CodeProperty | ||
{ | ||
Type = new CodeType { Name = Authentication.Basic.ToString(), IsExternal = true }, | ||
Kind = CodePropertyKind.Headers, | ||
DefaultValue = $"{securityScheme.Scheme}Auth" | ||
} | ||
); | ||
} | ||
|
||
private void AddApiKeySecurity(CodeClass codeClass, OpenApiSecurityScheme securityScheme) | ||
{ | ||
codeClass.AddProperty( | ||
new CodeProperty | ||
{ | ||
Type = new CodeType { Name = Authentication.APIKey.ToString(), IsExternal = true }, | ||
Kind = CodePropertyKind.Headers, | ||
DefaultValue = $"{securityScheme.Scheme}Auth" | ||
} | ||
); | ||
} | ||
|
||
private void AddOAuth2Security(CodeClass codeClass, OpenApiSecurityScheme securityScheme) | ||
{ | ||
codeClass.AddProperty( | ||
new CodeProperty | ||
{ | ||
Type = new CodeType { Name = Authentication.OAuthV2.ToString(), IsExternal = true }, | ||
Kind = CodePropertyKind.Headers, | ||
DefaultValue = $"{securityScheme.Scheme}Auth" | ||
} | ||
); | ||
} | ||
|
||
/// <summary> | ||
/// Manipulate CodeDOM for language specific issues | ||
/// </summary> | ||
|
@@ -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) | ||
|
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(); | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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<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 = "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<CodeNamespace>(); | ||
parentNameSpace?.RemoveChildElement(element); | ||
} | ||
CrawlTree(element, RemoveUnusedCodeElements); | ||
} | ||
|
||
private void AddPathParameters(CodeElement element) | ||
{ | ||
var parent = element.GetImmediateParentOfType<CodeNamespace>().Parent; | ||
while (parent is not null) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Would it make sense to call this from the refiner method directly as this would handle the traversal of the tree for you? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Can you give an example of how the traversal would look like? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. At the moment, So, if you have two classes in the same namespace, each class may be called as a parameter for Instead, you can simply add a method called directly in the |
||
{ | ||
var codeIndexer = parent.GetChildElements(false) | ||
.OfType<CodeClass>() | ||
.FirstOrDefault()? | ||
.GetChildElements(false) | ||
.OfType<CodeMethod>() | ||
.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<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)); | ||
} | ||
} |
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; | ||
} |
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); | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
move to a dedicated file, give it a more explicit name