Skip to content

Commit 55ce835

Browse files
authored
[Protocol] Mirror nuget.org's undocumented protocols (loic-sharma#249)
NuGet.org uses RDF to generate its V3 API. This adds properties that aren't part of the official API, however, certain clients depend on these undocumented properties.
1 parent f4e1d02 commit 55ce835

17 files changed

+249
-78
lines changed

src/BaGet.Core.Server/Controllers/Registration/RegistrationIndexController.cs

+3-2
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,7 @@ public async Task<IActionResult> Get(string id, CancellationToken cancellationTo
5151
// "Un-paged" example: https://api.nuget.org/v3/registration3/newtonsoft.json/index.json
5252
// Paged example: https://api.nuget.org/v3/registration3/fake/index.json
5353
return Json(new RegistrationIndex(
54+
type: RegistrationIndex.DefaultType,
5455
count: packages.Count,
5556
totalDownloads: packages.Sum(p => p.Downloads),
5657
pages: new[]
@@ -68,7 +69,7 @@ private RegistrationIndexPageItem ToRegistrationIndexPageItem(Package package) =
6869
new RegistrationIndexPageItem(
6970
leafUrl: Url.PackageRegistration(package.Id, package.Version),
7071
packageMetadata: new PackageMetadata(
71-
catalogUri: $"https://api.nuget.org/v3/catalog0/data/2015.02.01.06.24.15/{package.Id}.{package.Version}.json",
72+
catalogUri: Url.PackageRegistration(package.Id, package.Version),
7273
packageId: package.Id,
7374
version: package.Version,
7475
authors: string.Join(", ", package.Authors),
@@ -115,4 +116,4 @@ private IReadOnlyList<DependencyGroupItem> ToDependencyGroups(Package package)
115116
return groups;
116117
}
117118
}
118-
}
119+
}

src/BaGet.Core.Server/Controllers/Registration/RegistrationLeafController.cs

+2-1
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,7 @@ public async Task<IActionResult> Get(string id, string version, CancellationToke
4444
}
4545

4646
var result = new RegistrationLeaf(
47+
type: RegistrationLeaf.DefaultType,
4748
registrationUri: Url.PackageRegistration(id, nugetVersion),
4849
listed: package.Listed,
4950
downloads: package.Downloads,
@@ -54,4 +55,4 @@ public async Task<IActionResult> Get(string id, string version, CancellationToke
5455
return Json(result);
5556
}
5657
}
57-
}
58+
}

src/BaGet.Core.Server/Controllers/SearchController.cs

+6-2
Original file line numberDiff line numberDiff line change
@@ -43,14 +43,18 @@ public async Task<ActionResult<SearchResponse>> Get(
4343

4444
return new SearchResponse(
4545
totalHits: results.Count,
46-
data: results.Select(ToSearchResult).ToList());
46+
data: results.Select(ToSearchResult).ToList(),
47+
context: SearchContext.Default(Url.RegistrationsBase()));
4748
}
4849

4950
public async Task<ActionResult<AutocompleteResult>> Autocomplete([FromQuery(Name = "q")] string query = null)
5051
{
5152
var results = await _searchService.AutocompleteAsync(query);
5253

53-
return new AutocompleteResult(results.Count, results);
54+
return new AutocompleteResult(
55+
results.Count,
56+
results,
57+
AutocompleteContext.Default);
5458
}
5559

5660
public async Task<ActionResult<DependentResult>> Dependents([FromQuery(Name = "packageId")] string packageId)
+54
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
using System.IO;
2+
using System.Net;
3+
using System.Net.Http;
4+
using System.Threading.Tasks;
5+
using Newtonsoft.Json;
6+
7+
namespace BaGet.Protocol
8+
{
9+
internal static class HttpClientExtensions
10+
{
11+
private static readonly JsonSerializer Serializer = JsonSerializer.Create(Settings);
12+
13+
private static readonly JsonSerializerSettings Settings = new JsonSerializerSettings
14+
{
15+
DateTimeZoneHandling = DateTimeZoneHandling.Utc,
16+
DateParseHandling = DateParseHandling.DateTimeOffset,
17+
NullValueHandling = NullValueHandling.Ignore,
18+
};
19+
20+
public static async Task<ResponseAndResult<T>> DeserializeUrlAsync<T>(
21+
this HttpClient httpClient,
22+
string documentUrl)
23+
{
24+
using (var response = await httpClient.GetAsync(
25+
documentUrl,
26+
HttpCompletionOption.ResponseHeadersRead))
27+
{
28+
if (response.StatusCode != HttpStatusCode.OK)
29+
{
30+
return new ResponseAndResult<T>(
31+
HttpMethod.Get,
32+
documentUrl,
33+
response.StatusCode,
34+
response.ReasonPhrase,
35+
hasResult: false,
36+
result: default);
37+
}
38+
39+
using (var stream = await response.Content.ReadAsStreamAsync())
40+
using (var textReader = new StreamReader(stream))
41+
using (var jsonReader = new JsonTextReader(textReader))
42+
{
43+
return new ResponseAndResult<T>(
44+
HttpMethod.Get,
45+
documentUrl,
46+
response.StatusCode,
47+
response.ReasonPhrase,
48+
hasResult: true,
49+
result: Serializer.Deserialize<T>(jsonReader));
50+
}
51+
}
52+
}
53+
}
54+
}

