From b06d77ec987da17615c269eb07869bb6a1f29d12 Mon Sep 17 00:00:00 2001 From: Octokit Bot Date: Mon, 19 Aug 2024 13:50:01 -0700 Subject: [PATCH] feat: Introduces a builder pattern to enable configured requests (#40) * New updates to generated code * New updates to generated code * New updates to generated code --- README.md | 47 ++++++++-- cli/Authentication/AppInstallationToken.cs | 55 ++++++++++-- cli/Authentication/PersonalAccessToken.cs | 40 ++++++++- cli/Program.cs | 21 ++++- .../Authentication/GitHubAppTokenProvider.cs | 2 +- src/Client/ClientFactory.cs | 86 ++++++++++++++++++- src/Client/RequestAdapter.cs | 14 ++- .../Item/Rulesets/RulesetsPostRequestBody.cs | 1 + .../Item/Rulesets/RulesetsPostRequestBody.cs | 1 + src/GitHub/kiota-lock.json | 2 +- src/Middleware/Options/UserAgentOptions.cs | 6 +- src/Middleware/UserAgentHandler.cs | 4 +- 12 files changed, 244 insertions(+), 35 deletions(-) diff --git a/README.md b/README.md index efc6b8c37..ee89a7728 100644 --- a/README.md +++ b/README.md @@ -52,11 +52,10 @@ To install the package, you can use either of the following options: Given that the GHES platform is a self hosted instance when using this SDK you'll need to initialize it with your host and protocol: ```csharp - var tokenProvider = new TokenProvider(Environment.GetEnvironmentVariable("GITHUB_TOKEN") ?? ""); - var adapter = RequestAdapter.Create(new TokenAuthProvider(tokenProvider), "https://hosted.instance"); - var gitHubClient = new GitHubClient(adapter); +var tokenProvider = new TokenProvider(Environment.GetEnvironmentVariable("GITHUB_TOKEN") ?? ""); +var adapter = RequestAdapter.Create(new TokenAuthProvider(tokenProvider), "https://hosted.instance"); +var gitHubClient = new GitHubClient(adapter); ``` - ### Make your first request ```csharp @@ -64,15 +63,45 @@ using GitHub; using GitHub.Octokit.Client; using GitHub.Octokit.Client.Authentication; -var token = Environment.GetEnvironmentVariable("GITHUB_TOKEN") ?? ""; +var tokenProvider = new TokenProvider(Environment.GetEnvironmentVariable("GITHUB_TOKEN") ?? ""); var adapter = RequestAdapter.Create(new TokenAuthProvider(tokenProvider), "https://hosted.instance"); -var gitHubClient = new GitHubClient(adapter); +await MakeRequest(new GitHubClient(adapter)); + +try +{ + var response = await gitHubClient.Repositories.GetAsync(); + response?.ForEach(repo => Console.WriteLine(repo.FullName)); +} +catch (Exception e) +{ + Console.WriteLine(e.Message); +} +``` -var pullRequests = await gitHubClient.Repos["octokit"]["octokit.net"].Pulls.GetAsync(); +#### Custom configuration for requests -foreach (var pullRequest in pullRequests) +```csharp +using GitHub; +using GitHub.Octokit.Client; +using GitHub.Octokit.Client.Authentication; + +var tokenProvider = new TokenProvider(Environment.GetEnvironmentVariable("GITHUB_TOKEN") ?? ""); + +var adapter = new ClientFactory() + .WithAuthenticationProvider(new TokenAuthProvider(tokenProvider)) + .WithUserAgent("my-app", "1.0.0") + .WithRequestTimeout(TimeSpan.FromSeconds(100)) + .WithBaseUrl("https://hosted.instance") + .Build(); + +try +{ + var response = await gitHubClient.Repositories.GetAsync(); + response?.ForEach(repo => Console.WriteLine(repo.FullName)); +} +catch (Exception e) { - Console.WriteLine($"#{pullRequest.Number} {pullRequest.Title}"); + Console.WriteLine(e.Message); } ``` diff --git a/cli/Authentication/AppInstallationToken.cs b/cli/Authentication/AppInstallationToken.cs index 225e87f95..8ffd7440d 100644 --- a/cli/Authentication/AppInstallationToken.cs +++ b/cli/Authentication/AppInstallationToken.cs @@ -28,23 +28,57 @@ public class AppInstallationToken static readonly string CLIENT_ID = Environment.GetEnvironmentVariable("GITHUB_APP_CLIENT_ID") ?? ""; static readonly string PRIVATE_KEY_PATH = File.ReadAllText(Environment.GetEnvironmentVariable("GITHUB_APP_PRIVATE_KEY_PATH") ?? ""); - public static async Task Run() + + public static async Task Run(string approach) { - // The GitHub Enterprise Server URL + switch (approach) + { + case "builder": + await RunWithBuilder(); + break; + case "default": + await RunWithDefault(); + break; + default: + Console.WriteLine("Invalid approach. Please provide 'builder' or 'default'"); + break; + } + } + + private static async Task RunWithBuilder() + { + var githubAppTokenProvider = new GitHubAppTokenProvider(); + var rsa = BuildRSAKey(); + + var aiAccessTokenProvider = new AppInstallationTokenProvider(CLIENT_ID, rsa, INSTALLATION_ID, githubAppTokenProvider); var baseUrl = Environment.GetEnvironmentVariable("GITHUB_BASE_URL") ?? "https://api.github.com"; + var adapter = new ClientFactory() + .WithAuthenticationProvider(new AppInstallationAuthProvider(aiAccessTokenProvider)) + .WithUserAgent("my-app", "1.0.0") + .WithRequestTimeout(TimeSpan.FromSeconds(100)) + .WithBaseUrl(baseUrl) + .Build(); + await MakeRequest(new GitHubClient(adapter)); + } + + private static async Task RunWithDefault() + { var githubAppTokenProvider = new GitHubAppTokenProvider(); - var rsa = RSA.Create(); - rsa.ImportFromPem(PRIVATE_KEY_PATH); + var rsa = BuildRSAKey(); var aiAccessTokenProvider = new AppInstallationTokenProvider(CLIENT_ID, rsa, INSTALLATION_ID, githubAppTokenProvider); - var aiAdapter = RequestAdapter.Create(new AppInstallationAuthProvider(aiAccessTokenProvider), baseUrl); - var aiGitHubClient = new GitHubClient(aiAdapter); + var baseUrl = Environment.GetEnvironmentVariable("GITHUB_BASE_URL") ?? "https://api.github.com"; + var adapter = RequestAdapter.Create(new AppInstallationAuthProvider(aiAccessTokenProvider), baseUrl); + await MakeRequest(new GitHubClient(adapter)); + } + private static async Task MakeRequest(GitHubClient gitHubClient) + { try { - var response = await aiGitHubClient.Installation.Repositories.GetAsync(); + var response = await gitHubClient.Installation.Repositories.GetAsync(); response?.Repositories?.ForEach(repo => Console.WriteLine(repo.FullName)); } catch (Exception e) @@ -52,4 +86,11 @@ public static async Task Run() Console.WriteLine(e.Message); } } + + private static RSA BuildRSAKey() + { + var rsa = RSA.Create(); + rsa.ImportFromPem(PRIVATE_KEY_PATH); + return rsa; + } } diff --git a/cli/Authentication/PersonalAccessToken.cs b/cli/Authentication/PersonalAccessToken.cs index 998a95be2..c44f2a353 100644 --- a/cli/Authentication/PersonalAccessToken.cs +++ b/cli/Authentication/PersonalAccessToken.cs @@ -14,15 +14,47 @@ namespace cli.Authentication; public class PersonalAccessToken { - public static async Task Run() + public static async Task Run(string approach) + { + switch (approach) + { + case "builder": + await RunWithBuilder(); + break; + case "default": + await RunWithDefault(); + break; + default: + Console.WriteLine("Invalid approach. Please provide 'builder' or 'default'"); + break; + } + } + + private static async Task RunWithBuilder() + { + var tokenProvider = new TokenProvider(Environment.GetEnvironmentVariable("GITHUB_TOKEN") ?? ""); + var baseUrl = Environment.GetEnvironmentVariable("GITHUB_BASE_URL") ?? "https://api.github.com"; + + var adapter = new ClientFactory() + .WithAuthenticationProvider(new TokenAuthProvider(tokenProvider)) + .WithUserAgent("my-app", "1.0.0") + .WithRequestTimeout(TimeSpan.FromSeconds(100)) + .WithBaseUrl(baseUrl) + .Build(); + + await MakeRequest(new GitHubClient(adapter)); + } + + private static async Task RunWithDefault() { - // Personal Access Token authentication var tokenProvider = new TokenProvider(Environment.GetEnvironmentVariable("GITHUB_TOKEN") ?? ""); - // The GitHub Enterprise Server URL var baseUrl = Environment.GetEnvironmentVariable("GITHUB_BASE_URL") ?? "https://api.github.com"; var adapter = RequestAdapter.Create(new TokenAuthProvider(tokenProvider), baseUrl); - var gitHubClient = new GitHubClient(adapter); + await MakeRequest(new GitHubClient(adapter)); + } + private static async Task MakeRequest(GitHubClient gitHubClient) + { try { var response = await gitHubClient.Repositories.GetAsync(); diff --git a/cli/Program.cs b/cli/Program.cs index a2b0de4b6..fe6a32b27 100644 --- a/cli/Program.cs +++ b/cli/Program.cs @@ -6,19 +6,34 @@ class Program { static async Task Main(string[] args) { - if (args != null && args.Length == 0) + if (args == null || args.Length == 0) { Console.WriteLine("Please provide an argument: 'AppInstallationToken' or 'PersonalAccessToken'"); return; } + var approach = "default"; + + if (args.Length > 1) + { + if (args[1] == "builder" || args[1] == "default") + { + approach = args[1]; + } + else + { + Console.WriteLine("Invalid argument. Please provide 'builder' or 'default'"); + return; + } + } + switch (args[0]) { case "AppInstallationToken": - await AppInstallationToken.Run(); + await AppInstallationToken.Run(approach); break; case "PersonalAccessToken": - await PersonalAccessToken.Run(); + await PersonalAccessToken.Run(approach); break; default: Console.WriteLine("Invalid argument. Please provide 'AppInstallationToken' or 'PersonalAccessToken'"); diff --git a/src/Client/Authentication/GitHubAppTokenProvider.cs b/src/Client/Authentication/GitHubAppTokenProvider.cs index cfb67d39c..903c52246 100644 --- a/src/Client/Authentication/GitHubAppTokenProvider.cs +++ b/src/Client/Authentication/GitHubAppTokenProvider.cs @@ -63,7 +63,7 @@ public async Task GetGitHubAccessTokenAsync(string baseUrl, string jwt, request.Headers.Accept.Add(new MediaTypeWithQualityHeaderValue("application/vnd.github.v3+json")); var userAgentOptions = new UserAgentOptions(); - request.Headers.UserAgent.Add(new ProductInfoHeaderValue(userAgentOptions.ProductName, userAgentOptions.ProductVersion)); + request.Headers.UserAgent.Add(new ProductInfoHeaderValue(userAgentOptions.ProductName ?? string.Empty, userAgentOptions.ProductVersion)); var response = await client.SendAsync(request); response.EnsureSuccessStatusCode(); diff --git a/src/Client/ClientFactory.cs b/src/Client/ClientFactory.cs index 148d6d639..9ce76e662 100644 --- a/src/Client/ClientFactory.cs +++ b/src/Client/ClientFactory.cs @@ -2,6 +2,7 @@ using System.Net; using GitHub.Octokit.Client.Middleware; +using Microsoft.Kiota.Abstractions.Authentication; using Microsoft.Kiota.Http.HttpClientLibrary; namespace GitHub.Octokit.Client; @@ -9,8 +10,13 @@ namespace GitHub.Octokit.Client; /// /// Represents a client factory for creating . /// -public static class ClientFactory +public class ClientFactory { + private TimeSpan? _requestTimeout; + private string? _baseUrl; + + private IAuthenticationProvider? _authenticationProvider; + private readonly HttpMessageHandler? _finalHandler; private static readonly Lazy> s_handlers = new(() => [ @@ -19,6 +25,11 @@ public static class ClientFactory new RateLimitHandler(), ]); + public ClientFactory(HttpMessageHandler? finalHandler = null) + { + _finalHandler = finalHandler; + } + /// /// Creates an instance with the specified . /// If no is provided, a default one will be used. @@ -36,6 +47,56 @@ public static HttpClient Create(HttpMessageHandler? finalHandler = null) return handler is not null ? new HttpClient(handler) : new HttpClient(); } + public ClientFactory WithUserAgent(string productName, string productVersion) + { + AddOrCreateHandler(new UserAgentHandler(new Middleware.Options.UserAgentOptions { ProductName = productName, ProductVersion = productVersion })); + return this; + } + + public ClientFactory WithRequestTimeout(TimeSpan timeSpan) + { + _requestTimeout = timeSpan; + return this; + } + + public ClientFactory WithBaseUrl(string baseUrl) + { + _baseUrl = baseUrl; + return this; + } + + public ClientFactory WithAuthenticationProvider(IAuthenticationProvider authenticationProvider) + { + _authenticationProvider = authenticationProvider; + return this; + } + + public HttpClientRequestAdapter Build() + { + + if (_authenticationProvider == null) throw new ArgumentNullException("authenticationProvider"); + + var httpClient = new HttpClient(); + var defaultHandlers = CreateDefaultHandlers(); + var handler = ChainHandlersCollectionAndGetFirstLink(finalHandler: _finalHandler ?? GetDefaultHttpMessageHandler(), handlers: [.. defaultHandlers]); + + if (handler != null) + { + httpClient = new HttpClient(handler); + } + + if (_requestTimeout.HasValue) + { + httpClient.Timeout = _requestTimeout.Value; + } + + if (!string.IsNullOrEmpty(_baseUrl)) + { + httpClient.BaseAddress = new Uri(_baseUrl); + } + + return RequestAdapter.Create(_authenticationProvider, httpClient); ; + } /// /// Creates a list of default delegating handlers for the Octokit client. /// @@ -97,4 +158,27 @@ public static IList CreateDefaultHandlers() /// The default HTTP message handler. public static HttpMessageHandler GetDefaultHttpMessageHandler(IWebProxy? proxy = null) => new HttpClientHandler { Proxy = proxy, AllowAutoRedirect = false }; + + /// + /// In support of the constructor approach to building a client factory, this method allows for adding or updating + /// a handler in the list of handlers. + /// The final result of the list of handlers will be processed in the Build() method. + /// + /// + /// + private void AddOrCreateHandler(THandler handler) where THandler : DelegatingHandler + { + // Find the index of the handler that matches the specified type + int index = s_handlers.Value.FindIndex(h => h is THandler); + + // If the handler is found, replace it with the new handler otehrwise add the new handler to the list + if (index >= 0) + { + s_handlers.Value[index] = handler; + } + else + { + s_handlers.Value.Add(handler); + } + } } diff --git a/src/Client/RequestAdapter.cs b/src/Client/RequestAdapter.cs index aa5e342c4..e17de9886 100644 --- a/src/Client/RequestAdapter.cs +++ b/src/Client/RequestAdapter.cs @@ -1,16 +1,21 @@ // Copyright (c) GitHub 2023-2024 - Licensed as MIT. - using Microsoft.Kiota.Abstractions.Authentication; using Microsoft.Kiota.Http.HttpClientLibrary; namespace GitHub.Octokit.Client; -public static class RequestAdapter +/// +/// Represents an adapter for making HTTP requests using HttpClient with additional configuration options. +/// +public class RequestAdapter { /// - /// Represents an adapter for making HTTP requests using HttpClient. + /// Initializes and returns a new instance of the class. /// - /// TODO: Implement the missing props and methods + /// The authentication provider to use for making requests. + /// Optional: A custom HttpClient factory. If not provided, a default client will be created. + /// A new instance of the class. + public static HttpClientRequestAdapter Create(IAuthenticationProvider authenticationProvider, HttpClient? clientFactory = null) { clientFactory ??= ClientFactory.Create(); @@ -47,3 +52,4 @@ public static HttpClientRequestAdapter Create(IAuthenticationProvider authentica return gitHubRequestAdapter; } } + diff --git a/src/GitHub/Orgs/Item/Rulesets/RulesetsPostRequestBody.cs b/src/GitHub/Orgs/Item/Rulesets/RulesetsPostRequestBody.cs index 6c7827e80..ac053d8cb 100644 --- a/src/GitHub/Orgs/Item/Rulesets/RulesetsPostRequestBody.cs +++ b/src/GitHub/Orgs/Item/Rulesets/RulesetsPostRequestBody.cs @@ -54,6 +54,7 @@ public class RulesetsPostRequestBody : IAdditionalDataHolder, IParsable public RulesetsPostRequestBody() { AdditionalData = new Dictionary(); + Target = RulesetsPostRequestBody_target.Branch; } /// /// Creates a new instance of the appropriate class based on discriminator value diff --git a/src/GitHub/Repos/Item/Item/Rulesets/RulesetsPostRequestBody.cs b/src/GitHub/Repos/Item/Item/Rulesets/RulesetsPostRequestBody.cs index e66ca58b3..0eb24a873 100644 --- a/src/GitHub/Repos/Item/Item/Rulesets/RulesetsPostRequestBody.cs +++ b/src/GitHub/Repos/Item/Item/Rulesets/RulesetsPostRequestBody.cs @@ -54,6 +54,7 @@ public class RulesetsPostRequestBody : IAdditionalDataHolder, IParsable public RulesetsPostRequestBody() { AdditionalData = new Dictionary(); + Target = RulesetsPostRequestBody_target.Branch; } /// /// Creates a new instance of the appropriate class based on discriminator value diff --git a/src/GitHub/kiota-lock.json b/src/GitHub/kiota-lock.json index 1ccb897a8..1bfbfaf3b 100644 --- a/src/GitHub/kiota-lock.json +++ b/src/GitHub/kiota-lock.json @@ -1,5 +1,5 @@ { - "descriptionHash": "E1E6EB52BE727E0A96DE3E08F5D1954068287BEAA99611DD54B0BDC9464D9FECB01638B1A3592499F67E4FE79D71CC46A290A819F6CBB894E9DA9CFC4BFACCD1", + "descriptionHash": "7A1A56113B7CD6BAECE551AA2899C89F63DF4B382A1BE3B559BE575E4B270523A48A30AF096D2D1A9CCA014DD10863E754DC78084B48DEB85ACC6404333A22FF", "descriptionLocation": "../../../../../schemas/ghes-3.13.json", "lockFileVersion": "1.0.0", "kiotaVersion": "1.14.0", diff --git a/src/Middleware/Options/UserAgentOptions.cs b/src/Middleware/Options/UserAgentOptions.cs index 0bc502f15..ed70f75f9 100644 --- a/src/Middleware/Options/UserAgentOptions.cs +++ b/src/Middleware/Options/UserAgentOptions.cs @@ -10,19 +10,19 @@ namespace GitHub.Octokit.Client.Middleware.Options; public class UserAgentOptions : IRequestOption { - private const string DefaultProductName = "GitHub.Octokit.dotnet-sdk-enterprise-server"; + private const string DefaultProductName = "GitHub.Octokit.dotnet-sdk"; private const string DefaultProductVersion = "0.0.0"; /// /// Gets or sets the product name used in the user agent request header. /// Defaults to "GitHub.Octokit.dotnet-sdk". /// - public string ProductName { get; set; } = DefaultProductName; + public string? ProductName { get; set; } = DefaultProductName; /// /// Gets or sets the product version used in the user agent request header. /// - public string ProductVersion { get; set; } = DefaultProductVersion; + public string? ProductVersion { get; set; } = DefaultProductVersion; public static string GetFullSDKVersion() => DefaultProductName + ":" + DefaultProductVersion; diff --git a/src/Middleware/UserAgentHandler.cs b/src/Middleware/UserAgentHandler.cs index 0328efea1..dea1ef1c6 100644 --- a/src/Middleware/UserAgentHandler.cs +++ b/src/Middleware/UserAgentHandler.cs @@ -16,9 +16,9 @@ protected override Task SendAsync(HttpRequestMessage reques var userAgentHandlerOption = request.GetRequestOption() ?? _userAgentOption; - if (!request.Headers.UserAgent.Any(x => userAgentHandlerOption.ProductName.Equals(x.Product?.Name, StringComparison.OrdinalIgnoreCase))) + if (!request.Headers.UserAgent.Any(x => userAgentHandlerOption.ProductName != null && userAgentHandlerOption.ProductName.Equals(x.Product?.Name, StringComparison.OrdinalIgnoreCase))) { - request.Headers.UserAgent.Add(new ProductInfoHeaderValue(userAgentHandlerOption.ProductName, userAgentHandlerOption.ProductVersion)); + request.Headers.UserAgent.Add(new ProductInfoHeaderValue(userAgentHandlerOption.ProductName ?? string.Empty, userAgentHandlerOption.ProductVersion)); } return base.SendAsync(request, cancellationToken); }