Skip to content

Commit

Permalink
[Protocol] Mirror nuget.org's undocumented protocols (#249)
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
loic-sharma authored Apr 1, 2019
1 parent f4e1d02 commit 55ce835
Show file tree
Hide file tree
Showing 17 changed files with 249 additions and 78 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@ public async Task<IActionResult> Get(string id, CancellationToken cancellationTo
// "Un-paged" example: https://api.nuget.org/v3/registration3/newtonsoft.json/index.json
// Paged example: https://api.nuget.org/v3/registration3/fake/index.json
return Json(new RegistrationIndex(
type: RegistrationIndex.DefaultType,
count: packages.Count,
totalDownloads: packages.Sum(p => p.Downloads),
pages: new[]
Expand All @@ -68,7 +69,7 @@ private RegistrationIndexPageItem ToRegistrationIndexPageItem(Package package) =
new RegistrationIndexPageItem(
leafUrl: Url.PackageRegistration(package.Id, package.Version),
packageMetadata: new PackageMetadata(
catalogUri: $"https://api.nuget.org/v3/catalog0/data/2015.02.01.06.24.15/{package.Id}.{package.Version}.json",
catalogUri: Url.PackageRegistration(package.Id, package.Version),
packageId: package.Id,
version: package.Version,
authors: string.Join(", ", package.Authors),
Expand Down Expand Up @@ -115,4 +116,4 @@ private IReadOnlyList<DependencyGroupItem> ToDependencyGroups(Package package)
return groups;
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ public async Task<IActionResult> Get(string id, string version, CancellationToke
}

var result = new RegistrationLeaf(
type: RegistrationLeaf.DefaultType,
registrationUri: Url.PackageRegistration(id, nugetVersion),
listed: package.Listed,
downloads: package.Downloads,
Expand All @@ -54,4 +55,4 @@ public async Task<IActionResult> Get(string id, string version, CancellationToke
return Json(result);
}
}
}
}
8 changes: 6 additions & 2 deletions src/BaGet.Core.Server/Controllers/SearchController.cs
Original file line number Diff line number Diff line change
Expand Up @@ -43,14 +43,18 @@ public async Task<ActionResult<SearchResponse>> Get(

return new SearchResponse(
totalHits: results.Count,
data: results.Select(ToSearchResult).ToList());
data: results.Select(ToSearchResult).ToList(),
context: SearchContext.Default(Url.RegistrationsBase()));
}

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

return new AutocompleteResult(results.Count, results);
return new AutocompleteResult(
results.Count,
results,
AutocompleteContext.Default);
}

public async Task<ActionResult<DependentResult>> Dependents([FromQuery(Name = "packageId")] string packageId)
Expand Down
54 changes: 54 additions & 0 deletions src/BaGet.Protocol/HttpClientExtensions.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
using System.IO;
using System.Net;
using System.Net.Http;
using System.Threading.Tasks;
using Newtonsoft.Json;

namespace BaGet.Protocol
{
internal static class HttpClientExtensions
{
private static readonly JsonSerializer Serializer = JsonSerializer.Create(Settings);

private static readonly JsonSerializerSettings Settings = new JsonSerializerSettings
{
DateTimeZoneHandling = DateTimeZoneHandling.Utc,
DateParseHandling = DateParseHandling.DateTimeOffset,
NullValueHandling = NullValueHandling.Ignore,
};

public static async Task<ResponseAndResult<T>> DeserializeUrlAsync<T>(
this HttpClient httpClient,
string documentUrl)
{
using (var response = await httpClient.GetAsync(
documentUrl,
HttpCompletionOption.ResponseHeadersRead))
{
if (response.StatusCode != HttpStatusCode.OK)
{
return new ResponseAndResult<T>(
HttpMethod.Get,
documentUrl,
response.StatusCode,
response.ReasonPhrase,
hasResult: false,
result: default);
}

using (var stream = await response.Content.ReadAsStreamAsync())
using (var textReader = new StreamReader(stream))
using (var jsonReader = new JsonTextReader(textReader))
{
return new ResponseAndResult<T>(
HttpMethod.Get,
documentUrl,
response.StatusCode,
response.ReasonPhrase,
hasResult: true,
result: Serializer.Deserialize<T>(jsonReader));
}
}
}
}
}
29 changes: 0 additions & 29 deletions src/BaGet.Protocol/HttpContentExtensions.cs

This file was deleted.

8 changes: 3 additions & 5 deletions src/BaGet.Protocol/PackageContent/PackageContentClient.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
using System;
using System;
using System.IO;
using System.Net;
using System.Net.Http;
Expand All @@ -22,15 +22,13 @@ public PackageContentClient(HttpClient httpClient)
/// <returns>The package's versions, or null if the package does not exist</returns>
public async Task<PackageVersions> GetPackageVersionsOrNullAsync(string url)
{
var response = await _httpClient.GetAsync(url);
var response = await _httpClient.DeserializeUrlAsync<PackageVersions>(url);
if (response.StatusCode == HttpStatusCode.NotFound)
{
return null;
}

response.EnsureSuccessStatusCode();

return await response.Content.ReadAsAsync<PackageVersions>();
return response.GetResultOrThrow();
}

public async Task<Stream> GetPackageContentStreamAsync(string url)
Expand Down
27 changes: 27 additions & 0 deletions src/BaGet.Protocol/ProtocolException.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
using System;
using System.Net;
using System.Net.Http;

namespace BaGet.Protocol
{
public class ProtocolException : Exception
{
public ProtocolException(
string message,
HttpMethod method,
string requestUri,
HttpStatusCode statusCode,
string reasonPhrase) : base(message)
{
Method = method ?? throw new ArgumentNullException(nameof(method));
RequestUri = requestUri ?? throw new ArgumentNullException(nameof(requestUri));
StatusCode = statusCode;
ReasonPhrase = reasonPhrase ?? throw new ArgumentNullException(nameof(reasonPhrase));
}

public HttpMethod Method { get; }
public string RequestUri { get; }
public HttpStatusCode StatusCode { get; }
public string ReasonPhrase { get; }
}
}
20 changes: 7 additions & 13 deletions src/BaGet.Protocol/Registration/RegistrationClient.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
using System;
using System;
using System.Net;
using System.Net.Http;
using System.Threading.Tasks;
Expand All @@ -16,33 +16,27 @@ public RegistrationClient(HttpClient httpClient)

public async Task<RegistrationIndex> GetRegistrationIndexOrNullAsync(string indexUrl)
{
var response = await _httpClient.GetAsync(indexUrl);
var response = await _httpClient.DeserializeUrlAsync<RegistrationIndex>(indexUrl);
if (response.StatusCode == HttpStatusCode.NotFound)
{
return null;
}

response.EnsureSuccessStatusCode();

return await response.Content.ReadAsAsync<RegistrationIndex>();
return response.GetResultOrThrow();
}

public async Task<RegistrationIndexPage> GetRegistrationIndexPageAsync(string pageUrl)
{
var response = await _httpClient.GetAsync(pageUrl);

response.EnsureSuccessStatusCode();
var response = await _httpClient.DeserializeUrlAsync<RegistrationIndexPage>(pageUrl);

return await response.Content.ReadAsAsync<RegistrationIndexPage>();
return response.GetResultOrThrow();
}

public async Task<RegistrationLeaf> GetRegistrationLeafAsync(string leafUrl)
{
var response = await _httpClient.GetAsync(leafUrl);

response.EnsureSuccessStatusCode();
var response = await _httpClient.DeserializeUrlAsync<RegistrationLeaf>(leafUrl);

return await response.Content.ReadAsAsync<RegistrationLeaf>();
return response.GetResultOrThrow();
}
}
}
19 changes: 17 additions & 2 deletions src/BaGet.Protocol/Registration/RegistrationIndex.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
using System;
using System;
using System.Collections.Generic;
using Newtonsoft.Json;