src/BaGet.Protocol/HttpContentExtensions.cs

-29
This file was deleted.

src/BaGet.Protocol/PackageContent/PackageContentClient.cs

+3-5
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
using System;
1+
using System;
22
using System.IO;
33
using System.Net;
44
using System.Net.Http;
@@ -22,15 +22,13 @@ public PackageContentClient(HttpClient httpClient)
2222
/// <returns>The package's versions, or null if the package does not exist</returns>
2323
public async Task<PackageVersions> GetPackageVersionsOrNullAsync(string url)
2424
{
25-
var response = await _httpClient.GetAsync(url);
25+
var response = await _httpClient.DeserializeUrlAsync<PackageVersions>(url);
2626
if (response.StatusCode == HttpStatusCode.NotFound)
2727
{
2828
return null;
2929
}
3030

31-
response.EnsureSuccessStatusCode();
32-
33-
return await response.Content.ReadAsAsync<PackageVersions>();
31+
return response.GetResultOrThrow();
3432
}
3533

3634
public async Task<Stream> GetPackageContentStreamAsync(string url)
+27
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
using System;
2+
using System.Net;
3+
using System.Net.Http;
4+
5+
namespace BaGet.Protocol
6+
{
7+
public class ProtocolException : Exception
8+
{
9+
public ProtocolException(
10+
string message,
11+
HttpMethod method,
12+
string requestUri,
13+
HttpStatusCode statusCode,
14+
string reasonPhrase) : base(message)
15+
{
16+
Method = method ?? throw new ArgumentNullException(nameof(method));
17+
RequestUri = requestUri ?? throw new ArgumentNullException(nameof(requestUri));
18+
StatusCode = statusCode;
19+
ReasonPhrase = reasonPhrase ?? throw new ArgumentNullException(nameof(reasonPhrase));
20+
}
21+
22+
public HttpMethod Method { get; }
23+
public string RequestUri { get; }
24+
public HttpStatusCode StatusCode { get; }
25+
public string ReasonPhrase { get; }
26+
}
27+
}
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
using System;
1+
using System;
22
using System.Net;
33
using System.Net.Http;
44
using System.Threading.Tasks;
@@ -16,33 +16,27 @@ public RegistrationClient(HttpClient httpClient)
1616

1717
public async Task<RegistrationIndex> GetRegistrationIndexOrNullAsync(string indexUrl)
1818
{
19-
var response = await _httpClient.GetAsync(indexUrl);
19+
var response = await _httpClient.DeserializeUrlAsync<RegistrationIndex>(indexUrl);
2020
if (response.StatusCode == HttpStatusCode.NotFound)
2121
{
2222
return null;
2323
}
2424

25-
response.EnsureSuccessStatusCode();
26-
27-
return await response.Content.ReadAsAsync<RegistrationIndex>();
25+
return response.GetResultOrThrow();
2826
}
2927

3028
public async Task<RegistrationIndexPage> GetRegistrationIndexPageAsync(string pageUrl)
3129
{
32-
var response = await _httpClient.GetAsync(pageUrl);
33-
34-
response.EnsureSuccessStatusCode();
30+
var response = await _httpClient.DeserializeUrlAsync<RegistrationIndexPage>(pageUrl);
3531

36-
return await response.Content.ReadAsAsync<RegistrationIndexPage>();
32+
return response.GetResultOrThrow();
3733
}
3834

3935
public async Task<RegistrationLeaf> GetRegistrationLeafAsync(string leafUrl)
4036
{
41-
var response = await _httpClient.GetAsync(leafUrl);
42-
43-
response.EnsureSuccessStatusCode();
37+
var response = await _httpClient.DeserializeUrlAsync<RegistrationLeaf>(leafUrl);
4438

45-
return await response.Content.ReadAsAsync<RegistrationLeaf>();
39+
return response.GetResultOrThrow();
4640
}
4741
}
4842
}

src/BaGet.Protocol/Registration/RegistrationIndex.cs

+17-2
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
using System;
1+
using System;
22
using System.Collections.Generic;
33
using Newtonsoft.Json;
44

