Skip to content

Commit

Permalink
feat: Introduces a builder pattern to enable configured requests
Browse files Browse the repository at this point in the history
* New updates to generated code

* New updates to generated code

* New updates to generated code
  • Loading branch information
octokitbot authored Aug 19, 2024
1 parent 25124f7 commit 9499f48
Show file tree
Hide file tree
Showing 20 changed files with 464 additions and 51 deletions.
42 changes: 36 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -53,15 +53,45 @@ using GitHub;
using GitHub.Octokit.Client;
using GitHub.Octokit.Client.Authentication;

var token = Environment.GetEnvironmentVariable("GITHUB_TOKEN") ?? "";
var request = RequestAdapter.Create(new TokenAuthProvider(new TokenProvider(token)));
var gitHubClient = new GitHubClient(request);
var tokenProvider = new TokenProvider(Environment.GetEnvironmentVariable("GITHUB_TOKEN") ?? "");
var adapter = RequestAdapter.Create(new TokenAuthProvider(tokenProvider));
await MakeRequest(new GitHubClient(adapter));

var pullRequests = await gitHubClient.Repos["octokit"]["octokit.net"].Pulls.GetAsync();
try
{
var response = await gitHubClient.Repositories.GetAsync();
response?.ForEach(repo => Console.WriteLine(repo.FullName));
}
catch (Exception e)
{
Console.WriteLine(e.Message);
}
```

#### 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://api.github.com")
.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);
}
```

Expand Down
55 changes: 49 additions & 6 deletions cli/Authentication/AppInstallationToken.cs
Original file line number Diff line number Diff line change
Expand Up @@ -28,24 +28,67 @@ 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)
{
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 = RSA.Create();
rsa.ImportFromPem(PRIVATE_KEY_PATH);
var rsa = BuildRSAKey();

var aiAccessTokenProvider = new AppInstallationTokenProvider(CLIENT_ID, rsa, INSTALLATION_ID, githubAppTokenProvider);

var adapter = new ClientFactory()
.WithAuthenticationProvider(new AppInstallationAuthProvider(aiAccessTokenProvider))
.WithUserAgent("my-app", "1.0.0")
.WithRequestTimeout(TimeSpan.FromSeconds(100))
.WithBaseUrl("https://api.github.com")
.Build();

await MakeRequest(new GitHubClient(adapter));
}

private static async Task RunWithDefault()
{
var githubAppTokenProvider = new GitHubAppTokenProvider();
var rsa = BuildRSAKey();

var aiAccessTokenProvider = new AppInstallationTokenProvider(CLIENT_ID, rsa, INSTALLATION_ID, githubAppTokenProvider);
var aiAdapter = RequestAdapter.Create(new AppInstallationAuthProvider(aiAccessTokenProvider));
var aiGitHubClient = new GitHubClient(aiAdapter);
var adapter = RequestAdapter.Create(new AppInstallationAuthProvider(aiAccessTokenProvider));
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)
{
Console.WriteLine(e.Message);
}
}

private static RSA BuildRSAKey()
{
var rsa = RSA.Create();
rsa.ImportFromPem(PRIVATE_KEY_PATH);
return rsa;
}
}
40 changes: 36 additions & 4 deletions cli/Authentication/PersonalAccessToken.cs
Original file line number Diff line number Diff line change
Expand Up @@ -14,13 +14,45 @@ 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 adapter = new ClientFactory()
.WithAuthenticationProvider(new TokenAuthProvider(tokenProvider))
.WithUserAgent("my-app", "1.0.0")
.WithRequestTimeout(TimeSpan.FromSeconds(100))
.WithBaseUrl("https://api.github.com")
.Build();

await MakeRequest(new GitHubClient(adapter));
}

private static async Task RunWithDefault()
{
// Personal Access Token authentication
var tokenProvider = new TokenProvider(Environment.GetEnvironmentVariable("GITHUB_TOKEN") ?? "");
var adapter = RequestAdapter.Create(new TokenAuthProvider(tokenProvider), "https://api.github.com");
var gitHubClient = new GitHubClient(adapter);
var adapter = RequestAdapter.Create(new TokenAuthProvider(tokenProvider));
await MakeRequest(new GitHubClient(adapter));
}

