diff options
17 files changed, 293 insertions, 216 deletions
diff --git a/src/SMAPI.Toolkit/Framework/UpdateData/ModSiteKey.cs b/src/SMAPI.Toolkit/Framework/UpdateData/ModSiteKey.cs index d1dd9049..195b0367 100644 --- a/src/SMAPI.Toolkit/Framework/UpdateData/ModSiteKey.cs +++ b/src/SMAPI.Toolkit/Framework/UpdateData/ModSiteKey.cs @@ -21,7 +21,7 @@ namespace StardewModdingAPI.Toolkit.Framework.UpdateData /// <summary>The Nexus Mods mod repository.</summary> Nexus, - /// <summary>An arbitrary URL for an update manifest file.</summary> + /// <summary>An arbitrary URL to a JSON file containing update data.</summary> UpdateManifest } } diff --git a/src/SMAPI.Toolkit/Framework/UpdateData/UpdateKey.cs b/src/SMAPI.Toolkit/Framework/UpdateData/UpdateKey.cs index 7c2cc73c..3e8064fd 100644 --- a/src/SMAPI.Toolkit/Framework/UpdateData/UpdateKey.cs +++ b/src/SMAPI.Toolkit/Framework/UpdateData/UpdateKey.cs @@ -54,29 +54,6 @@ namespace StardewModdingAPI.Toolkit.Framework.UpdateData public UpdateKey(ModSiteKey site, string? id, string? subkey) : this(UpdateKey.GetString(site, id, subkey), site, id, subkey) { } - /// <summary> - /// Split a string into two at a delimiter. If the delimiter does not appear in the string then the second - /// value of the returned tuple is null. Both returned strings are trimmed of whitespace. - /// </summary> - /// <param name="s">The string to split.</param> - /// <param name="delimiter">The character on which to split.</param> - /// <param name="keepDelimiter"> - /// If <c>true</c> then the second string returned will include the delimiter character - /// (provided that the string is not <c>null</c>) - /// </param> - /// <returns> - /// A pair containing the string consisting of all characters in <paramref name="s"/> before the first - /// occurrence of <paramref name="delimiter"/>, and a string consisting of all characters in <paramref name="s"/> - /// after the first occurrence of <paramref name="delimiter"/> or <c>null</c> if the delimiter does not - /// exist in s. Both strings are trimmed of whitespace. - /// </returns> - private static (string, string?) Bifurcate(string s, char delimiter, bool keepDelimiter = false) { - int pos = s.IndexOf(delimiter); - if (pos < 0) - return (s.Trim(), null); - return (s.Substring(0, pos).Trim(), s.Substring(pos + (keepDelimiter ? 0 : 1)).Trim()); - } - /// <summary>Parse a raw update key.</summary> /// <param name="raw">The raw update key to parse.</param> public static UpdateKey Parse(string? raw) @@ -84,14 +61,14 @@ namespace StardewModdingAPI.Toolkit.Framework.UpdateData if (raw is null) return new UpdateKey(raw, ModSiteKey.Unknown, null, null); // extract site + ID - (string rawSite, string? id) = Bifurcate(raw, ':'); - if (string.IsNullOrWhiteSpace(id)) + (string rawSite, string? id) = UpdateKey.SplitTwoParts(raw, ':'); + if (string.IsNullOrEmpty(id)) id = null; // extract subkey string? subkey = null; if (id != null) - (id, subkey) = Bifurcate(id, '@', true); + (id, subkey) = UpdateKey.SplitTwoParts(id, '@', true); // parse if (!Enum.TryParse(rawSite, true, out ModSiteKey site)) @@ -160,5 +137,23 @@ namespace StardewModdingAPI.Toolkit.Framework.UpdateData { return $"{site}:{id}{subkey}".Trim(); } + + + /********* + ** Private methods + *********/ + /// <summary>Split a string into two parts at a delimiter and trim whitespace.</summary> + /// <param name="str">The string to split.</param> + /// <param name="delimiter">The character on which to split.</param> + /// <param name="keepDelimiter">Whether to include the delimiter in the second string.</param> + /// <returns>Returns a tuple containing the two strings, with the second value <c>null</c> if the delimiter wasn't found.</returns> + private static (string, string?) SplitTwoParts(string str, char delimiter, bool keepDelimiter = false) + { + int splitIndex = str.IndexOf(delimiter); + + return splitIndex >= 0 + ? (str.Substring(0, splitIndex).Trim(), str.Substring(splitIndex + (keepDelimiter ? 0 : 1)).Trim()) + : (str.Trim(), null); + } } } diff --git a/src/SMAPI.Web/Controllers/ModsApiController.cs b/src/SMAPI.Web/Controllers/ModsApiController.cs index 1c34f2af..5fc4987d 100644 --- a/src/SMAPI.Web/Controllers/ModsApiController.cs +++ b/src/SMAPI.Web/Controllers/ModsApiController.cs @@ -64,7 +64,7 @@ namespace StardewModdingAPI.Web.Controllers /// <param name="github">The GitHub API client.</param> /// <param name="modDrop">The ModDrop API client.</param> /// <param name="nexus">The Nexus API client.</param> - /// <param name="updateManifest">The UpdateManifest client.</param> + /// <param name="updateManifest">The API client for arbitrary update manifest URLs.</param> public ModsApiController(IWebHostEnvironment environment, IWikiCacheRepository wikiCache, IModCacheRepository modCache, IOptions<ModUpdateCheckConfig> config, IChucklefishClient chucklefish, ICurseForgeClient curseForge, IGitHubClient github, IModDropClient modDrop, INexusClient nexus, IUpdateManifestClient updateManifest) { this.ModDatabase = new ModToolkit().GetModDatabase(Path.Combine(environment.WebRootPath, "SMAPI.metadata.json")); @@ -163,9 +163,9 @@ namespace StardewModdingAPI.Web.Controllers // if there's only a prerelease version (e.g. from GitHub), don't override the main version ISemanticVersion? curMain = data.Version; - string? curMainUrl = data.MainVersionUrl; ISemanticVersion? curPreview = data.PreviewVersion; - string? curPreviewUrl = data.PreviewVersionUrl; + string? curMainUrl = data.MainModPageUrl; + string? curPreviewUrl = data.PreviewModPageUrl; if (curPreview == null && curMain?.IsPrerelease() == true) { curPreview = curMain; diff --git a/src/SMAPI.Web/Framework/Clients/GenericModDownload.cs b/src/SMAPI.Web/Framework/Clients/GenericModDownload.cs index 5cc03aba..6c9c08ef 100644 --- a/src/SMAPI.Web/Framework/Clients/GenericModDownload.cs +++ b/src/SMAPI.Web/Framework/Clients/GenericModDownload.cs @@ -17,10 +17,9 @@ namespace StardewModdingAPI.Web.Framework.Clients /// <summary>The download's file version.</summary> public string? Version { get; } - /// <summary> - /// The URL for this download, if it has one distinct from the mod page's URL. - /// </summary> - public string? Url { get; } + /// <summary>The mod URL page from which to download this update, if different from the URL of the mod page it was fetched from.</summary> + public string? ModPageUrl { get; } + /********* ** Public methods @@ -29,23 +28,21 @@ namespace StardewModdingAPI.Web.Framework.Clients /// <param name="name">The download's display name.</param> /// <param name="description">The download's description.</param> /// <param name="version">The download's file version.</param> - /// <param name="url">The download's URL (if different from the mod page's URL).</param> - public GenericModDownload(string name, string? description, string? version, string? url = null) + /// <param name="modPageUrl">The mod URL page from which to download this update, if different from the URL of the mod page it was fetched from.</param> + public GenericModDownload(string name, string? description, string? version, string? modPageUrl = null) { this.Name = name; this.Description = description; this.Version = version; - this.Url = url; + this.ModPageUrl = modPageUrl; } - /// <summary> - /// Return <see langword="true"/> if the subkey matches this download. A subkey matches if it appears as - /// a substring in the name or description. - /// </summary> - /// <param name="subkey">the subkey</param> - /// <returns><see langword="true"/> if <paramref name="subkey"/> matches this download, otherwise <see langword="false"/></returns> - public virtual bool MatchesSubkey(string subkey) { - return this.Name.Contains(subkey, StringComparison.OrdinalIgnoreCase) == true + /// <summary>Get whether the subkey matches this download.</summary> + /// <param name="subkey">The update subkey to check.</param> + public virtual bool MatchesSubkey(string subkey) + { + return + this.Name.Contains(subkey, StringComparison.OrdinalIgnoreCase) || this.Description?.Contains(subkey, StringComparison.OrdinalIgnoreCase) == true; } } diff --git a/src/SMAPI.Web/Framework/Clients/GenericModPage.cs b/src/SMAPI.Web/Framework/Clients/GenericModPage.cs index 4c66e1a0..e939f1d8 100644 --- a/src/SMAPI.Web/Framework/Clients/GenericModPage.cs +++ b/src/SMAPI.Web/Framework/Clients/GenericModPage.cs @@ -40,9 +40,10 @@ namespace StardewModdingAPI.Web.Framework.Clients [MemberNotNullWhen(true, nameof(IModPage.Name), nameof(IModPage.Url))] public bool IsValid => this.Status == RemoteModStatus.Ok; - /// <summary>Whether to use strict subkey matching or not.</summary> + /// <summary>Whether this mod page requires string subkey matching, in which case a subkey that isn't found will return no update instead of falling back to one without.</summary> public bool IsSubkeyStrict { get; set; } = false; + /********* ** Public methods *********/ @@ -82,17 +83,17 @@ namespace StardewModdingAPI.Web.Framework.Clients return this; } - /// <summary>Returns the mod page name.</summary> - /// <param name="subkey">ignored</param> - /// <returns>The mod page name.</returns> - public virtual string? GetName(string? subkey) { + /// <summary>Get the mod name for an update subkey, if different from the mod page name.</summary> + /// <param name="subkey">The update subkey.</param> + public virtual string? GetName(string? subkey) + { return this.Name; } - /// <summary>Returns the mod page URL.</summary> - /// <param name="subkey">ignored</param> - /// <returns>The mod page URL.</returns> - public virtual string? GetUrl(string? subkey) { + /// <summary>Get the mod page URL for an update subkey, if different from the mod page it was fetched from.</summary> + /// <param name="subkey">The update subkey.</param> + public virtual string? GetUrl(string? subkey) + { return this.Url; } } diff --git a/src/SMAPI.Web/Framework/Clients/UpdateManifest/IUpdateManifestClient.cs b/src/SMAPI.Web/Framework/Clients/UpdateManifest/IUpdateManifestClient.cs index 36d018c7..dd9f5811 100644 --- a/src/SMAPI.Web/Framework/Clients/UpdateManifest/IUpdateManifestClient.cs +++ b/src/SMAPI.Web/Framework/Clients/UpdateManifest/IUpdateManifestClient.cs @@ -3,7 +3,6 @@ using System; namespace StardewModdingAPI.Web.Framework.Clients.UpdateManifest { - /// <summary>An HTTP client for fetching an update manifest from an arbitrary URL.</summary> + /// <summary>An API client for fetching update metadata from an arbitrary JSON URL.</summary> internal interface IUpdateManifestClient : IModSiteClient, IDisposable { } } - diff --git a/src/SMAPI.Web/Framework/Clients/UpdateManifest/TextAsJsonMediaTypeFormatter.cs b/src/SMAPI.Web/Framework/Clients/UpdateManifest/TextAsJsonMediaTypeFormatter.cs index 48e3c294..02722cb1 100644 --- a/src/SMAPI.Web/Framework/Clients/UpdateManifest/TextAsJsonMediaTypeFormatter.cs +++ b/src/SMAPI.Web/Framework/Clients/UpdateManifest/TextAsJsonMediaTypeFormatter.cs @@ -1,14 +1,18 @@ -// Copyright 2022 Jamie Taylor +// Copyright 2022 Jamie Taylor using System.Net.Http.Formatting; using System.Net.Http.Headers; -namespace StardewModdingAPI.Web.Framework.Clients.UpdateManifest { - /// <summary> - /// A <see cref="JsonMediaTypeFormatter"/> that can parse from content of type <c>text/plain</c>. - /// </summary> - internal class TextAsJsonMediaTypeFormatter : JsonMediaTypeFormatter { +namespace StardewModdingAPI.Web.Framework.Clients.UpdateManifest +{ + /// <summary>A <see cref="JsonMediaTypeFormatter"/> that can parse from content of type <c>text/plain</c>.</summary> + internal class TextAsJsonMediaTypeFormatter : JsonMediaTypeFormatter + { + /********* + ** Public methods + *********/ /// <summary>Construct a new <see cref="JsonMediaTypeFormatter"/></summary> - public TextAsJsonMediaTypeFormatter() { + public TextAsJsonMediaTypeFormatter() + { this.SupportedMediaTypes.Add(new MediaTypeHeaderValue("text/plain")); } } diff --git a/src/SMAPI.Web/Framework/Clients/UpdateManifest/UpdateManifestClient.cs b/src/SMAPI.Web/Framework/Clients/UpdateManifest/UpdateManifestClient.cs index d7cf4945..88a5c2f6 100644 --- a/src/SMAPI.Web/Framework/Clients/UpdateManifest/UpdateManifestClient.cs +++ b/src/SMAPI.Web/Framework/Clients/UpdateManifest/UpdateManifestClient.cs @@ -1,19 +1,21 @@ // Copyright 2022 Jamie Taylor -using System; +using System.Net; +using System.Threading.Tasks; using Pathoschild.Http.Client; using StardewModdingAPI.Toolkit.Framework.UpdateData; -using System.Threading.Tasks; -using System.Net; -namespace StardewModdingAPI.Web.Framework.Clients.UpdateManifest { - /// <summary>An HTTP client for fetching an update manifest from an arbitrary URL.</summary> - internal class UpdateManifestClient : IUpdateManifestClient { +namespace StardewModdingAPI.Web.Framework.Clients.UpdateManifest +{ + /// <summary>An API client for fetching update metadata from an arbitrary JSON URL.</summary> + internal class UpdateManifestClient : IUpdateManifestClient + { /********* ** Fields *********/ /// <summary>The underlying HTTP client.</summary> private readonly IClient Client; + /********* ** Accessors *********/ @@ -26,30 +28,35 @@ namespace StardewModdingAPI.Web.Framework.Clients.UpdateManifest { *********/ /// <summary>Construct an instance.</summary> /// <param name="userAgent">The user agent for the API client.</param> - public UpdateManifestClient(string userAgent) { + public UpdateManifestClient(string userAgent) + { this.Client = new FluentClient() .SetUserAgent(userAgent); this.Client.Formatters.Add(new TextAsJsonMediaTypeFormatter()); } /// <summary>Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources.</summary> - public void Dispose() { + public void Dispose() + { this.Client.Dispose(); } /// <inheritdoc/> - public async Task<IModPage?> GetModData(string id) { + public async Task<IModPage?> GetModData(string id) + { UpdateManifestModel? manifest; - try { + try + { manifest = await this.Client.GetAsync(id).As<UpdateManifestModel?>(); - } catch (ApiException ex) when (ex.Status == HttpStatusCode.NotFound) { - return new GenericModPage(this.SiteKey, id).SetError(RemoteModStatus.DoesNotExist, $"No update manifest found at {id}"); } - if (manifest is null) { - return new GenericModPage(this.SiteKey, id).SetError(RemoteModStatus.DoesNotExist, $"Error parsing manifest at {id}"); + catch (ApiException ex) when (ex.Status == HttpStatusCode.NotFound) + { + return new GenericModPage(this.SiteKey, id).SetError(RemoteModStatus.DoesNotExist, $"No update manifest found at {id}"); } - return new UpdateManifestModPage(id, manifest); + return manifest is not null + ? new UpdateManifestModPage(id, manifest) + : new GenericModPage(this.SiteKey, id).SetError(RemoteModStatus.DoesNotExist, $"The update manifest at {id} has an invalid format"); } } } diff --git a/src/SMAPI.Web/Framework/Clients/UpdateManifest/UpdateManifestModDownload.cs b/src/SMAPI.Web/Framework/Clients/UpdateManifest/UpdateManifestModDownload.cs index 117ae15c..0a6d4736 100644 --- a/src/SMAPI.Web/Framework/Clients/UpdateManifest/UpdateManifestModDownload.cs +++ b/src/SMAPI.Web/Framework/Clients/UpdateManifest/UpdateManifestModDownload.cs @@ -1,27 +1,35 @@ // Copyright 2022 Jamie Taylor -using System; -namespace StardewModdingAPI.Web.Framework.Clients.UpdateManifest { +namespace StardewModdingAPI.Web.Framework.Clients.UpdateManifest +{ /// <summary>Metadata about a mod download in an update manifest file.</summary> - internal class UpdateManifestModDownload : GenericModDownload { - /// <summary>The subkey for this mod download</summary> - private readonly string subkey; + internal class UpdateManifestModDownload : GenericModDownload + { + /********* + ** Fields + *********/ + /// <summary>The update subkey for this mod download.</summary> + private readonly string Subkey; + + + /********* + ** Public methods + *********/ /// <summary>Construct an instance.</summary> - /// <param name="subkey">The subkey for this download.</param> + /// <param name="fieldName">The field name for this mod download in the manifest.</param> /// <param name="name">The mod name for this download.</param> /// <param name="version">The download's version.</param> /// <param name="url">The download's URL.</param> - public UpdateManifestModDownload(string subkey, string name, string? version, string? url) : base(name, null, version, url) { - this.subkey = subkey; + public UpdateManifestModDownload(string fieldName, string name, string? version, string? url) + : base(name, null, version, url) + { + this.Subkey = fieldName; } - /// <summary> - /// Returns <see langword="true"/> iff the given subkey is the same as the subkey for this download. - /// </summary> - /// <param name="subkey">The subkey to match</param> - /// <returns><see langword="true"/> if <paramref name="subkey"/> is the same as the subkey for this download, <see langword="false"/> otherwise.</returns> - public override bool MatchesSubkey(string subkey) { - return this.subkey == subkey; + /// <summary>Get whether the subkey matches this download.</summary> + /// <param name="subkey">The update subkey to check.</param> + public override bool MatchesSubkey(string subkey) + { + return subkey == this.Subkey; } } } - diff --git a/src/SMAPI.Web/Framework/Clients/UpdateManifest/UpdateManifestModModel.cs b/src/SMAPI.Web/Framework/Clients/UpdateManifest/UpdateManifestModModel.cs index 4ec9c03d..92642321 100644 --- a/src/SMAPI.Web/Framework/Clients/UpdateManifest/UpdateManifestModModel.cs +++ b/src/SMAPI.Web/Framework/Clients/UpdateManifest/UpdateManifestModModel.cs @@ -1,26 +1,34 @@ // Copyright 2022 Jamie Taylor -using System; -namespace StardewModdingAPI.Web.Framework.Clients.UpdateManifest { - /// <summary>Data model for a mod in an update manifest.</summary> - internal class UpdateManifestModModel { +namespace StardewModdingAPI.Web.Framework.Clients.UpdateManifest +{ + /// <summary>The data model for a mod in an update manifest file.</summary> + internal class UpdateManifestModModel + { + /********* + ** Accessors + *********/ /// <summary>The mod's name.</summary> - public string Name { get; } + public string? Name { get; } - /// <summary>The mod's URL.</summary> + /// <summary>The mod page URL from which to download updates.</summary> public string? Url { get; } - /// <summary>The versions for this mod.</summary> - public UpdateManifestVersionModel[] Versions { get; } + /// <summary>The available versions for this mod.</summary> + public UpdateManifestVersionModel[]? Versions { get; } + + /********* + ** Public methods + *********/ /// <summary>Construct an instance.</summary> /// <param name="name">The mod's name.</param> - /// <param name="url">The mod's URL.</param> - /// <param name="versions">The versions for this mod.</param> - public UpdateManifestModModel(string name, string? url, UpdateManifestVersionModel[] versions) { + /// <param name="url">The mod page URL from which to download updates.</param> + /// <param name="versions">The available versions for this mod.</param> + public UpdateManifestModModel(string? name, string? url, UpdateManifestVersionModel[]? versions) + { this.Name = name; this.Url = url; this.Versions = versions; } } } - diff --git a/src/SMAPI.Web/Framework/Clients/UpdateManifest/UpdateManifestModPage.cs b/src/SMAPI.Web/Framework/Clients/UpdateManifest/UpdateManifestModPage.cs index 109175b5..bbc7b5da 100644 --- a/src/SMAPI.Web/Framework/Clients/UpdateManifest/UpdateManifestModPage.cs +++ b/src/SMAPI.Web/Framework/Clients/UpdateManifest/UpdateManifestModPage.cs @@ -1,58 +1,74 @@ -// Copyright 2022 Jamie Taylor -using System; +// Copyright 2022 Jamie Taylor using System.Collections.Generic; using System.Linq; using StardewModdingAPI.Toolkit.Framework.UpdateData; -namespace StardewModdingAPI.Web.Framework.Clients.UpdateManifest { +namespace StardewModdingAPI.Web.Framework.Clients.UpdateManifest +{ /// <summary>Metadata about an update manifest "page".</summary> - internal class UpdateManifestModPage : GenericModPage { - /// <summary>The update manifest model.</summary> - private UpdateManifestModel manifest; - - /// <summary>Constuct an instance.</summary> - /// <param name="id">The "id" (i.e., URL) of this update manifest.</param> - /// <param name="manifest">The manifest object model.</param> - public UpdateManifestModPage(string id, UpdateManifestModel manifest) : base(ModSiteKey.UpdateManifest, id) { + internal class UpdateManifestModPage : GenericModPage + { + /********* + ** Fields + *********/ + /// <summary>The mods from the update manifest.</summary> + private readonly IDictionary<string, UpdateManifestModModel> Mods; + + + /********* + ** Public methods + *********/ + /// <summary>Construct an instance.</summary> + /// <param name="url">The URL of the update manifest file.</param> + /// <param name="manifest">The parsed update manifest.</param> + public UpdateManifestModPage(string url, UpdateManifestModel manifest) + : base(ModSiteKey.UpdateManifest, url) + { this.IsSubkeyStrict = true; - this.manifest = manifest; - this.SetInfo(name: id, url: id, version: null, downloads: TranslateDownloads(manifest).ToArray()); + this.Mods = manifest.Mods ?? new Dictionary<string, UpdateManifestModModel>(); + this.SetInfo(name: url, url: url, version: null, downloads: this.ParseDownloads(manifest.Mods).ToArray()); } /// <summary>Return the mod name for the given subkey, if it exists in this update manifest.</summary> /// <param name="subkey">The subkey.</param> /// <returns>The mod name for the given subkey, or <see langword="null"/> if this manifest does not contain the given subkey.</returns> - public override string? GetName(string? subkey) { - if (subkey is null) - return null; - this.manifest.Subkeys.TryGetValue(subkey, out UpdateManifestModModel? modModel); - return modModel?.Name; + public override string? GetName(string? subkey) + { + return subkey is not null && this.Mods.TryGetValue(subkey, out UpdateManifestModModel? modModel) + ? modModel.Name + : null; } /// <summary>Return the mod URL for the given subkey, if it exists in this update manifest.</summary> /// <param name="subkey">The subkey.</param> /// <returns>The mod URL for the given subkey, or <see langword="null"/> if this manifest does not contain the given subkey.</returns> - public override string? GetUrl(string? subkey) { - if (subkey is null) - return null; - this.manifest.Subkeys.TryGetValue(subkey, out UpdateManifestModModel? modModel); - return modModel?.Url; + public override string? GetUrl(string? subkey) + { + return subkey is not null && this.Mods.TryGetValue(subkey, out UpdateManifestModModel? modModel) + ? modModel.Url + : null; } /********* ** Private methods *********/ - /// <summary>Translate the downloads from the manifest's object model into <see cref="IModDownload"/> objects.</summary> - /// <param name="manifest">The manifest object model.</param> - /// <returns>An <see cref="IModDownload"/> for each <see cref="UpdateManifestVersionModel"/> in the manifest.</returns> - private static IEnumerable<IModDownload> TranslateDownloads(UpdateManifestModel manifest) { - foreach (var entry in manifest.Subkeys) { - foreach (var version in entry.Value.Versions) { - yield return new UpdateManifestModDownload(entry.Key, entry.Value.Name, version.Version, version.DownloadFileUrl ?? version.DownloadPageUrl); - } + /// <summary>Convert the raw download info from an update manifest to <see cref="IModDownload"/>.</summary> + /// <param name="mods">The mods from the update manifest.</param> + private IEnumerable<IModDownload> ParseDownloads(IDictionary<string, UpdateManifestModModel>? mods) + { + if (mods is null) + yield break; + + foreach ((string modKey, UpdateManifestModModel mod) in mods) + { + if (mod.Versions is null) + continue; + + foreach (UpdateManifestVersionModel version in mod.Versions) + yield return new UpdateManifestModDownload(modKey, mod.Name ?? modKey, version.Version, version.DownloadFileUrl ?? version.DownloadPageUrl); } } } -}
\ No newline at end of file +} diff --git a/src/SMAPI.Web/Framework/Clients/UpdateManifest/UpdateManifestModel.cs b/src/SMAPI.Web/Framework/Clients/UpdateManifest/UpdateManifestModel.cs index 03f89726..ad618022 100644 --- a/src/SMAPI.Web/Framework/Clients/UpdateManifest/UpdateManifestModel.cs +++ b/src/SMAPI.Web/Framework/Clients/UpdateManifest/UpdateManifestModel.cs @@ -1,23 +1,31 @@ // Copyright 2022 Jamie Taylor -using System; using System.Collections.Generic; -namespace StardewModdingAPI.Web.Framework.Clients.UpdateManifest { - /// <summary>Data model for an update manifest.</summary> - internal class UpdateManifestModel { +namespace StardewModdingAPI.Web.Framework.Clients.UpdateManifest +{ + /// <summary>The data model for an update manifest file.</summary> + internal class UpdateManifestModel + { + /********* + ** Accessors + *********/ /// <summary>The manifest format version.</summary> - public string ManifestVersion { get; } + public string? ManifestVersion { get; } - /// <summary>The subkeys in this update manifest.</summary> - public IDictionary<string, UpdateManifestModModel> Subkeys { get; } + /// <summary>The mod info in this update manifest.</summary> + public IDictionary<string, UpdateManifestModModel>? Mods { get; } + + /********* + ** Public methods + *********/ /// <summary>Construct an instance.</summary> /// <param name="manifestVersion">The manifest format version.</param> - /// <param name="subkeys">The subkeys in this update manifest.</param> - public UpdateManifestModel(string manifestVersion, IDictionary<string, UpdateManifestModModel> subkeys) { + /// <param name="mods">The mod info in this update manifest.</param> + public UpdateManifestModel(string manifestVersion, IDictionary<string, UpdateManifestModModel> mods) + { this.ManifestVersion = manifestVersion; - this.Subkeys = subkeys; + this.Mods = mods; } } } - diff --git a/src/SMAPI.Web/Framework/Clients/UpdateManifest/UpdateManifestVersionModel.cs b/src/SMAPI.Web/Framework/Clients/UpdateManifest/UpdateManifestVersionModel.cs index 55b6db61..90e054d8 100644 --- a/src/SMAPI.Web/Framework/Clients/UpdateManifest/UpdateManifestVersionModel.cs +++ b/src/SMAPI.Web/Framework/Clients/UpdateManifest/UpdateManifestVersionModel.cs @@ -1,10 +1,14 @@ // Copyright 2022 Jamie Taylor -using System; -namespace StardewModdingAPI.Web.Framework.Clients.UpdateManifest { +namespace StardewModdingAPI.Web.Framework.Clients.UpdateManifest +{ /// <summary>Data model for a Version in an update manifest.</summary> - internal class UpdateManifestVersionModel { - /// <summary>The semantic version string.</summary> - public string Version { get; } + internal class UpdateManifestVersionModel + { + /********* + ** Accessors + *********/ + /// <summary>The mod's semantic version.</summary> + public string? Version { get; } /// <summary>The URL for this version's download page (if any).</summary> public string? DownloadPageUrl { get; } @@ -12,15 +16,19 @@ namespace StardewModdingAPI.Web.Framework.Clients.UpdateManifest { /// <summary>The URL for this version's direct file download (if any).</summary> public string? DownloadFileUrl { get; } + + /********* + ** Public methods + *********/ /// <summary>Construct an instance.</summary> - /// <param name="version">The semantic version string.</param> - /// <param name="downloadPageUrl">This version's download page URL (if any).</param> - /// <param name="downloadFileUrl">This version's direct file download URL (if any).</param> - public UpdateManifestVersionModel(string version, string? downloadPageUrl, string? downloadFileUrl) { + /// <param name="version">The mod's semantic version.</param> + /// <param name="downloadPageUrl">This version's download page URL, if any.</param> + /// <param name="downloadFileUrl">This version's direct file download URL, if any.</param> + public UpdateManifestVersionModel(string version, string? downloadPageUrl, string? downloadFileUrl) + { this.Version = version; this.DownloadPageUrl = downloadPageUrl; this.DownloadFileUrl = downloadFileUrl; } } } - diff --git a/src/SMAPI.Web/Framework/IModDownload.cs b/src/SMAPI.Web/Framework/IModDownload.cs index b0e9a664..8cb82989 100644 --- a/src/SMAPI.Web/Framework/IModDownload.cs +++ b/src/SMAPI.Web/Framework/IModDownload.cs @@ -15,12 +15,15 @@ namespace StardewModdingAPI.Web.Framework /// <summary>The download's file version.</summary> string? Version { get; } - /// <summary>This download's URL (if it has a URL that is different from the containing mod page's URL).</summary> - string? Url { get; } + /// <summary>The mod URL page from which to download this update, if different from the URL of the mod page it was fetched from.</summary> + string? ModPageUrl { get; } - /// <summary>Return <see langword="true"/> iff the subkey matches this download</summary> - /// <param name="subkey">the subkey</param> - /// <returns><see langword="true"/> if <paramref name="subkey"/> matches this download, otherwise <see langword="false"/></returns> + + /********* + ** Methods + *********/ + /// <summary>Get whether the subkey matches this download.</summary> + /// <param name="subkey">The update subkey to check.</param> bool MatchesSubkey(string subkey); } } diff --git a/src/SMAPI.Web/Framework/IModPage.cs b/src/SMAPI.Web/Framework/IModPage.cs index 3fc8bb81..ef1513eb 100644 --- a/src/SMAPI.Web/Framework/IModPage.cs +++ b/src/SMAPI.Web/Framework/IModPage.cs @@ -39,25 +39,19 @@ namespace StardewModdingAPI.Web.Framework [MemberNotNullWhen(false, nameof(IModPage.Error))] bool IsValid { get; } - /// <summary> - /// Does this page use strict subkey matching. Pages that use string subkey matching do not fall back - /// to searching for versions without a subkey if there are no versions found when given a subkey. - /// Additionally, the leading <c>@</c> is stripped from the subkey value before searching for matches. - /// </summary> + /// <summary>Whether this mod page requires string subkey matching, in which case a subkey that isn't found will return no update instead of falling back to one without. Additionally, the leading <c>@</c> is stripped from the subkey value before searching for matches.</summary> bool IsSubkeyStrict { get; } + /********* ** Methods *********/ - - /// <summary>Get the mod name associated with the given subkey, if any.</summary> - /// <param name="subkey">The subkey.</param> - /// <returns>The mod name associated with the given subkey (if any)</returns> + /// <summary>Get the mod name for an update subkey, if different from the mod page name.</summary> + /// <param name="subkey">The update subkey.</param> string? GetName(string? subkey); - /// <summary>Get the URL for the mod associated with the given subkey, if any.</summary> - /// <param name="subkey">The subkey.</param> - /// <returns>The URL for the mod associated with the given subkey (if any)</returns> + /// <summary>Get the mod page URL for an update subkey, if different from the mod page it was fetched from.</summary> + /// <param name="subkey">The update subkey.</param> string? GetUrl(string? subkey); /// <summary>Set the fetched mod info.</summary> diff --git a/src/SMAPI.Web/Framework/ModInfoModel.cs b/src/SMAPI.Web/Framework/ModInfoModel.cs index 17415589..502c0827 100644 --- a/src/SMAPI.Web/Framework/ModInfoModel.cs +++ b/src/SMAPI.Web/Framework/ModInfoModel.cs @@ -27,11 +27,12 @@ namespace StardewModdingAPI.Web.Framework /// <summary>The error message indicating why the mod is invalid (if applicable).</summary> public string? Error { get; private set; } - /// <summary>The URL associated with the mod's latest version (if distinct from the mod page's URL).</summary> - public string? MainVersionUrl { get; private set; } + /// <summary>The mod page URL from which <see cref="Version"/> can be downloaded, if different from the <see cref="Url"/>.</summary> + public string? MainModPageUrl { get; private set; } + + /// <summary>The mod page URL from which <see cref="PreviewVersion"/> can be downloaded, if different from the <see cref="Url"/>.</summary> + public string? PreviewModPageUrl { get; private set; } - /// <summary>The URL associated with the mod's <see cref="PreviewVersion"/> (if distinct from the mod page's URL).</summary> - public string? PreviewVersionUrl { get; private set; } /********* ** Public methods @@ -51,7 +52,8 @@ namespace StardewModdingAPI.Web.Framework { this .SetBasicInfo(name, url) - .SetVersions(version!, previewVersion) + .SetMainVersion(version!) + .SetPreviewVersion(previewVersion) .SetError(status, error!); } @@ -67,18 +69,25 @@ namespace StardewModdingAPI.Web.Framework return this; } - /// <summary>Set the mod version info.</summary> - /// <param name="version">The semantic version for the mod's latest release.</param> - /// <param name="previewVersion">The semantic version for the mod's latest preview release, if available and different from <see cref="Version"/>.</param> - /// <param name="mainVersionUrl">The URL associated with <paramref name="version"/>, if different from the mod page's URL.</param> - /// <param name="previewVersionUrl">The URL associated with <paramref name="previewVersion"/>, if different from the mod page's URL.</param> + /// <summary>Set the mod's main version info.</summary> + /// <param name="version">The semantic version for the mod's latest stable release.</param> + /// <param name="modPageUrl">The mod page URL from which <paramref name="version"/> can be downloaded, if different from the <see cref="Url"/>.</param> [MemberNotNull(nameof(ModInfoModel.Version))] - public ModInfoModel SetVersions(ISemanticVersion version, ISemanticVersion? previewVersion = null, string? mainVersionUrl = null, string? previewVersionUrl = null) + public ModInfoModel SetMainVersion(ISemanticVersion version, string? modPageUrl = null) { this.Version = version; - this.PreviewVersion = previewVersion; - this.MainVersionUrl = mainVersionUrl; - this.PreviewVersionUrl = previewVersionUrl; + this.MainModPageUrl = modPageUrl; + + return this; + } + + /// <summary>Set the mod's preview version info.</summary> + /// <param name="version">The semantic version for the mod's latest preview release.</param> + /// <param name="modPageUrl">The mod page URL from which <paramref name="version"/> can be downloaded, if different from the <see cref="Url"/>.</param> + public ModInfoModel SetPreviewVersion(ISemanticVersion? version, string? modPageUrl = null) + { + this.PreviewVersion = version; + this.PreviewModPageUrl = modPageUrl; return this; } diff --git a/src/SMAPI.Web/Framework/ModSiteManager.cs b/src/SMAPI.Web/Framework/ModSiteManager.cs index 5581d799..350159a3 100644 --- a/src/SMAPI.Web/Framework/ModSiteManager.cs +++ b/src/SMAPI.Web/Framework/ModSiteManager.cs @@ -66,25 +66,34 @@ namespace StardewModdingAPI.Web.Framework { // get base model ModInfoModel model = new(); - if (!page.IsValid) { + if (!page.IsValid) + { model.SetError(page.Status, page.Error); return model; } - if (page.IsSubkeyStrict && subkey is not null) { - if (subkey.Length > 0 && subkey[0] == '@') { + // trim subkey in strict mode + if (page.IsSubkeyStrict && subkey is not null) + { + if (subkey.StartsWith('@')) subkey = subkey.Substring(1); - } } // fetch versions - bool hasVersions = this.TryGetLatestVersions(page, subkey, allowNonStandardVersions, mapRemoteVersions, out ISemanticVersion? mainVersion, out ISemanticVersion? previewVersion, out string? mainVersionUrl, out string? previewVersionUrl); + bool hasVersions = this.TryGetLatestVersions(page, subkey, allowNonStandardVersions, mapRemoteVersions, out ISemanticVersion? mainVersion, out ISemanticVersion? previewVersion, out string? mainModPageUrl, out string? previewModPageUrl); if (!hasVersions) return model.SetError(RemoteModStatus.InvalidData, $"The {page.Site} mod with ID '{page.Id}{subkey ?? ""}' has no valid versions."); - model.SetBasicInfo(page.GetName(subkey) ?? page.Name, page.GetUrl(subkey) ?? page.Url); + // apply mod page info + model.SetBasicInfo( + name: page.GetName(subkey) ?? page.Name, + url: page.GetUrl(subkey) ?? page.Url + ); + // return info - return model.SetVersions(mainVersion!, previewVersion, mainVersionUrl, previewVersionUrl); + return model + .SetMainVersion(mainVersion!, mainModPageUrl) + .SetPreviewVersion(previewVersion, previewModPageUrl); } /// <summary>Get a semantic local version for update checks.</summary> @@ -115,12 +124,14 @@ namespace StardewModdingAPI.Web.Framework /// <param name="mapRemoteVersions">The changes to apply to remote versions for update checks.</param> /// <param name="main">The main mod version.</param> /// <param name="preview">The latest prerelease version, if newer than <paramref name="main"/>.</param> - private bool TryGetLatestVersions(IModPage? mod, string? subkey, bool allowNonStandardVersions, ChangeDescriptor? mapRemoteVersions, [NotNullWhen(true)] out ISemanticVersion? main, out ISemanticVersion? preview, out string? mainVersionUrl, out string? previewVersionUrl) + /// <param name="mainModPageUrl">The mod page URL from which <paramref name="main"/> can be downloaded, if different from the <see cref="mod"/>'s URL.</param> + /// <param name="previewModPageUrl">The mod page URL from which <paramref name="preview"/> can be downloaded, if different from the <see cref="mod"/>'s URL.</param> + private bool TryGetLatestVersions(IModPage? mod, string? subkey, bool allowNonStandardVersions, ChangeDescriptor? mapRemoteVersions, [NotNullWhen(true)] out ISemanticVersion? main, out ISemanticVersion? preview, out string? mainModPageUrl, out string? previewModPageUrl) { main = null; preview = null; - mainVersionUrl = null; - previewVersionUrl = null; + mainModPageUrl = null; + previewModPageUrl = null; // parse all versions from the mod page IEnumerable<(IModDownload? download, ISemanticVersion? version)> GetAllVersions() @@ -152,12 +163,12 @@ namespace StardewModdingAPI.Web.Framework .ToArray(); // get main + preview versions - void TryGetVersions([NotNullWhen(true)] out ISemanticVersion? mainVersion, out ISemanticVersion? previewVersion, out string? mainVersionUrl, out string? previewVersionUrl, Func<(IModDownload? download, ISemanticVersion? version), bool>? filter = null) + void TryGetVersions([NotNullWhen(true)] out ISemanticVersion? mainVersion, out ISemanticVersion? previewVersion, out string? mainUrl, out string? previewUrl, Func<(IModDownload? download, ISemanticVersion? version), bool>? filter = null) { mainVersion = null; previewVersion = null; - mainVersionUrl = null; - previewVersionUrl = null; + mainUrl = null; + previewUrl = null; // get latest main + preview version foreach ((IModDownload? download, ISemanticVersion? version) entry in versions) @@ -165,14 +176,18 @@ namespace StardewModdingAPI.Web.Framework if (entry.version is null || filter?.Invoke(entry) == false) continue; - if (entry.version.IsPrerelease()) { - if (previewVersion is null) { + if (entry.version.IsPrerelease()) + { + if (previewVersion is null) + { previewVersion = entry.version; - previewVersionUrl = entry.download?.Url; + previewUrl = entry.download?.ModPageUrl; } - } else { + } + else + { mainVersion = entry.version; - mainVersionUrl = entry.download?.Url; + mainUrl = entry.download?.ModPageUrl; break; // any others will be older since entries are sorted by version } } @@ -180,26 +195,31 @@ namespace StardewModdingAPI.Web.Framework // normalize values if (previewVersion is not null) { - if (mainVersion is null) { + if (mainVersion is null) + { // if every version is prerelease, latest one is the main version mainVersion = previewVersion; - mainVersionUrl = previewVersionUrl; + mainUrl = previewUrl; } - if (!previewVersion.IsNewerThan(mainVersion)) { + if (!previewVersion.IsNewerThan(mainVersion)) + { previewVersion = null; - previewVersionUrl = null; + previewUrl = null; } } } - if (subkey is not null) { - TryGetVersions(out main, out preview, out mainVersionUrl, out previewVersionUrl, entry => entry.download?.MatchesSubkey(subkey) == true); + // get versions for subkey + if (subkey is not null) + { + TryGetVersions(out main, out preview, out mainModPageUrl, out previewModPageUrl, filter: entry => entry.download?.MatchesSubkey(subkey) == true); if (mod?.IsSubkeyStrict == true) return main != null; } - if (main is null) - TryGetVersions(out main, out preview, out mainVersionUrl, out previewVersionUrl); + // fallback to non-subkey versions + if (main is null) + TryGetVersions(out main, out preview, out mainModPageUrl, out previewModPageUrl); return main != null; } |