@@ -9,12 +9,27 @@ namespace BaGet.Protocol
99
/// </summary>
1010
public class RegistrationIndex
1111
{
12-
public RegistrationIndex(int count, long totalDownloads, IReadOnlyList<RegistrationIndexPage> pages)
12+
public static readonly IReadOnlyList<string> DefaultType = new List<string>
13+
{
14+
"catalog:CatalogRoot",
15+
"PackageRegistration",
16+
"catalog:Permalink"
17+
};
18+
19+
public RegistrationIndex(
20+
int count,
21+
long totalDownloads,
22+
IReadOnlyList<RegistrationIndexPage> pages,
23+
IReadOnlyList<string> type = null)
1324
{
1425
Count = count;
1526
Pages = pages ?? throw new ArgumentNullException(nameof(pages));
27+
Type = type;
1628
}
1729

30+
[JsonProperty(PropertyName = "@type")]
31+
public IReadOnlyList<string> Type { get; }
32+
1833
/// <summary>
1934
/// The number of registration pages. See <see cref="Pages"/>.
2035
/// </summary>

src/BaGet.Protocol/Registration/RegistrationLeaf.cs

+14-2
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
1-
using System;
1+
using System;
2+
using System.Collections.Generic;
23
using Newtonsoft.Json;
34

45
namespace BaGet.Protocol
@@ -9,25 +10,36 @@ namespace BaGet.Protocol
910
/// </summary>
1011
public class RegistrationLeaf
1112
{
13+
public static readonly IReadOnlyList<string> DefaultType = new List<string>
14+
{
15+
"Package",
16+
"http://schema.nuget.org/catalog#Permalink"
17+
};
18+
1219
public RegistrationLeaf(
1320
string registrationUri,
1421
bool listed,
1522
long downloads,
1623
string packageContentUrl,
1724
DateTimeOffset published,
18-
string registrationIndexUrl)
25+
string registrationIndexUrl,
26+
IReadOnlyList<string> type = null)
1927
{
2028
RegistrationUri = registrationUri ?? throw new ArgumentNullException(nameof(registrationIndexUrl));
2129
Listed = listed;
2230
Published = published;
2331
Downloads = downloads;
2432
PackageContentUrl = packageContentUrl ?? throw new ArgumentNullException(nameof(packageContentUrl));
2533
RegistrationIndexUrl = registrationIndexUrl ?? throw new ArgumentNullException(nameof(registrationIndexUrl));
34+
Type = type;
2635
}
2736

2837
[JsonProperty(PropertyName = "@id")]
2938
public string RegistrationUri { get; }
3039

40+
[JsonProperty(PropertyName = "@type")]
41+
public IReadOnlyList<string> Type { get; }
42+
3143
public bool Listed { get; }
3244

3345
public long Downloads { get; }
+49
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
using System;
2+
using System.Net;
3+
using System.Net.Http;
4+
5+
namespace BaGet.Protocol
6+
{
7+
internal class ResponseAndResult<T>
8+
{
9+
public ResponseAndResult(
10+
HttpMethod method,
11+
string requestUri,
12+
HttpStatusCode statusCode,
13+
string reasonPhrase,
14+
bool hasResult,
15+
T result)
16+
{
17+
Method = method ?? throw new ArgumentNullException(nameof(method));
18+
RequestUri = requestUri ?? throw new ArgumentNullException(nameof(requestUri));
19+
StatusCode = statusCode;
20+
ReasonPhrase = reasonPhrase ?? throw new ArgumentNullException(nameof(reasonPhrase));
21+
HasResult = hasResult;
22+
Result = result;
23+
}
24+
25+
public HttpMethod Method { get; }
26+
public string RequestUri { get; }
27+
public HttpStatusCode StatusCode { get; }
28+
public string ReasonPhrase { get; }
29+
public bool HasResult { get; }
30+
public T Result { get; }
31+
32+
public T GetResultOrThrow()
33+
{
34+
if (!HasResult)
35+
{
36+
throw new ProtocolException(
37+
$"The HTTP request failed.{Environment.NewLine}" +
38+
$"Request: {Method} {RequestUri}{Environment.NewLine}" +
39+
$"Response: {(int)StatusCode} {ReasonPhrase}",
40+
Method,
41+
RequestUri,
42+
StatusCode,
43+
ReasonPhrase);
44+
}
45+
46+
return Result;
47+
}
48+
}
49+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
using Newtonsoft.Json;
2+
3+
namespace BaGet.Protocol
4+
{
5+
public class AutocompleteContext
6+
{
7+
public static readonly AutocompleteContext Default = new AutocompleteContext
8+
{
9+
Vocab = "http://schema.nuget.org/schema#"
10+
};
11+
12+
[JsonProperty("@vocab")]
13+
public string Vocab { get; set; }
14+
}
15+
}

0 commit comments

Comments
 (0)