private static async Task MakeRequest(GitHubClient gitHubClient)
{
try
{
var response = await gitHubClient.Repositories.GetAsync();
Expand Down
21 changes: 18 additions & 3 deletions cli/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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'");
Expand Down
2 changes: 1 addition & 1 deletion cli/cli.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
<RestoreSources>$(RestoreSources);../src/bin/Debug;https://api.nuget.org/v3/index.json</RestoreSources>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.IdentityModel.JsonWebTokens" Version="7.5.1" />
<PackageReference Include="Microsoft.IdentityModel.JsonWebTokens" Version="8.0.1" />
<PackageReference Include="Microsoft.Kiota.Abstractions" Version="1.9.0" />
<PackageReference Include="Microsoft.Kiota.Http.HttpClientLibrary" Version="1.4.1" />
<PackageReference Include="Microsoft.Kiota.Serialization.Form" Version="1.2.0" />
Expand Down
2 changes: 1 addition & 1 deletion src/Client/Authentication/GitHubAppTokenProvider.cs
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,7 @@ public async Task<string> 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();
Expand Down
86 changes: 85 additions & 1 deletion src/Client/ClientFactory.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,15 +2,21 @@

using System.Net;
using GitHub.Octokit.Client.Middleware;
using Microsoft.Kiota.Abstractions.Authentication;
using Microsoft.Kiota.Http.HttpClientLibrary;

namespace GitHub.Octokit.Client;

/// <summary>
/// Represents a client factory for creating <see cref="HttpClient"/>.
/// </summary>
public static class ClientFactory
public class ClientFactory
{
private TimeSpan? _requestTimeout;
private string? _baseUrl;

private IAuthenticationProvider? _authenticationProvider;
private readonly HttpMessageHandler? _finalHandler;
private static readonly Lazy<List<DelegatingHandler>> s_handlers =
new(() =>
[
Expand All @@ -19,6 +25,11 @@ public static class ClientFactory
new RateLimitHandler(),
]);

public ClientFactory(HttpMessageHandler? finalHandler = null)
{
_finalHandler = finalHandler;
}

/// <summary>
/// Creates an <see cref="HttpClient"/> instance with the specified <see cref="HttpMessageHandler"/>.
/// If no <see cref="HttpMessageHandler"/> is provided, a default one will be used.
Expand All @@ -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); ;
}
/// <summary>
/// Creates a list of default delegating handlers for the Octokit client.
/// </summary>
Expand Down Expand Up @@ -97,4 +158,27 @@ public static IList<DelegatingHandler> CreateDefaultHandlers()
/// <returns>The default HTTP message handler.</returns>
public static HttpMessageHandler GetDefaultHttpMessageHandler(IWebProxy? proxy = null) =>
new HttpClientHandler { Proxy = proxy, AllowAutoRedirect = false };

/// <summary>
/// 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.
/// </summary>
/// <typeparam name="THandler"></typeparam>
/// <param name="handler"></param>
private void AddOrCreateHandler<THandler>(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);
}
}
}
14 changes: 10 additions & 4 deletions src/Client/RequestAdapter.cs
Original file line number Diff line number Diff line change
@@ -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
/// <summary>
/// Represents an adapter for making HTTP requests using HttpClient with additional configuration options.
/// </summary>
public class RequestAdapter
{
/// <summary>
/// Represents an adapter for making HTTP requests using HttpClient.
/// Initializes and returns a new instance of the <see cref="HttpClientRequestAdapter"/> class.
/// </summary>
/// TODO: Implement the missing props and methods
/// <param name="authenticationProvider">The authentication provider to use for making requests.</param>
/// <param name="clientFactory">Optional: A custom HttpClient factory. If not provided, a default client will be created.</param>
/// <returns>A new instance of the <see cref="HttpClientRequestAdapter"/> class.</returns>

public static HttpClientRequestAdapter Create(IAuthenticationProvider authenticationProvider, HttpClient? clientFactory = null)
{
clientFactory ??= ClientFactory.Create();
Expand All @@ -26,3 +31,4 @@ public static HttpClientRequestAdapter Create(IAuthenticationProvider authentica
return gitHubRequestAdapter;
}
}

Loading

0 comments on commit 9499f48

Please sign in to comment.