Expand All @@ -9,12 +9,27 @@ namespace BaGet.Protocol
/// </summary>
public class RegistrationIndex
{
public RegistrationIndex(int count, long totalDownloads, IReadOnlyList<RegistrationIndexPage> pages)
public static readonly IReadOnlyList<string> DefaultType = new List<string>
{
"catalog:CatalogRoot",
"PackageRegistration",
"catalog:Permalink"
};

public RegistrationIndex(
int count,
long totalDownloads,
IReadOnlyList<RegistrationIndexPage> pages,
IReadOnlyList<string> type = null)
{
Count = count;
Pages = pages ?? throw new ArgumentNullException(nameof(pages));
Type = type;
}

[JsonProperty(PropertyName = "@type")]
public IReadOnlyList<string> Type { get; }

/// <summary>
/// The number of registration pages. See <see cref="Pages"/>.
/// </summary>
Expand Down
16 changes: 14 additions & 2 deletions src/BaGet.Protocol/Registration/RegistrationLeaf.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
using System;
using System;
using System.Collections.Generic;
using Newtonsoft.Json;

namespace BaGet.Protocol
Expand All @@ -9,25 +10,36 @@ namespace BaGet.Protocol
/// </summary>
public class RegistrationLeaf
{
public static readonly IReadOnlyList<string> DefaultType = new List<string>
{
"Package",
"http://schema.nuget.org/catalog#Permalink"
};

public RegistrationLeaf(
string registrationUri,
bool listed,
long downloads,
string packageContentUrl,
DateTimeOffset published,
string registrationIndexUrl)
string registrationIndexUrl,
IReadOnlyList<string> type = null)
{
RegistrationUri = registrationUri ?? throw new ArgumentNullException(nameof(registrationIndexUrl));
Listed = listed;
Published = published;
Downloads = downloads;
PackageContentUrl = packageContentUrl ?? throw new ArgumentNullException(nameof(packageContentUrl));
RegistrationIndexUrl = registrationIndexUrl ?? throw new ArgumentNullException(nameof(registrationIndexUrl));
Type = type;
}

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

[JsonProperty(PropertyName = "@type")]
public IReadOnlyList<string> Type { get; }

public bool Listed { get; }

public long Downloads { get; }
Expand Down
49 changes: 49 additions & 0 deletions src/BaGet.Protocol/ResponseAndResult.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
using System;
using System.Net;
using System.Net.Http;

namespace BaGet.Protocol
{
internal class ResponseAndResult<T>
{
public ResponseAndResult(
HttpMethod method,
string requestUri,
HttpStatusCode statusCode,
string reasonPhrase,
bool hasResult,
T result)
{
Method = method ?? throw new ArgumentNullException(nameof(method));
RequestUri = requestUri ?? throw new ArgumentNullException(nameof(requestUri));
StatusCode = statusCode;
ReasonPhrase = reasonPhrase ?? throw new ArgumentNullException(nameof(reasonPhrase));
HasResult = hasResult;
Result = result;
}

public HttpMethod Method { get; }
public string RequestUri { get; }
public HttpStatusCode StatusCode { get; }
public string ReasonPhrase { get; }
public bool HasResult { get; }
public T Result { get; }

public T GetResultOrThrow()
{
if (!HasResult)
{
throw new ProtocolException(
$"The HTTP request failed.{Environment.NewLine}" +
$"Request: {Method} {RequestUri}{Environment.NewLine}" +
$"Response: {(int)StatusCode} {ReasonPhrase}",
Method,
RequestUri,
StatusCode,
ReasonPhrase);
}

return Result;
}
}
}
15 changes: 15 additions & 0 deletions src/BaGet.Protocol/Search/AutocompleteContext.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
using Newtonsoft.Json;

namespace BaGet.Protocol
{
public class AutocompleteContext
{
public static readonly AutocompleteContext Default = new AutocompleteContext
{
Vocab = "http://schema.nuget.org/schema#"
};

[JsonProperty("@vocab")]
public string Vocab { get; set; }
}
}
Loading

0 comments on commit 55ce835

Please sign in to comment.