From 0b48c1748b354458059c7607415288de072b01e9 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Tue, 12 Apr 2022 19:15:39 -0400 Subject: enable nullable annotations in the web project & related code (#837) --- .../Framework/AllowLargePostsAttribute.cs | 4 +- src/SMAPI.Web/Framework/Caching/Cached.cs | 11 ++-- .../Framework/Caching/Mods/IModCacheRepository.cs | 5 +- .../Caching/Mods/ModCacheMemoryRepository.cs | 5 +- .../Framework/Caching/Wiki/IWikiCacheRepository.cs | 9 ++-- .../Caching/Wiki/WikiCacheMemoryRepository.cs | 11 ++-- .../Framework/Caching/Wiki/WikiMetadata.cs | 11 ++-- .../Clients/Chucklefish/ChucklefishClient.cs | 12 ++--- .../Clients/CurseForge/CurseForgeClient.cs | 14 +++-- .../CurseForge/ResponseModels/ModFileModel.cs | 22 ++++++-- .../Clients/CurseForge/ResponseModels/ModModel.cs | 30 ++++++++--- .../Framework/Clients/GenericModDownload.cs | 13 ++--- src/SMAPI.Web/Framework/Clients/GenericModPage.cs | 23 ++++---- src/SMAPI.Web/Framework/Clients/GitHub/GitAsset.cs | 26 +++++++-- .../Framework/Clients/GitHub/GitHubClient.cs | 30 +++++------ .../Framework/Clients/GitHub/GitLicense.cs | 26 +++++++-- .../Framework/Clients/GitHub/GitRelease.cs | 36 ++++++++++--- src/SMAPI.Web/Framework/Clients/GitHub/GitRepo.cs | 26 +++++++-- .../Framework/Clients/GitHub/IGitHubClient.cs | 6 +-- src/SMAPI.Web/Framework/Clients/IModSiteClient.cs | 4 +- .../Framework/Clients/ModDrop/ModDropClient.cs | 20 +++---- .../ModDrop/ResponseModels/FileDataModel.cs | 42 +++++++++++---- .../Clients/ModDrop/ResponseModels/ModDataModel.cs | 24 +++++++-- .../Clients/ModDrop/ResponseModels/ModListModel.cs | 7 +-- .../Clients/ModDrop/ResponseModels/ModModel.cs | 22 ++++++-- .../Framework/Clients/Nexus/NexusClient.cs | 63 +++++++++++----------- .../Clients/Nexus/ResponseModels/NexusMod.cs | 43 ++++++++++++--- .../Framework/Clients/Pastebin/IPastebinClient.cs | 2 - .../Framework/Clients/Pastebin/PasteInfo.cs | 28 ++++++++-- .../Framework/Clients/Pastebin/PastebinClient.cs | 14 +++-- src/SMAPI.Web/Framework/Compression/GzipHelper.cs | 9 ++-- src/SMAPI.Web/Framework/Compression/IGzipHelper.cs | 5 +- .../Framework/ConfigModels/ApiClientsConfig.cs | 36 ++++++------- .../Framework/ConfigModels/ModOverrideConfig.cs | 6 +-- .../Framework/ConfigModels/ModUpdateCheckConfig.cs | 10 ++-- src/SMAPI.Web/Framework/ConfigModels/SiteConfig.cs | 6 +-- .../Framework/ConfigModels/SmapiInfoConfig.cs | 8 +-- src/SMAPI.Web/Framework/Extensions.cs | 8 ++- src/SMAPI.Web/Framework/IModDownload.cs | 9 ++-- src/SMAPI.Web/Framework/IModPage.cs | 18 ++++--- .../Framework/InternalControllerFeatureProvider.cs | 2 - .../Framework/JobDashboardAuthorizationFilter.cs | 2 - src/SMAPI.Web/Framework/ModInfoModel.cs | 33 +++++++----- src/SMAPI.Web/Framework/ModSiteManager.cs | 58 ++++++++++---------- .../RedirectRules/RedirectHostsToUrlsRule.cs | 10 ++-- .../Framework/RedirectRules/RedirectMatchRule.cs | 6 +-- .../RedirectRules/RedirectPathsToUrlsRule.cs | 6 +-- .../Framework/RedirectRules/RedirectToHttpsRule.cs | 6 +-- .../Framework/Storage/IStorageProvider.cs | 2 - src/SMAPI.Web/Framework/Storage/StorageProvider.cs | 52 ++++++------------ src/SMAPI.Web/Framework/Storage/StoredFileInfo.cs | 41 +++++++++++--- src/SMAPI.Web/Framework/Storage/UploadResult.cs | 14 ++--- src/SMAPI.Web/Framework/VersionConstraint.cs | 6 +-- 53 files changed, 557 insertions(+), 385 deletions(-) (limited to 'src/SMAPI.Web/Framework') diff --git a/src/SMAPI.Web/Framework/AllowLargePostsAttribute.cs b/src/SMAPI.Web/Framework/AllowLargePostsAttribute.cs index 108ceff7..bd414ea2 100644 --- a/src/SMAPI.Web/Framework/AllowLargePostsAttribute.cs +++ b/src/SMAPI.Web/Framework/AllowLargePostsAttribute.cs @@ -1,5 +1,3 @@ -#nullable disable - using System; using Microsoft.AspNetCore.Http.Features; using Microsoft.AspNetCore.Mvc.Filters; @@ -42,7 +40,7 @@ namespace StardewModdingAPI.Web.Framework public void OnAuthorization(AuthorizationFilterContext context) { IFeatureCollection features = context.HttpContext.Features; - IFormFeature formFeature = features.Get(); + IFormFeature? formFeature = features.Get(); if (formFeature?.Form == null) { diff --git a/src/SMAPI.Web/Framework/Caching/Cached.cs b/src/SMAPI.Web/Framework/Caching/Cached.cs index aabbf146..b393e1e1 100644 --- a/src/SMAPI.Web/Framework/Caching/Cached.cs +++ b/src/SMAPI.Web/Framework/Caching/Cached.cs @@ -1,5 +1,3 @@ -#nullable disable - using System; namespace StardewModdingAPI.Web.Framework.Caching @@ -12,21 +10,18 @@ namespace StardewModdingAPI.Web.Framework.Caching ** Accessors *********/ /// The cached data. - public T Data { get; set; } + public T Data { get; } /// When the data was last updated. - public DateTimeOffset LastUpdated { get; set; } + public DateTimeOffset LastUpdated { get; } /// When the data was last requested through the mod API. - public DateTimeOffset LastRequested { get; set; } + public DateTimeOffset LastRequested { get; internal set; } /********* ** Public methods *********/ - /// Construct an empty instance. - public Cached() { } - /// Construct an instance. /// The cached data. public Cached(T data) diff --git a/src/SMAPI.Web/Framework/Caching/Mods/IModCacheRepository.cs b/src/SMAPI.Web/Framework/Caching/Mods/IModCacheRepository.cs index 2020d747..fb74e9da 100644 --- a/src/SMAPI.Web/Framework/Caching/Mods/IModCacheRepository.cs +++ b/src/SMAPI.Web/Framework/Caching/Mods/IModCacheRepository.cs @@ -1,6 +1,5 @@ -#nullable disable - using System; +using System.Diagnostics.CodeAnalysis; using StardewModdingAPI.Toolkit.Framework.UpdateData; namespace StardewModdingAPI.Web.Framework.Caching.Mods @@ -16,7 +15,7 @@ namespace StardewModdingAPI.Web.Framework.Caching.Mods /// The mod's unique ID within the . /// The fetched mod. /// Whether to update the mod's 'last requested' date. - bool TryGetMod(ModSiteKey site, string id, out Cached mod, bool markRequested = true); + bool TryGetMod(ModSiteKey site, string id, [NotNullWhen(true)] out Cached? mod, bool markRequested = true); /// Save data fetched for a mod. /// The mod site on which the mod is found. diff --git a/src/SMAPI.Web/Framework/Caching/Mods/ModCacheMemoryRepository.cs b/src/SMAPI.Web/Framework/Caching/Mods/ModCacheMemoryRepository.cs index 338562d8..4ba0bd20 100644 --- a/src/SMAPI.Web/Framework/Caching/Mods/ModCacheMemoryRepository.cs +++ b/src/SMAPI.Web/Framework/Caching/Mods/ModCacheMemoryRepository.cs @@ -1,7 +1,6 @@ -#nullable disable - using System; using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; using System.Linq; using StardewModdingAPI.Toolkit.Framework.UpdateData; @@ -25,7 +24,7 @@ namespace StardewModdingAPI.Web.Framework.Caching.Mods /// The mod's unique ID within the . /// The fetched mod. /// Whether to update the mod's 'last requested' date. - public bool TryGetMod(ModSiteKey site, string id, out Cached mod, bool markRequested = true) + public bool TryGetMod(ModSiteKey site, string id, [NotNullWhen(true)] out Cached? mod, bool markRequested = true) { // get mod if (!this.Mods.TryGetValue(this.GetKey(site, id), out var cachedMod)) diff --git a/src/SMAPI.Web/Framework/Caching/Wiki/IWikiCacheRepository.cs b/src/SMAPI.Web/Framework/Caching/Wiki/IWikiCacheRepository.cs index 6edafddc..b8a0df34 100644 --- a/src/SMAPI.Web/Framework/Caching/Wiki/IWikiCacheRepository.cs +++ b/src/SMAPI.Web/Framework/Caching/Wiki/IWikiCacheRepository.cs @@ -1,7 +1,6 @@ -#nullable disable - using System; using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; using StardewModdingAPI.Toolkit.Framework.Clients.Wiki; namespace StardewModdingAPI.Web.Framework.Caching.Wiki @@ -14,16 +13,16 @@ namespace StardewModdingAPI.Web.Framework.Caching.Wiki *********/ /// Get the cached wiki metadata. /// The fetched metadata. - bool TryGetWikiMetadata(out Cached metadata); + bool TryGetWikiMetadata([NotNullWhen(true)] out Cached? metadata); /// Get the cached wiki mods. /// A filter to apply, if any. - IEnumerable> GetWikiMods(Func filter = null); + IEnumerable> GetWikiMods(Func? filter = null); /// Save data fetched from the wiki compatibility list. /// The current stable Stardew Valley version. /// The current beta Stardew Valley version. /// The mod data. - void SaveWikiData(string stableVersion, string betaVersion, IEnumerable mods); + void SaveWikiData(string? stableVersion, string? betaVersion, IEnumerable mods); } } diff --git a/src/SMAPI.Web/Framework/Caching/Wiki/WikiCacheMemoryRepository.cs b/src/SMAPI.Web/Framework/Caching/Wiki/WikiCacheMemoryRepository.cs index d1ccb9c7..8b4338e2 100644 --- a/src/SMAPI.Web/Framework/Caching/Wiki/WikiCacheMemoryRepository.cs +++ b/src/SMAPI.Web/Framework/Caching/Wiki/WikiCacheMemoryRepository.cs @@ -1,7 +1,6 @@ -#nullable disable - using System; using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; using System.Linq; using StardewModdingAPI.Toolkit.Framework.Clients.Wiki; @@ -14,7 +13,7 @@ namespace StardewModdingAPI.Web.Framework.Caching.Wiki ** Fields *********/ /// The saved wiki metadata. - private Cached Metadata; + private Cached? Metadata; /// The cached wiki data. private Cached[] Mods = Array.Empty>(); @@ -25,7 +24,7 @@ namespace StardewModdingAPI.Web.Framework.Caching.Wiki *********/ /// Get the cached wiki metadata. /// The fetched metadata. - public bool TryGetWikiMetadata(out Cached metadata) + public bool TryGetWikiMetadata([NotNullWhen(true)] out Cached? metadata) { metadata = this.Metadata; return metadata != null; @@ -33,7 +32,7 @@ namespace StardewModdingAPI.Web.Framework.Caching.Wiki /// Get the cached wiki mods. /// A filter to apply, if any. - public IEnumerable> GetWikiMods(Func filter = null) + public IEnumerable> GetWikiMods(Func? filter = null) { foreach (var mod in this.Mods) { @@ -46,7 +45,7 @@ namespace StardewModdingAPI.Web.Framework.Caching.Wiki /// The current stable Stardew Valley version. /// The current beta Stardew Valley version. /// The mod data. - public void SaveWikiData(string stableVersion, string betaVersion, IEnumerable mods) + public void SaveWikiData(string? stableVersion, string? betaVersion, IEnumerable mods) { this.Metadata = new Cached(new WikiMetadata(stableVersion, betaVersion)); this.Mods = mods.Select(mod => new Cached(mod)).ToArray(); diff --git a/src/SMAPI.Web/Framework/Caching/Wiki/WikiMetadata.cs b/src/SMAPI.Web/Framework/Caching/Wiki/WikiMetadata.cs index 6ae42488..f53ea201 100644 --- a/src/SMAPI.Web/Framework/Caching/Wiki/WikiMetadata.cs +++ b/src/SMAPI.Web/Framework/Caching/Wiki/WikiMetadata.cs @@ -1,5 +1,3 @@ -#nullable disable - namespace StardewModdingAPI.Web.Framework.Caching.Wiki { /// The model for cached wiki metadata. @@ -9,22 +7,19 @@ namespace StardewModdingAPI.Web.Framework.Caching.Wiki ** Accessors *********/ /// The current stable Stardew Valley version. - public string StableVersion { get; set; } + public string? StableVersion { get; } /// The current beta Stardew Valley version. - public string BetaVersion { get; set; } + public string? BetaVersion { get; } /********* ** Public methods *********/ - /// Construct an instance. - public WikiMetadata() { } - /// Construct an instance. /// The current stable Stardew Valley version. /// The current beta Stardew Valley version. - public WikiMetadata(string stableVersion, string betaVersion) + public WikiMetadata(string? stableVersion, string? betaVersion) { this.StableVersion = stableVersion; this.BetaVersion = betaVersion; diff --git a/src/SMAPI.Web/Framework/Clients/Chucklefish/ChucklefishClient.cs b/src/SMAPI.Web/Framework/Clients/Chucklefish/ChucklefishClient.cs index 4d041c1b..ce0f1122 100644 --- a/src/SMAPI.Web/Framework/Clients/Chucklefish/ChucklefishClient.cs +++ b/src/SMAPI.Web/Framework/Clients/Chucklefish/ChucklefishClient.cs @@ -1,5 +1,3 @@ -#nullable disable - using System; using System.Net; using System.Threading.Tasks; @@ -44,7 +42,7 @@ namespace StardewModdingAPI.Web.Framework.Clients.Chucklefish /// Get update check info about a mod. /// The mod ID. - public async Task GetModData(string id) + public async Task GetModData(string id) { IModPage page = new GenericModPage(this.SiteKey, id); @@ -53,7 +51,7 @@ namespace StardewModdingAPI.Web.Framework.Clients.Chucklefish return page.SetError(RemoteModStatus.DoesNotExist, $"The value '{id}' isn't a valid Chucklefish mod ID, must be an integer ID."); // fetch HTML - string html; + string? html; try { html = await this.Client @@ -69,7 +67,7 @@ namespace StardewModdingAPI.Web.Framework.Clients.Chucklefish // extract mod info string url = this.GetModUrl(parsedId); - string version = doc.DocumentNode.SelectSingleNode("//h1/span")?.InnerText; + string? version = doc.DocumentNode.SelectSingleNode("//h1/span")?.InnerText; string name = doc.DocumentNode.SelectSingleNode("//h1").ChildNodes[0].InnerText.Trim(); if (name.StartsWith("[SMAPI]")) name = name.Substring("[SMAPI]".Length).TrimStart(); @@ -81,7 +79,7 @@ namespace StardewModdingAPI.Web.Framework.Clients.Chucklefish /// Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources. public void Dispose() { - this.Client?.Dispose(); + this.Client.Dispose(); } @@ -92,7 +90,7 @@ namespace StardewModdingAPI.Web.Framework.Clients.Chucklefish /// The mod ID. private string GetModUrl(uint id) { - UriBuilder builder = new(this.Client.BaseClient.BaseAddress); + UriBuilder builder = new(this.Client.BaseClient.BaseAddress!); builder.Path += string.Format(this.ModPageUrlFormat, id); return builder.Uri.ToString(); } diff --git a/src/SMAPI.Web/Framework/Clients/CurseForge/CurseForgeClient.cs b/src/SMAPI.Web/Framework/Clients/CurseForge/CurseForgeClient.cs index 5ef369d5..d351b42d 100644 --- a/src/SMAPI.Web/Framework/Clients/CurseForge/CurseForgeClient.cs +++ b/src/SMAPI.Web/Framework/Clients/CurseForge/CurseForgeClient.cs @@ -1,5 +1,3 @@ -#nullable disable - using System.Collections.Generic; using System.Text.RegularExpressions; using System.Threading.Tasks; @@ -42,7 +40,7 @@ namespace StardewModdingAPI.Web.Framework.Clients.CurseForge /// Get update check info about a mod. /// The mod ID. - public async Task GetModData(string id) + public async Task GetModData(string id) { IModPage page = new GenericModPage(this.SiteKey, id); @@ -51,9 +49,9 @@ namespace StardewModdingAPI.Web.Framework.Clients.CurseForge return page.SetError(RemoteModStatus.DoesNotExist, $"The value '{id}' isn't a valid CurseForge mod ID, must be an integer ID."); // get raw data - ModModel mod = await this.Client + ModModel? mod = await this.Client .GetAsync($"addon/{parsedId}") - .As(); + .As(); if (mod == null) return page.SetError(RemoteModStatus.DoesNotExist, "Found no CurseForge mod with this ID."); @@ -73,7 +71,7 @@ namespace StardewModdingAPI.Web.Framework.Clients.CurseForge /// Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources. public void Dispose() { - this.Client?.Dispose(); + this.Client.Dispose(); } @@ -82,9 +80,9 @@ namespace StardewModdingAPI.Web.Framework.Clients.CurseForge *********/ /// Get a raw version string for a mod file, if available. /// The file whose version to get. - private string GetRawVersion(ModFileModel file) + private string? GetRawVersion(ModFileModel file) { - Match match = this.VersionInNamePattern.Match(file.DisplayName); + Match match = this.VersionInNamePattern.Match(file.DisplayName ?? ""); if (!match.Success) match = this.VersionInNamePattern.Match(file.FileName); diff --git a/src/SMAPI.Web/Framework/Clients/CurseForge/ResponseModels/ModFileModel.cs b/src/SMAPI.Web/Framework/Clients/CurseForge/ResponseModels/ModFileModel.cs index eabef9f0..e9adcf20 100644 --- a/src/SMAPI.Web/Framework/Clients/CurseForge/ResponseModels/ModFileModel.cs +++ b/src/SMAPI.Web/Framework/Clients/CurseForge/ResponseModels/ModFileModel.cs @@ -1,14 +1,28 @@ -#nullable disable - namespace StardewModdingAPI.Web.Framework.Clients.CurseForge.ResponseModels { /// Metadata from the CurseForge API about a mod file. public class ModFileModel { + /********* + ** Accessors + *********/ /// The file name as downloaded. - public string FileName { get; set; } + public string FileName { get; } /// The file display name. - public string DisplayName { get; set; } + public string? DisplayName { get; } + + + /********* + ** Public methods + *********/ + /// Construct an instance. + /// The file name as downloaded. + /// The file display name. + public ModFileModel(string fileName, string? displayName) + { + this.FileName = fileName; + this.DisplayName = displayName; + } } } diff --git a/src/SMAPI.Web/Framework/Clients/CurseForge/ResponseModels/ModModel.cs b/src/SMAPI.Web/Framework/Clients/CurseForge/ResponseModels/ModModel.cs index a95df7f1..fd7796f2 100644 --- a/src/SMAPI.Web/Framework/Clients/CurseForge/ResponseModels/ModModel.cs +++ b/src/SMAPI.Web/Framework/Clients/CurseForge/ResponseModels/ModModel.cs @@ -1,20 +1,38 @@ -#nullable disable - namespace StardewModdingAPI.Web.Framework.Clients.CurseForge.ResponseModels { /// An mod from the CurseForge API. public class ModModel { + /********* + ** Accessors + *********/ /// The mod's unique ID on CurseForge. - public int ID { get; set; } + public int ID { get; } /// The mod name. - public string Name { get; set; } + public string Name { get; } /// The web URL for the mod page. - public string WebsiteUrl { get; set; } + public string WebsiteUrl { get; } /// The available file downloads. - public ModFileModel[] LatestFiles { get; set; } + public ModFileModel[] LatestFiles { get; } + + + /********* + ** Public methods + *********/ + /// Construct an instance. + /// The mod's unique ID on CurseForge. + /// The mod name. + /// The web URL for the mod page. + /// The available file downloads. + public ModModel(int id, string name, string websiteUrl, ModFileModel[] latestFiles) + { + this.ID = id; + this.Name = name; + this.WebsiteUrl = websiteUrl; + this.LatestFiles = latestFiles; + } } } diff --git a/src/SMAPI.Web/Framework/Clients/GenericModDownload.cs b/src/SMAPI.Web/Framework/Clients/GenericModDownload.cs index 919072b0..548f17c3 100644 --- a/src/SMAPI.Web/Framework/Clients/GenericModDownload.cs +++ b/src/SMAPI.Web/Framework/Clients/GenericModDownload.cs @@ -1,5 +1,3 @@ -#nullable disable - namespace StardewModdingAPI.Web.Framework.Clients { /// Generic metadata about a file download on a mod page. @@ -9,26 +7,23 @@ namespace StardewModdingAPI.Web.Framework.Clients ** Accessors *********/ /// The download's display name. - public string Name { get; set; } + public string Name { get; } /// The download's description. - public string Description { get; set; } + public string? Description { get; } /// The download's file version. - public string Version { get; set; } + public string? Version { get; } /********* ** Public methods *********/ - /// Construct an empty instance. - public GenericModDownload() { } - /// Construct an instance. /// The download's display name. /// The download's description. /// The download's file version. - public GenericModDownload(string name, string description, string version) + public GenericModDownload(string name, string? description, string? version) { this.Name = name; this.Description = description; diff --git a/src/SMAPI.Web/Framework/Clients/GenericModPage.cs b/src/SMAPI.Web/Framework/Clients/GenericModPage.cs index 4788aa2a..5353c7e1 100644 --- a/src/SMAPI.Web/Framework/Clients/GenericModPage.cs +++ b/src/SMAPI.Web/Framework/Clients/GenericModPage.cs @@ -1,7 +1,6 @@ -#nullable disable - using System; using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; using System.Linq; using StardewModdingAPI.Toolkit.Framework.UpdateData; @@ -20,30 +19,31 @@ namespace StardewModdingAPI.Web.Framework.Clients public string Id { get; set; } /// The mod name. - public string Name { get; set; } + public string? Name { get; set; } /// The mod's semantic version number. - public string Version { get; set; } + public string? Version { get; set; } /// The mod's web URL. - public string Url { get; set; } + public string? Url { get; set; } /// The mod downloads. public IModDownload[] Downloads { get; set; } = Array.Empty(); /// The mod availability status on the remote site. - public RemoteModStatus Status { get; set; } = RemoteModStatus.Ok; + public RemoteModStatus Status { get; set; } = RemoteModStatus.InvalidData; /// A user-friendly error which indicates why fetching the mod info failed (if applicable). - public string Error { get; set; } + public string? Error { get; set; } + + /// Whether the mod data is valid. + [MemberNotNullWhen(true, nameof(IModPage.Name), nameof(IModPage.Url))] + public bool IsValid => this.Status == RemoteModStatus.Ok; /********* ** Public methods *********/ - /// Construct an empty instance. - public GenericModPage() { } - /// Construct an instance. /// The mod site containing the mod. /// The mod's unique ID within the site. @@ -58,12 +58,13 @@ namespace StardewModdingAPI.Web.Framework.Clients /// The mod's semantic version number. /// The mod's web URL. /// The mod downloads. - public IModPage SetInfo(string name, string version, string url, IEnumerable downloads) + public IModPage SetInfo(string name, string? version, string url, IEnumerable downloads) { this.Name = name; this.Version = version; this.Url = url; this.Downloads = downloads.ToArray(); + this.Status = RemoteModStatus.Ok; return this; } diff --git a/src/SMAPI.Web/Framework/Clients/GitHub/GitAsset.cs b/src/SMAPI.Web/Framework/Clients/GitHub/GitAsset.cs index 39ebf94e..dbce9368 100644 --- a/src/SMAPI.Web/Framework/Clients/GitHub/GitAsset.cs +++ b/src/SMAPI.Web/Framework/Clients/GitHub/GitAsset.cs @@ -1,5 +1,3 @@ -#nullable disable - using Newtonsoft.Json; namespace StardewModdingAPI.Web.Framework.Clients.GitHub @@ -7,16 +5,34 @@ namespace StardewModdingAPI.Web.Framework.Clients.GitHub /// A GitHub download attached to a release. internal class GitAsset { + /********* + ** Accessors + *********/ /// The file name. [JsonProperty("name")] - public string FileName { get; set; } + public string FileName { get; } /// The file content type. [JsonProperty("content_type")] - public string ContentType { get; set; } + public string ContentType { get; } /// The download URL. [JsonProperty("browser_download_url")] - public string DownloadUrl { get; set; } + public string DownloadUrl { get; } + + + /********* + ** Public methods + *********/ + /// Construct an instance. + /// The file name. + /// The file content type. + /// The download URL. + public GitAsset(string fileName, string contentType, string downloadUrl) + { + this.FileName = fileName; + this.ContentType = contentType; + this.DownloadUrl = downloadUrl; + } } } diff --git a/src/SMAPI.Web/Framework/Clients/GitHub/GitHubClient.cs b/src/SMAPI.Web/Framework/Clients/GitHub/GitHubClient.cs index 0e68e2c2..785979a5 100644 --- a/src/SMAPI.Web/Framework/Clients/GitHub/GitHubClient.cs +++ b/src/SMAPI.Web/Framework/Clients/GitHub/GitHubClient.cs @@ -1,5 +1,3 @@ -#nullable disable - using System; using System.Linq; using System.Net; @@ -35,26 +33,26 @@ namespace StardewModdingAPI.Web.Framework.Clients.GitHub /// 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 userAgent, string acceptHeader, string username, string password) + public GitHubClient(string baseUrl, string userAgent, string acceptHeader, string? username, string? password) { this.Client = new FluentClient(baseUrl) .SetUserAgent(userAgent) .AddDefault(req => req.WithHeader("Accept", acceptHeader)); if (!string.IsNullOrWhiteSpace(username)) - this.Client = this.Client.SetBasicAuthentication(username, password); + this.Client = this.Client.SetBasicAuthentication(username, password!); } /// Get basic metadata for a GitHub repository, if available. /// The repository key (like Pathoschild/SMAPI). /// Returns the repository info if it exists, else null. - public async Task GetRepositoryAsync(string repo) + public async Task GetRepositoryAsync(string repo) { this.AssertKeyFormat(repo); try { return await this.Client .GetAsync($"repos/{repo}") - .As(); + .As(); } catch (ApiException ex) when (ex.Status == HttpStatusCode.NotFound) { @@ -66,7 +64,7 @@ namespace StardewModdingAPI.Web.Framework.Clients.GitHub /// The repository key (like Pathoschild/SMAPI). /// 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) + public async Task GetLatestReleaseAsync(string repo, bool includePrerelease = false) { this.AssertKeyFormat(repo); try @@ -81,7 +79,7 @@ namespace StardewModdingAPI.Web.Framework.Clients.GitHub return await this.Client .GetAsync($"repos/{repo}/releases/latest") - .As(); + .As(); } catch (ApiException ex) when (ex.Status == HttpStatusCode.NotFound) { @@ -91,7 +89,7 @@ namespace StardewModdingAPI.Web.Framework.Clients.GitHub /// Get update check info about a mod. /// The mod ID. - public async Task GetModData(string id) + public async Task GetModData(string id) { IModPage page = new GenericModPage(this.SiteKey, id); @@ -99,15 +97,15 @@ namespace StardewModdingAPI.Web.Framework.Clients.GitHub return page.SetError(RemoteModStatus.DoesNotExist, $"The value '{id}' isn't a valid GitHub mod ID, must be a username and project name like 'Pathoschild/SMAPI'."); // fetch repo info - GitRepo repository = await this.GetRepositoryAsync(id); + GitRepo? repository = await this.GetRepositoryAsync(id); if (repository == null) return page.SetError(RemoteModStatus.DoesNotExist, "Found no GitHub repository for this ID."); string name = repository.FullName; string url = $"{repository.WebUrl}/releases"; // get releases - GitRelease latest; - GitRelease preview; + GitRelease? latest; + GitRelease? preview; { // get latest release (whether preview or stable) latest = await this.GetLatestReleaseAsync(id, includePrerelease: true); @@ -118,7 +116,7 @@ namespace StardewModdingAPI.Web.Framework.Clients.GitHub preview = null; if (latest.IsPrerelease) { - GitRelease release = await this.GetLatestReleaseAsync(id, includePrerelease: false); + GitRelease? release = await this.GetLatestReleaseAsync(id, includePrerelease: false); if (release != null) { preview = latest; @@ -129,8 +127,8 @@ namespace StardewModdingAPI.Web.Framework.Clients.GitHub // get downloads IModDownload[] downloads = new[] { latest, preview } - .Where(release => release != null) - .Select(release => (IModDownload)new GenericModDownload(release.Name, release.Body, release.Tag)) + .Where(release => release is not null) + .Select(release => (IModDownload)new GenericModDownload(release!.Name, release.Body, release.Tag)) .ToArray(); // return info @@ -140,7 +138,7 @@ namespace StardewModdingAPI.Web.Framework.Clients.GitHub /// Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources. public void Dispose() { - this.Client?.Dispose(); + this.Client.Dispose(); } diff --git a/src/SMAPI.Web/Framework/Clients/GitHub/GitLicense.cs b/src/SMAPI.Web/Framework/Clients/GitHub/GitLicense.cs index 275c775a..24d6c3c5 100644 --- a/src/SMAPI.Web/Framework/Clients/GitHub/GitLicense.cs +++ b/src/SMAPI.Web/Framework/Clients/GitHub/GitLicense.cs @@ -1,5 +1,3 @@ -#nullable disable - using Newtonsoft.Json; namespace StardewModdingAPI.Web.Framework.Clients.GitHub @@ -7,16 +5,34 @@ namespace StardewModdingAPI.Web.Framework.Clients.GitHub /// The license info for a GitHub project. internal class GitLicense { + /********* + ** Accessors + *********/ /// The license display name. [JsonProperty("name")] - public string Name { get; set; } + public string Name { get; } /// The SPDX ID for the license. [JsonProperty("spdx_id")] - public string SpdxId { get; set; } + public string SpdxId { get; } /// The URL for the license info. [JsonProperty("url")] - public string Url { get; set; } + public string Url { get; } + + + /********* + ** Public methods + *********/ + /// Construct an instance. + /// The license display name. + /// The SPDX ID for the license. + /// The URL for the license info. + public GitLicense(string name, string spdxId, string url) + { + this.Name = name; + this.SpdxId = spdxId; + this.Url = url; + } } } diff --git a/src/SMAPI.Web/Framework/Clients/GitHub/GitRelease.cs b/src/SMAPI.Web/Framework/Clients/GitHub/GitRelease.cs index 383775d2..9de6f020 100644 --- a/src/SMAPI.Web/Framework/Clients/GitHub/GitRelease.cs +++ b/src/SMAPI.Web/Framework/Clients/GitHub/GitRelease.cs @@ -1,5 +1,4 @@ -#nullable disable - +using System; using Newtonsoft.Json; namespace StardewModdingAPI.Web.Framework.Clients.GitHub @@ -12,24 +11,45 @@ namespace StardewModdingAPI.Web.Framework.Clients.GitHub *********/ /// The display name. [JsonProperty("name")] - public string Name { get; set; } + public string Name { get; } /// The semantic version string. [JsonProperty("tag_name")] - public string Tag { get; set; } + public string Tag { get; } /// The Markdown description for the release. - public string Body { get; set; } + public string Body { get; internal set; } /// Whether this is a draft version. [JsonProperty("draft")] - public bool IsDraft { get; set; } + public bool IsDraft { get; } /// Whether this is a prerelease version. [JsonProperty("prerelease")] - public bool IsPrerelease { get; set; } + public bool IsPrerelease { get; } /// The attached files. - public GitAsset[] Assets { get; set; } + public GitAsset[] Assets { get; } + + + /********* + ** Public methods + *********/ + /// Construct an instance. + /// The display name. + /// The semantic version string. + /// The Markdown description for the release. + /// Whether this is a draft version. + /// Whether this is a prerelease version. + /// The attached files. + public GitRelease(string name, string tag, string? body, bool isDraft, bool isPrerelease, GitAsset[]? assets) + { + this.Name = name; + this.Tag = tag; + this.Body = body ?? string.Empty; + this.IsDraft = isDraft; + this.IsPrerelease = isPrerelease; + this.Assets = assets ?? Array.Empty(); + } } } diff --git a/src/SMAPI.Web/Framework/Clients/GitHub/GitRepo.cs b/src/SMAPI.Web/Framework/Clients/GitHub/GitRepo.cs index 5b5ce6a6..879b5e49 100644 --- a/src/SMAPI.Web/Framework/Clients/GitHub/GitRepo.cs +++ b/src/SMAPI.Web/Framework/Clients/GitHub/GitRepo.cs @@ -1,5 +1,3 @@ -#nullable disable - using Newtonsoft.Json; namespace StardewModdingAPI.Web.Framework.Clients.GitHub @@ -7,16 +5,34 @@ namespace StardewModdingAPI.Web.Framework.Clients.GitHub /// Basic metadata about a GitHub project. internal class GitRepo { + /********* + ** Accessors + *********/ /// The full repository name, including the owner. [JsonProperty("full_name")] - public string FullName { get; set; } + public string FullName { get; } /// The URL to the repository web page, if any. [JsonProperty("html_url")] - public string WebUrl { get; set; } + public string? WebUrl { get; } /// The code license, if any. [JsonProperty("license")] - public GitLicense License { get; set; } + public GitLicense? License { get; } + + + /********* + ** Public methods + *********/ + /// Construct an instance. + /// The full repository name, including the owner. + /// The URL to the repository web page, if any. + /// The code license, if any. + public GitRepo(string fullName, string? webUrl, GitLicense? license) + { + this.FullName = fullName; + this.WebUrl = webUrl; + this.License = license; + } } } diff --git a/src/SMAPI.Web/Framework/Clients/GitHub/IGitHubClient.cs b/src/SMAPI.Web/Framework/Clients/GitHub/IGitHubClient.cs index e1961416..886e32d3 100644 --- a/src/SMAPI.Web/Framework/Clients/GitHub/IGitHubClient.cs +++ b/src/SMAPI.Web/Framework/Clients/GitHub/IGitHubClient.cs @@ -1,5 +1,3 @@ -#nullable disable - using System; using System.Threading.Tasks; @@ -14,12 +12,12 @@ namespace StardewModdingAPI.Web.Framework.Clients.GitHub /// Get basic metadata for a GitHub repository, if available. /// The repository key (like Pathoschild/SMAPI). /// Returns the repository info if it exists, else null. - Task GetRepositoryAsync(string repo); + Task GetRepositoryAsync(string repo); /// Get the latest release for a GitHub repository. /// The repository key (like Pathoschild/SMAPI). /// Whether to return a prerelease version if it's latest. /// Returns the release if found, else null. - Task GetLatestReleaseAsync(string repo, bool includePrerelease = false); + Task GetLatestReleaseAsync(string repo, bool includePrerelease = false); } } diff --git a/src/SMAPI.Web/Framework/Clients/IModSiteClient.cs b/src/SMAPI.Web/Framework/Clients/IModSiteClient.cs index 2cd1f635..3697ffae 100644 --- a/src/SMAPI.Web/Framework/Clients/IModSiteClient.cs +++ b/src/SMAPI.Web/Framework/Clients/IModSiteClient.cs @@ -1,5 +1,3 @@ -#nullable disable - using System.Threading.Tasks; using StardewModdingAPI.Toolkit.Framework.UpdateData; @@ -20,6 +18,6 @@ namespace StardewModdingAPI.Web.Framework.Clients *********/ /// Get update check info about a mod. /// The mod ID. - Task GetModData(string id); + Task GetModData(string id); } } diff --git a/src/SMAPI.Web/Framework/Clients/ModDrop/ModDropClient.cs b/src/SMAPI.Web/Framework/Clients/ModDrop/ModDropClient.cs index 1a11a606..c60b2c90 100644 --- a/src/SMAPI.Web/Framework/Clients/ModDrop/ModDropClient.cs +++ b/src/SMAPI.Web/Framework/Clients/ModDrop/ModDropClient.cs @@ -1,6 +1,5 @@ -#nullable disable - using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; using System.Threading.Tasks; using Pathoschild.Http.Client; using StardewModdingAPI.Toolkit.Framework.UpdateData; @@ -43,9 +42,10 @@ namespace StardewModdingAPI.Web.Framework.Clients.ModDrop /// Get update check info about a mod. /// The mod ID. - public async Task GetModData(string id) + [SuppressMessage("ReSharper", "ConstantConditionalAccessQualifier", Justification = "The nullability is validated in this method.")] + public async Task GetModData(string id) { - var page = new GenericModPage(this.SiteKey, id); + IModPage page = new GenericModPage(this.SiteKey, id); if (!long.TryParse(id, out long parsedId)) return page.SetError(RemoteModStatus.DoesNotExist, $"The value '{id}' isn't a valid ModDrop mod ID, must be an integer ID."); @@ -60,9 +60,11 @@ namespace StardewModdingAPI.Web.Framework.Clients.ModDrop Mods = true }) .As(); - ModModel mod = response.Mods[parsedId]; - if (mod.Mod?.Title == null || mod.Mod.ErrorCode.HasValue) - return null; + + if (!response.Mods.TryGetValue(parsedId, out ModModel? mod) || mod?.Mod is null) + return page.SetError(RemoteModStatus.DoesNotExist, "Found no ModDrop page with this ID."); + if (mod.Mod.ErrorCode is not null) + return page.SetError(RemoteModStatus.InvalidData, $"ModDrop returned error code {mod.Mod.ErrorCode} for mod ID '{id}'."); // get files var downloads = new List(); @@ -77,7 +79,7 @@ namespace StardewModdingAPI.Web.Framework.Clients.ModDrop } // return info - string name = mod.Mod?.Title; + string name = mod.Mod.Title; string url = string.Format(this.ModUrlFormat, id); return page.SetInfo(name: name, version: null, url: url, downloads: downloads); } @@ -85,7 +87,7 @@ namespace StardewModdingAPI.Web.Framework.Clients.ModDrop /// Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources. public void Dispose() { - this.Client?.Dispose(); + this.Client.Dispose(); } } } diff --git a/src/SMAPI.Web/Framework/Clients/ModDrop/ResponseModels/FileDataModel.cs b/src/SMAPI.Web/Framework/Clients/ModDrop/ResponseModels/FileDataModel.cs index dd6a95e0..31905338 100644 --- a/src/SMAPI.Web/Framework/Clients/ModDrop/ResponseModels/FileDataModel.cs +++ b/src/SMAPI.Web/Framework/Clients/ModDrop/ResponseModels/FileDataModel.cs @@ -1,5 +1,3 @@ -#nullable disable - using Newtonsoft.Json; namespace StardewModdingAPI.Web.Framework.Clients.ModDrop.ResponseModels @@ -7,27 +5,53 @@ namespace StardewModdingAPI.Web.Framework.Clients.ModDrop.ResponseModels /// Metadata from the ModDrop API about a mod file. public class FileDataModel { + /********* + ** Accessors + *********/ /// The file title. [JsonProperty("title")] - public string Name { get; set; } + public string Name { get; } /// The file description. [JsonProperty("desc")] - public string Description { get; set; } + public string Description { get; } /// The file version. - public string Version { get; set; } + public string Version { get; } /// Whether the file is deleted. - public bool IsDeleted { get; set; } + public bool IsDeleted { get; } /// Whether the file is hidden from users. - public bool IsHidden { get; set; } + public bool IsHidden { get; } /// Whether this is the default file for the mod. - public bool IsDefault { get; set; } + public bool IsDefault { get; } /// Whether this is an archived file. - public bool IsOld { get; set; } + public bool IsOld { get; } + + + /********* + ** Public methods + *********/ + /// Construct an instance. + /// The file title. + /// The file description. + /// The file version. + /// Whether the file is deleted. + /// Whether the file is hidden from users. + /// Whether this is the default file for the mod. + /// Whether this is an archived file. + public FileDataModel(string name, string description, string version, bool isDeleted, bool isHidden, bool isDefault, bool isOld) + { + this.Name = name; + this.Description = description; + this.Version = version; + this.IsDeleted = isDeleted; + this.IsHidden = isHidden; + this.IsDefault = isDefault; + this.IsOld = isOld; + } } } diff --git a/src/SMAPI.Web/Framework/Clients/ModDrop/ResponseModels/ModDataModel.cs b/src/SMAPI.Web/Framework/Clients/ModDrop/ResponseModels/ModDataModel.cs index 6cae16d9..0654b576 100644 --- a/src/SMAPI.Web/Framework/Clients/ModDrop/ResponseModels/ModDataModel.cs +++ b/src/SMAPI.Web/Framework/Clients/ModDrop/ResponseModels/ModDataModel.cs @@ -1,17 +1,33 @@ -#nullable disable - namespace StardewModdingAPI.Web.Framework.Clients.ModDrop.ResponseModels { /// Metadata about a mod from the ModDrop API. public class ModDataModel { + /********* + ** Accessors + *********/ /// The mod's unique ID on ModDrop. public int ID { get; set; } + /// The mod name. + public string Title { get; set; } + /// The error code, if any. public int? ErrorCode { get; set; } - /// The mod name. - public string Title { get; set; } + + /********* + ** Public methods + *********/ + /// Construct an instance. + /// The mod's unique ID on ModDrop. + /// The mod name. + /// The error code, if any. + public ModDataModel(int id, string title, int? errorCode) + { + this.ID = id; + this.Title = title; + this.ErrorCode = errorCode; + } } } diff --git a/src/SMAPI.Web/Framework/Clients/ModDrop/ResponseModels/ModListModel.cs b/src/SMAPI.Web/Framework/Clients/ModDrop/ResponseModels/ModListModel.cs index 445e25cb..cb4be35c 100644 --- a/src/SMAPI.Web/Framework/Clients/ModDrop/ResponseModels/ModListModel.cs +++ b/src/SMAPI.Web/Framework/Clients/ModDrop/ResponseModels/ModListModel.cs @@ -1,5 +1,3 @@ -#nullable disable - using System.Collections.Generic; namespace StardewModdingAPI.Web.Framework.Clients.ModDrop.ResponseModels @@ -7,7 +5,10 @@ namespace StardewModdingAPI.Web.Framework.Clients.ModDrop.ResponseModels /// A list of mods from the ModDrop API. public class ModListModel { + /********* + ** Accessors + *********/ /// The mod data. - public IDictionary Mods { get; set; } + public IDictionary Mods { get; } = new Dictionary(); } } diff --git a/src/SMAPI.Web/Framework/Clients/ModDrop/ResponseModels/ModModel.cs b/src/SMAPI.Web/Framework/Clients/ModDrop/ResponseModels/ModModel.cs index 8869193e..60b818d6 100644 --- a/src/SMAPI.Web/Framework/Clients/ModDrop/ResponseModels/ModModel.cs +++ b/src/SMAPI.Web/Framework/Clients/ModDrop/ResponseModels/ModModel.cs @@ -1,14 +1,28 @@ -#nullable disable - namespace StardewModdingAPI.Web.Framework.Clients.ModDrop.ResponseModels { /// An entry in a mod list from the ModDrop API. public class ModModel { + /********* + ** Accessors + *********/ /// The available file downloads. - public FileDataModel[] Files { get; set; } + public FileDataModel[] Files { get; } /// The mod metadata. - public ModDataModel Mod { get; set; } + public ModDataModel Mod { get; } + + + /********* + ** Public methods + *********/ + /// Construct an instance. + /// The available file downloads. + /// The mod metadata. + public ModModel(FileDataModel[] files, ModDataModel mod) + { + this.Files = files; + this.Mod = mod; + } } } diff --git a/src/SMAPI.Web/Framework/Clients/Nexus/NexusClient.cs b/src/SMAPI.Web/Framework/Clients/Nexus/NexusClient.cs index dd0bb94f..23b25f95 100644 --- a/src/SMAPI.Web/Framework/Clients/Nexus/NexusClient.cs +++ b/src/SMAPI.Web/Framework/Clients/Nexus/NexusClient.cs @@ -1,5 +1,3 @@ -#nullable disable - using System; using System.Collections.Generic; using System.Linq; @@ -61,7 +59,7 @@ namespace StardewModdingAPI.Web.Framework.Clients.Nexus /// Get update check info about a mod. /// The mod ID. - public async Task GetModData(string id) + public async Task GetModData(string id) { IModPage page = new GenericModPage(this.SiteKey, id); @@ -72,7 +70,7 @@ namespace StardewModdingAPI.Web.Framework.Clients.Nexus // adult content are hidden for anonymous users, so fall back to the API in that case. // Note that the API has very restrictive rate limits which means we can't just use it // for all cases. - NexusMod mod = await this.GetModFromWebsiteAsync(parsedId); + NexusMod? mod = await this.GetModFromWebsiteAsync(parsedId); if (mod?.Status == NexusModStatus.AdultContentForbidden) mod = await this.GetModFromApiAsync(parsedId); @@ -81,16 +79,16 @@ namespace StardewModdingAPI.Web.Framework.Clients.Nexus return page.SetError(RemoteModStatus.DoesNotExist, "Found no Nexus mod with this ID."); // return info - page.SetInfo(name: mod.Name, url: mod.Url, version: mod.Version, downloads: mod.Downloads); + page.SetInfo(name: mod.Name ?? parsedId.ToString(), url: mod.Url ?? this.GetModUrl(parsedId), version: mod.Version, downloads: mod.Downloads); if (mod.Status != NexusModStatus.Ok) - page.SetError(RemoteModStatus.TemporaryError, mod.Error); + page.SetError(RemoteModStatus.TemporaryError, mod.Error!); return page; } /// Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources. public void Dispose() { - this.WebClient?.Dispose(); + this.WebClient.Dispose(); } @@ -100,7 +98,7 @@ namespace StardewModdingAPI.Web.Framework.Clients.Nexus /// Get metadata about a mod by scraping the Nexus website. /// The Nexus mod ID. /// Returns the mod info if found, else null. - private async Task GetModFromWebsiteAsync(uint id) + private async Task GetModFromWebsiteAsync(uint id) { // fetch HTML string html; @@ -116,35 +114,38 @@ namespace StardewModdingAPI.Web.Framework.Clients.Nexus } // parse HTML - var doc = new HtmlDocument(); + HtmlDocument doc = new(); doc.LoadHtml(html); // handle Nexus error message - HtmlNode node = doc.DocumentNode.SelectSingleNode("//div[contains(@class, 'site-notice')][contains(@class, 'warning')]"); + HtmlNode? node = doc.DocumentNode.SelectSingleNode("//div[contains(@class, 'site-notice')][contains(@class, 'warning')]"); if (node != null) { string[] errorParts = node.InnerText.Trim().Split(new[] { '\n' }, 2, System.StringSplitOptions.RemoveEmptyEntries); string errorCode = errorParts[0]; - string errorText = errorParts.Length > 1 ? errorParts[1] : null; + string? errorText = errorParts.Length > 1 ? errorParts[1] : null; switch (errorCode.Trim().ToLower()) { case "not found": return null; default: - return new NexusMod { Error = $"Nexus error: {errorCode} ({errorText}).", Status = this.GetWebStatus(errorCode) }; + return new NexusMod( + status: this.GetWebStatus(errorCode), + error: $"Nexus error: {errorCode} ({errorText})." + ); } } // extract mod info string url = this.GetModUrl(id); - string name = doc.DocumentNode.SelectSingleNode("//div[@id='pagetitle']//h1")?.InnerText.Trim(); - string version = doc.DocumentNode.SelectSingleNode("//ul[contains(@class, 'stats')]//li[@class='stat-version']//div[@class='stat']")?.InnerText.Trim(); - SemanticVersion.TryParse(version, out ISemanticVersion parsedVersion); + string? name = doc.DocumentNode.SelectSingleNode("//div[@id='pagetitle']//h1")?.InnerText.Trim(); + string? version = doc.DocumentNode.SelectSingleNode("//ul[contains(@class, 'stats')]//li[@class='stat-version']//div[@class='stat']")?.InnerText.Trim(); + SemanticVersion.TryParse(version, out ISemanticVersion? parsedVersion); // extract files var downloads = new List(); - foreach (var fileSection in doc.DocumentNode.SelectNodes("//div[contains(@class, 'files-tabs')]")) + foreach (HtmlNode fileSection in doc.DocumentNode.SelectNodes("//div[contains(@class, 'files-tabs')]")) { string sectionName = fileSection.Descendants("h2").First().InnerText; if (sectionName != "Main files" && sectionName != "Optional files") @@ -154,7 +155,7 @@ namespace StardewModdingAPI.Web.Framework.Clients.Nexus { string fileName = container.GetDataAttribute("name").Value; string fileVersion = container.GetDataAttribute("version").Value; - string description = container.SelectSingleNode("following-sibling::*[1][self::dd]//div").InnerText?.Trim(); // get text of next
tag; derived from https://stackoverflow.com/a/25535623/262123 + string? description = container.SelectSingleNode("following-sibling::*[1][self::dd]//div").InnerText?.Trim(); // get text of next
tag; derived from https://stackoverflow.com/a/25535623/262123 downloads.Add( new GenericModDownload(fileName, description, fileVersion) @@ -163,13 +164,12 @@ namespace StardewModdingAPI.Web.Framework.Clients.Nexus } // yield info - return new NexusMod - { - Name = name, - Version = parsedVersion?.ToString() ?? version, - Url = url, - Downloads = downloads.ToArray() - }; + return new NexusMod( + name: name ?? id.ToString(), + version: parsedVersion?.ToString() ?? version, + url: url, + downloads: downloads.ToArray() + ); } /// Get metadata about a mod from the Nexus API. @@ -182,22 +182,21 @@ namespace StardewModdingAPI.Web.Framework.Clients.Nexus ModFileList files = await this.ApiClient.ModFiles.GetModFiles("stardewvalley", (int)id, FileCategory.Main, FileCategory.Optional); // yield info - return new NexusMod - { - Name = mod.Name, - Version = SemanticVersion.TryParse(mod.Version, out ISemanticVersion version) ? version?.ToString() : mod.Version, - Url = this.GetModUrl(id), - Downloads = files.Files + return new NexusMod( + name: mod.Name, + version: SemanticVersion.TryParse(mod.Version, out ISemanticVersion? version) ? version.ToString() : mod.Version, + url: this.GetModUrl(id), + downloads: files.Files .Select(file => (IModDownload)new GenericModDownload(file.Name, file.Description, file.FileVersion)) .ToArray() - }; + ); } /// Get the full mod page URL for a given ID. /// The mod ID. private string GetModUrl(uint id) { - UriBuilder builder = new(this.WebClient.BaseClient.BaseAddress); + UriBuilder builder = new(this.WebClient.BaseClient.BaseAddress!); builder.Path += string.Format(this.WebModUrlFormat, id); return builder.Uri.ToString(); } diff --git a/src/SMAPI.Web/Framework/Clients/Nexus/ResponseModels/NexusMod.cs b/src/SMAPI.Web/Framework/Clients/Nexus/ResponseModels/NexusMod.cs index 358c4633..3155cfda 100644 --- a/src/SMAPI.Web/Framework/Clients/Nexus/ResponseModels/NexusMod.cs +++ b/src/SMAPI.Web/Framework/Clients/Nexus/ResponseModels/NexusMod.cs @@ -1,5 +1,4 @@ -#nullable disable - +using System; using Newtonsoft.Json; namespace StardewModdingAPI.Web.Framework.Clients.Nexus.ResponseModels @@ -11,25 +10,53 @@ namespace StardewModdingAPI.Web.Framework.Clients.Nexus.ResponseModels ** Accessors *********/ /// The mod name. - public string Name { get; set; } + public string? Name { get; } /// The mod's semantic version number. - public string Version { get; set; } + public string? Version { get; } /// The mod's web URL. [JsonProperty("mod_page_uri")] - public string Url { get; set; } + public string? Url { get; } /// The mod's publication status. [JsonIgnore] - public NexusModStatus Status { get; set; } = NexusModStatus.Ok; + public NexusModStatus Status { get; } /// The files available to download. [JsonIgnore] - public IModDownload[] Downloads { get; set; } + public IModDownload[] Downloads { get; } /// A custom user-friendly error which indicates why fetching the mod info failed (if applicable). [JsonIgnore] - public string Error { get; set; } + public string? Error { get; } + + + /********* + ** Public methods + *********/ + /// Construct an instance. + /// The mod name + /// The mod's semantic version number. + /// The mod's web URL. + /// The files available to download. + public NexusMod(string name, string? version, string url, IModDownload[] downloads) + { + this.Name = name; + this.Version = version; + this.Url = url; + this.Status = NexusModStatus.Ok; + this.Downloads = downloads; + } + + /// Construct an instance. + /// The mod's publication status. + /// A custom user-friendly error which indicates why fetching the mod info failed (if applicable). + public NexusMod(NexusModStatus status, string error) + { + this.Status = status; + this.Error = error; + this.Downloads = Array.Empty(); + } } } diff --git a/src/SMAPI.Web/Framework/Clients/Pastebin/IPastebinClient.cs b/src/SMAPI.Web/Framework/Clients/Pastebin/IPastebinClient.cs index 03c78e01..431fed7b 100644 --- a/src/SMAPI.Web/Framework/Clients/Pastebin/IPastebinClient.cs +++ b/src/SMAPI.Web/Framework/Clients/Pastebin/IPastebinClient.cs @@ -1,5 +1,3 @@ -#nullable disable - using System; using System.Threading.Tasks; diff --git a/src/SMAPI.Web/Framework/Clients/Pastebin/PasteInfo.cs b/src/SMAPI.Web/Framework/Clients/Pastebin/PasteInfo.cs index 2d48a7ae..7f40e713 100644 --- a/src/SMAPI.Web/Framework/Clients/Pastebin/PasteInfo.cs +++ b/src/SMAPI.Web/Framework/Clients/Pastebin/PasteInfo.cs @@ -1,17 +1,35 @@ -#nullable disable +using System.Diagnostics.CodeAnalysis; namespace StardewModdingAPI.Web.Framework.Clients.Pastebin { /// The response for a get-paste request. internal class PasteInfo { + /********* + ** Accessors + *********/ /// Whether the log was successfully fetched. - public bool Success { get; set; } + [MemberNotNullWhen(tru