Skip to content
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

feat: Introduces a builder pattern to enable configured requests #39

Merged
merged 3 commits into from
Aug 19, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
47 changes: 38 additions & 9 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -52,27 +52,56 @@ 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
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);
}
```

Expand Down
55 changes: 48 additions & 7 deletions cli/Authentication/AppInstallationToken.cs
Original file line number Diff line number Diff line change
Expand Up @@ -28,28 +28,69 @@ 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)
{
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,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();
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 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 Down Expand Up @@ -47,3 +52,4 @@ public static HttpClientRequestAdapter Create(IAuthenticationProvider authentica
return gitHubRequestAdapter;
}
}

2 changes: 1 addition & 1 deletion src/GitHub/kiota-lock.json
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
{
"descriptionHash": "57124E4F6C0F181EEC09318221A85CF68B41212A105E8E79D1C412032EE45DE46177B1DA49DDB844E3F741D54F04A64A4182933B16DE85CA88ADE33EFC626371",
"descriptionHash": "3432CE051EB2E49BDF50641D87DB95EE4B4E1627CF6886161E5AADC33F9CA4664DD5EFD92682714391D22CAD05B5918F05BD4F45CB49FFF0EE99A0F62F3CB263",
"descriptionLocation": "../../../../../schemas/ghes-3.10.json",
"lockFileVersion": "1.0.0",
"kiotaVersion": "1.14.0",
Expand Down
Loading
Loading