summaryrefslogtreecommitdiff
path: root/src/SMAPI.Web/Framework
diff options
context:
space:
mode:
Diffstat (limited to 'src/SMAPI.Web/Framework')
-rw-r--r--src/SMAPI.Web/Framework/AllowLargePostsAttribute.cs2
-rw-r--r--src/SMAPI.Web/Framework/Caching/Cached.cs9
-rw-r--r--src/SMAPI.Web/Framework/Caching/Mods/IModCacheRepository.cs4
-rw-r--r--src/SMAPI.Web/Framework/Caching/Mods/ModCacheMemoryRepository.cs4
-rw-r--r--src/SMAPI.Web/Framework/Caching/Wiki/IWikiCacheRepository.cs7
-rw-r--r--src/SMAPI.Web/Framework/Caching/Wiki/WikiCacheMemoryRepository.cs11
-rw-r--r--src/SMAPI.Web/Framework/Caching/Wiki/WikiMetadata.cs9
-rw-r--r--src/SMAPI.Web/Framework/Clients/Chucklefish/ChucklefishClient.cs12
-rw-r--r--src/SMAPI.Web/Framework/Clients/CurseForge/CurseForgeClient.cs14
-rw-r--r--src/SMAPI.Web/Framework/Clients/CurseForge/ResponseModels/ModFileModel.cs20
-rw-r--r--src/SMAPI.Web/Framework/Clients/CurseForge/ResponseModels/ModModel.cs28
-rw-r--r--src/SMAPI.Web/Framework/Clients/GenericModDownload.cs11
-rw-r--r--src/SMAPI.Web/Framework/Clients/GenericModPage.cs24
-rw-r--r--src/SMAPI.Web/Framework/Clients/GitHub/GitAsset.cs24
-rw-r--r--src/SMAPI.Web/Framework/Clients/GitHub/GitHubClient.cs28
-rw-r--r--src/SMAPI.Web/Framework/Clients/GitHub/GitLicense.cs24
-rw-r--r--src/SMAPI.Web/Framework/Clients/GitHub/GitRelease.cs34
-rw-r--r--src/SMAPI.Web/Framework/Clients/GitHub/GitRepo.cs24
-rw-r--r--src/SMAPI.Web/Framework/Clients/GitHub/IGitHubClient.cs4
-rw-r--r--src/SMAPI.Web/Framework/Clients/IModSiteClient.cs2
-rw-r--r--src/SMAPI.Web/Framework/Clients/ModDrop/ModDropClient.cs18
-rw-r--r--src/SMAPI.Web/Framework/Clients/ModDrop/ResponseModels/FileDataModel.cs40
-rw-r--r--src/SMAPI.Web/Framework/Clients/ModDrop/ResponseModels/ModDataModel.cs22
-rw-r--r--src/SMAPI.Web/Framework/Clients/ModDrop/ResponseModels/ModListModel.cs5
-rw-r--r--src/SMAPI.Web/Framework/Clients/ModDrop/ResponseModels/ModModel.cs20
-rw-r--r--src/SMAPI.Web/Framework/Clients/Nexus/DisabledNexusClient.cs31
-rw-r--r--src/SMAPI.Web/Framework/Clients/Nexus/NexusClient.cs63
-rw-r--r--src/SMAPI.Web/Framework/Clients/Nexus/ResponseModels/NexusMod.cs41
-rw-r--r--src/SMAPI.Web/Framework/Clients/Pastebin/PasteInfo.cs28
-rw-r--r--src/SMAPI.Web/Framework/Clients/Pastebin/PastebinClient.cs12
-rw-r--r--src/SMAPI.Web/Framework/Compression/GzipHelper.cs15
-rw-r--r--src/SMAPI.Web/Framework/Compression/IGzipHelper.cs5
-rw-r--r--src/SMAPI.Web/Framework/ConfigModels/ApiClientsConfig.cs34
-rw-r--r--src/SMAPI.Web/Framework/ConfigModels/ModOverrideConfig.cs4
-rw-r--r--src/SMAPI.Web/Framework/ConfigModels/ModUpdateCheckConfig.cs10
-rw-r--r--src/SMAPI.Web/Framework/ConfigModels/SiteConfig.cs4
-rw-r--r--src/SMAPI.Web/Framework/ConfigModels/SmapiInfoConfig.cs8
-rw-r--r--src/SMAPI.Web/Framework/Extensions.cs10
-rw-r--r--src/SMAPI.Web/Framework/IModDownload.cs7
-rw-r--r--src/SMAPI.Web/Framework/IModPage.cs16
-rw-r--r--src/SMAPI.Web/Framework/JobDashboardAuthorizationFilter.cs2
-rw-r--r--src/SMAPI.Web/Framework/LogParsing/LogMessageBuilder.cs25
-rw-r--r--src/SMAPI.Web/Framework/LogParsing/LogParseException.cs3
-rw-r--r--src/SMAPI.Web/Framework/LogParsing/LogParser.cs82
-rw-r--r--src/SMAPI.Web/Framework/LogParsing/Models/LogMessage.cs38
-rw-r--r--src/SMAPI.Web/Framework/LogParsing/Models/LogModInfo.cs74
-rw-r--r--src/SMAPI.Web/Framework/LogParsing/Models/ParsedLog.cs20
-rw-r--r--src/SMAPI.Web/Framework/ModInfoModel.cs33
-rw-r--r--src/SMAPI.Web/Framework/ModSiteManager.cs56
-rw-r--r--src/SMAPI.Web/Framework/RedirectRules/RedirectHostsToUrlsRule.cs10
-rw-r--r--src/SMAPI.Web/Framework/RedirectRules/RedirectMatchRule.cs4
-rw-r--r--src/SMAPI.Web/Framework/RedirectRules/RedirectPathsToUrlsRule.cs4
-rw-r--r--src/SMAPI.Web/Framework/RedirectRules/RedirectToHttpsRule.cs6
-rw-r--r--src/SMAPI.Web/Framework/Storage/StorageProvider.cs54
-rw-r--r--src/SMAPI.Web/Framework/Storage/StoredFileInfo.cs39
-rw-r--r--src/SMAPI.Web/Framework/Storage/UploadResult.cs14
-rw-r--r--src/SMAPI.Web/Framework/VersionConstraint.cs4
57 files changed, 771 insertions, 385 deletions
diff --git a/src/SMAPI.Web/Framework/AllowLargePostsAttribute.cs b/src/SMAPI.Web/Framework/AllowLargePostsAttribute.cs
index 864aa215..bd414ea2 100644
--- a/src/SMAPI.Web/Framework/AllowLargePostsAttribute.cs
+++ b/src/SMAPI.Web/Framework/AllowLargePostsAttribute.cs
@@ -40,7 +40,7 @@ namespace StardewModdingAPI.Web.Framework
public void OnAuthorization(AuthorizationFilterContext context)
{
IFeatureCollection features = context.HttpContext.Features;
- IFormFeature formFeature = features.Get<IFormFeature>();
+ IFormFeature? formFeature = features.Get<IFormFeature>();
if (formFeature?.Form == null)
{
diff --git a/src/SMAPI.Web/Framework/Caching/Cached.cs b/src/SMAPI.Web/Framework/Caching/Cached.cs
index 52041a16..b393e1e1 100644
--- a/src/SMAPI.Web/Framework/Caching/Cached.cs
+++ b/src/SMAPI.Web/Framework/Caching/Cached.cs
@@ -10,21 +10,18 @@ namespace StardewModdingAPI.Web.Framework.Caching
** Accessors
*********/
/// <summary>The cached data.</summary>
- public T Data { get; set; }
+ public T Data { get; }
/// <summary>When the data was last updated.</summary>
- public DateTimeOffset LastUpdated { get; set; }
+ public DateTimeOffset LastUpdated { get; }
/// <summary>When the data was last requested through the mod API.</summary>
- public DateTimeOffset LastRequested { get; set; }
+ public DateTimeOffset LastRequested { get; internal set; }
/*********
** Public methods
*********/
- /// <summary>Construct an empty instance.</summary>
- public Cached() { }
-
/// <summary>Construct an instance.</summary>
/// <param name="data">The cached data.</param>
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 0d912c7b..fb74e9da 100644
--- a/src/SMAPI.Web/Framework/Caching/Mods/IModCacheRepository.cs
+++ b/src/SMAPI.Web/Framework/Caching/Mods/IModCacheRepository.cs
@@ -1,6 +1,6 @@
using System;
+using System.Diagnostics.CodeAnalysis;
using StardewModdingAPI.Toolkit.Framework.UpdateData;
-using StardewModdingAPI.Web.Framework.Clients;
namespace StardewModdingAPI.Web.Framework.Caching.Mods
{
@@ -15,7 +15,7 @@ namespace StardewModdingAPI.Web.Framework.Caching.Mods
/// <param name="id">The mod's unique ID within the <paramref name="site"/>.</param>
/// <param name="mod">The fetched mod.</param>
/// <param name="markRequested">Whether to update the mod's 'last requested' date.</param>
- bool TryGetMod(ModSiteKey site, string id, out Cached<IModPage> mod, bool markRequested = true);
+ bool TryGetMod(ModSiteKey site, string id, [NotNullWhen(true)] out Cached<IModPage>? mod, bool markRequested = true);
/// <summary>Save data fetched for a mod.</summary>
/// <param name="site">The mod site on which the mod is found.</param>
diff --git a/src/SMAPI.Web/Framework/Caching/Mods/ModCacheMemoryRepository.cs b/src/SMAPI.Web/Framework/Caching/Mods/ModCacheMemoryRepository.cs
index 9769793c..4ba0bd20 100644
--- a/src/SMAPI.Web/Framework/Caching/Mods/ModCacheMemoryRepository.cs
+++ b/src/SMAPI.Web/Framework/Caching/Mods/ModCacheMemoryRepository.cs
@@ -1,8 +1,8 @@
using System;
using System.Collections.Generic;
+using System.Diagnostics.CodeAnalysis;
using System.Linq;
using StardewModdingAPI.Toolkit.Framework.UpdateData;
-using StardewModdingAPI.Web.Framework.Clients;
namespace StardewModdingAPI.Web.Framework.Caching.Mods
{
@@ -24,7 +24,7 @@ namespace StardewModdingAPI.Web.Framework.Caching.Mods
/// <param name="id">The mod's unique ID within the <paramref name="site"/>.</param>
/// <param name="mod">The fetched mod.</param>
/// <param name="markRequested">Whether to update the mod's 'last requested' date.</param>
- public bool TryGetMod(ModSiteKey site, string id, out Cached<IModPage> mod, bool markRequested = true)
+ public bool TryGetMod(ModSiteKey site, string id, [NotNullWhen(true)] out Cached<IModPage>? 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 2ab7ea5a..b8a0df34 100644
--- a/src/SMAPI.Web/Framework/Caching/Wiki/IWikiCacheRepository.cs
+++ b/src/SMAPI.Web/Framework/Caching/Wiki/IWikiCacheRepository.cs
@@ -1,5 +1,6 @@
using System;
using System.Collections.Generic;
+using System.Diagnostics.CodeAnalysis;
using StardewModdingAPI.Toolkit.Framework.Clients.Wiki;
namespace StardewModdingAPI.Web.Framework.Caching.Wiki
@@ -12,16 +13,16 @@ namespace StardewModdingAPI.Web.Framework.Caching.Wiki
*********/
/// <summary>Get the cached wiki metadata.</summary>
/// <param name="metadata">The fetched metadata.</param>
- bool TryGetWikiMetadata(out Cached<WikiMetadata> metadata);
+ bool TryGetWikiMetadata([NotNullWhen(true)] out Cached<WikiMetadata>? metadata);
/// <summary>Get the cached wiki mods.</summary>
/// <param name="filter">A filter to apply, if any.</param>
- IEnumerable<Cached<WikiModEntry>> GetWikiMods(Func<WikiModEntry, bool> filter = null);
+ IEnumerable<Cached<WikiModEntry>> GetWikiMods(Func<WikiModEntry, bool>? filter = null);
/// <summary>Save data fetched from the wiki compatibility list.</summary>
/// <param name="stableVersion">The current stable Stardew Valley version.</param>
/// <param name="betaVersion">The current beta Stardew Valley version.</param>
/// <param name="mods">The mod data.</param>
- void SaveWikiData(string stableVersion, string betaVersion, IEnumerable<WikiModEntry> mods);
+ void SaveWikiData(string? stableVersion, string? betaVersion, IEnumerable<WikiModEntry> mods);
}
}
diff --git a/src/SMAPI.Web/Framework/Caching/Wiki/WikiCacheMemoryRepository.cs b/src/SMAPI.Web/Framework/Caching/Wiki/WikiCacheMemoryRepository.cs
index 064a7c3c..8b4338e2 100644
--- a/src/SMAPI.Web/Framework/Caching/Wiki/WikiCacheMemoryRepository.cs
+++ b/src/SMAPI.Web/Framework/Caching/Wiki/WikiCacheMemoryRepository.cs
@@ -1,5 +1,6 @@
using System;
using System.Collections.Generic;
+using System.Diagnostics.CodeAnalysis;
using System.Linq;
using StardewModdingAPI.Toolkit.Framework.Clients.Wiki;
@@ -12,10 +13,10 @@ namespace StardewModdingAPI.Web.Framework.Caching.Wiki
** Fields
*********/
/// <summary>The saved wiki metadata.</summary>
- private Cached<WikiMetadata> Metadata;
+ private Cached<WikiMetadata>? Metadata;
/// <summary>The cached wiki data.</summary>
- private Cached<WikiModEntry>[] Mods = new Cached<WikiModEntry>[0];
+ private Cached<WikiModEntry>[] Mods = Array.Empty<Cached<WikiModEntry>>();
/*********
@@ -23,7 +24,7 @@ namespace StardewModdingAPI.Web.Framework.Caching.Wiki
*********/
/// <summary>Get the cached wiki metadata.</summary>
/// <param name="metadata">The fetched metadata.</param>
- public bool TryGetWikiMetadata(out Cached<WikiMetadata> metadata)
+ public bool TryGetWikiMetadata([NotNullWhen(true)] out Cached<WikiMetadata>? metadata)
{
metadata = this.Metadata;
return metadata != null;
@@ -31,7 +32,7 @@ namespace StardewModdingAPI.Web.Framework.Caching.Wiki
/// <summary>Get the cached wiki mods.</summary>
/// <param name="filter">A filter to apply, if any.</param>
- public IEnumerable<Cached<WikiModEntry>> GetWikiMods(Func<WikiModEntry, bool> filter = null)
+ public IEnumerable<Cached<WikiModEntry>> GetWikiMods(Func<WikiModEntry, bool>? filter = null)
{
foreach (var mod in this.Mods)
{
@@ -44,7 +45,7 @@ namespace StardewModdingAPI.Web.Framework.Caching.Wiki
/// <param name="stableVersion">The current stable Stardew Valley version.</param>
/// <param name="betaVersion">The current beta Stardew Valley version.</param>
/// <param name="mods">The mod data.</param>
- public void SaveWikiData(string stableVersion, string betaVersion, IEnumerable<WikiModEntry> mods)
+ public void SaveWikiData(string? stableVersion, string? betaVersion, IEnumerable<WikiModEntry> mods)
{
this.Metadata = new Cached<WikiMetadata>(new WikiMetadata(stableVersion, betaVersion));
this.Mods = mods.Select(mod => new Cached<WikiModEntry>(mod)).ToArray();
diff --git a/src/SMAPI.Web/Framework/Caching/Wiki/WikiMetadata.cs b/src/SMAPI.Web/Framework/Caching/Wiki/WikiMetadata.cs
index c04de4a5..f53ea201 100644
--- a/src/SMAPI.Web/Framework/Caching/Wiki/WikiMetadata.cs
+++ b/src/SMAPI.Web/Framework/Caching/Wiki/WikiMetadata.cs
@@ -7,22 +7,19 @@ namespace StardewModdingAPI.Web.Framework.Caching.Wiki
** Accessors
*********/
/// <summary>The current stable Stardew Valley version.</summary>
- public string StableVersion { get; set; }
+ public string? StableVersion { get; }
/// <summary>The current beta Stardew Valley version.</summary>
- public string BetaVersion { get; set; }
+ public string? BetaVersion { get; }
/*********
** Public methods
*********/
/// <summary>Construct an instance.</summary>
- public WikiMetadata() { }
-
- /// <summary>Construct an instance.</summary>
/// <param name="stableVersion">The current stable Stardew Valley version.</param>
/// <param name="betaVersion">The current beta Stardew Valley version.</param>
- 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 b8b05878..ce0f1122 100644
--- a/src/SMAPI.Web/Framework/Clients/Chucklefish/ChucklefishClient.cs
+++ b/src/SMAPI.Web/Framework/Clients/Chucklefish/ChucklefishClient.cs
@@ -42,7 +42,7 @@ namespace StardewModdingAPI.Web.Framework.Clients.Chucklefish
/// <summary>Get update check info about a mod.</summary>
/// <param name="id">The mod ID.</param>
- public async Task<IModPage> GetModData(string id)
+ public async Task<IModPage?> GetModData(string id)
{
IModPage page = new GenericModPage(this.SiteKey, id);
@@ -51,14 +51,14 @@ 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
.GetAsync(string.Format(this.ModPageUrlFormat, parsedId))
.AsString();
}
- catch (ApiException ex) when (ex.Status == HttpStatusCode.NotFound || ex.Status == HttpStatusCode.Forbidden)
+ catch (ApiException ex) when (ex.Status is HttpStatusCode.NotFound or HttpStatusCode.Forbidden)
{
return page.SetError(RemoteModStatus.DoesNotExist, "Found no Chucklefish mod with this ID.");
}
@@ -67,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();
@@ -79,7 +79,7 @@ namespace StardewModdingAPI.Web.Framework.Clients.Chucklefish
/// <summary>Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources.</summary>
public void Dispose()
{
- this.Client?.Dispose();
+ this.Client.Dispose();
}
@@ -90,7 +90,7 @@ namespace StardewModdingAPI.Web.Framework.Clients.Chucklefish
/// <param name="id">The mod ID.</param>
private string GetModUrl(uint id)
{
- UriBuilder builder = new UriBuilder(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 d8008721..d351b42d 100644
--- a/src/SMAPI.Web/Framework/Clients/CurseForge/CurseForgeClient.cs
+++ b/src/SMAPI.Web/Framework/Clients/CurseForge/CurseForgeClient.cs
@@ -17,7 +17,7 @@ namespace StardewModdingAPI.Web.Framework.Clients.CurseForge
private readonly IClient Client;
/// <summary>A regex pattern which matches a version number in a CurseForge mod file name.</summary>
- private readonly Regex VersionInNamePattern = new Regex(@"^(?:.+? | *)v?(\d+\.\d+(?:\.\d+)?(?:-.+?)?) *(?:\.(?:zip|rar|7z))?$", RegexOptions.Compiled);
+ private readonly Regex VersionInNamePattern = new(@"^(?:.+? | *)v?(\d+\.\d+(?:\.\d+)?(?:-.+?)?) *(?:\.(?:zip|rar|7z))?$", RegexOptions.Compiled);
/*********
@@ -40,7 +40,7 @@ namespace StardewModdingAPI.Web.Framework.Clients.CurseForge
/// <summary>Get update check info about a mod.</summary>
/// <param name="id">The mod ID.</param>
- public async Task<IModPage> GetModData(string id)
+ public async Task<IModPage?> GetModData(string id)
{
IModPage page = new GenericModPage(this.SiteKey, id);
@@ -49,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<ModModel>();
+ .As<ModModel?>();
if (mod == null)
return page.SetError(RemoteModStatus.DoesNotExist, "Found no CurseForge mod with this ID.");
@@ -71,7 +71,7 @@ namespace StardewModdingAPI.Web.Framework.Clients.CurseForge
/// <summary>Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources.</summary>
public void Dispose()
{
- this.Client?.Dispose();
+ this.Client.Dispose();
}
@@ -80,9 +80,9 @@ namespace StardewModdingAPI.Web.Framework.Clients.CurseForge
*********/
/// <summary>Get a raw version string for a mod file, if available.</summary>
/// <param name="file">The file whose version to get.</param>
- 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 9de74847..e9adcf20 100644
--- a/src/SMAPI.Web/Framework/Clients/CurseForge/ResponseModels/ModFileModel.cs
+++ b/src/SMAPI.Web/Framework/Clients/CurseForge/ResponseModels/ModFileModel.cs
@@ -3,10 +3,26 @@ namespace StardewModdingAPI.Web.Framework.Clients.CurseForge.ResponseModels
/// <summary>Metadata from the CurseForge API about a mod file.</summary>
public class ModFileModel
{
+ /*********
+ ** Accessors
+ *********/
/// <summary>The file name as downloaded.</summary>
- public string FileName { get; set; }
+ public string FileName { get; }
/// <summary>The file display name.</summary>
- public string DisplayName { get; set; }
+ public string? DisplayName { get; }
+
+
+ /*********
+ ** Public methods
+ *********/
+ /// <summary>Construct an instance.</summary>
+ /// <param name="fileName">The file name as downloaded.</param>
+ /// <param name="displayName">The file display name.</param>
+ 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 48cd185b..fd7796f2 100644
--- a/src/SMAPI.Web/Framework/Clients/CurseForge/ResponseModels/ModModel.cs
+++ b/src/SMAPI.Web/Framework/Clients/CurseForge/ResponseModels/ModModel.cs
@@ -3,16 +3,36 @@ namespace StardewModdingAPI.Web.Framework.Clients.CurseForge.ResponseModels
/// <summary>An mod from the CurseForge API.</summary>
public class ModModel
{
+ /*********
+ ** Accessors
+ *********/
/// <summary>The mod's unique ID on CurseForge.</summary>
- public int ID { get; set; }
+ public int ID { get; }
/// <summary>The mod name.</summary>
- public string Name { get; set; }
+ public string Name { get; }
/// <summary>The web URL for the mod page.</summary>
- public string WebsiteUrl { get; set; }
+ public string WebsiteUrl { get; }
/// <summary>The available file downloads.</summary>
- public ModFileModel[] LatestFiles { get; set; }
+ public ModFileModel[] LatestFiles { get; }
+
+
+ /*********
+ ** Public methods
+ *********/
+ /// <summary>Construct an instance.</summary>
+ /// <param name="id">The mod's unique ID on CurseForge.</param>
+ /// <param name="name">The mod name.</param>
+ /// <param name="websiteUrl">The web URL for the mod page.</param>
+ /// <param name="latestFiles">The available file downloads.</param>
+ 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 f08b471c..548f17c3 100644
--- a/src/SMAPI.Web/Framework/Clients/GenericModDownload.cs
+++ b/src/SMAPI.Web/Framework/Clients/GenericModDownload.cs
@@ -7,26 +7,23 @@ namespace StardewModdingAPI.Web.Framework.Clients
** Accessors
*********/
/// <summary>The download's display name.</summary>
- public string Name { get; set; }
+ public string Name { get; }
/// <summary>The download's description.</summary>
- public string Description { get; set; }
+ public string? Description { get; }
/// <summary>The download's file version.</summary>
- public string Version { get; set; }
+ public string? Version { get; }
/*********
** Public methods
*********/
- /// <summary>Construct an empty instance.</summary>
- public GenericModDownload() { }
-
/// <summary>Construct an instance.</summary>
/// <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>
- 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 622e6c56..5353c7e1 100644
--- a/src/SMAPI.Web/Framework/Clients/GenericModPage.cs
+++ b/src/SMAPI.Web/Framework/Clients/GenericModPage.cs
@@ -1,4 +1,6 @@
+using System;
using System.Collections.Generic;
+using System.Diagnostics.CodeAnalysis;
using System.Linq;
using StardewModdingAPI.Toolkit.Framework.UpdateData;
@@ -17,30 +19,31 @@ namespace StardewModdingAPI.Web.Framework.Clients
public string Id { get; set; }
/// <summary>The mod name.</summary>
- public string Name { get; set; }
+ public string? Name { get; set; }
/// <summary>The mod's semantic version number.</summary>
- public string Version { get; set; }
+ public string? Version { get; set; }
/// <summary>The mod's web URL.</summary>
- public string Url { get; set; }
+ public string? Url { get; set; }
/// <summary>The mod downloads.</summary>
- public IModDownload[] Downloads { get; set; } = new IModDownload[0];
+ public IModDownload[] Downloads { get; set; } = Array.Empty<IModDownload>();
/// <summary>The mod availability status on the remote site.</summary>
- public RemoteModStatus Status { get; set; } = RemoteModStatus.Ok;
+ public RemoteModStatus Status { get; set; } = RemoteModStatus.InvalidData;
/// <summary>A user-friendly error which indicates why fetching the mod info failed (if applicable).</summary>
- public string Error { get; set; }
+ public string? Error { get; set; }
+
+ /// <summary>Whether the mod data is valid.</summary>
+ [MemberNotNullWhen(true, nameof(IModPage.Name), nameof(IModPage.Url))]
+ public bool IsValid => this.Status == RemoteModStatus.Ok;
/*********
** Public methods
*********/
- /// <summary>Construct an empty instance.</summary>
- public GenericModPage() { }
-
/// <summary>Construct an instance.</summary>
/// <param name="site">The mod site containing the mod.</param>
/// <param name="id">The mod's unique ID within the site.</param>
@@ -55,12 +58,13 @@ namespace StardewModdingAPI.Web.Framework.Clients
/// <param name="version">The mod's semantic version number.</param>
/// <param name="url">The mod's web URL.</param>
/// <param name="downloads">The mod downloads.</param>
- public IModPage SetInfo(string name, string version, string url, IEnumerable<IModDownload> downloads)
+ public IModPage SetInfo(string name, string? version, string url, IEnumerable<IModDownload> 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 73ce4025..dbce9368 100644
--- a/src/SMAPI.Web/Framework/Clients/GitHub/GitAsset.cs
+++ b/src/SMAPI.Web/Framework/Clients/GitHub/GitAsset.cs
@@ -5,16 +5,34 @@ namespace StardewModdingAPI.Web.Framework.Clients.GitHub
/// <summary>A GitHub download attached to a release.</summary>
internal class GitAsset
{
+ /*********
+ ** Accessors
+ *********/
/// <summary>The file name.</summary>
[JsonProperty("name")]
- public string FileName { get; set; }
+ public string FileName { get; }
/// <summary>The file content type.</summary>
[JsonProperty("content_type")]
- public string ContentType { get; set; }
+ public string ContentType { get; }
/// <summary>The download URL.</summary>
[JsonProperty("browser_download_url")]
- public string DownloadUrl { get; set; }
+ public string DownloadUrl { get; }
+
+
+ /*********
+ ** Public methods
+ *********/
+ /// <summary>Construct an instance.</summary>
+ /// <param name="fileName">The file name.</param>
+ /// <param name="contentType">The file content type.</param>
+ /// <param name="downloadUrl">The download URL.</param>
+ 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 671f077c..785979a5 100644
--- a/src/SMAPI.Web/Framework/Clients/GitHub/GitHubClient.cs
+++ b/src/SMAPI.Web/Framework/Clients/GitHub/GitHubClient.cs
@@ -33,26 +33,26 @@ namespace StardewModdingAPI.Web.Framework.Clients.GitHub
/// <param name="acceptHeader">The Accept header value expected by the GitHub API.</param>
/// <param name="username">The username with which to authenticate to the GitHub API.</param>
/// <param name="password">The password with which to authenticate to the GitHub API.</param>
- 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!);
}
/// <summary>Get basic metadata for a GitHub repository, if available.</summary>
/// <param name="repo">The repository key (like <c>Pathoschild/SMAPI</c>).</param>
/// <returns>Returns the repository info if it exists, else <c>null</c>.</returns>
- public async Task<GitRepo> GetRepositoryAsync(string repo)
+ public async Task<GitRepo?> GetRepositoryAsync(string repo)
{
this.AssertKeyFormat(repo);
try
{
return await this.Client
.GetAsync($"repos/{repo}")
- .As<GitRepo>();
+ .As<GitRepo?>();
}
catch (ApiException ex) when (ex.Status == HttpStatusCode.NotFound)
{
@@ -64,7 +64,7 @@ namespace StardewModdingAPI.Web.Framework.Clients.GitHub
/// <param name="repo">The repository key (like <c>Pathoschild/SMAPI</c>).</param>
/// <param name="includePrerelease">Whether to return a prerelease version if it's latest.</param>
/// <returns>Returns the release if found, else <c>null</c>.</returns>
- public async Task<GitRelease> GetLatestReleaseAsync(string repo, bool includePrerelease = false)
+ public async Task<GitRelease?> GetLatestReleaseAsync(string repo, bool includePrerelease = false)
{
this.AssertKeyFormat(repo);
try
@@ -79,7 +79,7 @@ namespace StardewModdingAPI.Web.Framework.Clients.GitHub
return await this.Client
.GetAsync($"repos/{repo}/releases/latest")
- .As<GitRelease>();
+ .As<GitRelease?>();
}
catch (ApiException ex) when (ex.Status == HttpStatusCode.NotFound)
{
@@ -89,7 +89,7 @@ namespace StardewModdingAPI.Web.Framework.Clients.GitHub
/// <summary>Get update check info about a mod.</summary>
/// <param name="id">The mod ID.</param>
- public async Task<IModPage> GetModData(string id)
+ public async Task<IModPage?> GetModData(string id)
{
IModPage page = new GenericModPage(this.SiteKey, id);
@@ -97,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);
@@ -116,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;
@@ -127,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
@@ -138,7 +138,7 @@ namespace StardewModdingAPI.Web.Framework.Clients.GitHub
/// <summary>Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources.</summary>
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 736efbe6..24d6c3c5 100644
--- a/src/SMAPI.Web/Framework/Clients/GitHub/GitLicense.cs
+++ b/src/SMAPI.Web/Framework/Clients/GitHub/GitLicense.cs
@@ -5,16 +5,34 @@ namespace StardewModdingAPI.Web.Framework.Clients.GitHub
/// <summary>The license info for a GitHub project.</summary>
internal class GitLicense
{
+ /*********
+ ** Accessors
+ *********/
/// <summary>The license display name.</summary>
[JsonProperty("name")]
- public string Name { get; set; }
+ public string Name { get; }
/// <summary>The SPDX ID for the license.</summary>
[JsonProperty("spdx_id")]
- public string SpdxId { get; set; }
+ public string SpdxId { get; }
/// <summary>The URL for the license info.</summary>
[JsonProperty("url")]
- public string Url { get; set; }
+ public string Url { get; }
+
+
+ /*********
+ ** Public methods
+ *********/
+ /// <summary>Construct an instance.</summary>
+ /// <param name="name">The license display name.</param>
+ /// <param name="spdxId">The SPDX ID for the license.</param>
+ /// <param name="url">The URL for the license info.</param>
+ 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 d0db5297..9de6f020 100644
--- a/src/SMAPI.Web/Framework/Clients/GitHub/GitRelease.cs
+++ b/src/SMAPI.Web/Framework/Clients/GitHub/GitRelease.cs
@@ -1,3 +1,4 @@
+using System;
using Newtonsoft.Json;
namespace StardewModdingAPI.Web.Framework.Clients.GitHub
@@ -10,24 +11,45 @@ namespace StardewModdingAPI.Web.Framework.Clients.GitHub
*********/
/// <summary>The display name.</summary>
[JsonProperty("name")]
- public string Name { get; set; }
+ public string Name { get; }
/// <summary>The semantic version string.</summary>
[JsonProperty("tag_name")]
- public string Tag { get; set; }
+ public string Tag { get; }
/// <summary>The Markdown description for the release.</summary>
- public string Body { get; set; }
+ public string Body { get; internal set; }
/// <summary>Whether this is a draft version.</summary>
[JsonProperty("draft")]
- public bool IsDraft { get; set; }
+ public bool IsDraft { get; }
/// <summary>Whether this is a prerelease version.</summary>
[JsonProperty("prerelease")]
- public bool IsPrerelease { get; set; }
+ public bool IsPrerelease { get; }
/// <summary>The attached files.</summary>
- public GitAsset[] Assets { get; set; }
+ public GitAsset[] Assets { get; }
+
+
+ /*********
+ ** Public methods
+ *********/
+ /// <summary>Construct an instance.</summary>
+ /// <param name="name">The display name.</param>
+ /// <param name="tag">The semantic version string.</param>
+ /// <param name="body">The Markdown description for the release.</param>
+ /// <param name="isDraft">Whether this is a draft version.</param>
+ /// <param name="isPrerelease">Whether this is a prerelease version.</param>
+ /// <param name="assets">The attached files.</param>
+ 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<GitAsset>();
+ }
}
}
diff --git a/src/SMAPI.Web/Framework/Clients/GitHub/GitRepo.cs b/src/SMAPI.Web/Framework/Clients/GitHub/GitRepo.cs
index 7d80576e..879b5e49 100644
--- a/src/SMAPI.Web/Framework/Clients/GitHub/GitRepo.cs
+++ b/src/SMAPI.Web/Framework/Clients/GitHub/GitRepo.cs
@@ -5,16 +5,34 @@ namespace StardewModdingAPI.Web.Framework.Clients.GitHub
/// <summary>Basic metadata about a GitHub project.</summary>
internal class GitRepo
{
+ /*********
+ ** Accessors
+ *********/
/// <summary>The full repository name, including the owner.</summary>
[JsonProperty("full_name")]
- public string FullName { get; set; }
+ public string FullName { get; }
/// <summary>The URL to the repository web page, if any.</summary>
[JsonProperty("html_url")]
- public string WebUrl { get; set; }
+ public string? WebUrl { get; }
/// <summary>The code license, if any.</summary>
[JsonProperty("license")]
- public GitLicense License { get; set; }
+ public GitLicense? License { get; }
+
+
+ /*********
+ ** Public methods
+ *********/
+ /// <summary>Construct an instance.</summary>
+ /// <param name="fullName">The full repository name, including the owner.</param>
+ /// <param name="webUrl">The URL to the repository web page, if any.</param>
+ /// <param name="license">The code license, if any.</param>
+ 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 0d6f4643..886e32d3 100644
--- a/src/SMAPI.Web/Framework/Clients/GitHub/IGitHubClient.cs
+++ b/src/SMAPI.Web/Framework/Clients/GitHub/IGitHubClient.cs
@@ -12,12 +12,12 @@ namespace StardewModdingAPI.Web.Framework.Clients.GitHub
/// <summary>Get basic metadata for a GitHub repository, if available.</summary>
/// <param name="repo">The repository key (like <c>Pathoschild/SMAPI</c>).</param>
/// <returns>Returns the repository info if it exists, else <c>null</c>.</returns>
- Task<GitRepo> GetRepositoryAsync(string repo);
+ Task<GitRepo?> GetRepositoryAsync(string repo);
/// <summary>Get the latest release for a GitHub repository.</summary>
/// <param name="repo">The repository key (like <c>Pathoschild/SMAPI</c>).</param>
/// <param name="includePrerelease">Whether to return a prerelease version if it's latest.</param>
/// <returns>Returns the release if found, else <c>null</c>.</returns>
- Task<GitRelease> GetLatestReleaseAsync(string repo, bool includePrerelease = false);
+ Task<GitRelease?> 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 33277711..3697ffae 100644
--- a/src/SMAPI.Web/Framework/Clients/IModSiteClient.cs
+++ b/src/SMAPI.Web/Framework/Clients/IModSiteClient.cs
@@ -18,6 +18,6 @@ namespace StardewModdingAPI.Web.Framework.Clients
*********/
/// <summary>Get update check info about a mod.</summary>
/// <param name="id">The mod ID.</param>
- Task<IModPage> GetModData(string id);
+ Task<IModPage?> GetModData(string id);
}
}
diff --git a/src/SMAPI.Web/Framework/Clients/ModDrop/ModDropClient.cs b/src/SMAPI.Web/Framework/Clients/ModDrop/ModDropClient.cs
index 3a1c5b9d..c60b2c90 100644
--- a/src/SMAPI.Web/Framework/Clients/ModDrop/ModDropClient.cs
+++ b/src/SMAPI.Web/Framework/Clients/ModDrop/ModDropClient.cs
@@ -1,4 +1,5 @@
using System.Collections.Generic;
+using System.Diagnostics.CodeAnalysis;
using System.Threading.Tasks;
using Pathoschild.Http.Client;
using StardewModdingAPI.Toolkit.Framework.UpdateData;
@@ -41,9 +42,10 @@ namespace StardewModdingAPI.Web.Framework.Clients.ModDrop
/// <summary>Get update check info about a mod.</summary>
/// <param name="id">The mod ID.</param>
- public async Task<IModPage> GetModData(string id)
+ [SuppressMessage("ReSharper", "ConstantConditionalAccessQualifier", Justification = "The nullability is validated in this method.")]
+ public async Task<IModPage?> 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.");
@@ -58,9 +60,11 @@ namespace StardewModdingAPI.Web.Framework.Clients.ModDrop
Mods = true
})
.As<ModListModel>();
- 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<IModDownload>();
@@ -75,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);
}
@@ -83,7 +87,7 @@ namespace StardewModdingAPI.Web.Framework.Clients.ModDrop
/// <summary>Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources.</summary>
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 b01196f4..31905338 100644
--- a/src/SMAPI.Web/Framework/Clients/ModDrop/ResponseModels/FileDataModel.cs
+++ b/src/SMAPI.Web/Framework/Clients/ModDrop/ResponseModels/FileDataModel.cs
@@ -5,27 +5,53 @@ namespace StardewModdingAPI.Web.Framework.Clients.ModDrop.ResponseModels
/// <summary>Metadata from the ModDrop API about a mod file.</summary>
public class FileDataModel
{
+ /*********
+ ** Accessors
+ *********/
/// <summary>The file title.</summary>
[JsonProperty("title")]
- public string Name { get; set; }
+ public string Name { get; }
/// <summary>The file description.</summary>
[JsonProperty("desc")]
- public string Description { get; set; }
+ public string Description { get; }
/// <summary>The file version.</summary>
- public string Version { get; set; }
+ public string Version { get; }
/// <summary>Whether the file is deleted.</summary>
- public bool IsDeleted { get; set; }
+ public bool IsDeleted { get; }
/// <summary>Whether the file is hidden from users.</summary>
- public bool IsHidden { get; set; }
+ public bool IsHidden { get; }
/// <summary>Whether this is the default file for the mod.</summary>
- public bool IsDefault { get; set; }
+ public bool IsDefault { get; }
/// <summary>Whether this is an archived file.</summary>
- public bool IsOld { get; set; }
+ public bool IsOld { get; }
+
+
+ /*********
+ ** Public methods
+ *********/
+ /// <summary>Construct an instance.</summary>
+ /// <param name="name">The file title.</param>
+ /// <param name="description">The file description.</param>
+ /// <param name="version">The file version.</param>
+ /// <param name="isDeleted">Whether the file is deleted.</param>
+ /// <param name="isHidden">Whether the file is hidden from users.</param>
+ /// <param name="isDefault">Whether this is the default file for the mod.</param>
+ /// <param name="isOld">Whether this is an archived file.</param>
+ 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 cfdd6a4e..0654b576 100644
--- a/src/SMAPI.Web/Framework/Clients/ModDrop/ResponseModels/ModDataModel.cs
+++ b/src/SMAPI.Web/Framework/Clients/ModDrop/ResponseModels/ModDataModel.cs
@@ -3,13 +3,31 @@ namespace StardewModdingAPI.Web.Framework.Clients.ModDrop.ResponseModels
/// <summary>Metadata about a mod from the ModDrop API.</summary>
public class ModDataModel
{
+ /*********
+ ** Accessors
+ *********/
/// <summary>The mod's unique ID on ModDrop.</summary>
public int ID { get; set; }
+ /// <summary>The mod name.</summary>
+ public string Title { get; set; }
+
/// <summary>The error code, if any.</summary>
public int? ErrorCode { get; set; }
- /// <summary>The mod name.</summary>
- public string Title { get; set; }
+
+ /*********
+ ** Public methods
+ *********/
+ /// <summary>Construct an instance.</summary>
+ /// <param name="id">The mod's unique ID on ModDrop.</param>
+ /// <param name="title">The mod name.</param>
+ /// <param name="errorCode">The error code, if any.</param>
+ 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 7f692ca1..cb4be35c 100644
--- a/src/SMAPI.Web/Framework/Clients/ModDrop/ResponseModels/ModListModel.cs
+++ b/src/SMAPI.Web/Framework/Clients/ModDrop/ResponseModels/ModListModel.cs
@@ -5,7 +5,10 @@ namespace StardewModdingAPI.Web.Framework.Clients.ModDrop.ResponseModels
/// <summary>A list of mods from the ModDrop API.</summary>
public class ModListModel
{
+ /*********
+ ** Accessors
+ *********/
/// <summary>The mod data.</summary>
- public IDictionary<long, ModModel> Mods { get; set; }
+ public IDictionary<long, ModModel> Mods { get; } = new Dictionary<long, ModModel>();
}
}
diff --git a/src/SMAPI.Web/Framework/Clients/ModDrop/ResponseModels/ModModel.cs b/src/SMAPI.Web/Framework/Clients/ModDrop/ResponseModels/ModModel.cs
index 9f4b2c6f..60b818d6 100644
--- a/src/SMAPI.Web/Framework/Clients/ModDrop/ResponseModels/ModModel.cs
+++ b/src/SMAPI.Web/Framework/Clients/ModDrop/ResponseModels/ModModel.cs
@@ -3,10 +3,26 @@ namespace StardewModdingAPI.Web.Framework.Clients.ModDrop.ResponseModels
/// <summary>An entry in a mod list from the ModDrop API.</summary>
public class ModModel
{
+ /*********
+ ** Accessors
+ *********/
/// <summary>The available file downloads.</summary>
- public FileDataModel[] Files { get; set; }
+ public FileDataModel[] Files { get; }
/// <summary>The mod metadata.</summary>
- public ModDataModel Mod { get; set; }
+ public ModDataModel Mod { get; }
+
+
+ /*********
+ ** Public methods
+ *********/
+ /// <summary>Construct an instance.</summary>
+ /// <param name="files">The available file downloads.</param>
+ /// <param name="mod">The mod metadata.</param>
+ public ModModel(FileDataModel[] files, ModDataModel mod)
+ {
+ this.Files = files;
+ this.Mod = mod;
+ }
}
}
diff --git a/src/SMAPI.Web/Framework/Clients/Nexus/DisabledNexusClient.cs b/src/SMAPI.Web/Framework/Clients/Nexus/DisabledNexusClient.cs
new file mode 100644
index 00000000..6edd5f64
--- /dev/null
+++ b/src/SMAPI.Web/Framework/Clients/Nexus/DisabledNexusClient.cs
@@ -0,0 +1,31 @@
+using System.Threading.Tasks;
+using StardewModdingAPI.Toolkit.Framework.UpdateData;
+
+namespace StardewModdingAPI.Web.Framework.Clients.Nexus
+{
+ /// <summary>A client for the Nexus website which does nothing, used for local development.</summary>
+ internal class DisabledNexusClient : INexusClient
+ {
+ /*********
+ ** Accessors
+ *********/
+ /// <inheritdoc />
+ public ModSiteKey SiteKey => ModSiteKey.Nexus;
+
+
+ /*********
+ ** Public methods
+ *********/
+ /// <summary>Get update check info about a mod.</summary>
+ /// <param name="id">The mod ID.</param>
+ public Task<IModPage?> GetModData(string id)
+ {
+ return Task.FromResult<IModPage?>(
+ new GenericModPage(ModSiteKey.Nexus, id).SetError(RemoteModStatus.TemporaryError, "The Nexus client is currently disabled due to the configuration.")
+ );
+ }
+
+ /// <inheritdoc />
+ public void Dispose() { }
+ }
+}
diff --git a/src/SMAPI.Web/Framework/Clients/Nexus/NexusClient.cs b/src/SMAPI.Web/Framework/Clients/Nexus/NexusClient.cs
index 4ba94f81..23b25f95 100644
--- a/src/SMAPI.Web/Framework/Clients/Nexus/NexusClient.cs
+++ b/src/SMAPI.Web/Framework/Clients/Nexus/NexusClient.cs
@@ -59,7 +59,7 @@ namespace StardewModdingAPI.Web.Framework.Clients.Nexus
/// <summary>Get update check info about a mod.</summary>
/// <param name="id">The mod ID.</param>
- public async Task<IModPage> GetModData(string id)
+ public async Task<IModPage?> GetModData(string id)
{
IModPage page = new GenericModPage(this.SiteKey, id);
@@ -70,25 +70,25 @@ 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);
// page doesn't exist
- if (mod == null || mod.Status == NexusModStatus.Hidden || mod.Status == NexusModStatus.NotPublished)
+ if (mod == null || mod.Status is NexusModStatus.Hidden or NexusModStatus.NotPublished)
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;
}
/// <summary>Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources.</summary>
public void Dispose()
{
- this.WebClient?.Dispose();
+ this.WebClient.Dispose();
}
@@ -98,7 +98,7 @@ namespace StardewModdingAPI.Web.Framework.Clients.Nexus
/// <summary>Get metadata about a mod by scraping the Nexus website.</summary>
/// <param name="id">The Nexus mod ID.</param>
/// <returns>Returns the mod info if found, else <c>null</c>.</returns>
- private async Task<NexusMod> GetModFromWebsiteAsync(uint id)
+ private async Task<NexusMod?> GetModFromWebsiteAsync(uint id)
{
// fetch HTML
string html;
@@ -114,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<IModDownload>();
- 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")
@@ -152,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 <dd> 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 <dd> tag; derived from https://stackoverflow.com/a/25535623/262123
downloads.Add(
new GenericModDownload(fileName, description, fileVersion)
@@ -161,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()
+ );
}
/// <summary>Get metadata about a mod from the Nexus API.</summary>
@@ -180,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()
- };
+ );
}
/// <summary>Get the full mod page URL for a given ID.</summary>
/// <param name="id">The mod ID.</param>
private string GetModUrl(uint id)
{
- UriBuilder builder = new UriBuilder(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 aef90ede..3155cfda 100644
--- a/src/SMAPI.Web/Framework/Clients/Nexus/ResponseModels/NexusMod.cs
+++ b/src/SMAPI.Web/Framework/Clients/Nexus/ResponseModels/NexusMod.cs
@@ -1,3 +1,4 @@
+using System;
using Newtonsoft.Json;
namespace StardewModdingAPI.Web.Framework.Clients.Nexus.ResponseModels
@@ -9,25 +10,53 @@ namespace StardewModdingAPI.Web.Framework.Clients.Nexus.ResponseModels
** Accessors
*********/
/// <summary>The mod name.</summary>
- public string Name { get; set; }
+ public string? Name { get; }
/// <summary>The mod's semantic version number.</summary>
- public string Version { get; set; }
+ public string? Version { get; }
/// <summary>The mod's web URL.</summary>
[JsonProperty("mod_page_uri")]
- public string Url { get; set; }
+ public string? Url { get; }
/// <summary>The mod's publication status.</summary>
[JsonIgnore]
- public NexusModStatus Status { get; set; } = NexusModStatus.Ok;
+ public NexusModStatus Status { get; }
/// <summary>The files available to download.</summary>
[JsonIgnore]
- public IModDownload[] Downloads { get; set; }
+ public IModDownload[] Downloads { get; }
/// <summary>A custom user-friendly error which indicates why fetching the mod info failed (if applicable).</summary>
[JsonIgnore]
- public string Error { get; set; }
+ public string? Error { get; }
+
+
+ /*********
+ ** Public methods
+ *********/
+ /// <summary>Construct an instance.</summary>
+ /// <param name="name">The mod name</param>
+ /// <param name="version">The mod's semantic version number.</param>
+ /// <param name="url">The mod's web URL.</param>
+ /// <param name="downloads">The files available to download.</param>
+ 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;
+ }
+
+ /// <summary>Construct an instance.</summary>
+ /// <param name="status">The mod's publication status.</param>
+ /// <param name="error">A custom user-friendly error which indicates why fetching the mod info failed (if applicable).</param>
+ public NexusMod(NexusModStatus status, string error)
+ {
+ this.Status = status;
+ this.Error = error;
+ this.Downloads = Array.Empty<IModDownload>();
+ }
}
}
diff --git a/src/SMAPI.Web/Framework/Clients/Pastebin/PasteInfo.cs b/src/SMAPI.Web/Framework/Clients/Pastebin/PasteInfo.cs
index 813ea115..7f40e713 100644
--- a/src/SMAPI.Web/Framework/Clients/Pastebin/PasteInfo.cs
+++ b/src/SMAPI.Web/Framework/Clients/Pastebin/PasteInfo.cs
@@ -1,15 +1,35 @@
+using System.Diagnostics.CodeAnalysis;
+
namespace StardewModdingAPI.Web.Framework.Clients.Pastebin
{
/// <summary>The response for a get-paste request.</summary>
internal class PasteInfo
{
+ /*********
+ ** Accessors
+ *********/
/// <summary>Whether the log was successfully fetched.</summary>
- public bool Success { get; set; }
+ [MemberNotNullWhen(true, nameof(PasteInfo.Content))]
+ [MemberNotNullWhen(false, nameof(PasteInfo.Error))]
+ public bool Success => this.Error == null || this.Content != null;
/// <summary>The fetched paste content (if <see cref="Success"/> is <c>true</c>).</summary>
- public string Content { get; set; }
+ public string? Content { get; internal set; }
+
+ /// <summary>The error message (if <see cref="Success"/> is <c>false</c>).</summary>
+ public string? Error { get; }
+
- /// <summary>The error message if saving failed.</summary>
- public string Error { get; set; }
+ /*********
+ ** Public methods
+ *********/
+ /// <summary>Construct an instance.</summary>
+ /// <param name="content">The fetched paste content.</param>
+ /// <param name="error">The error message, if it failed.</param>
+ public PasteInfo(string? content, string? error)
+ {
+ this.Content = content;
+ this.Error = error;
+ }
}
}
diff --git a/src/SMAPI.Web/Framework/Clients/Pastebin/PastebinClient.cs b/src/SMAPI.Web/Framework/Clients/Pastebin/PastebinClient.cs
index 1be00be7..0e00f071 100644
--- a/src/SMAPI.Web/Framework/Clients/Pastebin/PastebinClient.cs
+++ b/src/SMAPI.Web/Framework/Clients/Pastebin/PastebinClient.cs
@@ -33,24 +33,24 @@ namespace StardewModdingAPI.Web.Framework.Clients.Pastebin
try
{
// get from API
- string content = await this.Client
+ string? content = await this.Client
.GetAsync($"raw/{id}")
.AsString();
// handle Pastebin errors
if (string.IsNullOrWhiteSpace(content))
- return new PasteInfo { Error = "Received an empty response from Pastebin." };
+ return new PasteInfo(null, "Received an empty response from Pastebin.");
if (content.StartsWith("<!DOCTYPE"))
- return new PasteInfo { Error = $"Received a captcha challenge from Pastebin. Please visit https://pastebin.com/{id} in a new window to solve it." };
- return new PasteInfo { Success = true, Content = content };
+ return new PasteInfo(null, $"Received a captcha challenge from Pastebin. Please visit https://pastebin.com/{id} in a new window to solve it.");
+ return new PasteInfo(content, null);
}
catch (ApiException ex) when (ex.Status == HttpStatusCode.NotFound)
{
- return new PasteInfo { Error = "There's no log with that ID." };
+ return new PasteInfo(null, "There's no log with that ID.");
}
catch (Exception ex)
{
- return new PasteInfo { Error = $"Pastebin error: {ex}" };
+ return new PasteInfo(null, $"Pastebin error: {ex}");
}
}
diff --git a/src/SMAPI.Web/Framework/Compression/GzipHelper.cs b/src/SMAPI.Web/Framework/Compression/GzipHelper.cs
index 676d660d..e7a2df13 100644
--- a/src/SMAPI.Web/Framework/Compression/GzipHelper.cs
+++ b/src/SMAPI.Web/Framework/Compression/GzipHelper.cs
@@ -1,4 +1,5 @@
using System;
+using System.Diagnostics.CodeAnalysis;
using System.IO;
using System.IO.Compression;
using System.Text;
@@ -29,9 +30,9 @@ namespace StardewModdingAPI.Web.Framework.Compression
// compressed
byte[] compressedData;
- using (MemoryStream stream = new MemoryStream())
+ using (MemoryStream stream = new())
{
- using (GZipStream zipStream = new GZipStream(stream, CompressionLevel.Optimal, leaveOpen: true))
+ using (GZipStream zipStream = new(stream, CompressionLevel.Optimal, leaveOpen: true))
zipStream.Write(buffer, 0, buffer.Length);
stream.Position = 0;
@@ -51,8 +52,12 @@ namespace StardewModdingAPI.Web.Framework.Compression
/// <summary>Decompress a string.</summary>
/// <param name="rawText">The compressed text.</param>
/// <remarks>Derived from <a href="https://stackoverflow.com/a/17993002/262123"/>.</remarks>
- public string DecompressString(string rawText)
+ [return: NotNullIfNotNull("rawText")]
+ public string? DecompressString(string? rawText)
{
+ if (rawText is null)
+ return rawText;
+
// get raw bytes
byte[] zipBuffer;
try
@@ -69,7 +74,7 @@ namespace StardewModdingAPI.Web.Framework.Compression
return rawText;
// decompress
- using MemoryStream memoryStream = new MemoryStream();
+ using MemoryStream memoryStream = new();
{
// read length prefix
int dataLength = BitConverter.ToInt32(zipBuffer, 0);
@@ -78,7 +83,7 @@ namespace StardewModdingAPI.Web.Framework.Compression
// read data
byte[] buffer = new byte[dataLength];
memoryStream.Position = 0;
- using (GZipStream gZipStream = new GZipStream(memoryStream, CompressionMode.Decompress))
+ using (GZipStream gZipStream = new(memoryStream, CompressionMode.Decompress))
gZipStream.Read(buffer, 0, buffer.Length);
// return original string
diff --git a/src/SMAPI.Web/Framework/Compression/IGzipHelper.cs b/src/SMAPI.Web/Framework/Compression/IGzipHelper.cs
index a000865e..ef2d5696 100644
--- a/src/SMAPI.Web/Framework/Compression/IGzipHelper.cs
+++ b/src/SMAPI.Web/Framework/Compression/IGzipHelper.cs
@@ -1,3 +1,5 @@
+using System.Diagnostics.CodeAnalysis;
+
namespace StardewModdingAPI.Web.Framework.Compression
{
/// <summary>Handles GZip compression logic.</summary>
@@ -12,6 +14,7 @@ namespace StardewModdingAPI.Web.Framework.Compression
/// <summary>Decompress a string.</summary>
/// <param name="rawText">The compressed text.</param>
- string DecompressString(string rawText);
+ [return: NotNullIfNotNull("rawText")]
+ string? DecompressString(string? rawText);
}
}
diff --git a/src/SMAPI.Web/Framework/ConfigModels/ApiClientsConfig.cs b/src/SMAPI.Web/Framework/ConfigModels/ApiClientsConfig.cs
index 878130bf..b582b2b0 100644
--- a/src/SMAPI.Web/Framework/ConfigModels/ApiClientsConfig.cs
+++ b/src/SMAPI.Web/Framework/ConfigModels/ApiClientsConfig.cs
@@ -10,17 +10,17 @@ namespace StardewModdingAPI.Web.Framework.ConfigModels
** Generic
****/
/// <summary>The user agent for API clients, where {0} is the SMAPI version.</summary>
- public string UserAgent { get; set; }
+ public string UserAgent { get; set; } = null!;
/****
** Azure
****/
/// <summary>The connection string for the Azure Blob storage account.</summary>
- public string AzureBlobConnectionString { get; set; }
+ public string? AzureBlobConnectionString { get; set; }
/// <summary>The Azure Blob container in which to store temporary uploaded logs.</summary>
- public string AzureBlobTempContainer { get; set; }
+ public string AzureBlobTempContainer { get; set; } = null!;
/// <summary>The number of days since the blob's last-modified date when it will be deleted.</summary>
public int AzureBlobTempExpiryDays { get; set; }
@@ -30,65 +30,65 @@ namespace StardewModdingAPI.Web.Framework.ConfigModels
** Chucklefish
****/
/// <summary>The base URL for the Chucklefish mod site.</summary>
- public string ChucklefishBaseUrl { get; set; }
+ public string ChucklefishBaseUrl { get; set; } = null!;
/// <summary>The URL for a mod page on the Chucklefish mod site excluding the <see cref="GitHubBaseUrl"/>, where {0} is the mod ID.</summary>
- public string ChucklefishModPageUrlFormat { get; set; }
+ public string ChucklefishModPageUrlFormat { get; set; } = null!;
/****
** CurseForge
****/
/// <summary>The base URL for the CurseForge API.</summary>
- public string CurseForgeBaseUrl { get; set; }
+ public string CurseForgeBaseUrl { get; set; } = null!;
/****
** GitHub
****/
/// <summary>The base URL for the GitHub API.</summary>
- public string GitHubBaseUrl { get; set; }
+ public string GitHubBaseUrl { get; set; } = null!;
/// <summary>The Accept header value expected by the GitHub API.</summary>
- public string GitHubAcceptHeader { get; set; }
+ public string GitHubAcceptHeader { get; set; } = null!;
/// <summary>The username with which to authenticate to the GitHub API (if any).</summary>
- public string GitHubUsername { get; set; }
+ public string? GitHubUsername { get; set; }
/// <summary>The password with which to authenticate to the GitHub API (if any).</summary>
- public string GitHubPassword { get; set; }
+ public string? GitHubPassword { get; set; }
/****
** ModDrop
****/
/// <summary>The base URL for the ModDrop API.</summary>
- public string ModDropApiUrl { get; set; }
+ public string ModDropApiUrl { get; set; } = null!;
/// <summary>The URL for a ModDrop mod page for the user, where {0} is the mod ID.</summary>
- public string ModDropModPageUrl { get; set; }
+ public string ModDropModPageUrl { get; set; } = null!;
/****
** Nexus Mods
****/
/// <summary>The base URL for the Nexus Mods API.</summary>
- public string NexusBaseUrl { get; set; }
+ public string NexusBaseUrl { get; set; } = null!;
/// <summary>The URL for a Nexus mod page for the user, excluding the <see cref="NexusBaseUrl"/>, where {0} is the mod ID.</summary>
- public string NexusModUrlFormat { get; set; }
+ public string NexusModUrlFormat { get; set; } = null!;
/// <summary>The URL for a Nexus mod page to scrape for versions, excluding the <see cref="NexusBaseUrl"/>, where {0} is the mod ID.</summary>
- public string NexusModScrapeUrlFormat { get; set; }
+ public string NexusModScrapeUrlFormat { get; set; } = null!;
/// <summary>The Nexus API authentication key.</summary>
- public string NexusApiKey { get; set; }
+ public string? NexusApiKey { get; set; }
/****
** Pastebin
****/
/// <summary>The base URL for the Pastebin API.</summary>
- public string PastebinBaseUrl { get; set; }
+ public string PastebinBaseUrl { get; set; } = null!;
}
}
diff --git a/src/SMAPI.Web/Framework/ConfigModels/ModOverrideConfig.cs b/src/SMAPI.Web/Framework/ConfigModels/ModOverrideConfig.cs
index f382d7b5..e46ecf2b 100644
--- a/src/SMAPI.Web/Framework/ConfigModels/ModOverrideConfig.cs
+++ b/src/SMAPI.Web/Framework/ConfigModels/ModOverrideConfig.cs
@@ -4,12 +4,12 @@ namespace StardewModdingAPI.Web.Framework.ConfigModels
internal class ModOverrideConfig
{
/// <summary>The unique ID from the mod's manifest.</summary>
- public string ID { get; set; }
+ public string ID { get; set; } = null!;
/// <summary>Whether to allow non-standard versions.</summary>
public bool AllowNonStandardVersions { get; set; }
/// <summary>The mod page URL to use regardless of which site has the update, or <c>null</c> to use the site URL.</summary>
- public string SetUrl { get; set; }
+ public string? SetUrl { get; set; }
}
}
diff --git a/src/SMAPI.Web/Framework/ConfigModels/ModUpdateCheckConfig.cs b/src/SMAPI.Web/Framework/ConfigModels/ModUpdateCheckConfig.cs
index aea695b8..c3b136e8 100644
--- a/src/SMAPI.Web/Framework/ConfigModels/ModUpdateCheckConfig.cs
+++ b/src/SMAPI.Web/Framework/ConfigModels/ModUpdateCheckConfig.cs
@@ -1,3 +1,5 @@
+using System;
+
namespace StardewModdingAPI.Web.Framework.ConfigModels
{
/// <summary>The config settings for mod update checks.</summary>
@@ -6,16 +8,16 @@ namespace StardewModdingAPI.Web.Framework.ConfigModels
/*********
** Accessors
*********/
- /// <summary>The number of minutes successful update checks should be cached before refetching them.</summary>
+ /// <summary>The number of minutes successful update checks should be cached before re-fetching them.</summary>
public int SuccessCacheMinutes { get; set; }
- /// <summary>The number of minutes failed update checks should be cached before refetching them.</summary>
+ /// <summary>The number of minutes failed update checks should be cached before re-fetching them.</summary>
public int ErrorCacheMinutes { get; set; }
/// <summary>Update-check metadata to override.</summary>
- public ModOverrideConfig[] ModOverrides { get; set; }
+ public ModOverrideConfig[] ModOverrides { get; set; } = Array.Empty<ModOverrideConfig>();
/// <summary>The update-check config for SMAPI's own update checks.</summary>
- public SmapiInfoConfig SmapiInfo { get; set; }
+ public SmapiInfoConfig SmapiInfo { get; set; } = null!;
}
}
diff --git a/src/SMAPI.Web/Framework/ConfigModels/SiteConfig.cs b/src/SMAPI.Web/Framework/ConfigModels/SiteConfig.cs
index 664dbef3..62685e47 100644
--- a/src/SMAPI.Web/Framework/ConfigModels/SiteConfig.cs
+++ b/src/SMAPI.Web/Framework/ConfigModels/SiteConfig.cs
@@ -7,9 +7,9 @@ namespace StardewModdingAPI.Web.Framework.ConfigModels
** Accessors
*********/
/// <summary>A message to show below the download button (e.g. for details on downloading a beta version), in Markdown format.</summary>
- public string OtherBlurb { get; set; }
+ public string? OtherBlurb { get; set; }
/// <summary>A list of supports to credit on the main page, in Markdown format.</summary>
- public string SupporterList { get; set; }
+ public string? SupporterList { get; set; }
}
}
diff --git a/src/SMAPI.Web/Framework/ConfigModels/SmapiInfoConfig.cs b/src/SMAPI.Web/Framework/ConfigModels/SmapiInfoConfig.cs
index d69fabb3..a95e0048 100644
--- a/src/SMAPI.Web/Framework/ConfigModels/SmapiInfoConfig.cs
+++ b/src/SMAPI.Web/Framework/ConfigModels/SmapiInfoConfig.cs
@@ -1,15 +1,17 @@
+using System;
+
namespace StardewModdingAPI.Web.Framework.ConfigModels
{
/// <summary>The update-check config for SMAPI's own update checks.</summary>
internal class SmapiInfoConfig
{
/// <summary>The mod ID used for SMAPI update checks.</summary>
- public string ID { get; set; }
+ public string ID { get; set; } = null!;
/// <summary>The default update key used for SMAPI update checks.</summary>
- public string DefaultUpdateKey { get; set; }
+ public string DefaultUpdateKey { get; set; } = null!;
/// <summary>The update keys to add for SMAPI update checks when the player has a beta version installed.</summary>
- public string[] AddBetaUpdateKeys { get; set; }
+ public string[] AddBetaUpdateKeys { get; set; } = Array.Empty<string>();
}
}
diff --git a/src/SMAPI.Web/Framework/Extensions.cs b/src/SMAPI.Web/Framework/Extensions.cs
index 5305b142..62a23155 100644
--- a/src/SMAPI.Web/Framework/Extensions.cs
+++ b/src/SMAPI.Web/Framework/Extensions.cs
@@ -26,10 +26,10 @@ namespace StardewModdingAPI.Web.Framework
/// <param name="values">An object that contains route values.</param>
/// <param name="absoluteUrl">Get an absolute URL instead of a server-relative path/</param>
/// <returns>The generated URL.</returns>
- public static string PlainAction(this IUrlHelper helper, [AspMvcAction] string action, [AspMvcController] string controller, object values = null, bool absoluteUrl = false)
+ public static string? PlainAction(this IUrlHelper helper, [AspMvcAction] string action, [AspMvcController] string controller, object? values = null, bool absoluteUrl = false)
{
// get route values
- RouteValueDictionary valuesDict = new RouteValueDictionary(values);
+ RouteValueDictionary valuesDict = new(values);
foreach (var value in helper.ActionContext.RouteData.Values)
{
if (!valuesDict.ContainsKey(value.Key))
@@ -37,7 +37,7 @@ namespace StardewModdingAPI.Web.Framework
}
// get relative URL
- string url = helper.Action(action, controller, valuesDict);
+ string? url = helper.Action(action, controller, valuesDict);
if (url == null && action.EndsWith("Async"))
url = helper.Action(action[..^"Async".Length], controller, valuesDict);
@@ -45,7 +45,7 @@ namespace StardewModdingAPI.Web.Framework
if (absoluteUrl)
{
HttpRequest request = helper.ActionContext.HttpContext.Request;
- Uri baseUri = new Uri($"{request.Scheme}://{request.Host}");
+ Uri baseUri = new($"{request.Scheme}://{request.Host}");
url = new Uri(baseUri, url).ToString();
}
@@ -57,7 +57,7 @@ namespace StardewModdingAPI.Web.Framework
/// <param name="value">The value to serialize.</param>
/// <returns>The serialized JSON.</returns>
/// <remarks>This bypasses unnecessary validation (e.g. not allowing null values) in <see cref="IJsonHelper.Serialize"/>.</remarks>
- public static IHtmlContent ForJson(this RazorPageBase page, object value)
+ public static IHtmlContent ForJson(this RazorPageBase page, object? value)
{
string json = JsonConvert.SerializeObject(value);
return new HtmlString(json);
diff --git a/src/SMAPI.Web/Framework/IModDownload.cs b/src/SMAPI.Web/Framework/IModDownload.cs
index dc058bcb..fe171785 100644
--- a/src/SMAPI.Web/Framework/IModDownload.cs
+++ b/src/SMAPI.Web/Framework/IModDownload.cs
@@ -3,13 +3,16 @@ namespace StardewModdingAPI.Web.Framework
/// <summary>Generic metadata about a file download on a mod page.</summary>
internal interface IModDownload
{
+ /*********
+ ** Accessors
+ *********/
/// <summary>The download's display name.</summary>
string Name { get; }
/// <summary>The download's description.</summary>
- string Description { get; }
+ string? Description { get; }
/// <summary>The download's file version.</summary>
- string Version { get; }
+ string? Version { get; }
}
}
diff --git a/src/SMAPI.Web/Framework/IModPage.cs b/src/SMAPI.Web/Framework/IModPage.cs
index e66d401f..4d0a8d61 100644
--- a/src/SMAPI.Web/Framework/IModPage.cs
+++ b/src/SMAPI.Web/Framework/IModPage.cs
@@ -1,4 +1,5 @@
using System.Collections.Generic;
+using System.Diagnostics.CodeAnalysis;
using StardewModdingAPI.Toolkit.Framework.UpdateData;
namespace StardewModdingAPI.Web.Framework
@@ -16,13 +17,13 @@ namespace StardewModdingAPI.Web.Framework
string Id { get; }
/// <summary>The mod name.</summary>
- string Name { get; }
+ string? Name { get; }
/// <summary>The mod's semantic version number.</summary>
- string Version { get; }
+ string? Version { get; }
/// <summary>The mod's web URL.</summary>
- string Url { get; }
+ string? Url { get; }
/// <summary>The mod downloads.</summary>
IModDownload[] Downloads { get; }
@@ -31,7 +32,12 @@ namespace StardewModdingAPI.Web.Framework
RemoteModStatus Status { get; }
/// <summary>A user-friendly error which indicates why fetching the mod info failed (if applicable).</summary>
- string Error { get; }
+ string? Error { get; }
+
+ /// <summary>Whether the mod data is valid.</summary>
+ [MemberNotNullWhen(true, nameof(IModPage.Name), nameof(IModPage.Url))]
+ [MemberNotNullWhen(false, nameof(IModPage.Error))]
+ bool IsValid { get; }
/*********
@@ -42,7 +48,7 @@ namespace StardewModdingAPI.Web.Framework
/// <param name="version">The mod's semantic version number.</param>
/// <param name="url">The mod's web URL.</param>
/// <param name="downloads">The mod downloads.</param>
- IModPage SetInfo(string name, string version, string url, IEnumerable<IModDownload> downloads);
+ IModPage SetInfo(string name, string? version, string url, IEnumerable<IModDownload> downloads);
/// <summary>Set a mod fetch error.</summary>
/// <param name="status">The mod availability status on the remote site.</param>
diff --git a/src/SMAPI.Web/Framework/JobDashboardAuthorizationFilter.cs b/src/SMAPI.Web/Framework/JobDashboardAuthorizationFilter.cs
index 385c0c91..3c1405eb 100644
--- a/src/SMAPI.Web/Framework/JobDashboardAuthorizationFilter.cs
+++ b/src/SMAPI.Web/Framework/JobDashboardAuthorizationFilter.cs
@@ -9,7 +9,7 @@ namespace StardewModdingAPI.Web.Framework
** Fields
*********/
/// <summary>An authorization filter that allows local requests.</summary>
- private static readonly LocalRequestsOnlyAuthorizationFilter LocalRequestsOnlyFilter = new LocalRequestsOnlyAuthorizationFilter();
+ private static readonly LocalRequestsOnlyAuthorizationFilter LocalRequestsOnlyFilter = new();
/*********
diff --git a/src/SMAPI.Web/Framework/LogParsing/LogMessageBuilder.cs b/src/SMAPI.Web/Framework/LogParsing/LogMessageBuilder.cs
index 992876ef..a1384b8f 100644
--- a/src/SMAPI.Web/Framework/LogParsing/LogMessageBuilder.cs
+++ b/src/SMAPI.Web/Framework/LogParsing/LogMessageBuilder.cs
@@ -1,4 +1,5 @@
using System;
+using System.Diagnostics.CodeAnalysis;
using System.Text;
using StardewModdingAPI.Web.Framework.LogParsing.Models;
@@ -11,7 +12,7 @@ namespace StardewModdingAPI.Web.Framework.LogParsing
** Fields
*********/
/// <summary>The local time when the next log was posted.</summary>
- public string Time { get; set; }
+ public string? Time { get; set; }
/// <summary>The log level for the next log message.</summary>
public LogLevel Level { get; set; }
@@ -20,16 +21,17 @@ namespace StardewModdingAPI.Web.Framework.LogParsing
public int ScreenId { get; set; }
/// <summary>The mod name for the next log message.</summary>
- public string Mod { get; set; }
+ public string? Mod { get; set; }
/// <summary>The text for the next log message.</summary>
- private readonly StringBuilder Text = new StringBuilder();
+ private readonly StringBuilder Text = new();
/*********
** Accessors
*********/
/// <summary>Whether the next log message has been started.</summary>
+ [MemberNotNullWhen(true, nameof(LogMessageBuilder.Time), nameof(LogMessageBuilder.Mod))]
public bool Started { get; private set; }
@@ -70,19 +72,18 @@ namespace StardewModdingAPI.Web.Framework.LogParsing
}
/// <summary>Get a log message for the accumulated values.</summary>
- public LogMessage Build()
+ public LogMessage? Build()
{
if (!this.Started)
return null;
- return new LogMessage
- {
- Time = this.Time,
- Level = this.Level,
- ScreenId = this.ScreenId,
- Mod = this.Mod,
- Text = this.Text.ToString()
- };
+ return new LogMessage(
+ time: this.Time,
+ level: this.Level,
+ screenId: this.ScreenId,
+ mod: this.Mod,
+ text: this.Text.ToString()
+ );
}
/// <summary>Reset to start a new log message.</summary>
diff --git a/src/SMAPI.Web/Framework/LogParsing/LogParseException.cs b/src/SMAPI.Web/Framework/LogParsing/LogParseException.cs
index 5d4c8c08..3f815e3e 100644
--- a/src/SMAPI.Web/Framework/LogParsing/LogParseException.cs
+++ b/src/SMAPI.Web/Framework/LogParsing/LogParseException.cs
@@ -10,6 +10,7 @@ namespace StardewModdingAPI.Web.Framework.LogParsing
*********/
/// <summary>Construct an instance.</summary>
/// <param name="message">The user-friendly error message.</param>
- public LogParseException(string message) : base(message) { }
+ public LogParseException(string message)
+ : base(message) { }
}
}
diff --git a/src/SMAPI.Web/Framework/LogParsing/LogParser.cs b/src/SMAPI.Web/Framework/LogParsing/LogParser.cs
index 887d0105..55272b23 100644
--- a/src/SMAPI.Web/Framework/LogParsing/LogParser.cs
+++ b/src/SMAPI.Web/Framework/LogParsing/LogParser.cs
@@ -14,38 +14,38 @@ namespace StardewModdingAPI.Web.Framework.LogParsing
** Fields
*********/
/// <summary>A regex pattern matching the start of a SMAPI message.</summary>
- private readonly Regex MessageHeaderPattern = new Regex(@"^\[(?<time>\d\d[:\.]\d\d[:\.]\d\d) (?<level>[a-z]+)(?: +screen_(?<screen>\d+))? +(?<modName>[^\]]+)\] ", RegexOptions.Compiled | RegexOptions.IgnoreCase);
+ private readonly Regex MessageHeaderPattern = new(@"^\[(?<time>\d\d[:\.]\d\d[:\.]\d\d) (?<level>[a-z]+)(?: +screen_(?<screen>\d+))? +(?<modName>[^\]]+)\] ", RegexOptions.Compiled | RegexOptions.IgnoreCase);
/// <summary>A regex pattern matching SMAPI's initial platform info message.</summary>
- private readonly Regex InfoLinePattern = new Regex(@"^SMAPI (?<apiVersion>.+) with Stardew Valley (?<gameVersion>.+) on (?<os>.+)", RegexOptions.Compiled | RegexOptions.IgnoreCase);
+ private readonly Regex InfoLinePattern = new(@"^SMAPI (?<apiVersion>.+) with Stardew Valley (?<gameVersion>.+) on (?<os>.+)", RegexOptions.Compiled | RegexOptions.IgnoreCase);
/// <summary>A regex pattern matching SMAPI's mod folder path line.</summary>
- private readonly Regex ModPathPattern = new Regex(@"^Mods go here: (?<path>.+)", RegexOptions.Compiled | RegexOptions.IgnoreCase);
+ private readonly Regex ModPathPattern = new(@"^Mods go here: (?<path>.+)", RegexOptions.Compiled | RegexOptions.IgnoreCase);
/// <summary>A regex pattern matching SMAPI's log timestamp line.</summary>
- private readonly Regex LogStartedAtPattern = new Regex(@"^Log started at (?<timestamp>.+) UTC", RegexOptions.Compiled | RegexOptions.IgnoreCase);
+ private readonly Regex LogStartedAtPattern = new(@"^Log started at (?<timestamp>.+) UTC", RegexOptions.Compiled | RegexOptions.IgnoreCase);
/// <summary>A regex pattern matching the start of SMAPI's mod list.</summary>
- private readonly Regex ModListStartPattern = new Regex(@"^Loaded \d+ mods:$", RegexOptions.Compiled | RegexOptions.IgnoreCase);
+ private readonly Regex ModListStartPattern = new(@"^Loaded \d+ mods:$", RegexOptions.Compiled | RegexOptions.IgnoreCase);
/// <summary>A regex pattern matching an entry in SMAPI's mod list.</summary>
/// <remarks>The author name and description are optional.</remarks>
- private readonly Regex ModListEntryPattern = new Regex(@"^ (?<name>.+?) (?<version>[^\s]+)(?: by (?<author>[^\|]+))?(?: \| (?<description>.+))?$", RegexOptions.Compiled | RegexOptions.IgnoreCase);
+ private readonly Regex ModListEntryPattern = new(@"^ (?<name>.+?) (?<version>[^\s]+)(?: by (?<author>[^\|]+))?(?: \| (?<description>.+))?$", RegexOptions.Compiled | RegexOptions.IgnoreCase);
/// <summary>A regex pattern matching the start of SMAPI's content pack list.</summary>
- private readonly Regex ContentPackListStartPattern = new Regex(@"^Loaded \d+ content packs:$", RegexOptions.Compiled | RegexOptions.IgnoreCase);
+ private readonly Regex ContentPackListStartPattern = new(@"^Loaded \d+ content packs:$", RegexOptions.Compiled | RegexOptions.IgnoreCase);
/// <summary>A regex pattern matching an entry in SMAPI's content pack list.</summary>
- private readonly Regex ContentPackListEntryPattern = new Regex(@"^ (?<name>.+?) (?<version>[^\s]+)(?: by (?<author>[^\|]+))? \| for (?<for>[^\|]+)(?: \| (?<description>.+))?$", RegexOptions.Compiled | RegexOptions.IgnoreCase);
+ private readonly Regex ContentPackListEntryPattern = new(@"^ (?<name>.+?) (?<version>[^\s]+)(?: by (?<author>[^\|]+))? \| for (?<for>[^\|]+)(?: \| (?<description>.+))?$", RegexOptions.Compiled | RegexOptions.IgnoreCase);
/// <summary>A regex pattern matching the start of SMAPI's mod update list.</summary>
- private readonly Regex ModUpdateListStartPattern = new Regex(@"^You can update \d+ mods?:$", RegexOptions.Compiled | RegexOptions.IgnoreCase);
+ private readonly Regex ModUpdateListStartPattern = new(@"^You can update \d+ mods?:$", RegexOptions.Compiled | RegexOptions.IgnoreCase);
/// <summary>A regex pattern matching an entry in SMAPI's mod update list.</summary>
- private readonly Regex ModUpdateListEntryPattern = new Regex(@"^ (?<name>.+) (?<version>[^\s]+): (?<link>.+)$", RegexOptions.Compiled | RegexOptions.IgnoreCase);
+ private readonly Regex ModUpdateListEntryPattern = new(@"^ (?<name>.+) (?<version>[^\s]+): (?<link>.+)$", RegexOptions.Compiled | RegexOptions.IgnoreCase);
/// <summary>A regex pattern matching SMAPI's update line.</summary>
- private readonly Regex SmapiUpdatePattern = new Regex(@"^You can update SMAPI to (?<version>[^\s]+): (?<link>.+)$", RegexOptions.Compiled | RegexOptions.IgnoreCase);
+ private readonly Regex SmapiUpdatePattern = new(@"^You can update SMAPI to (?<version>[^\s]+): (?<link>.+)$", RegexOptions.Compiled | RegexOptions.IgnoreCase);
/*********
@@ -53,7 +53,7 @@ namespace StardewModdingAPI.Web.Framework.LogParsing
*********/
/// <summary>Parse SMAPI log text.</summary>
/// <param name="logText">The SMAPI log text.</param>
- public ParsedLog Parse(string logText)
+ public ParsedLog Parse(string? logText)
{
try
{
@@ -69,7 +69,7 @@ namespace StardewModdingAPI.Web.Framework.LogParsing
}
// init log
- ParsedLog log = new ParsedLog
+ ParsedLog log = new()
{
IsValid = true,
RawText = logText,
@@ -77,8 +77,8 @@ namespace StardewModdingAPI.Web.Framework.LogParsing
};
// parse log messages
- LogModInfo smapiMod = new LogModInfo { Name = "SMAPI", Author = "Pathoschild", Description = "", Loaded = true };
- LogModInfo gameMod = new LogModInfo { Name = "game", Author = "", Description = "", Loaded = true };
+ LogModInfo smapiMod = new(name: "SMAPI", author: "Pathoschild", version: "", description: "", loaded: true);
+ LogModInfo gameMod = new(name: "game", author: "", version: "", description: "", loaded: true);
IDictionary<string, List<LogModInfo>> mods = new Dictionary<string, List<LogModInfo>>();
bool inModList = false;
bool inContentPackList = false;
@@ -101,7 +101,7 @@ namespace StardewModdingAPI.Web.Framework.LogParsing
default:
if (mods.TryGetValue(message.Mod, out var entries))
{
- foreach (var entry in entries)
+ foreach (LogModInfo entry in entries)
entry.Errors++;
}
break;
@@ -131,9 +131,9 @@ namespace StardewModdingAPI.Web.Framework.LogParsing
string author = match.Groups["author"].Value;
string description = match.Groups["description"].Value;
- if (!mods.TryGetValue(name, out List<LogModInfo> entries))
+ if (!mods.TryGetValue(name, out List<LogModInfo>? entries))
mods[name] = entries = new List<LogModInfo>();
- entries.Add(new LogModInfo { Name = name, Author = author, Version = version, Description = description, Loaded = true });
+ entries.Add(new LogModInfo(name: name, author: author, version: version, description: description, loaded: true));
message.Section = LogSection.ModsList;
}
@@ -154,9 +154,9 @@ namespace StardewModdingAPI.Web.Framework.LogParsing
string description = match.Groups["description"].Value;
string forMod = match.Groups["for"].Value;
- if (!mods.TryGetValue(name, out List<LogModInfo> entries))
+ if (!mods.TryGetValue(name, out List<LogModInfo>? entries))
mods[name] = entries = new List<LogModInfo>();
- entries.Add(new LogModInfo { Name = name, Author = author, Version = version, Description = description, ContentPackFor = forMod, Loaded = true });
+ entries.Add(new LogModInfo(name: name, author: author, version: version, description: description, contentPackFor: forMod, loaded: true));
message.Section = LogSection.ContentPackList;
}
@@ -177,23 +177,19 @@ namespace StardewModdingAPI.Web.Framework.LogParsing
if (mods.TryGetValue(name, out var entries))
{
- foreach (var entry in entries)
- {
- entry.UpdateLink = link;
- entry.UpdateVersion = version;
- }
+ foreach (LogModInfo entry in entries)
+ entry.SetUpdate(version, link);
}
message.Section = LogSection.ModUpdateList;
}
-
else if (message.Level == LogLevel.Alert && this.SmapiUpdatePattern.IsMatch(message.Text))
{
Match match = this.SmapiUpdatePattern.Match(message.Text);
string version = match.Groups["version"].Value;
string link = match.Groups["link"].Value;
- smapiMod.UpdateVersion = version;
- smapiMod.UpdateLink = link;
+
+ smapiMod.SetUpdate(version, link);
}
// platform info line
@@ -203,7 +199,7 @@ namespace StardewModdingAPI.Web.Framework.LogParsing
log.ApiVersion = match.Groups["apiVersion"].Value;
log.GameVersion = match.Groups["gameVersion"].Value;
log.OperatingSystem = match.Groups["os"].Value;
- smapiMod.Version = log.ApiVersion;
+ smapiMod.OverrideVersion(log.ApiVersion);
}
// mod path line
@@ -211,9 +207,9 @@ namespace StardewModdingAPI.Web.Framework.LogParsing
{
Match match = this.ModPathPattern.Match(message.Text);
log.ModPath = match.Groups["path"].Value;
- int lastDelimiterPos = log.ModPath.LastIndexOfAny(new char[] { '/', '\\' });
+ int lastDelimiterPos = log.ModPath.LastIndexOfAny(new[] { '/', '\\' });
log.GamePath = lastDelimiterPos >= 0
- ? log.ModPath.Substring(0, lastDelimiterPos)
+ ? log.ModPath[..lastDelimiterPos]
: log.ModPath;
}
@@ -227,7 +223,8 @@ namespace StardewModdingAPI.Web.Framework.LogParsing
}
// finalize log
- gameMod.Version = log.GameVersion;
+ if (log.GameVersion != null)
+ gameMod.OverrideVersion(log.GameVersion);
log.Mods = new[] { gameMod, smapiMod }.Concat(mods.Values.SelectMany(p => p).OrderBy(p => p.Name)).ToArray();
return log;
}
@@ -259,7 +256,8 @@ namespace StardewModdingAPI.Web.Framework.LogParsing
/// <param name="messages">The messages to filter.</param>
private IEnumerable<LogMessage> CollapseRepeats(IEnumerable<LogMessage> messages)
{
- LogMessage next = null;
+ LogMessage? next = null;
+
foreach (LogMessage message in messages)
{
// new message
@@ -280,7 +278,9 @@ namespace StardewModdingAPI.Web.Framework.LogParsing
yield return next;
next = message;
}
- yield return next;
+
+ if (next != null)
+ yield return next;
}
/// <summary>Split a SMAPI log into individual log messages.</summary>
@@ -288,12 +288,12 @@ namespace StardewModdingAPI.Web.Framework.LogParsing
/// <exception cref="LogParseException">The log text can't be parsed successfully.</exception>
private IEnumerable<LogMessage> GetMessages(string logText)
{
- LogMessageBuilder builder = new LogMessageBuilder();
- using StringReader reader = new StringReader(logText);
+ LogMessageBuilder builder = new();
+ using StringReader reader = new(logText);
while (true)
{
// read line
- string line = reader.ReadLine();
+ string? line = reader.ReadLine();
if (line == null)
break;
@@ -306,17 +306,17 @@ namespace StardewModdingAPI.Web.Framework.LogParsing
{
if (builder.Started)
{
- yield return builder.Build();
+ yield return builder.Build()!;
builder.Clear();
}
- var screenGroup = header.Groups["screen"];
+ Group screenGroup = header.Groups["screen"];
builder.Start(
time: header.Groups["time"].Value,
level: Enum.Parse<LogLevel>(header.Groups["level"].Value, ignoreCase: true),
screenId: screenGroup.Success ? int.Parse(screenGroup.Value) : 0, // main player is always screen ID 0
mod: header.Groups["modName"].Value,
- text: line.Substring(header.Length)
+ text: line[header.Length..]
);
}
else
@@ -330,7 +330,7 @@ namespace StardewModdingAPI.Web.Framework.LogParsing
// end last message
if (builder.Started)
- yield return builder.Build();
+ yield return builder.Build()!;
}
}
}
diff --git a/src/SMAPI.Web/Framework/LogParsing/Models/LogMessage.cs b/src/SMAPI.Web/Framework/LogParsing/Models/LogMessage.cs
index 1e08be78..7a5f32e0 100644
--- a/src/SMAPI.Web/Framework/LogParsing/Models/LogMessage.cs
+++ b/src/SMAPI.Web/Framework/LogParsing/Models/LogMessage.cs
@@ -1,3 +1,5 @@
+using System.Diagnostics.CodeAnalysis;
+
namespace StardewModdingAPI.Web.Framework.LogParsing.Models
{
/// <summary>A parsed log message.</summary>
@@ -7,19 +9,19 @@ namespace StardewModdingAPI.Web.Framework.LogParsing.Models
** Accessors
*********/
/// <summary>The local time when the log was posted.</summary>
- public string Time { get; set; }
+ public string Time { get; }
/// <summary>The log level.</summary>
- public LogLevel Level { get; set; }
+ public LogLevel Level { get; }
/// <summary>The screen ID in split-screen mode.</summary>
- public int ScreenId { get; set; }
+ public int ScreenId { get; }
/// <summary>The mod name.</summary>
- public string Mod { get; set; }
+ public string Mod { get; }
/// <summary>The log text.</summary>
- public string Text { get; set; }
+ public string Text { get; }
/// <summary>The number of times this message was repeated consecutively.</summary>
public int Repeated { get; set; }
@@ -28,6 +30,32 @@ namespace StardewModdingAPI.Web.Framework.LogParsing.Models
public LogSection? Section { get; set; }
/// <summary>Whether this message is the first one of its section.</summary>
+ [MemberNotNullWhen(true, nameof(LogMessage.Section))]
public bool IsStartOfSection { get; set; }
+
+
+ /*********
+ ** Public methods
+ *********/
+ /// <summary>Construct an instance/</summary>
+ /// <param name="time">The local time when the log was posted.</param>
+ /// <param name="level">The log level.</param>
+ /// <param name="screenId">The screen ID in split-screen mode.</param>
+ /// <param name="mod">The mod name.</param>
+ /// <param name="text">The log text.</param>
+ /// <param name="repeated">The number of times this message was repeated consecutively.</param>
+ /// <param name="section">The section that this log message belongs to.</param>
+ /// <param name="isStartOfSection">Whether this message is the first one of its section.</param>
+ public LogMessage(string time, LogLevel level, int screenId, string mod, string text, int repeated = 0, LogSection? section = null, bool isStartOfSection = false)
+ {
+ this.Time = time;
+ this.Level = level;
+ this.ScreenId = screenId;
+ this.Mod = mod;
+ this.Text = text;
+ this.Repeated = repeated;
+ this.Section = section;
+ this.IsStartOfSection = isStartOfSection;
+ }
}
}
diff --git a/src/SMAPI.Web/Framework/LogParsing/Models/LogModInfo.cs b/src/SMAPI.Web/Framework/LogParsing/Models/LogModInfo.cs
index 067e4df4..a6b9165c 100644
--- a/src/SMAPI.Web/Framework/LogParsing/Models/LogModInfo.cs
+++ b/src/SMAPI.Web/Framework/LogParsing/Models/LogModInfo.cs
@@ -1,3 +1,5 @@
+using System.Diagnostics.CodeAnalysis;
+
namespace StardewModdingAPI.Web.Framework.LogParsing.Models
{
/// <summary>Metadata about a mod or content pack in the log.</summary>
@@ -7,33 +9,81 @@ namespace StardewModdingAPI.Web.Framework.LogParsing.Models
** Accessors
*********/
/// <summary>The mod name.</summary>
- public string Name { get; set; }
+ public string Name { get; }
/// <summary>The mod author.</summary>
- public string Author { get; set; }
-
- /// <summary>The update version.</summary>
- public string UpdateVersion { get; set; }
-
- /// <summary>The update link.</summary>
- public string UpdateLink { get; set; }
+ public string Author { get; }
/// <summary>The mod version.</summary>
- public string Version { get; set; }
+ public string Version { get; private set; }
/// <summary>The mod description.</summary>
- public string Description { get; set; }
+ public string Description { get; }
+
+ /// <summary>The update version.</summary>
+ public string? UpdateVersion { get; private set; }
+
+ /// <summary>The update link.</summary>
+ public string? UpdateLink { get; private set; }
/// <summary>The name of the mod for which this is a content pack (if applicable).</summary>
- public string ContentPackFor { get; set; }
+ public string? ContentPackFor { get; }
/// <summary>The number of errors logged by this mod.</summary>
public int Errors { get; set; }
/// <summary>Whether the mod was loaded into the game.</summary>
- public bool Loaded { get; set; }
+ public bool Loaded { get; }
/// <summary>Whether the mod has an update available.</summary>
+ [MemberNotNullWhen(true, nameof(LogModInfo.UpdateVersion), nameof(LogModInfo.UpdateLink))]
public bool HasUpdate => this.UpdateVersion != null && this.Version != this.UpdateVersion;
+
+ /// <summary>Whether the mod is a content pack for another mod.</summary>
+ [MemberNotNullWhen(true, nameof(LogModInfo.ContentPackFor))]
+ public bool IsContentPack => !string.IsNullOrWhiteSpace(this.ContentPackFor);
+
+
+ /*********
+ ** Public methods
+ *********/
+ /// <summary>Construct an instance.</summary>
+ /// <param name="name">The mod name.</param>
+ /// <param name="author">The mod author.</param>
+ /// <param name="version">The mod version.</param>
+ /// <param name="description">The mod description.</param>
+ /// <param name="updateVersion">The update version.</param>
+ /// <param name="updateLink">The update link.</param>
+ /// <param name="contentPackFor">The name of the mod for which this is a content pack (if applicable).</param>
+ /// <param name="errors">The number of errors logged by this mod.</param>
+ /// <param name="loaded">Whether the mod was loaded into the game.</param>
+ public LogModInfo(string name, string author, string version, string description, string? updateVersion = null, string? updateLink = null, string? contentPackFor = null, int errors = 0, bool loaded = true)
+ {
+ this.Name = name;
+ this.Author = author;
+ this.Version = version;
+ this.Description = description;
+ this.UpdateVersion = updateVersion;
+ this.UpdateLink = updateLink;
+ this.ContentPackFor = contentPackFor;
+ this.Errors = errors;
+ this.Loaded = loaded;
+ }
+
+ /// <summary>Add an update alert for this mod.</summary>
+ /// <param name="updateVersion">The update version.</param>
+ /// <param name="updateLink">The update link.</param>
+ public void SetUpdate(string updateVersion, string updateLink)
+ {
+ this.UpdateVersion = updateVersion;
+ this.UpdateLink = updateLink;
+ }
+
+ /// <summary>Override the version number, for cases like SMAPI itself where the version is only known later during parsing.</summary>
+ /// <param name="version">The new mod version.</param>
+ public void OverrideVersion(string version)
+ {
+ this.Version = version;
+ }
}
}
diff --git a/src/SMAPI.Web/Framework/LogParsing/Models/ParsedLog.cs b/src/SMAPI.Web/Framework/LogParsing/Models/ParsedLog.cs
index 87b20eb0..6951e434 100644
--- a/src/SMAPI.Web/Framework/LogParsing/Models/ParsedLog.cs
+++ b/src/SMAPI.Web/Framework/LogParsing/Models/ParsedLog.cs
@@ -1,4 +1,5 @@
using System;
+using System.Diagnostics.CodeAnalysis;
namespace StardewModdingAPI.Web.Framework.LogParsing.Models
{
@@ -12,39 +13,40 @@ namespace StardewModdingAPI.Web.Framework.LogParsing.Models
** Metadata
****/
/// <summary>Whether the log file was successfully parsed.</summary>
+ [MemberNotNullWhen(true, nameof(ParsedLog.RawText))]
public bool IsValid { get; set; }
/// <summary>An error message indicating why the log file is invalid.</summary>
- public string Error { get; set; }
+ public string? Error { get; set; }
/// <summary>The raw log text.</summary>
- public string RawText { get; set; }
+ public string? RawText { get; set; }
/****
** Log data
****/
/// <summary>The SMAPI version.</summary>
- public string ApiVersion { get; set; }
+ public string? ApiVersion { get; set; }
/// <summary>The game version.</summary>
- public string GameVersion { get; set; }
+ public string? GameVersion { get; set; }
/// <summary>The player's operating system.</summary>
- public string OperatingSystem { get; set; }
+ public string? OperatingSystem { get; set; }
/// <summary>The game install path.</summary>
- public string GamePath { get; set; }
+ public string? GamePath { get; set; }
/// <summary>The mod folder path.</summary>
- public string ModPath { get; set; }
+ public string? ModPath { get; set; }
/// <summary>The ISO 8601 timestamp when the log was started.</summary>
public DateTimeOffset Timestamp { get; set; }
/// <summary>Metadata about installed mods and content packs.</summary>
- public LogModInfo[] Mods { get; set; } = new LogModInfo[0];
+ public LogModInfo[] Mods { get; set; } = Array.Empty<LogModInfo>();
/// <summary>The log messages.</summary>
- public LogMessage[] Messages { get; set; }
+ public LogMessage[] Messages { get; set; } = Array.Empty<LogMessage>();
}
}
diff --git a/src/SMAPI.Web/Framework/ModInfoModel.cs b/src/SMAPI.Web/Framework/ModInfoModel.cs
index 7845b8c5..e70b60bf 100644
--- a/src/SMAPI.Web/Framework/ModInfoModel.cs
+++ b/src/SMAPI.Web/Framework/ModInfoModel.cs
@@ -1,4 +1,5 @@
-using StardewModdingAPI.Web.Framework.Clients;
+using System.Diagnostics.CodeAnalysis;
+using Newtonsoft.Json;
namespace StardewModdingAPI.Web.Framework
{
@@ -9,22 +10,22 @@ namespace StardewModdingAPI.Web.Framework
** Accessors
*********/
/// <summary>The mod name.</summary>
- public string Name { get; set; }
+ public string? Name { get; private set; }
+
+ /// <summary>The mod's web URL.</summary>
+ public string? Url { get; private set; }
/// <summary>The mod's latest version.</summary>
- public ISemanticVersion Version { get; set; }
+ public ISemanticVersion? Version { get; private set; }
/// <summary>The mod's latest optional or prerelease version, if newer than <see cref="Version"/>.</summary>
- public ISemanticVersion PreviewVersion { get; set; }
-
- /// <summary>The mod's web URL.</summary>
- public string Url { get; set; }
+ public ISemanticVersion? PreviewVersion { get; private set; }
/// <summary>The mod availability status on the remote site.</summary>
- public RemoteModStatus Status { get; set; } = RemoteModStatus.Ok;
+ public RemoteModStatus Status { get; private set; }
/// <summary>The error message indicating why the mod is invalid (if applicable).</summary>
- public string Error { get; set; }
+ public string? Error { get; private set; }
/*********
@@ -35,19 +36,24 @@ namespace StardewModdingAPI.Web.Framework
/// <summary>Construct an instance.</summary>
/// <param name="name">The mod name.</param>
+ /// <param name="url">The mod's web URL.</param>
/// <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="url">The mod's web URL.</param>
- public ModInfoModel(string name, ISemanticVersion version, string url, ISemanticVersion previewVersion = null)
+ /// <param name="status">The mod availability status on the remote site.</param>
+ /// <param name="error">The error message indicating why the mod is invalid (if applicable).</param>
+ [JsonConstructor]
+ public ModInfoModel(string name, string url, ISemanticVersion? version, ISemanticVersion? previewVersion = null, RemoteModStatus status = RemoteModStatus.Ok, string? error = null)
{
this
.SetBasicInfo(name, url)
- .SetVersions(version, previewVersion);
+ .SetVersions(version!, previewVersion)
+ .SetError(status, error!);
}
/// <summary>Set the basic mod info.</summary>
/// <param name="name">The mod name.</param>
/// <param name="url">The mod's web URL.</param>
+ [MemberNotNull(nameof(ModInfoModel.Name), nameof(ModInfoModel.Url))]
public ModInfoModel SetBasicInfo(string name, string url)
{
this.Name = name;
@@ -59,7 +65,8 @@ namespace StardewModdingAPI.Web.Framework
/// <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>
- public ModInfoModel SetVersions(ISemanticVersion version, ISemanticVersion previewVersion = null)
+ [MemberNotNull(nameof(ModInfoModel.Version))]
+ public ModInfoModel SetVersions(ISemanticVersion version, ISemanticVersion? previewVersion = null)
{
this.Version = version;
this.PreviewVersion = previewVersion;
diff --git a/src/SMAPI.Web/Framework/ModSiteManager.cs b/src/SMAPI.Web/Framework/ModSiteManager.cs
index a2b92aa4..674b9ffc 100644
--- a/src/SMAPI.Web/Framework/ModSiteManager.cs
+++ b/src/SMAPI.Web/Framework/ModSiteManager.cs
@@ -1,5 +1,6 @@
using System;
using System.Collections.Generic;
+using System.Diagnostics.CodeAnalysis;
using System.Linq;
using System.Text.RegularExpressions;
using System.Threading.Tasks;
@@ -34,12 +35,15 @@ namespace StardewModdingAPI.Web.Framework
/// <param name="updateKey">The namespaced update key.</param>
public async Task<IModPage> GetModPageAsync(UpdateKey updateKey)
{
+ if (!updateKey.LooksValid)
+ return new GenericModPage(updateKey.Site, updateKey.ID!).SetError(RemoteModStatus.DoesNotExist, $"Invalid update key '{updateKey}'.");
+
// get site
- if (!this.ModSites.TryGetValue(updateKey.Site, out IModSiteClient client))
+ if (!this.ModSites.TryGetValue(updateKey.Site, out IModSiteClient? client))
return new GenericModPage(updateKey.Site, updateKey.ID).SetError(RemoteModStatus.DoesNotExist, $"There's no mod site with key '{updateKey.Site}'. Expected one of [{string.Join(", ", this.ModSites.Keys)}].");
// fetch mod
- IModPage mod;
+ IModPage? mod;
try
{
mod = await client.GetModData(updateKey.ID);
@@ -58,39 +62,42 @@ namespace StardewModdingAPI.Web.Framework
/// <param name="subkey">The optional update subkey to match in available files. (If no file names or descriptions contain the subkey, it'll be ignored.)</param>
/// <param name="mapRemoteVersions">The changes to apply to remote versions for update checks.</param>
/// <param name="allowNonStandardVersions">Whether to allow non-standard versions.</param>
- public ModInfoModel GetPageVersions(IModPage page, string subkey, bool allowNonStandardVersions, ChangeDescriptor mapRemoteVersions)
+ public ModInfoModel GetPageVersions(IModPage page, string? subkey, bool allowNonStandardVersions, ChangeDescriptor? mapRemoteVersions)
{
// get base model
- ModInfoModel model = new ModInfoModel()
- .SetBasicInfo(page.Name, page.Url)
- .SetError(page.Status, page.Error);
- if (page.Status != RemoteModStatus.Ok)
+ ModInfoModel model = new();
+ if (page.IsValid)
+ model.SetBasicInfo(page.Name, page.Url);
+ else
+ {
+ model.SetError(page.Status, page.Error);
return model;
+ }
// fetch versions
- bool hasVersions = this.TryGetLatestVersions(page, subkey, allowNonStandardVersions, mapRemoteVersions, out ISemanticVersion mainVersion, out ISemanticVersion previewVersion);
+ bool hasVersions = this.TryGetLatestVersions(page, subkey, allowNonStandardVersions, mapRemoteVersions, out ISemanticVersion? mainVersion, out ISemanticVersion? previewVersion);
if (!hasVersions && subkey != null)
hasVersions = this.TryGetLatestVersions(page, null, allowNonStandardVersions, mapRemoteVersions, out mainVersion, out previewVersion);
if (!hasVersions)
return model.SetError(RemoteModStatus.InvalidData, $"The {page.Site} mod with ID '{page.Id}' has no valid versions.");
// return info
- return model.SetVersions(mainVersion, previewVersion);
+ return model.SetVersions(mainVersion!, previewVersion);
}
/// <summary>Get a semantic local version for update checks.</summary>
/// <param name="version">The version to parse.</param>
/// <param name="map">Changes to apply to the raw version, if any.</param>
/// <param name="allowNonStandard">Whether to allow non-standard versions.</param>
- public ISemanticVersion GetMappedVersion(string version, ChangeDescriptor map, bool allowNonStandard)
+ public ISemanticVersion? GetMappedVersion(string? version, ChangeDescriptor? map, bool allowNonStandard)
{
// try mapped version
- string rawNewVersion = this.GetRawMappedVersion(version, map, allowNonStandard);
- if (SemanticVersion.TryParse(rawNewVersion, allowNonStandard, out ISemanticVersion parsedNew))
+ string? rawNewVersion = this.GetRawMappedVersion(version, map);
+ if (SemanticVersion.TryParse(rawNewVersion, allowNonStandard, out ISemanticVersion? parsedNew))
return parsedNew;
// return original version
- return SemanticVersion.TryParse(version, allowNonStandard, out ISemanticVersion parsedOld)
+ return SemanticVersion.TryParse(version, allowNonStandard, out ISemanticVersion? parsedOld)
? parsedOld
: null;
}
@@ -106,31 +113,31 @@ 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, out ISemanticVersion main, out ISemanticVersion preview)
+ private bool TryGetLatestVersions(IModPage? mod, string? subkey, bool allowNonStandardVersions, ChangeDescriptor? mapRemoteVersions, [NotNullWhen(true)] out ISemanticVersion? main, out ISemanticVersion? preview)
{
main = null;
preview = null;
// parse all versions from the mod page
- IEnumerable<(string name, string description, ISemanticVersion version)> GetAllVersions()
+ IEnumerable<(string? name, string? description, ISemanticVersion? version)> GetAllVersions()
{
if (mod != null)
{
- ISemanticVersion ParseAndMapVersion(string raw)
+ ISemanticVersion? ParseAndMapVersion(string? raw)
{
raw = this.NormalizeVersion(raw);
return this.GetMappedVersion(raw, mapRemoteVersions, allowNonStandardVersions);
}
// get mod version
- ISemanticVersion modVersion = ParseAndMapVersion(mod.Version);
+ ISemanticVersion? modVersion = ParseAndMapVersion(mod.Version);
if (modVersion != null)
yield return (name: null, description: null, version: ParseAndMapVersion(mod.Version));
// get file versions
foreach (IModDownload download in mod.Downloads)
{
- ISemanticVersion cur = ParseAndMapVersion(download.Version);
+ ISemanticVersion? cur = ParseAndMapVersion(download.Version);
if (cur != null)
yield return (download.Name, download.Description, cur);
}
@@ -141,15 +148,15 @@ namespace StardewModdingAPI.Web.Framework
.ToArray();
// get main + preview versions
- void TryGetVersions(out ISemanticVersion mainVersion, out ISemanticVersion previewVersion, Func<(string name, string description, ISemanticVersion version), bool> filter = null)
+ void TryGetVersions([NotNullWhen(true)] out ISemanticVersion? mainVersion, out ISemanticVersion? previewVersion, Func<(string? name, string? description, ISemanticVersion? version), bool>? filter = null)
{
mainVersion = null;
previewVersion = null;
// get latest main + preview version
- foreach (var entry in versions)
+ foreach ((string? name, string? description, ISemanticVersion? version) entry in versions)
{
- if (filter?.Invoke(entry) == false)
+ if (entry.version is null || filter?.Invoke(entry) == false)
continue;
if (entry.version.IsPrerelease())
@@ -158,7 +165,7 @@ namespace StardewModdingAPI.Web.Framework
mainVersion ??= entry.version;
if (mainVersion != null)
- break; // any other values will be older
+ break; // any others will be older since entries are sorted by version
}
// normalize values
@@ -181,8 +188,7 @@ namespace StardewModdingAPI.Web.Framework
/// <summary>Get a semantic local version for update checks.</summary>
/// <param name="version">The version to map.</param>
/// <param name="map">Changes to apply to the raw version, if any.</param>
- /// <param name="allowNonStandard">Whether to allow non-standard versions.</param>
- private string GetRawMappedVersion(string version, ChangeDescriptor map, bool allowNonStandard)
+ private string? GetRawMappedVersion(string? version, ChangeDescriptor? map)
{
if (version == null || map?.HasChanges != true)
return version;
@@ -195,7 +201,7 @@ namespace StardewModdingAPI.Web.Framework
/// <summary>Normalize a version string.</summary>
/// <param name="version">The version to normalize.</param>
- private string NormalizeVersion(string version)
+ private string? NormalizeVersion(string? version)
{
if (string.IsNullOrWhiteSpace(version))
return null;
diff --git a/src/SMAPI.Web/Framework/RedirectRules/RedirectHostsToUrlsRule.cs b/src/SMAPI.Web/Framework/RedirectRules/RedirectHostsToUrlsRule.cs
index d75ee791..7b8f0ec9 100644
--- a/src/SMAPI.Web/Framework/RedirectRules/RedirectHostsToUrlsRule.cs
+++ b/src/SMAPI.Web/Framework/RedirectRules/RedirectHostsToUrlsRule.cs
@@ -11,7 +11,7 @@ namespace StardewModdingAPI.Web.Framework.RedirectRules
** Fields
*********/
/// <summary>Maps a lowercase hostname to the resulting redirect URL.</summary>
- private readonly Func<string, string> Map;
+ private readonly Func<string, string?> Map;
/*********
@@ -20,7 +20,7 @@ namespace StardewModdingAPI.Web.Framework.RedirectRules
/// <summary>Construct an instance.</summary>
/// <param name="statusCode">The status code to use for redirects.</param>
/// <param name="map">Hostnames mapped to the resulting redirect URL.</param>
- public RedirectHostsToUrlsRule(HttpStatusCode statusCode, Func<string, string> map)
+ public RedirectHostsToUrlsRule(HttpStatusCode statusCode, Func<string, string?> map)
{
this.StatusCode = statusCode;
this.Map = map ?? throw new ArgumentNullException(nameof(map));
@@ -33,12 +33,10 @@ namespace StardewModdingAPI.Web.Framework.RedirectRules
/// <summary>Get the new redirect URL.</summary>
/// <param name="context">The rewrite context.</param>
/// <returns>Returns the redirect URL, or <c>null</c> if the redirect doesn't apply.</returns>
- protected override string GetNewUrl(RewriteContext context)
+ protected override string? GetNewUrl(RewriteContext context)
{
// get requested host
- string host = context.HttpContext.Request.Host.Host;
- if (host == null)
- return null;
+ string? host = context.HttpContext.Request.Host.Host;
// get new host
host = this.Map(host);
diff --git a/src/SMAPI.Web/Framework/RedirectRules/RedirectMatchRule.cs b/src/SMAPI.Web/Framework/RedirectRules/RedirectMatchRule.cs
index 6e81c4ca..b46e8f69 100644
--- a/src/SMAPI.Web/Framework/RedirectRules/RedirectMatchRule.cs
+++ b/src/SMAPI.Web/Framework/RedirectRules/RedirectMatchRule.cs
@@ -22,7 +22,7 @@ namespace StardewModdingAPI.Web.Framework.RedirectRules
/// <param name="context">The rewrite context.</param>
public void ApplyRule(RewriteContext context)
{
- string newUrl = this.GetNewUrl(context);
+ string? newUrl = this.GetNewUrl(context);
if (newUrl == null)
return;
@@ -39,7 +39,7 @@ namespace StardewModdingAPI.Web.Framework.RedirectRules
/// <summary>Get the new redirect URL.</summary>
/// <param name="context">The rewrite context.</param>
/// <returns>Returns the redirect URL, or <c>null</c> if the redirect doesn't apply.</returns>
- protected abstract string GetNewUrl(RewriteContext context);
+ protected abstract string? GetNewUrl(RewriteContext context);
/// <summary>Get the full request URL.</summary>
/// <param name="request">The request.</param>
diff --git a/src/SMAPI.Web/Framework/RedirectRules/RedirectPathsToUrlsRule.cs b/src/SMAPI.Web/Framework/RedirectRules/RedirectPathsToUrlsRule.cs
index d9d44641..e691ffba 100644
--- a/src/SMAPI.Web/Framework/RedirectRules/RedirectPathsToUrlsRule.cs
+++ b/src/SMAPI.Web/Framework/RedirectRules/RedirectPathsToUrlsRule.cs
@@ -37,9 +37,9 @@ namespace StardewModdingAPI.Web.Framework.RedirectRules
/// <summary>Get the new redirect URL.</summary>
/// <param name="context">The rewrite context.</param>
/// <returns>Returns the redirect URL, or <c>null</c> if the redirect doesn't apply.</returns>
- protected override string GetNewUrl(RewriteContext context)
+ protected override string? GetNewUrl(RewriteContext context)
{
- string path = context.HttpContext.Request.Path.Value;
+ string? path = context.HttpContext.Request.Path.Value;
if (!string.IsNullOrWhiteSpace(path))
{
diff --git a/src/SMAPI.Web/Framework/RedirectRules/RedirectToHttpsRule.cs b/src/SMAPI.Web/Framework/RedirectRules/RedirectToHttpsRule.cs
index 2a503ae3..01807608 100644
--- a/src/SMAPI.Web/Framework/RedirectRules/RedirectToHttpsRule.cs
+++ b/src/SMAPI.Web/Framework/RedirectRules/RedirectToHttpsRule.cs
@@ -20,9 +20,9 @@ namespace StardewModdingAPI.Web.Framework.RedirectRules
*********/
/// <summary>Construct an instance.</summary>
/// <param name="except">Matches requests which should be ignored.</param>
- public RedirectToHttpsRule(Func<HttpRequest, bool> except = null)
+ public RedirectToHttpsRule(Func<HttpRequest, bool>? except = null)
{
- this.Except = except ?? (req => false);
+ this.Except = except ?? (_ => false);
this.StatusCode = HttpStatusCode.RedirectKeepVerb;
}
@@ -33,7 +33,7 @@ namespace StardewModdingAPI.Web.Framework.RedirectRules
/// <summary>Get the new redirect URL.</summary>
/// <param name="context">The rewrite context.</param>
/// <returns>Returns the redirect URL, or <c>null</c> if the redirect doesn't apply.</returns>
- protected override string GetNewUrl(RewriteContext context)
+ protected override string? GetNewUrl(RewriteContext context)
{
HttpRequest request = context.HttpContext.Request;
if (request.IsHttps || this.Except(request))
diff --git a/src/SMAPI.Web/Framework/Storage/StorageProvider.cs b/src/SMAPI.Web/Framework/Storage/StorageProvider.cs
index c6f8bac1..effbbc9f 100644
--- a/src/SMAPI.Web/Framework/Storage/StorageProvider.cs
+++ b/src/SMAPI.Web/Framework/Storage/StorageProvider.cs
@@ -63,11 +63,11 @@ namespace StardewModdingAPI.Web.Framework.Storage
BlobClient blob = this.GetAzureBlobClient(id);
await blob.UploadAsync(stream);
- return new UploadResult(true, id, null);
+ return new UploadResult(id, null);
}
catch (Exception ex)
{
- return new UploadResult(false, null, ex.Message);
+ return new UploadResult(null, ex.Message);
}
}
@@ -75,10 +75,10 @@ namespace StardewModdingAPI.Web.Framework.Storage
else
{
string path = this.GetDevFilePath(id);
- Directory.CreateDirectory(Path.GetDirectoryName(path));
+ Directory.CreateDirectory(Path.GetDirectoryName(path)!);
File.WriteAllText(path, content);
- return new UploadResult(true, id, null);
+ return new UploadResult(id, null);
}
}
@@ -103,26 +103,20 @@ namespace StardewModdingAPI.Web.Framework.Storage
// fetch file
Response<BlobDownloadInfo> response = await blob.DownloadAsync();
using BlobDownloadInfo result = response.Value;
- using StreamReader reader = new StreamReader(result.Content);
+ using StreamReader reader = new(result.Content);
DateTimeOffset expiry = result.Details.LastModified + TimeSpan.FromDays(this.ExpiryDays);
string content = this.GzipHelper.DecompressString(reader.ReadToEnd());
// build model
- return new StoredFileInfo
- {
- Success = true,
- Content = content,
- Expiry = expiry.UtcDateTime
- };
+ return new StoredFileInfo(content, expiry);
}
catch (RequestFailedException ex)
{
- return new StoredFileInfo
- {
- Error = ex.ErrorCode == "BlobNotFound"
+ return new StoredFileInfo(
+ error: ex.ErrorCode == "BlobNotFound"
? "There's no file with that ID."
: $"Could not fetch that file from storage ({ex.ErrorCode}: {ex.Message})."
- };
+ );
}
}
@@ -130,15 +124,12 @@ namespace StardewModdingAPI.Web.Framework.Storage
else
{
// get file
- FileInfo file = new FileInfo(this.GetDevFilePath(id));
+ FileInfo file = new(this.GetDevFilePath(id));
if (file.Exists && file.LastWriteTimeUtc.AddDays(this.ExpiryDays) < DateTime.UtcNow) // expired
file.Delete();
if (!file.Exists)
{
- return new StoredFileInfo
- {
- Error = "There's no file with that ID."
- };
+ return new StoredFileInfo(error: "There's no file with that ID.");
}
// renew
@@ -149,13 +140,11 @@ namespace StardewModdingAPI.Web.Framework.Storage
}
// build model
- return new StoredFileInfo
- {
- Success = true,
- Content = File.ReadAllText(file.FullName),
- Expiry = DateTime.UtcNow.AddDays(this.ExpiryDays),
- Warning = "This file was saved temporarily to the local computer. This should only happen in a local development environment."
- };
+ return new StoredFileInfo(
+ content: File.ReadAllText(file.FullName),
+ expiry: DateTime.UtcNow.AddDays(this.ExpiryDays),
+ warning: "This file was saved temporarily to the local computer. This should only happen in a local development environment."
+ );
}
}
@@ -164,12 +153,7 @@ namespace StardewModdingAPI.Web.Framework.Storage
{
PasteInfo response = await this.Pastebin.GetAsync(id);
response.Content = this.GzipHelper.DecompressString(response.Content);
- return new StoredFileInfo
- {
- Success = response.Success,
- Content = response.Content,
- Error = response.Error
- };
+ return new StoredFileInfo(response.Content, null, error: response.Error);
}
}
@@ -177,8 +161,8 @@ namespace StardewModdingAPI.Web.Framework.Storage
/// <param name="id">The file ID.</param>
private BlobClient GetAzureBlobClient(string id)
{
- var azure = new BlobServiceClient(this.ClientsConfig.AzureBlobConnectionString);
- var container = azure.GetBlobContainerClient(this.ClientsConfig.AzureBlobTempContainer);
+ BlobServiceClient azure = new(this.ClientsConfig.AzureBlobConnectionString);
+ BlobContainerClient container = azure.GetBlobContainerClient(this.ClientsConfig.AzureBlobTempContainer);
return container.GetBlobClient($"uploads/{id}");
}
diff --git a/src/SMAPI.Web/Framework/Storage/StoredFileInfo.cs b/src/SMAPI.Web/Framework/Storage/StoredFileInfo.cs
index 30676c88..bbbcf2a9 100644
--- a/src/SMAPI.Web/Framework/Storage/StoredFileInfo.cs
+++ b/src/SMAPI.Web/Framework/Storage/StoredFileInfo.cs
@@ -1,23 +1,52 @@
using System;
+using System.Diagnostics.CodeAnalysis;
namespace StardewModdingAPI.Web.Framework.Storage
{
/// <summary>The response for a get-file request.</summary>
internal class StoredFileInfo
{
+ /*********
+ ** Accessors
+ *********/
/// <summary>Whether the file was successfully fetched.</summary>
- public bool Success { get; set; }
+ [MemberNotNullWhen(true, nameof(StoredFileInfo.Content))]
+ public bool Success => this.Content != null && this.Error == null;
/// <summary>The fetched file content (if <see cref="Success"/> is <c>true</c>).</summary>
- public string Content { get; set; }
+ public string? Content { get; }
/// <summary>When the file will no longer be available.</summary>
- public DateTime? Expiry { get; set; }
+ public DateTimeOffset? Expiry { get; }
/// <summary>The error message if saving succeeded, but a non-blocking issue was encountered.</summary>
- public string Warning { get; set; }
+ public string? Warning { get; }
/// <summary>The error message if saving failed.</summary>
- public string Error { get; set; }
+ public string? Error { get; }
+
+
+ /*********
+ ** Public methods
+ *********/
+ /// <summary>Construct an instance.</summary>
+ /// <param name="content">The fetched file content (if <see cref="Success"/> is <c>true</c>).</param>
+ /// <param name="expiry">When the file will no longer be available.</param>
+ /// <param name="warning">The error message if saving succeeded, but a non-blocking issue was encountered.</param>
+ /// <param name="error">The error message if saving failed.</param>
+ public StoredFileInfo(string? content, DateTimeOffset? expiry, string? warning = null, string? error = null)
+ {
+ this.Content = content;
+ this.Expiry = expiry;
+ this.Warning = warning;
+ this.Error = error;
+ }
+
+ /// <summary>Construct an instance.</summary>
+ /// <param name="error">The error message if saving failed.</param>
+ public StoredFileInfo(string error)
+ {
+ this.Error = error;
+ }
}
}
diff --git a/src/SMAPI.Web/Framework/Storage/UploadResult.cs b/src/SMAPI.Web/Framework/Storage/UploadResult.cs
index 483c1769..92993d42 100644
--- a/src/SMAPI.Web/Framework/Storage/UploadResult.cs
+++ b/src/SMAPI.Web/Framework/Storage/UploadResult.cs
@@ -1,3 +1,5 @@
+using System.Diagnostics.CodeAnalysis;
+
namespace StardewModdingAPI.Web.Framework.Storage
{
/// <summary>The result of an attempt to upload a file.</summary>
@@ -7,25 +9,25 @@ namespace StardewModdingAPI.Web.Framework.Storage
** Accessors
*********/
/// <summary>Whether the file upload succeeded.</summary>
- public bool Succeeded { get; }
+ [MemberNotNullWhen(true, nameof(UploadResult.ID))]
+ [MemberNotNullWhen(false, nameof(UploadResult.UploadError))]
+ public bool Succeeded => this.ID != null && this.UploadError == null;
/// <summary>The file ID, if applicable.</summary>
- public string ID { get; }
+ public string? ID { get; }
/// <summary>The upload error, if any.</summary>
- public string UploadError { get; }
+ public string? UploadError { get; }
/*********
** Public methods
*********/
/// <summary>Construct an instance.</summary>
- /// <param name="succeeded">Whether the file upload succeeded.</param>
/// <param name="id">The file ID, if applicable.</param>
/// <param name="uploadError">The upload error, if any.</param>
- public UploadResult(bool succeeded, string id, string uploadError)
+ public UploadResult(string? id, string? uploadError)
{
- this.Succeeded = succeeded;
this.ID = id;
this.UploadError = uploadError;
}
diff --git a/src/SMAPI.Web/Framework/VersionConstraint.cs b/src/SMAPI.Web/Framework/VersionConstraint.cs
index f0c57c41..1b1abd81 100644
--- a/src/SMAPI.Web/Framework/VersionConstraint.cs
+++ b/src/SMAPI.Web/Framework/VersionConstraint.cs
@@ -18,7 +18,7 @@ namespace StardewModdingAPI.Web.Framework
/// <param name="values">A dictionary that contains the parameters for the URL.</param>
/// <param name="routeDirection">An object that indicates whether the constraint check is being performed when an incoming request is being handled or when a URL is being generated.</param>
/// <returns><c>true</c> if the URL parameter contains a valid value; otherwise, <c>false</c>.</returns>
- public bool Match(HttpContext httpContext, IRouter route, string routeKey, RouteValueDictionary values, RouteDirection routeDirection)
+ public bool Match(HttpContext? httpContext, IRouter? route, string routeKey, RouteValueDictionary values, RouteDirection routeDirection)
{
if (routeKey == null)
throw new ArgumentNullException(nameof(routeKey));
@@ -26,7 +26,7 @@ namespace StardewModdingAPI.Web.Framework
throw new ArgumentNullException(nameof(values));
return
- values.TryGetValue(routeKey, out object routeValue)
+ values.TryGetValue(routeKey, out object? routeValue)
&& routeValue is string routeStr
&& SemanticVersion.TryParse(routeStr, allowNonStandard: true, out _);
}