Skip to content

Commit 76e31c3

Browse files
committed
Swap to PaginatedEndpoint & IHttpClientProxy from GitLabCli
1 parent b6f7bfd commit 76e31c3

10 files changed

Lines changed: 506 additions & 88 deletions

File tree

Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,97 @@
1+
using System.Diagnostics.CodeAnalysis;
2+
using System.Net;
3+
4+
namespace Ryujinx.Systems.Update.Server.Helpers.Http;
5+
6+
public interface IHttpClientProxy
7+
{
8+
public Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, HttpCompletionOption? option = null, CancellationToken? token = null);
9+
10+
#region Convenience overloads for SendAsync
11+
12+
public Task<HttpResponseMessage> GetAsync(
13+
[StringSyntax(StringSyntaxAttribute.Uri)]
14+
string requestUri,
15+
HttpCompletionOption? option = null, CancellationToken? token = null
16+
) => GetAsync(CreateUri(requestUri)!, option, token);
17+
18+
public Task<HttpResponseMessage> PostAsync(
19+
[StringSyntax(StringSyntaxAttribute.Uri)]
20+
string requestUri,
21+
HttpContent? content = null,
22+
HttpCompletionOption? option = null, CancellationToken? token = null
23+
) => PostAsync(CreateUri(requestUri)!, content, option, token);
24+
25+
public Task<HttpResponseMessage> PutAsync(
26+
[StringSyntax(StringSyntaxAttribute.Uri)]
27+
string requestUri,
28+
HttpContent? content = null,
29+
HttpCompletionOption? option = null, CancellationToken? token = null
30+
) => PutAsync(CreateUri(requestUri)!, content, option, token);
31+
32+
public Task<HttpResponseMessage> PatchAsync(
33+
[StringSyntax(StringSyntaxAttribute.Uri)]
34+
string requestUri,
35+
HttpContent? content = null,
36+
HttpCompletionOption? option = null, CancellationToken? token = null
37+
) => PatchAsync(CreateUri(requestUri)!, content, option, token);
38+
39+
public Task<HttpResponseMessage> DeleteAsync(
40+
[StringSyntax(StringSyntaxAttribute.Uri)]
41+
string requestUri,
42+
HttpContent? content = null,
43+
HttpCompletionOption? option = null, CancellationToken? token = null
44+
) => DeleteAsync(CreateUri(requestUri)!, content, option, token);
45+
46+
#region Uri overloads
47+
48+
public Task<HttpResponseMessage> GetAsync(
49+
Uri requestUri,
50+
HttpCompletionOption? option = null, CancellationToken? token = null
51+
) => SendAsync(CreateRequestMessage(HttpMethod.Get, requestUri), option, token);
52+
53+
public Task<HttpResponseMessage> PostAsync(
54+
Uri requestUri,
55+
HttpContent? content = null,
56+
HttpCompletionOption? option = null, CancellationToken? token = null
57+
) => SendAsync(CreateRequestMessageWithContent(HttpMethod.Post, requestUri, content), option, token);
58+
59+
public Task<HttpResponseMessage> PutAsync(
60+
Uri requestUri,
61+
HttpContent? content = null,
62+
HttpCompletionOption? option = null, CancellationToken? token = null
63+
) => SendAsync(CreateRequestMessageWithContent(HttpMethod.Put, requestUri, content), option, token);
64+
65+
public Task<HttpResponseMessage> PatchAsync(
66+
Uri requestUri,
67+
HttpContent? content = null,
68+
HttpCompletionOption? option = null, CancellationToken? token = null
69+
) => SendAsync(CreateRequestMessageWithContent(HttpMethod.Patch, requestUri, content), option, token);
70+
71+
public Task<HttpResponseMessage> DeleteAsync(
72+
Uri requestUri,
73+
HttpContent? content = null,
74+
HttpCompletionOption? option = null, CancellationToken? token = null
75+
) => SendAsync(CreateRequestMessageWithContent(HttpMethod.Delete, requestUri, content), option, token);
76+
77+
#endregion
78+
79+
#endregion
80+
81+
#region Overload Helpers
82+
83+
private static HttpRequestMessage CreateRequestMessage(HttpMethod method, Uri? uri)
84+
=> new(method, uri) { Version = HttpVersion.Version11, VersionPolicy = HttpVersionPolicy.RequestVersionOrLower };
85+
86+
private static HttpRequestMessage CreateRequestMessageWithContent(HttpMethod method, Uri? uri, HttpContent? requestContent)
87+
{
88+
var req = CreateRequestMessage(method, uri);
89+
req.Content = requestContent;
90+
return req;
91+
}
92+
93+
private static Uri? CreateUri(string? uri) =>
94+
string.IsNullOrEmpty(uri) ? null : new Uri(uri, UriKind.RelativeOrAbsolute);
95+
96+
#endregion
97+
}
Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
using System.Net.Http.Json;
2+
using System.Text.Json.Serialization.Metadata;
3+
using Gommon;
4+
using Ryujinx.Systems.Update.Server.Services;
5+
6+
namespace Ryujinx.Systems.Update.Server.Helpers.Http;
7+
8+
public partial class PaginatedEndpoint<T>
9+
{
10+
public static BuilderApi Builder(IHttpClientProxy httpClient) => new(httpClient);
11+
12+
public static BuilderApi Builder(HttpClient httpClient) => new(new DefaultHttpClientProxy(httpClient));
13+
14+
public class BuilderApi
15+
{
16+
public BuilderApi(IHttpClientProxy httpClient)
17+
{
18+
_http = httpClient;
19+
}
20+
21+
private readonly IHttpClientProxy _http;
22+
23+
public string BaseUrl { get; private set; } = null!;
24+
public HttpContentParser ContentParser { get; private set; } = null!;
25+
public int PerPage { get; private set; } = 100;
26+
27+
public Dictionary<string, object> QueryStringParameters { get; private set; } = new();
28+
29+
public BuilderApi WithBaseUrl(string url)
30+
{
31+
BaseUrl = url;
32+
return this;
33+
}
34+
35+
public BuilderApi WithContentParser(HttpContentParser contentParser)
36+
{
37+
ContentParser = contentParser;
38+
return this;
39+
}
40+
41+
public BuilderApi WithJsonContentParser(JsonTypeInfo<IEnumerable<T>> typeInfo)
42+
{
43+
ContentParser = content => content.ReadFromJsonAsync(typeInfo)!;
44+
return this;
45+
}
46+
47+
public BuilderApi WithPerPageCount(int perPage)
48+
{
49+
PerPage = perPage;
50+
return this;
51+
}
52+
53+
public BuilderApi WithQueryStringParameters(params (string, object)[] parameters)
54+
{
55+
QueryStringParameters = Collections.NewSafeDictionary(parameters);
56+
return this;
57+
}
58+
59+
public PaginatedEndpoint<T> Build() => new(_http, BaseUrl, ContentParser, QueryStringParameters, PerPage);
60+
61+
public static implicit operator PaginatedEndpoint<T>(BuilderApi builder) => builder.Build();
62+
}
63+
}
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
using System.Text;
2+
3+
namespace Ryujinx.Systems.Update.Server.Helpers.Http;
4+
5+
public partial class PaginatedEndpoint<T>
6+
{
7+
private readonly IHttpClientProxy _http;
8+
private readonly string _baseUrl;
9+
private readonly HttpContentParser _parsePage;
10+
private readonly Dictionary<string, object> _queryStringParams;
11+
private string? _constructedUrl;
12+
13+
private string GetUrl(int pageNumber)
14+
{
15+
if (_constructedUrl is null)
16+
{
17+
var sb = new StringBuilder(_baseUrl.TrimEnd('/'));
18+
foreach (var (index, (param, value)) in _queryStringParams.Index())
19+
{
20+
sb.Append(index is 0 ? "?" : "&");
21+
22+
sb.Append(param).Append('=').Append(value);
23+
}
24+
25+
_constructedUrl = sb.ToString();
26+
}
27+
28+
return $"{_constructedUrl}&page={pageNumber}";
29+
}
30+
31+
public delegate Task<IEnumerable<T>> HttpContentParser(HttpContent content);
32+
}
Lines changed: 172 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,172 @@
1+
using System.Net;
2+
3+
namespace Ryujinx.Systems.Update.Server.Helpers.Http;
4+
5+
public partial class PaginatedEndpoint<T>
6+
{
7+
private PaginatedEndpoint(IHttpClientProxy client,
8+
string baseUrl,
9+
HttpContentParser parsePage,
10+
Dictionary<string, object> queryStringParams,
11+
int perPage = 100)
12+
{
13+
_http = client;
14+
_baseUrl = baseUrl;
15+
_parsePage = parsePage;
16+
_queryStringParams = queryStringParams;
17+
_queryStringParams["per_page"] = perPage;
18+
}
19+
20+
public async Task<T?> FindOneAsync(Func<T, bool> predicate,
21+
Action<HttpStatusCode>? onNonSuccess = null)
22+
{
23+
var currentPage = 1;
24+
var response = await _http.GetAsync(GetUrl(currentPage));
25+
26+
if (!response.IsSuccessStatusCode)
27+
{
28+
onNonSuccess?.Invoke(response.StatusCode);
29+
return default;
30+
}
31+
32+
IEnumerable<T> returned = await _parsePage(response.Content);
33+
34+
if (returned.TryGetFirst(predicate, out var matched))
35+
return matched;
36+
37+
if (!response.Headers.GetValues("x-total-pages").ToString().TryParse<int>(out var pageCount) || pageCount > 1)
38+
{
39+
currentPage++;
40+
do
41+
{
42+
response = await _http.GetAsync(GetUrl(currentPage));
43+
44+
if (!response.IsSuccessStatusCode)
45+
{
46+
onNonSuccess?.Invoke(response.StatusCode);
47+
return default;
48+
}
49+
50+
returned = await _parsePage(response.Content);
51+
52+
if (returned.TryGetFirst(predicate, out matched))
53+
return matched;
54+
55+
currentPage++;
56+
} while (currentPage <= pageCount);
57+
}
58+
59+
return default;
60+
}
61+
62+
public async Task<T?> FindOneAsync(Action<HttpStatusCode>? onNonSuccess = null)
63+
{
64+
var currentPage = 1;
65+
var response = await _http.GetAsync(GetUrl(currentPage));
66+
67+
if (!response.IsSuccessStatusCode)
68+
{
69+
onNonSuccess?.Invoke(response.StatusCode);
70+
return default;
71+
}
72+
73+
var returned = (await _parsePage(response.Content)).ToArray();
74+
if (returned.Length > 0)
75+
return returned[0];
76+
77+
if (!response.Headers.GetValues("x-total-pages").ToString().TryParse<int>(out var pageCount) || pageCount > 1)
78+
{
79+
currentPage++;
80+
do
81+
{
82+
response = await _http.GetAsync(GetUrl(currentPage));
83+
84+
if (!response.IsSuccessStatusCode)
85+
{
86+
onNonSuccess?.Invoke(response.StatusCode);
87+
return default;
88+
}
89+
90+
returned = (await _parsePage(response.Content)).ToArray();
91+
if (returned.Length > 0)
92+
return returned[0];
93+
94+
currentPage++;
95+
} while (currentPage <= pageCount);
96+
}
97+
98+
return default;
99+
}
100+
101+
public async Task<IEnumerable<T>?> GetAllAsync(Func<T, bool> predicate,
102+
Action<HttpStatusCode>? onNonSuccess = null)
103+
{
104+
var currentPage = 1;
105+
var response = await _http.GetAsync(GetUrl(currentPage));
106+
107+
if (!response.IsSuccessStatusCode)
108+
{
109+
onNonSuccess?.Invoke(response.StatusCode);
110+
return null;
111+
}
112+
113+
IEnumerable<T> accumulated = await _parsePage(response.Content);
114+
115+
if (!response.Headers.GetValues("x-total-pages").ToString().TryParse<int>(out var pageCount) || pageCount > 1)
116+
{
117+
currentPage++;
118+
do
119+
{
120+
response = await _http.GetAsync(GetUrl(currentPage));
121+
122+
if (!response.IsSuccessStatusCode)
123+
{
124+
onNonSuccess?.Invoke(response.StatusCode);
125+
return null;
126+
}
127+
128+
accumulated = accumulated.Concat(await _parsePage(response.Content));
129+
130+
currentPage++;
131+
} while (currentPage <= pageCount);
132+
}
133+
134+
return accumulated.Where(predicate);
135+
}
136+
137+
public async Task<IEnumerable<T>?> GetAllAsync(
138+
Action<HttpStatusCode>? onNonSuccess = null)
139+
{
140+
var currentPage = 1;
141+
var response = await _http.GetAsync(GetUrl(currentPage));
142+
143+
if (!response.IsSuccessStatusCode)
144+
{
145+
onNonSuccess?.Invoke(response.StatusCode);
146+
return null;
147+
}
148+
149+
IEnumerable<T> accumulated = await _parsePage(response.Content);
150+
151+
if (!response.Headers.GetValues("x-total-pages").ToString().TryParse<int>(out var pageCount) || pageCount > 1)
152+
{
153+
currentPage++;
154+
do
155+
{
156+
response = await _http.GetAsync(GetUrl(currentPage));
157+
158+
if (!response.IsSuccessStatusCode)
159+
{
160+
onNonSuccess?.Invoke(response.StatusCode);
161+
return null;
162+
}
163+
164+
accumulated = accumulated.Concat(await _parsePage(response.Content));
165+
166+
currentPage++;
167+
} while (currentPage <= pageCount);
168+
}
169+
170+
return accumulated;
171+
}
172+
}
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
namespace Ryujinx.Systems.Update.Server.Helpers.Http;
2+
3+
public static class QueryParams
4+
{
5+
public static (string, object) Sort(string type) => ("sort", type);
6+
public static (string, object) OrderBy(string ordering) => ("order_by", ordering);
7+
}

src/Server/Program.cs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
using Ryujinx.Systems.Update.Server;
22
using Ryujinx.Systems.Update.Server.Helpers;
3+
using Ryujinx.Systems.Update.Server.Services;
34
using Ryujinx.Systems.Update.Server.Services.GitLab;
45

56
CommandLineState.Init(args);
@@ -12,6 +13,7 @@
1213
if (CommandLineState.UseHttpLogging)
1314
builder.Services.AddHttpLogging();
1415

16+
builder.Services.AddSingleton<DefaultHttpClientProxy>();
1517
builder.Services.AddSingleton<GitLabService>();
1618
builder.Services.AddKeyedSingleton<VersionCache>("stableCache");
1719
builder.Services.AddKeyedSingleton<VersionCache>("canaryCache");

0 commit comments

Comments
 (0)