From 436c071ba4ecbe43769b438277d441057d05403e Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Thu, 15 Mar 2018 19:52:18 -0400 Subject: [PATCH] add support for preview GitHub releases (#457) --- src/SMAPI.Common/Models/ModInfoModel.cs | 12 +++-- .../Framework/Clients/GitHub/GitHubClient.cs | 50 ++++++++++++++----- .../Framework/Clients/GitHub/GitRelease.cs | 4 ++ .../Framework/Clients/GitHub/IGitHubClient.cs | 5 +- .../ConfigModels/ApiClientsConfig.cs | 7 ++- .../ModRepositories/ChucklefishRepository.cs | 2 +- .../ModRepositories/GitHubRepository.cs | 19 +++++-- .../ModRepositories/NexusRepository.cs | 2 +- src/SMAPI.Web/Startup.cs | 3 +- src/SMAPI.Web/appsettings.json | 3 +- 10 files changed, 78 insertions(+), 29 deletions(-) diff --git a/src/SMAPI.Common/Models/ModInfoModel.cs b/src/SMAPI.Common/Models/ModInfoModel.cs index 48305cb8..48df235a 100644 --- a/src/SMAPI.Common/Models/ModInfoModel.cs +++ b/src/SMAPI.Common/Models/ModInfoModel.cs @@ -9,9 +9,12 @@ namespace StardewModdingAPI.Common.Models /// The mod name. public string Name { get; set; } - /// The mod's semantic version number. + /// The semantic version for the mod's latest release. public string Version { get; set; } + /// The semantic version for the mod's latest preview release, if available and different from . + public string PreviewVersion { get; set; } + /// The mod's web URL. public string Url { get; set; } @@ -28,16 +31,17 @@ namespace StardewModdingAPI.Common.Models // needed for JSON deserialising } - /// Construct an instance. /// The mod name. - /// The mod's semantic version number. + /// The semantic version for the mod's latest release. + /// The semantic version for the mod's latest preview release, if available and different from . /// The mod's web URL. /// The error message indicating why the mod is invalid (if applicable). - public ModInfoModel(string name, string version, string url, string error = null) + public ModInfoModel(string name, string version, string url, string previewVersion = null, string error = null) { this.Name = name; this.Version = version; + this.PreviewVersion = previewVersion; this.Url = url; this.Error = error; // mainly initialised here for the JSON deserialiser } diff --git a/src/SMAPI.Web/Framework/Clients/GitHub/GitHubClient.cs b/src/SMAPI.Web/Framework/Clients/GitHub/GitHubClient.cs index 0b205660..4abe0737 100644 --- a/src/SMAPI.Web/Framework/Clients/GitHub/GitHubClient.cs +++ b/src/SMAPI.Web/Framework/Clients/GitHub/GitHubClient.cs @@ -1,4 +1,5 @@ using System; +using System.Linq; using System.Net; using System.Threading.Tasks; using Pathoschild.Http.Client; @@ -11,8 +12,11 @@ namespace StardewModdingAPI.Web.Framework.Clients.GitHub /********* ** Properties *********/ - /// The URL for a GitHub releases API query excluding the base URL, where {0} is the repository owner and name. - private readonly string ReleaseUrlFormat; + /// The URL for a GitHub API query for the latest stable release, excluding the base URL, where {0} is the organisation and project name. + private readonly string StableReleaseUrlFormat; + + /// The URL for a GitHub API query for the latest release (including prerelease), excluding the base URL, where {0} is the organisation and project name. + private readonly string AnyReleaseUrlFormat; /// The underlying HTTP client. private readonly IClient Client; @@ -23,14 +27,16 @@ namespace StardewModdingAPI.Web.Framework.Clients.GitHub *********/ /// Construct an instance. /// The base URL for the GitHub API. - /// The URL for a GitHub releases API query excluding the , where {0} is the repository owner and name. + /// The URL for a GitHub API query for the latest stable release, excluding the , where {0} is the organisation and project name. + /// The URL for a GitHub API query for the latest release (including prerelease), excluding the , where {0} is the organisation and project name. /// The user agent for the API client. /// The Accept header value expected by the GitHub API. /// The username with which to authenticate to the GitHub API. /// The password with which to authenticate to the GitHub API. - public GitHubClient(string baseUrl, string releaseUrlFormat, string userAgent, string acceptHeader, string username, string password) + public GitHubClient(string baseUrl, string stableReleaseUrlFormat, string anyReleaseUrlFormat, string userAgent, string acceptHeader, string username, string password) { - this.ReleaseUrlFormat = releaseUrlFormat; + this.StableReleaseUrlFormat = stableReleaseUrlFormat; + this.AnyReleaseUrlFormat = anyReleaseUrlFormat; this.Client = new FluentClient(baseUrl) .SetUserAgent(userAgent) @@ -41,18 +47,23 @@ namespace StardewModdingAPI.Web.Framework.Clients.GitHub /// Get the latest release for a GitHub repository. /// The repository key (like Pathoschild/SMAPI). - /// Returns the latest release if found, else null. - public async Task GetLatestReleaseAsync(string repo) + /// Whether to return a prerelease version if it's latest. + /// Returns the release if found, else null. + public async Task GetLatestReleaseAsync(string repo, bool includePrerelease = false) { - // validate key format - if (!repo.Contains("/") || repo.IndexOf("/", StringComparison.InvariantCultureIgnoreCase) != repo.LastIndexOf("/", StringComparison.InvariantCultureIgnoreCase)) - throw new ArgumentException($"The value '{repo}' isn't a valid GitHub repository key, must be a username and project name like 'Pathoschild/SMAPI'.", nameof(repo)); - - // fetch info + this.AssetKeyFormat(repo); try { + if (includePrerelease) + { + GitRelease[] results = await this.Client + .GetAsync(string.Format(this.AnyReleaseUrlFormat, repo)) + .AsArray(); + return results.FirstOrDefault(); + } + return await this.Client - .GetAsync(string.Format(this.ReleaseUrlFormat, repo)) + .GetAsync(string.Format(this.StableReleaseUrlFormat, repo)) .As(); } catch (ApiException ex) when (ex.Status == HttpStatusCode.NotFound) @@ -66,5 +77,18 @@ namespace StardewModdingAPI.Web.Framework.Clients.GitHub { this.Client?.Dispose(); } + + + /********* + ** Private methods + *********/ + /// Assert that a repository key is formatted correctly. + /// The repository key (like Pathoschild/SMAPI). + /// The repository key is invalid. + private void AssetKeyFormat(string repo) + { + if (repo == null || !repo.Contains("/") || repo.IndexOf("/", StringComparison.InvariantCultureIgnoreCase) != repo.LastIndexOf("/", StringComparison.InvariantCultureIgnoreCase)) + throw new ArgumentException($"The value '{repo}' isn't a valid GitHub repository key, must be a username and project name like 'Pathoschild/SMAPI'.", nameof(repo)); + } } } diff --git a/src/SMAPI.Web/Framework/Clients/GitHub/GitRelease.cs b/src/SMAPI.Web/Framework/Clients/GitHub/GitRelease.cs index b944088d..827374fb 100644 --- a/src/SMAPI.Web/Framework/Clients/GitHub/GitRelease.cs +++ b/src/SMAPI.Web/Framework/Clients/GitHub/GitRelease.cs @@ -19,6 +19,10 @@ namespace StardewModdingAPI.Web.Framework.Clients.GitHub /// The Markdown description for the release. public string Body { get; set; } + /// Whether this is a prerelease version. + [JsonProperty("prerelease")] + public bool IsPrerelease { get; set; } + /// The attached files. public GitAsset[] Assets { get; set; } } diff --git a/src/SMAPI.Web/Framework/Clients/GitHub/IGitHubClient.cs b/src/SMAPI.Web/Framework/Clients/GitHub/IGitHubClient.cs index 6e8eadff..9519c26f 100644 --- a/src/SMAPI.Web/Framework/Clients/GitHub/IGitHubClient.cs +++ b/src/SMAPI.Web/Framework/Clients/GitHub/IGitHubClient.cs @@ -11,7 +11,8 @@ namespace StardewModdingAPI.Web.Framework.Clients.GitHub *********/ /// Get the latest release for a GitHub repository. /// The repository key (like Pathoschild/SMAPI). - /// Returns the latest release if found, else null. - Task GetLatestReleaseAsync(string repo); + /// Whether to return a prerelease version if it's latest. + /// Returns the release if found, else null. + Task GetLatestReleaseAsync(string repo, bool includePrerelease = false); } } diff --git a/src/SMAPI.Web/Framework/ConfigModels/ApiClientsConfig.cs b/src/SMAPI.Web/Framework/ConfigModels/ApiClientsConfig.cs index 61219414..de6c024a 100644 --- a/src/SMAPI.Web/Framework/ConfigModels/ApiClientsConfig.cs +++ b/src/SMAPI.Web/Framework/ConfigModels/ApiClientsConfig.cs @@ -29,8 +29,11 @@ namespace StardewModdingAPI.Web.Framework.ConfigModels /// The base URL for the GitHub API. public string GitHubBaseUrl { get; set; } - /// The URL for a GitHub API latest-release query excluding the , where {0} is the organisation and project name. - public string GitHubReleaseUrlFormat { get; set; } + /// The URL for a GitHub API query for the latest stable release, excluding the , where {0} is the organisation and project name. + public string GitHubStableReleaseUrlFormat { get; set; } + + /// The URL for a GitHub API query for the latest release (including prerelease), excluding the , where {0} is the organisation and project name. + public string GitHubAnyReleaseUrlFormat { get; set; } /// The Accept header value expected by the GitHub API. public string GitHubAcceptHeader { get; set; } diff --git a/src/SMAPI.Web/Framework/ModRepositories/ChucklefishRepository.cs b/src/SMAPI.Web/Framework/ModRepositories/ChucklefishRepository.cs index 266055a6..3e5a4272 100644 --- a/src/SMAPI.Web/Framework/ModRepositories/ChucklefishRepository.cs +++ b/src/SMAPI.Web/Framework/ModRepositories/ChucklefishRepository.cs @@ -43,7 +43,7 @@ namespace StardewModdingAPI.Web.Framework.ModRepositories return new ModInfoModel("Found no mod with this ID."); // create model - return new ModInfoModel(mod.Name, this.NormaliseVersion(mod.Version), mod.Url); + return new ModInfoModel(name: mod.Name, version: this.NormaliseVersion(mod.Version), url: mod.Url); } catch (Exception ex) { diff --git a/src/SMAPI.Web/Framework/ModRepositories/GitHubRepository.cs b/src/SMAPI.Web/Framework/ModRepositories/GitHubRepository.cs index 7bad6127..59eb8cd1 100644 --- a/src/SMAPI.Web/Framework/ModRepositories/GitHubRepository.cs +++ b/src/SMAPI.Web/Framework/ModRepositories/GitHubRepository.cs @@ -38,10 +38,21 @@ namespace StardewModdingAPI.Web.Framework.ModRepositories // fetch info try { - GitRelease release = await this.Client.GetLatestReleaseAsync(id); - return release != null - ? new ModInfoModel(id, this.NormaliseVersion(release.Tag), $"https://github.com/{id}/releases") - : new ModInfoModel("Found no mod with this ID."); + // get latest release + GitRelease latest = await this.Client.GetLatestReleaseAsync(id, includePrerelease: true); + GitRelease preview = null; + if (latest == null) + return new ModInfoModel("Found no mod with this ID."); + + // get latest stable release (if not latest) + if (latest.IsPrerelease) + { + preview = latest; + latest = await this.Client.GetLatestReleaseAsync(id, includePrerelease: false); + } + + // return data + return new ModInfoModel(name: id, version: this.NormaliseVersion(latest?.Tag), previewVersion: this.NormaliseVersion(preview?.Tag), url: $"https://github.com/{id}/releases"); } catch (Exception ex) { diff --git a/src/SMAPI.Web/Framework/ModRepositories/NexusRepository.cs b/src/SMAPI.Web/Framework/ModRepositories/NexusRepository.cs index e1dc0fcc..6411ad4c 100644 --- a/src/SMAPI.Web/Framework/ModRepositories/NexusRepository.cs +++ b/src/SMAPI.Web/Framework/ModRepositories/NexusRepository.cs @@ -43,7 +43,7 @@ namespace StardewModdingAPI.Web.Framework.ModRepositories return new ModInfoModel("Found no mod with this ID."); if (mod.Error != null) return new ModInfoModel(mod.Error); - return new ModInfoModel(mod.Name, this.NormaliseVersion(mod.Version), mod.Url); + return new ModInfoModel(name: mod.Name, version: this.NormaliseVersion(mod.Version), url: mod.Url); } catch (Exception ex) { diff --git a/src/SMAPI.Web/Startup.cs b/src/SMAPI.Web/Startup.cs index d7d4d074..47102e5c 100644 --- a/src/SMAPI.Web/Startup.cs +++ b/src/SMAPI.Web/Startup.cs @@ -74,7 +74,8 @@ namespace StardewModdingAPI.Web services.AddSingleton(new GitHubClient( baseUrl: api.GitHubBaseUrl, - releaseUrlFormat: api.GitHubReleaseUrlFormat, + stableReleaseUrlFormat: api.GitHubStableReleaseUrlFormat, + anyReleaseUrlFormat: api.GitHubAnyReleaseUrlFormat, userAgent: userAgent, acceptHeader: api.GitHubAcceptHeader, username: api.GitHubUsername, diff --git a/src/SMAPI.Web/appsettings.json b/src/SMAPI.Web/appsettings.json index 3cf72ddb..bfe827fa 100644 --- a/src/SMAPI.Web/appsettings.json +++ b/src/SMAPI.Web/appsettings.json @@ -24,7 +24,8 @@ "ChucklefishModPageUrlFormat": "resources/{0}", "GitHubBaseUrl": "https://api.github.com", - "GitHubReleaseUrlFormat": "repos/{0}/releases/latest", + "GitHubStableReleaseUrlFormat": "repos/{0}/releases/latest", + "GitHubAnyReleaseUrlFormat": "repos/{0}/releases?per_page=1", "GitHubAcceptHeader": "application/vnd.github.v3+json", "GitHubUsername": null, // see top note "GitHubPassword": null, // see top note