From 0b48c1748b354458059c7607415288de072b01e9 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Tue, 12 Apr 2022 19:15:39 -0400 Subject: [PATCH] enable nullable annotations in the web project & related code (#837) --- build/common.targets | 4 +- .../ISemanticVersion.cs | 4 + .../Framework/Clients/WebApi/ModEntryModel.cs | 19 ++- .../Clients/WebApi/ModEntryVersionModel.cs | 9 +- .../WebApi/ModExtendedMetadataModel.cs | 36 ++-- .../Clients/WebApi/ModSearchEntryModel.cs | 27 +-- .../Clients/WebApi/ModSearchModel.cs | 10 +- .../Framework/Clients/WebApi/WebApiClient.cs | 5 +- .../Clients/Wiki/ChangeDescriptor.cs | 12 +- .../Framework/Clients/Wiki/WikiClient.cs | 159 ++++++++++-------- .../Clients/Wiki/WikiCompatibilityInfo.cs | 33 +++- .../Clients/Wiki/WikiCompatibilityStatus.cs | 2 - .../Framework/Clients/Wiki/WikiModEntry.cs | 96 ++++++++--- .../Framework/Clients/Wiki/WikiModList.cs | 23 ++- src/SMAPI.Toolkit/SemanticVersion.cs | 3 + src/SMAPI.Toolkit/SemanticVersionComparer.cs | 2 +- src/SMAPI.Web/BackgroundService.cs | 24 ++- src/SMAPI.Web/Controllers/IndexController.cs | 12 +- .../Controllers/JsonValidatorController.cs | 46 +++-- .../Controllers/LogParserController.cs | 6 +- .../Controllers/ModsApiController.cs | 70 ++++---- src/SMAPI.Web/Controllers/ModsController.cs | 7 +- .../Framework/AllowLargePostsAttribute.cs | 4 +- src/SMAPI.Web/Framework/Caching/Cached.cs | 11 +- .../Caching/Mods/IModCacheRepository.cs | 5 +- .../Caching/Mods/ModCacheMemoryRepository.cs | 5 +- .../Caching/Wiki/IWikiCacheRepository.cs | 9 +- .../Caching/Wiki/WikiCacheMemoryRepository.cs | 11 +- .../Framework/Caching/Wiki/WikiMetadata.cs | 11 +- .../Clients/Chucklefish/ChucklefishClient.cs | 12 +- .../Clients/CurseForge/CurseForgeClient.cs | 14 +- .../CurseForge/ResponseModels/ModFileModel.cs | 22 ++- .../CurseForge/ResponseModels/ModModel.cs | 30 +++- .../Framework/Clients/GenericModDownload.cs | 13 +- .../Framework/Clients/GenericModPage.cs | 23 +-- .../Framework/Clients/GitHub/GitAsset.cs | 26 ++- .../Framework/Clients/GitHub/GitHubClient.cs | 30 ++-- .../Framework/Clients/GitHub/GitLicense.cs | 26 ++- .../Framework/Clients/GitHub/GitRelease.cs | 36 +++- .../Framework/Clients/GitHub/GitRepo.cs | 26 ++- .../Framework/Clients/GitHub/IGitHubClient.cs | 6 +- .../Framework/Clients/IModSiteClient.cs | 4 +- .../Clients/ModDrop/ModDropClient.cs | 20 ++- .../ModDrop/ResponseModels/FileDataModel.cs | 42 ++++- .../ModDrop/ResponseModels/ModDataModel.cs | 24 ++- .../ModDrop/ResponseModels/ModListModel.cs | 7 +- .../ModDrop/ResponseModels/ModModel.cs | 22 ++- .../Framework/Clients/Nexus/NexusClient.cs | 63 ++++--- .../Clients/Nexus/ResponseModels/NexusMod.cs | 43 ++++- .../Clients/Pastebin/IPastebinClient.cs | 2 - .../Framework/Clients/Pastebin/PasteInfo.cs | 28 ++- .../Clients/Pastebin/PastebinClient.cs | 14 +- .../Framework/Compression/GzipHelper.cs | 9 +- .../Framework/Compression/IGzipHelper.cs | 5 +- .../ConfigModels/ApiClientsConfig.cs | 36 ++-- .../ConfigModels/ModOverrideConfig.cs | 6 +- .../ConfigModels/ModUpdateCheckConfig.cs | 10 +- .../Framework/ConfigModels/SiteConfig.cs | 6 +- .../Framework/ConfigModels/SmapiInfoConfig.cs | 8 +- src/SMAPI.Web/Framework/Extensions.cs | 8 +- src/SMAPI.Web/Framework/IModDownload.cs | 9 +- src/SMAPI.Web/Framework/IModPage.cs | 18 +- .../InternalControllerFeatureProvider.cs | 2 - .../JobDashboardAuthorizationFilter.cs | 2 - src/SMAPI.Web/Framework/ModInfoModel.cs | 37 ++-- src/SMAPI.Web/Framework/ModSiteManager.cs | 58 ++++--- .../RedirectRules/RedirectHostsToUrlsRule.cs | 10 +- .../RedirectRules/RedirectMatchRule.cs | 6 +- .../RedirectRules/RedirectPathsToUrlsRule.cs | 6 +- .../RedirectRules/RedirectToHttpsRule.cs | 6 +- .../Framework/Storage/IStorageProvider.cs | 2 - .../Framework/Storage/StorageProvider.cs | 52 ++---- .../Framework/Storage/StoredFileInfo.cs | 41 ++++- .../Framework/Storage/UploadResult.cs | 14 +- src/SMAPI.Web/Framework/VersionConstraint.cs | 6 +- src/SMAPI.Web/Program.cs | 2 - src/SMAPI.Web/Startup.cs | 4 +- src/SMAPI.Web/ViewModels/IndexModel.cs | 13 +- src/SMAPI.Web/ViewModels/IndexVersionModel.cs | 15 +- .../JsonValidator/JsonValidatorErrorModel.cs | 15 +- .../JsonValidator/JsonValidatorModel.cs | 29 ++-- .../JsonValidatorRequestModel.cs | 19 ++- src/SMAPI.Web/ViewModels/LogParserModel.cs | 2 +- .../ViewModels/ModCompatibilityModel.cs | 27 ++- src/SMAPI.Web/ViewModels/ModLinkModel.cs | 6 +- src/SMAPI.Web/ViewModels/ModListModel.cs | 19 +-- src/SMAPI.Web/ViewModels/ModModel.cs | 65 +++++-- src/SMAPI.Web/Views/Index/Index.cshtml | 6 +- src/SMAPI.Web/Views/Index/Privacy.cshtml | 4 - .../Views/JsonValidator/Index.cshtml | 12 +- src/SMAPI.Web/Views/LogParser/Index.cshtml | 2 +- src/SMAPI.Web/Views/Mods/Index.cshtml | 6 +- src/SMAPI.Web/Views/Shared/_Layout.cshtml | 4 - src/SMAPI.Web/Views/_ViewStart.cshtml | 4 - src/SMAPI.Web/appsettings.json | 3 +- src/SMAPI.sln.DotSettings | 1 + src/SMAPI/SemanticVersion.cs | 3 + 97 files changed, 1046 insertions(+), 770 deletions(-) diff --git a/build/common.targets b/build/common.targets index c227190a..c04546d0 100644 --- a/build/common.targets +++ b/build/common.targets @@ -7,8 +7,8 @@ $(AssemblySearchPaths);{GAC} - enable - $(NoWarn);CS8632 + enable + $(NoWarn);CS8632 $(DefineConstants);SMAPI_FOR_WINDOWS diff --git a/src/SMAPI.Toolkit.CoreInterfaces/ISemanticVersion.cs b/src/SMAPI.Toolkit.CoreInterfaces/ISemanticVersion.cs index 7998272f..dc226b7c 100644 --- a/src/SMAPI.Toolkit.CoreInterfaces/ISemanticVersion.cs +++ b/src/SMAPI.Toolkit.CoreInterfaces/ISemanticVersion.cs @@ -1,4 +1,5 @@ using System; +using System.Diagnostics.CodeAnalysis; namespace StardewModdingAPI { @@ -28,6 +29,9 @@ namespace StardewModdingAPI ** Accessors *********/ /// Whether this is a prerelease version. +#if NET5_0_OR_GREATER + [MemberNotNullWhen(true, nameof(ISemanticVersion.PrereleaseTag))] +#endif bool IsPrerelease(); /// Get whether this version is older than the specified version. diff --git a/src/SMAPI.Toolkit/Framework/Clients/WebApi/ModEntryModel.cs b/src/SMAPI.Toolkit/Framework/Clients/WebApi/ModEntryModel.cs index d5ca2034..4fc4ea54 100644 --- a/src/SMAPI.Toolkit/Framework/Clients/WebApi/ModEntryModel.cs +++ b/src/SMAPI.Toolkit/Framework/Clients/WebApi/ModEntryModel.cs @@ -1,5 +1,3 @@ -#nullable disable - using System; namespace StardewModdingAPI.Toolkit.Framework.Clients.WebApi @@ -11,15 +9,26 @@ namespace StardewModdingAPI.Toolkit.Framework.Clients.WebApi ** Accessors *********/ /// The mod's unique ID (if known). - public string ID { get; set; } + public string ID { get; } /// The update version recommended by the web API based on its version update and mapping rules. - public ModEntryVersionModel SuggestedUpdate { get; set; } + public ModEntryVersionModel? SuggestedUpdate { get; set; } /// Optional extended data which isn't needed for update checks. - public ModExtendedMetadataModel Metadata { get; set; } + public ModExtendedMetadataModel? Metadata { get; set; } /// The errors that occurred while fetching update data. public string[] Errors { get; set; } = Array.Empty(); + + + /********* + ** Public methods + *********/ + /// Construct an instance. + /// The mod's unique ID (if known). + public ModEntryModel(string id) + { + this.ID = id; + } } } diff --git a/src/SMAPI.Toolkit/Framework/Clients/WebApi/ModEntryVersionModel.cs b/src/SMAPI.Toolkit/Framework/Clients/WebApi/ModEntryVersionModel.cs index 9aac7fd3..a1e78986 100644 --- a/src/SMAPI.Toolkit/Framework/Clients/WebApi/ModEntryVersionModel.cs +++ b/src/SMAPI.Toolkit/Framework/Clients/WebApi/ModEntryVersionModel.cs @@ -1,5 +1,3 @@ -#nullable disable - using Newtonsoft.Json; using StardewModdingAPI.Toolkit.Serialization.Converters; @@ -13,18 +11,15 @@ namespace StardewModdingAPI.Toolkit.Framework.Clients.WebApi *********/ /// The version number. [JsonConverter(typeof(NonStandardSemanticVersionConverter))] - public ISemanticVersion Version { get; set; } + public ISemanticVersion Version { get; } /// The mod page URL. - public string Url { get; set; } + public string Url { get; } /********* ** Public methods *********/ - /// Construct an instance. - public ModEntryVersionModel() { } - /// Construct an instance. /// The version number. /// The mod page URL. diff --git a/src/SMAPI.Toolkit/Framework/Clients/WebApi/ModExtendedMetadataModel.cs b/src/SMAPI.Toolkit/Framework/Clients/WebApi/ModExtendedMetadataModel.cs index eb54ec78..272a2063 100644 --- a/src/SMAPI.Toolkit/Framework/Clients/WebApi/ModExtendedMetadataModel.cs +++ b/src/SMAPI.Toolkit/Framework/Clients/WebApi/ModExtendedMetadataModel.cs @@ -1,5 +1,3 @@ -#nullable disable - using System; using System.Collections.Generic; using System.Linq; @@ -23,7 +21,7 @@ namespace StardewModdingAPI.Toolkit.Framework.Clients.WebApi public string[] ID { get; set; } = Array.Empty(); /// The mod's display name. - public string Name { get; set; } + public string? Name { get; set; } /// The mod ID on Nexus. public int? NexusID { get; set; } @@ -35,31 +33,31 @@ namespace StardewModdingAPI.Toolkit.Framework.Clients.WebApi public int? CurseForgeID { get; set; } /// The mod key in the CurseForge mod repo (used in mod page URLs). - public string CurseForgeKey { get; set; } + public string? CurseForgeKey { get; set; } /// The mod ID in the ModDrop mod repo. public int? ModDropID { get; set; } /// The GitHub repository in the form 'owner/repo'. - public string GitHubRepo { get; set; } + public string? GitHubRepo { get; set; } /// The URL to a non-GitHub source repo. - public string CustomSourceUrl { get; set; } + public string? CustomSourceUrl { get; set; } /// The custom mod page URL (if applicable). - public string CustomUrl { get; set; } + public string? CustomUrl { get; set; } /// The main version. - public ModEntryVersionModel Main { get; set; } + public ModEntryVersionModel? Main { get; set; } /// The latest optional version, if newer than . - public ModEntryVersionModel Optional { get; set; } + public ModEntryVersionModel? Optional { get; set; } /// The latest unofficial version, if newer than and . - public ModEntryVersionModel Unofficial { get; set; } + public ModEntryVersionModel? Unofficial { get; set; } /// The latest unofficial version for the current Stardew Valley or SMAPI beta, if any. - public ModEntryVersionModel UnofficialForBeta { get; set; } + public ModEntryVersionModel? UnofficialForBeta { get; set; } /**** ** Stable compatibility @@ -69,10 +67,10 @@ namespace StardewModdingAPI.Toolkit.Framework.Clients.WebApi public WikiCompatibilityStatus? CompatibilityStatus { get; set; } /// The human-readable summary of the compatibility status or workaround, without HTML formatting. - public string CompatibilitySummary { get; set; } + public string? CompatibilitySummary { get; set; } /// The game or SMAPI version which broke this mod, if applicable. - public string BrokeIn { get; set; } + public string? BrokeIn { get; set; } /**** ** Beta compatibility @@ -82,22 +80,22 @@ namespace StardewModdingAPI.Toolkit.Framework.Clients.WebApi public WikiCompatibilityStatus? BetaCompatibilityStatus { get; set; } /// The human-readable summary of the compatibility status or workaround for the Stardew Valley beta (if any), without HTML formatting. - public string BetaCompatibilitySummary { get; set; } + public string? BetaCompatibilitySummary { get; set; } /// The beta game or SMAPI version which broke this mod, if applicable. - public string BetaBrokeIn { get; set; } + public string? BetaBrokeIn { get; set; } /**** ** Version mappings ****/ /// A serialized change descriptor to apply to the local version during update checks (see ). - public string ChangeLocalVersions { get; set; } + public string? ChangeLocalVersions { get; set; } /// A serialized change descriptor to apply to the remote version during update checks (see ). - public string ChangeRemoteVersions { get; set; } + public string? ChangeRemoteVersions { get; set; } /// A serialized change descriptor to apply to the update keys during update checks (see ). - public string ChangeUpdateKeys { get; set; } + public string? ChangeUpdateKeys { get; set; } /********* @@ -113,7 +111,7 @@ namespace StardewModdingAPI.Toolkit.Framework.Clients.WebApi /// The latest optional version, if newer than . /// The latest unofficial version, if newer than and . /// The latest unofficial version for the current Stardew Valley or SMAPI beta, if any. - public ModExtendedMetadataModel(WikiModEntry wiki, ModDataRecord db, ModEntryVersionModel main, ModEntryVersionModel optional, ModEntryVersionModel unofficial, ModEntryVersionModel unofficialForBeta) + public ModExtendedMetadataModel(WikiModEntry? wiki, ModDataRecord? db, ModEntryVersionModel? main, ModEntryVersionModel? optional, ModEntryVersionModel? unofficial, ModEntryVersionModel? unofficialForBeta) { // versions this.Main = main; diff --git a/src/SMAPI.Toolkit/Framework/Clients/WebApi/ModSearchEntryModel.cs b/src/SMAPI.Toolkit/Framework/Clients/WebApi/ModSearchEntryModel.cs index 8fe8fa2a..9c11e1db 100644 --- a/src/SMAPI.Toolkit/Framework/Clients/WebApi/ModSearchEntryModel.cs +++ b/src/SMAPI.Toolkit/Framework/Clients/WebApi/ModSearchEntryModel.cs @@ -1,6 +1,5 @@ -#nullable disable - using System; +using System.Linq; namespace StardewModdingAPI.Toolkit.Framework.Clients.WebApi { @@ -11,37 +10,39 @@ namespace StardewModdingAPI.Toolkit.Framework.Clients.WebApi ** Accessors *********/ /// The unique mod ID. - public string ID { get; set; } + public string ID { get; } /// The namespaced mod update keys (if available). - public string[] UpdateKeys { get; set; } + public string[] UpdateKeys { get; private set; } /// The mod version installed by the local player. This is used for version mapping in some cases. - public ISemanticVersion InstalledVersion { get; set; } + public ISemanticVersion? InstalledVersion { get; } /// Whether the installed version is broken or could not be loaded. - public bool IsBroken { get; set; } + public bool IsBroken { get; } /********* ** Public methods *********/ - /// Construct an empty instance. - public ModSearchEntryModel() - { - // needed for JSON deserializing - } - /// Construct an instance. /// The unique mod ID. /// The version installed by the local player. This is used for version mapping in some cases. /// The namespaced mod update keys (if available). /// Whether the installed version is broken or could not be loaded. - public ModSearchEntryModel(string id, ISemanticVersion installedVersion, string[] updateKeys, bool isBroken = false) + public ModSearchEntryModel(string id, ISemanticVersion? installedVersion, string[]? updateKeys, bool isBroken = false) { this.ID = id; this.InstalledVersion = installedVersion; this.UpdateKeys = updateKeys ?? Array.Empty(); + this.IsBroken = isBroken; + } + + /// Add update keys for the mod. + /// The update keys to add. + public void AddUpdateKeys(params string[] updateKeys) + { + this.UpdateKeys = this.UpdateKeys.Concat(updateKeys).ToArray(); } } } diff --git a/src/SMAPI.Toolkit/Framework/Clients/WebApi/ModSearchModel.cs b/src/SMAPI.Toolkit/Framework/Clients/WebApi/ModSearchModel.cs index 393391f7..a0cd9d4d 100644 --- a/src/SMAPI.Toolkit/Framework/Clients/WebApi/ModSearchModel.cs +++ b/src/SMAPI.Toolkit/Framework/Clients/WebApi/ModSearchModel.cs @@ -1,5 +1,3 @@ -#nullable disable - using System.Linq; using StardewModdingAPI.Toolkit.Utilities; @@ -24,18 +22,12 @@ namespace StardewModdingAPI.Toolkit.Framework.Clients.WebApi public ISemanticVersion GameVersion { get; set; } /// The OS on which the player plays. - public Platform? Platform { get; set; } + public Platform Platform { get; set; } /********* ** Public methods *********/ - /// Construct an empty instance. - public ModSearchModel() - { - // needed for JSON deserializing - } - /// Construct an instance. /// The mods to search. /// The SMAPI version installed by the player. If this is null, the API won't provide a recommended update. diff --git a/src/SMAPI.Toolkit/Framework/Clients/WebApi/WebApiClient.cs b/src/SMAPI.Toolkit/Framework/Clients/WebApi/WebApiClient.cs index 56acb768..d4282617 100644 --- a/src/SMAPI.Toolkit/Framework/Clients/WebApi/WebApiClient.cs +++ b/src/SMAPI.Toolkit/Framework/Clients/WebApi/WebApiClient.cs @@ -1,5 +1,3 @@ -#nullable disable - using System; using System.Collections.Generic; using System.Linq; @@ -72,7 +70,8 @@ namespace StardewModdingAPI.Toolkit.Framework.Clients.WebApi client.Headers["Content-Type"] = "application/json"; client.Headers["User-Agent"] = $"SMAPI/{this.Version}"; string response = client.UploadString(fullUrl, data); - return JsonConvert.DeserializeObject(response, this.JsonSettings); + return JsonConvert.DeserializeObject(response, this.JsonSettings) + ?? throw new InvalidOperationException($"Could not parse the response from POST {url}."); } } } diff --git a/src/SMAPI.Toolkit/Framework/Clients/Wiki/ChangeDescriptor.cs b/src/SMAPI.Toolkit/Framework/Clients/Wiki/ChangeDescriptor.cs index 910bf793..5978803e 100644 --- a/src/SMAPI.Toolkit/Framework/Clients/Wiki/ChangeDescriptor.cs +++ b/src/SMAPI.Toolkit/Framework/Clients/Wiki/ChangeDescriptor.cs @@ -1,5 +1,3 @@ -#nullable disable - using System; using System.Collections.Generic; using System.Collections.ObjectModel; @@ -49,7 +47,7 @@ namespace StardewModdingAPI.Toolkit.Framework.Clients.Wiki /// Apply the change descriptors to a comma-delimited field. /// The raw field text. /// Returns the modified field. - public string ApplyToCopy(string rawField) + public string? ApplyToCopy(string? rawField) { // get list List values = !string.IsNullOrWhiteSpace(rawField) @@ -75,12 +73,12 @@ namespace StardewModdingAPI.Toolkit.Framework.Clients.Wiki { for (int i = values.Count - 1; i >= 0; i--) { - string value = this.FormatValue(values[i]?.Trim() ?? string.Empty); + string value = this.FormatValue(values[i].Trim()); if (this.Remove.Contains(value)) values.RemoveAt(i); - else if (this.Replace.TryGetValue(value, out string newValue)) + else if (this.Replace.TryGetValue(value, out string? newValue)) values[i] = newValue; } } @@ -88,7 +86,7 @@ namespace StardewModdingAPI.Toolkit.Framework.Clients.Wiki // add values if (this.Add.Any()) { - HashSet curValues = new HashSet(values.Select(p => p?.Trim() ?? string.Empty), StringComparer.OrdinalIgnoreCase); + HashSet curValues = new HashSet(values.Select(p => p.Trim()), StringComparer.OrdinalIgnoreCase); foreach (string add in this.Add) { if (!curValues.Contains(add)) @@ -121,7 +119,7 @@ namespace StardewModdingAPI.Toolkit.Framework.Clients.Wiki /// The raw change descriptor. /// The human-readable error message describing any invalid values that were ignored. /// Format a raw value into a normalized form if needed. - public static ChangeDescriptor Parse(string descriptor, out string[] errors, Func formatValue = null) + public static ChangeDescriptor Parse(string descriptor, out string[] errors, Func? formatValue = null) { // init formatValue ??= p => p; diff --git a/src/SMAPI.Toolkit/Framework/Clients/Wiki/WikiClient.cs b/src/SMAPI.Toolkit/Framework/Clients/Wiki/WikiClient.cs index 86c3bd75..7f06d170 100644 --- a/src/SMAPI.Toolkit/Framework/Clients/Wiki/WikiClient.cs +++ b/src/SMAPI.Toolkit/Framework/Clients/Wiki/WikiClient.cs @@ -1,5 +1,3 @@ -#nullable disable - using System; using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; @@ -53,8 +51,8 @@ namespace StardewModdingAPI.Toolkit.Framework.Clients.Wiki doc.LoadHtml(html); // fetch game versions - string stableVersion = doc.DocumentNode.SelectSingleNode("//div[@class='game-stable-version']")?.InnerText; - string betaVersion = doc.DocumentNode.SelectSingleNode("//div[@class='game-beta-version']")?.InnerText; + string? stableVersion = doc.DocumentNode.SelectSingleNode("//div[@class='game-stable-version']")?.InnerText; + string? betaVersion = doc.DocumentNode.SelectSingleNode("//div[@class='game-beta-version']")?.InnerText; if (betaVersion == stableVersion) betaVersion = null; @@ -65,7 +63,7 @@ namespace StardewModdingAPI.Toolkit.Framework.Clients.Wiki if (modNodes == null) throw new InvalidOperationException("Can't parse wiki compatibility list, no mod data overrides section found."); - foreach (var entry in this.ParseOverrideEntries(modNodes)) + foreach (WikiDataOverrideEntry entry in this.ParseOverrideEntries(modNodes)) { if (entry.Ids.Any() != true || !entry.HasChanges) continue; @@ -85,18 +83,17 @@ namespace StardewModdingAPI.Toolkit.Framework.Clients.Wiki } // build model - return new WikiModList - { - StableVersion = stableVersion, - BetaVersion = betaVersion, - Mods = mods - }; + return new WikiModList( + stableVersion: stableVersion, + betaVersion: betaVersion, + mods: mods + ); } /// Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources. public void Dispose() { - this.Client?.Dispose(); + this.Client.Dispose(); } @@ -118,71 +115,68 @@ namespace StardewModdingAPI.Toolkit.Framework.Clients.Wiki int? nexusID = this.GetAttributeAsNullableInt(node, "data-nexus-id"); int? chucklefishID = this.GetAttributeAsNullableInt(node, "data-cf-id"); int? curseForgeID = this.GetAttributeAsNullableInt(node, "data-curseforge-id"); - string curseForgeKey = this.GetAttribute(node, "data-curseforge-key"); + string? curseForgeKey = this.GetAttribute(node, "data-curseforge-key"); int? modDropID = this.GetAttributeAsNullableInt(node, "data-moddrop-id"); - string githubRepo = this.GetAttribute(node, "data-github"); - string customSourceUrl = this.GetAttribute(node, "data-custom-source"); - string customUrl = this.GetAttribute(node, "data-url"); - string anchor = this.GetAttribute(node, "id"); - string contentPackFor = this.GetAttribute(node, "data-content-pack-for"); - string devNote = this.GetAttribute(node, "data-dev-note"); - string pullRequestUrl = this.GetAttribute(node, "data-pr"); + string? githubRepo = this.GetAttribute(node, "data-github"); + string? customSourceUrl = this.GetAttribute(node, "data-custom-source"); + string? customUrl = this.GetAttribute(node, "data-url"); + string? anchor = this.GetAttribute(node, "id"); + string? contentPackFor = this.GetAttribute(node, "data-content-pack-for"); + string? devNote = this.GetAttribute(node, "data-dev-note"); + string? pullRequestUrl = this.GetAttribute(node, "data-pr"); // parse stable compatibility - WikiCompatibilityInfo compatibility = new() - { - Status = this.GetAttributeAsEnum(node, "data-status") ?? WikiCompatibilityStatus.Ok, - BrokeIn = this.GetAttribute(node, "data-broke-in"), - UnofficialVersion = this.GetAttributeAsSemanticVersion(node, "data-unofficial-version"), - UnofficialUrl = this.GetAttribute(node, "data-unofficial-url"), - Summary = this.GetInnerHtml(node, "mod-summary")?.Trim() - }; + WikiCompatibilityInfo compatibility = new( + status: this.GetAttributeAsEnum(node, "data-status") ?? WikiCompatibilityStatus.Ok, + brokeIn: this.GetAttribute(node, "data-broke-in"), + unofficialVersion: this.GetAttributeAsSemanticVersion(node, "data-unofficial-version"), + unofficialUrl: this.GetAttribute(node, "data-unofficial-url"), + summary: this.GetInnerHtml(node, "mod-summary")?.Trim() + ); // parse beta compatibility - WikiCompatibilityInfo betaCompatibility = null; + WikiCompatibilityInfo? betaCompatibility = null; { WikiCompatibilityStatus? betaStatus = this.GetAttributeAsEnum(node, "data-beta-status"); if (betaStatus.HasValue) { - betaCompatibility = new WikiCompatibilityInfo - { - Status = betaStatus.Value, - BrokeIn = this.GetAttribute(node, "data-beta-broke-in"), - UnofficialVersion = this.GetAttributeAsSemanticVersion(node, "data-beta-unofficial-version"), - UnofficialUrl = this.GetAttribute(node, "data-beta-unofficial-url"), - Summary = this.GetInnerHtml(node, "mod-beta-summary") - }; + betaCompatibility = new WikiCompatibilityInfo( + status: betaStatus.Value, + brokeIn: this.GetAttribute(node, "data-beta-broke-in"), + unofficialVersion: this.GetAttributeAsSemanticVersion(node, "data-beta-unofficial-version"), + unofficialUrl: this.GetAttribute(node, "data-beta-unofficial-url"), + summary: this.GetInnerHtml(node, "mod-beta-summary") + ); } } // find data overrides - WikiDataOverrideEntry overrides = ids + WikiDataOverrideEntry? overrides = ids .Select(id => overridesById.TryGetValue(id, out overrides) ? overrides : null) .FirstOrDefault(p => p != null); // yield model - yield return new WikiModEntry - { - ID = ids, - Name = names, - Author = authors, - NexusID = nexusID, - ChucklefishID = chucklefishID, - CurseForgeID = curseForgeID, - CurseForgeKey = curseForgeKey, - ModDropID = modDropID, - GitHubRepo = githubRepo, - CustomSourceUrl = customSourceUrl, - CustomUrl = customUrl, - ContentPackFor = contentPackFor, - Compatibility = compatibility, - BetaCompatibility = betaCompatibility, - Warnings = warnings, - PullRequestUrl = pullRequestUrl, - DevNote = devNote, - Overrides = overrides, - Anchor = anchor - }; + yield return new WikiModEntry( + id: ids, + name: names, + author: authors, + nexusId: nexusID, + chucklefishId: chucklefishID, + curseForgeId: curseForgeID, + curseForgeKey: curseForgeKey, + modDropId: modDropID, + githubRepo: githubRepo, + customSourceUrl: customSourceUrl, + customUrl: customUrl, + contentPackFor: contentPackFor, + compatibility: compatibility, + betaCompatibility: betaCompatibility, + warnings: warnings, + pullRequestUrl: pullRequestUrl, + devNote: devNote, + overrides: overrides, + anchor: anchor + ); } } @@ -196,10 +190,10 @@ namespace StardewModdingAPI.Toolkit.Framework.Clients.Wiki { Ids = this.GetAttributeAsCsv(node, "data-id"), ChangeLocalVersions = this.GetAttributeAsChangeDescriptor(node, "data-local-version", - raw => SemanticVersion.TryParse(raw, out ISemanticVersion version) ? version.ToString() : raw + raw => SemanticVersion.TryParse(raw, out ISemanticVersion? version) ? version.ToString() : raw ), ChangeRemoteVersions = this.GetAttributeAsChangeDescriptor(node, "data-remote-version", - raw => SemanticVersion.TryParse(raw, out ISemanticVersion version) ? version.ToString() : raw + raw => SemanticVersion.TryParse(raw, out ISemanticVersion? version) ? version.ToString() : raw ), ChangeUpdateKeys = this.GetAttributeAsChangeDescriptor(node, "data-update-keys", @@ -212,7 +206,7 @@ namespace StardewModdingAPI.Toolkit.Framework.Clients.Wiki /// Get an attribute value. /// The element whose attributes to read. /// The attribute name. - private string GetAttribute(HtmlNode element, string name) + private string? GetAttribute(HtmlNode element, string name) { string value = element.GetAttributeValue(name, null); if (string.IsNullOrWhiteSpace(value)) @@ -225,9 +219,9 @@ namespace StardewModdingAPI.Toolkit.Framework.Clients.Wiki /// The element whose attributes to read. /// The attribute name. /// Format an raw entry value when applying changes. - private ChangeDescriptor GetAttributeAsChangeDescriptor(HtmlNode element, string name, Func formatValue) + private ChangeDescriptor? GetAttributeAsChangeDescriptor(HtmlNode element, string name, Func formatValue) { - string raw = this.GetAttribute(element, name); + string? raw = this.GetAttribute(element, name); return raw != null ? ChangeDescriptor.Parse(raw, out _, formatValue) : null; @@ -238,7 +232,7 @@ namespace StardewModdingAPI.Toolkit.Framework.Clients.Wiki /// The attribute name. private string[] GetAttributeAsCsv(HtmlNode element, string name) { - string raw = this.GetAttribute(element, name); + string? raw = this.GetAttribute(element, name); return !string.IsNullOrWhiteSpace(raw) ? raw.Split(new[] { ',' }, StringSplitOptions.RemoveEmptyEntries).Select(p => p.Trim()).ToArray() : Array.Empty(); @@ -250,7 +244,7 @@ namespace StardewModdingAPI.Toolkit.Framework.Clients.Wiki /// The attribute name. private TEnum? GetAttributeAsEnum(HtmlNode element, string name) where TEnum : struct { - string raw = this.GetAttribute(element, name); + string? raw = this.GetAttribute(element, name); if (raw == null) return null; if (!Enum.TryParse(raw, true, out TEnum value) && Enum.IsDefined(typeof(TEnum), value)) @@ -261,10 +255,10 @@ namespace StardewModdingAPI.Toolkit.Framework.Clients.Wiki /// Get an attribute value and parse it as a semantic version. /// The element whose attributes to read. /// The attribute name. - private ISemanticVersion GetAttributeAsSemanticVersion(HtmlNode element, string name) + private ISemanticVersion? GetAttributeAsSemanticVersion(HtmlNode element, string name) { - string raw = this.GetAttribute(element, name); - return SemanticVersion.TryParse(raw, out ISemanticVersion version) + string? raw = this.GetAttribute(element, name); + return SemanticVersion.TryParse(raw, out ISemanticVersion? version) ? version : null; } @@ -274,7 +268,7 @@ namespace StardewModdingAPI.Toolkit.Framework.Clients.Wiki /// The attribute name. private int? GetAttributeAsNullableInt(HtmlNode element, string name) { - string raw = this.GetAttribute(element, name); + string? raw = this.GetAttribute(element, name); if (raw != null && int.TryParse(raw, out int value)) return value; return null; @@ -283,7 +277,7 @@ namespace StardewModdingAPI.Toolkit.Framework.Clients.Wiki /// Get the text of an element with the given class name. /// The metadata container. /// The field name. - private string GetInnerHtml(HtmlNode container, string className) + private string? GetInnerHtml(HtmlNode container, string className) { return container.Descendants().FirstOrDefault(p => p.HasClass(className))?.InnerHtml; } @@ -293,8 +287,22 @@ namespace StardewModdingAPI.Toolkit.Framework.Clients.Wiki [SuppressMessage("ReSharper", "UnusedAutoPropertyAccessor.Local")] private class ResponseModel { + /********* + ** Accessors + *********/ /// The parse API results. - public ResponseParseModel Parse { get; set; } + public ResponseParseModel Parse { get; } + + + /********* + ** Public methods + *********/ + /// Construct an instance. + /// The parse API results. + public ResponseModel(ResponseParseModel parse) + { + this.Parse = parse; + } } /// The inner response model for the MediaWiki parse API. @@ -303,8 +311,11 @@ namespace StardewModdingAPI.Toolkit.Framework.Clients.Wiki [SuppressMessage("ReSharper", "UnusedAutoPropertyAccessor.Local")] private class ResponseParseModel { + /********* + ** Accessors + *********/ /// The parsed text. - public IDictionary Text { get; set; } + public IDictionary Text { get; } = new Dictionary(); } } } diff --git a/src/SMAPI.Toolkit/Framework/Clients/Wiki/WikiCompatibilityInfo.cs b/src/SMAPI.Toolkit/Framework/Clients/Wiki/WikiCompatibilityInfo.cs index 30e76d04..71c90d0c 100644 --- a/src/SMAPI.Toolkit/Framework/Clients/Wiki/WikiCompatibilityInfo.cs +++ b/src/SMAPI.Toolkit/Framework/Clients/Wiki/WikiCompatibilityInfo.cs @@ -1,5 +1,3 @@ -#nullable disable - namespace StardewModdingAPI.Toolkit.Framework.Clients.Wiki { /// Compatibility info for a mod. @@ -9,18 +7,37 @@ namespace StardewModdingAPI.Toolkit.Framework.Clients.Wiki ** Accessors *********/ /// The compatibility status. - public WikiCompatibilityStatus Status { get; set; } + public WikiCompatibilityStatus Status { get; } /// The human-readable summary of the compatibility status or workaround, without HTML formatting. - public string Summary { get; set; } + public string? Summary { get; } - /// The game or SMAPI version which broke this mod (if applicable). - public string BrokeIn { get; set; } + /// The game or SMAPI version which broke this mod, if applicable. + public string? BrokeIn { get; } /// The version of the latest unofficial update, if applicable. - public ISemanticVersion UnofficialVersion { get; set; } + public ISemanticVersion? UnofficialVersion { get; } /// The URL to the latest unofficial update, if applicable. - public string UnofficialUrl { get; set; } + public string? UnofficialUrl { get; } + + + /********* + ** Accessors + *********/ + /// Construct an instance. + /// The compatibility status. + /// The human-readable summary of the compatibility status or workaround, without HTML formatting. + /// The game or SMAPI version which broke this mod, if applicable. + /// The version of the latest unofficial update, if applicable. + /// The URL to the latest unofficial update, if applicable. + public WikiCompatibilityInfo(WikiCompatibilityStatus status, string? summary, string? brokeIn, ISemanticVersion? unofficialVersion, string? unofficialUrl) + { + this.Status = status; + this.Summary = summary; + this.BrokeIn = brokeIn; + this.UnofficialVersion = unofficialVersion; + this.UnofficialUrl = unofficialUrl; + } } } diff --git a/src/SMAPI.Toolkit/Framework/Clients/Wiki/WikiCompatibilityStatus.cs b/src/SMAPI.Toolkit/Framework/Clients/Wiki/WikiCompatibilityStatus.cs index 2c222b71..5cdf489f 100644 --- a/src/SMAPI.Toolkit/Framework/Clients/Wiki/WikiCompatibilityStatus.cs +++ b/src/SMAPI.Toolkit/Framework/Clients/Wiki/WikiCompatibilityStatus.cs @@ -1,5 +1,3 @@ -#nullable disable - namespace StardewModdingAPI.Toolkit.Framework.Clients.Wiki { /// The compatibility status for a mod. diff --git a/src/SMAPI.Toolkit/Framework/Clients/Wiki/WikiModEntry.cs b/src/SMAPI.Toolkit/Framework/Clients/Wiki/WikiModEntry.cs index 91943ff9..fc50125f 100644 --- a/src/SMAPI.Toolkit/Framework/Clients/Wiki/WikiModEntry.cs +++ b/src/SMAPI.Toolkit/Framework/Clients/Wiki/WikiModEntry.cs @@ -1,4 +1,4 @@ -#nullable disable +using System.Diagnostics.CodeAnalysis; namespace StardewModdingAPI.Toolkit.Framework.Clients.Wiki { @@ -8,64 +8,114 @@ namespace StardewModdingAPI.Toolkit.Framework.Clients.Wiki /********* ** Accessors *********/ - /// The mod's unique ID. If the mod has alternate/old IDs, they're listed in latest to newest order. - public string[] ID { get; set; } + /// The mod's unique ID. If the mod has alternate/old IDs, they're listed in latest to oldest order. + public string[] ID { get; } /// The mod's display name. If the mod has multiple names, the first one is the most canonical name. - public string[] Name { get; set; } + public string[] Name { get; } - /// The mod's author name. If the author has multiple names, the first one is the most canonical name. - public string[] Author { get; set; } + /// The mod's author name. If the author has multiple names, the first one is the most canonical name. + public string[] Author { get; } /// The mod ID on Nexus. - public int? NexusID { get; set; } + public int? NexusID { get; } /// The mod ID in the Chucklefish mod repo. - public int? ChucklefishID { get; set; } + public int? ChucklefishID { get; } /// The mod ID in the CurseForge mod repo. - public int? CurseForgeID { get; set; } + public int? CurseForgeID { get; } /// The mod key in the CurseForge mod repo (used in mod page URLs). - public string CurseForgeKey { get; set; } + public string? CurseForgeKey { get; } /// The mod ID in the ModDrop mod repo. - public int? ModDropID { get; set; } + public int? ModDropID { get; } /// The GitHub repository in the form 'owner/repo'. - public string GitHubRepo { get; set; } + public string? GitHubRepo { get; } /// The URL to a non-GitHub source repo. - public string CustomSourceUrl { get; set; } + public string? CustomSourceUrl { get; } /// The custom mod page URL (if applicable). - public string CustomUrl { get; set; } + public string? CustomUrl { get; } /// The name of the mod which loads this content pack, if applicable. - public string ContentPackFor { get; set; } + public string? ContentPackFor { get; } /// The mod's compatibility with the latest stable version of the game. - public WikiCompatibilityInfo Compatibility { get; set; } + public WikiCompatibilityInfo Compatibility { get; } /// The mod's compatibility with the latest beta version of the game (if any). - public WikiCompatibilityInfo BetaCompatibility { get; set; } + public WikiCompatibilityInfo? BetaCompatibility { get; } /// Whether a Stardew Valley or SMAPI beta which affects mod compatibility is in progress. If this is true, should be used for beta versions of SMAPI instead of . +#if NET5_0_OR_GREATER + [MemberNotNullWhen(true, nameof(WikiModEntry.BetaCompatibility))] +#endif public bool HasBetaInfo => this.BetaCompatibility != null; /// The human-readable warnings for players about this mod. - public string[] Warnings { get; set; } + public string[] Warnings { get; } /// The URL of the pull request which submits changes for an unofficial update to the author, if any. - public string PullRequestUrl { get; set; } + public string? PullRequestUrl { get; } - /// Special notes intended for developers who maintain unofficial updates or submit pull requests. - public string DevNote { get; set; } + /// Special notes intended for developers who maintain unofficial updates or submit pull requests. + public string? DevNote { get; } /// The data overrides to apply to the mod's manifest or remote mod page data, if any. - public WikiDataOverrideEntry Overrides { get; set; } + public WikiDataOverrideEntry? Overrides { get; } /// The link anchor for the mod entry in the wiki compatibility list. - public string Anchor { get; set; } + public string? Anchor { get; } + + + /********* + ** Public methods + *********/ + /// Construct an instance. + /// The mod's unique ID. If the mod has alternate/old IDs, they're listed in latest to oldest order. + /// The mod's display name. If the mod has multiple names, the first one is the most canonical name. + /// The mod's author name. If the author has multiple names, the first one is the most canonical name. + /// The mod ID on Nexus. + /// The mod ID in the Chucklefish mod repo. + /// The mod ID in the CurseForge mod repo. + /// The mod ID in the CurseForge mod repo. + /// The mod ID in the ModDrop mod repo. + /// The GitHub repository in the form 'owner/repo'. + /// The URL to a non-GitHub source repo. + /// The custom mod page URL (if applicable). + /// The name of the mod which loads this content pack, if applicable. + /// The mod's compatibility with the latest stable version of the game. + /// The mod's compatibility with the latest beta version of the game (if any). + /// The human-readable warnings for players about this mod. + /// The URL of the pull request which submits changes for an unofficial update to the author, if any. + /// Special notes intended for developers who maintain unofficial updates or submit pull requests. + /// The data overrides to apply to the mod's manifest or remote mod page data, if any. + /// The link anchor for the mod entry in the wiki compatibility list. + public WikiModEntry(string[] id, string[] name, string[] author, int? nexusId, int? chucklefishId, int? curseForgeId, string? curseForgeKey, int? modDropId, string? githubRepo, string? customSourceUrl, string? customUrl, string? contentPackFor, WikiCompatibilityInfo compatibility, WikiCompatibilityInfo? betaCompatibility, string[] warnings, string? pullRequestUrl, string? devNote, WikiDataOverrideEntry? overrides, string? anchor) + { + this.ID = id; + this.Name = name; + this.Author = author; + this.NexusID = nexusId; + this.ChucklefishID = chucklefishId; + this.CurseForgeID = curseForgeId; + this.CurseForgeKey = curseForgeKey; + this.ModDropID = modDropId; + this.GitHubRepo = githubRepo; + this.CustomSourceUrl = customSourceUrl; + this.CustomUrl = customUrl; + this.ContentPackFor = contentPackFor; + this.Compatibility = compatibility; + this.BetaCompatibility = betaCompatibility; + this.Warnings = warnings; + this.PullRequestUrl = pullRequestUrl; + this.DevNote = devNote; + this.Overrides = overrides; + this.Anchor = anchor; + } } } diff --git a/src/SMAPI.Toolkit/Framework/Clients/Wiki/WikiModList.cs b/src/SMAPI.Toolkit/Framework/Clients/Wiki/WikiModList.cs index 1787197a..24548078 100644 --- a/src/SMAPI.Toolkit/Framework/Clients/Wiki/WikiModList.cs +++ b/src/SMAPI.Toolkit/Framework/Clients/Wiki/WikiModList.cs @@ -1,5 +1,3 @@ -#nullable disable - namespace StardewModdingAPI.Toolkit.Framework.Clients.Wiki { /// Metadata from the wiki's mod compatibility list. @@ -9,12 +7,27 @@ namespace StardewModdingAPI.Toolkit.Framework.Clients.Wiki ** Accessors *********/ /// The stable game version. - public string StableVersion { get; set; } + public string? StableVersion { get; } /// The beta game version (if any). - public string BetaVersion { get; set; } + public string? BetaVersion { get; } /// The mods on the wiki. - public WikiModEntry[] Mods { get; set; } + public WikiModEntry[] Mods { get; } + + + /********* + ** Public methods + *********/ + /// Construct an instance. + /// The stable game version. + /// The beta game version (if any). + /// The mods on the wiki. + public WikiModList(string? stableVersion, string? betaVersion, WikiModEntry[] mods) + { + this.StableVersion = stableVersion; + this.BetaVersion = betaVersion; + this.Mods = mods; + } } } diff --git a/src/SMAPI.Toolkit/SemanticVersion.cs b/src/SMAPI.Toolkit/SemanticVersion.cs index 2cb27e11..3713758f 100644 --- a/src/SMAPI.Toolkit/SemanticVersion.cs +++ b/src/SMAPI.Toolkit/SemanticVersion.cs @@ -119,6 +119,9 @@ namespace StardewModdingAPI.Toolkit } /// +#if NET5_0_OR_GREATER + [MemberNotNullWhen(true, nameof(SemanticVersion.PrereleaseTag))] +#endif public bool IsPrerelease() { return !string.IsNullOrWhiteSpace(this.PrereleaseTag); diff --git a/src/SMAPI.Toolkit/SemanticVersionComparer.cs b/src/SMAPI.Toolkit/SemanticVersionComparer.cs index 85c974bd..2eca30df 100644 --- a/src/SMAPI.Toolkit/SemanticVersionComparer.cs +++ b/src/SMAPI.Toolkit/SemanticVersionComparer.cs @@ -3,7 +3,7 @@ using System.Collections.Generic; namespace StardewModdingAPI.Toolkit { /// A comparer for semantic versions based on the field. - public class SemanticVersionComparer : IComparer + public class SemanticVersionComparer : IComparer { /********* ** Accessors diff --git a/src/SMAPI.Web/BackgroundService.cs b/src/SMAPI.Web/BackgroundService.cs index 7706b276..49356f76 100644 --- a/src/SMAPI.Web/BackgroundService.cs +++ b/src/SMAPI.Web/BackgroundService.cs @@ -1,5 +1,3 @@ -#nullable disable - using System; using System.Diagnostics.CodeAnalysis; using System.Threading; @@ -21,13 +19,17 @@ namespace StardewModdingAPI.Web ** Fields *********/ /// The background task server. - private static BackgroundJobServer JobServer; + private static BackgroundJobServer? JobServer; /// The cache in which to store wiki metadata. - private static IWikiCacheRepository WikiCache; + private static IWikiCacheRepository? WikiCache; /// The cache in which to store mod data. - private static IModCacheRepository ModCache; + private static IModCacheRepository? ModCache; + + /// Whether the service has been started. + [MemberNotNullWhen(true, nameof(BackgroundService.JobServer), nameof(BackgroundService.WikiCache), nameof(BackgroundService.ModCache))] + private static bool IsStarted { get; set; } /********* @@ -61,6 +63,8 @@ namespace StardewModdingAPI.Web RecurringJob.AddOrUpdate(() => BackgroundService.UpdateWikiAsync(), "*/10 * * * *"); // every 10 minutes RecurringJob.AddOrUpdate(() => BackgroundService.RemoveStaleModsAsync(), "0 * * * *"); // hourly + BackgroundService.IsStarted = true; + return Task.CompletedTask; } @@ -68,6 +72,8 @@ namespace StardewModdingAPI.Web /// Tracks whether the shutdown process should no longer be graceful. public async Task StopAsync(CancellationToken cancellationToken) { + BackgroundService.IsStarted = false; + if (BackgroundService.JobServer != null) await BackgroundService.JobServer.WaitForShutdownAsync(cancellationToken); } @@ -75,6 +81,8 @@ namespace StardewModdingAPI.Web /// Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources. public void Dispose() { + BackgroundService.IsStarted = false; + BackgroundService.JobServer?.Dispose(); } @@ -85,6 +93,9 @@ namespace StardewModdingAPI.Web [AutomaticRetry(Attempts = 3, DelaysInSeconds = new[] { 30, 60, 120 })] public static async Task UpdateWikiAsync() { + if (!BackgroundService.IsStarted) + throw new InvalidOperationException($"Must call {nameof(BackgroundService.StartAsync)} before scheduling tasks."); + WikiModList wikiCompatList = await new ModToolkit().GetWikiCompatibilityListAsync(); BackgroundService.WikiCache.SaveWikiData(wikiCompatList.StableVersion, wikiCompatList.BetaVersion, wikiCompatList.Mods); } @@ -92,6 +103,9 @@ namespace StardewModdingAPI.Web /// Remove mods which haven't been requested in over 48 hours. public static Task RemoveStaleModsAsync() { + if (!BackgroundService.IsStarted) + throw new InvalidOperationException($"Must call {nameof(BackgroundService.StartAsync)} before scheduling tasks."); + BackgroundService.ModCache.RemoveStaleMods(TimeSpan.FromHours(48)); return Task.CompletedTask; } diff --git a/src/SMAPI.Web/Controllers/IndexController.cs b/src/SMAPI.Web/Controllers/IndexController.cs index f7834b9c..522d77cd 100644 --- a/src/SMAPI.Web/Controllers/IndexController.cs +++ b/src/SMAPI.Web/Controllers/IndexController.cs @@ -1,5 +1,3 @@ -#nullable disable - using System; using System.Collections.Generic; using System.Linq; @@ -59,8 +57,8 @@ namespace StardewModdingAPI.Web.Controllers { // choose versions ReleaseVersion[] versions = await this.GetReleaseVersionsAsync(); - ReleaseVersion stableVersion = versions.LastOrDefault(version => !version.IsForDevs); - ReleaseVersion stableVersionForDevs = versions.LastOrDefault(version => version.IsForDevs); + ReleaseVersion? stableVersion = versions.LastOrDefault(version => !version.IsForDevs); + ReleaseVersion? stableVersionForDevs = versions.LastOrDefault(version => version.IsForDevs); // render view IndexVersionModel stableVersionModel = stableVersion != null @@ -91,7 +89,7 @@ namespace StardewModdingAPI.Web.Controllers entry.AbsoluteExpiration = DateTimeOffset.UtcNow.Add(this.CacheTime); // get latest stable release - GitRelease release = await this.GitHub.GetLatestReleaseAsync(this.RepositoryName, includePrerelease: false); + GitRelease? release = await this.GitHub.GetLatestReleaseAsync(this.RepositoryName, includePrerelease: false); // strip 'noinclude' blocks from release description if (release != null) @@ -113,7 +111,7 @@ namespace StardewModdingAPI.Web.Controllers /// Get a parsed list of SMAPI downloads for a release. /// The GitHub release. - private IEnumerable ParseReleaseVersions(GitRelease release) + private IEnumerable ParseReleaseVersions(GitRelease? release) { if (release?.Assets == null) yield break; @@ -124,7 +122,7 @@ namespace StardewModdingAPI.Web.Controllers continue; Match match = Regex.Match(asset.FileName, @"SMAPI-(?[\d\.]+(?:-.+)?)-installer(?-for-developers)?.zip"); - if (!match.Success || !SemanticVersion.TryParse(match.Groups["version"].Value, out ISemanticVersion version)) + if (!match.Success || !SemanticVersion.TryParse(match.Groups["version"].Value, out ISemanticVersion? version)) continue; bool isForDevs = match.Groups["forDevs"].Success; diff --git a/src/SMAPI.Web/Controllers/JsonValidatorController.cs b/src/SMAPI.Web/Controllers/JsonValidatorController.cs index 5791d834..e78aeeb6 100644 --- a/src/SMAPI.Web/Controllers/JsonValidatorController.cs +++ b/src/SMAPI.Web/Controllers/JsonValidatorController.cs @@ -1,5 +1,3 @@ -#nullable disable - using System; using System.Collections.Generic; using System.IO; @@ -66,7 +64,7 @@ namespace StardewModdingAPI.Web.Controllers [Route("json/{schemaName}")] [Route("json/{schemaName}/{id}")] [Route("json/{schemaName}/{id}/{operation}")] - public async Task Index(string schemaName = null, string id = null, string operation = null) + public async Task Index(string? schemaName = null, string? id = null, string? operation = null) { // parse arguments schemaName = this.NormalizeSchemaName(schemaName); @@ -81,7 +79,7 @@ namespace StardewModdingAPI.Web.Controllers return this.View("Index", result); // fetch raw JSON - StoredFileInfo file = await this.Storage.GetAsync(id, renew); + StoredFileInfo file = await this.Storage.GetAsync(id!, renew); if (string.IsNullOrWhiteSpace(file.Content)) return this.View("Index", result.SetUploadError("The JSON file seems to be empty.")); result.SetContent(file.Content, expiry: file.Expiry, uploadWarning: file.Warning); @@ -105,7 +103,7 @@ namespace StardewModdingAPI.Web.Controllers } catch (JsonReaderException ex) { - return this.View("Index", result.AddErrors(new JsonValidatorErrorModel(ex.LineNumber, ex.Path, ex.Message, ErrorType.None))); + return this.View("Index", result.AddErrors(new JsonValidatorErrorModel(ex.LineNumber, ex.Path!, ex.Message, ErrorType.None))); } // format JSON @@ -121,7 +119,7 @@ namespace StardewModdingAPI.Web.Controllers // load schema JSchema schema; { - FileInfo schemaFile = this.FindSchemaFile(schemaName); + FileInfo? schemaFile = this.FindSchemaFile(schemaName); if (schemaFile == null) return this.View("Index", result.SetParseError($"Invalid schema '{schemaName}'.")); schema = JSchema.Parse(System.IO.File.ReadAllText(schemaFile.FullName)); @@ -144,7 +142,7 @@ namespace StardewModdingAPI.Web.Controllers /// Save raw JSON data. [HttpPost, AllowLargePosts] [Route("json")] - public async Task PostAsync(JsonValidatorRequestModel request) + public async Task PostAsync(JsonValidatorRequestModel? request) { if (request == null) return this.View("Index", this.GetModel(null, null, isEditView: true).SetUploadError("The request seems to be invalid.")); @@ -163,7 +161,7 @@ namespace StardewModdingAPI.Web.Controllers return this.View("Index", this.GetModel(result.ID, schemaName, isEditView: true).SetContent(input, null).SetUploadError(result.UploadError)); // redirect to view - return this.Redirect(this.Url.PlainAction("Index", "JsonValidator", new { schemaName, id = result.ID })); + return this.Redirect(this.Url.PlainAction("Index", "JsonValidator", new { schemaName, id = result.ID })!); } @@ -174,14 +172,14 @@ namespace StardewModdingAPI.Web.Controllers /// The stored file ID. /// The schema name with which the JSON was validated. /// Whether to show the edit view. - private JsonValidatorModel GetModel(string pasteID, string schemaName, bool isEditView) + private JsonValidatorModel GetModel(string? pasteID, string? schemaName, bool isEditView) { return new JsonValidatorModel(pasteID, schemaName, this.SchemaFormats, isEditView); } /// Get a normalized schema name, or the if blank. /// The raw schema name to normalize. - private string NormalizeSchemaName(string schemaName) + private string NormalizeSchemaName(string? schemaName) { schemaName = schemaName?.Trim().ToLower(); return !string.IsNullOrWhiteSpace(schemaName) @@ -191,7 +189,7 @@ namespace StardewModdingAPI.Web.Controllers /// Get the schema file given its unique ID. /// The schema ID. - private FileInfo FindSchemaFile(string id) + private FileInfo? FindSchemaFile(string? id) { // normalize ID id = id?.Trim().ToLower(); @@ -216,13 +214,13 @@ namespace StardewModdingAPI.Web.Controllers // skip through transparent errors if (this.IsTransparentError(error)) { - foreach (var model in error.ChildErrors.SelectMany(this.GetErrorModels)) + foreach (JsonValidatorErrorModel model in error.ChildErrors.SelectMany(this.GetErrorModels)) yield return model; yield break; } // get message - string message = this.GetOverrideError(error); + string? message = this.GetOverrideError(error); if (message == null || message == this.TransparentToken) message = this.FlattenErrorMessage(error); @@ -236,7 +234,7 @@ namespace StardewModdingAPI.Web.Controllers private string FlattenErrorMessage(ValidationError error, int indent = 0) { // get override - string message = this.GetOverrideError(error); + string? message = this.GetOverrideError(error); if (message != null && message != this.TransparentToken) return message; @@ -257,7 +255,7 @@ namespace StardewModdingAPI.Web.Controllers break; case ErrorType.Required: - message = $"Missing required fields: {string.Join(", ", (List)error.Value)}."; + message = $"Missing required fields: {string.Join(", ", (List)error.Value!)}."; break; } @@ -274,7 +272,7 @@ namespace StardewModdingAPI.Web.Controllers if (!error.ChildErrors.Any()) return false; - string @override = this.GetOverrideError(error); + string? @override = this.GetOverrideError(error); return @override == this.TransparentToken || (error.ErrorType == ErrorType.Then && @override == null); @@ -282,18 +280,18 @@ namespace StardewModdingAPI.Web.Controllers /// Get an override error from the JSON schema, if any. /// The schema validation error. - private string GetOverrideError(ValidationError error) + private string? GetOverrideError(ValidationError error) { - string GetRawOverrideError() + string? GetRawOverrideError() { // get override errors - IDictionary errors = this.GetExtensionField>(error.Schema, "@errorMessages"); + IDictionary? errors = this.GetExtensionField>(error.Schema, "@errorMessages"); if (errors == null) return null; - errors = new Dictionary(errors, StringComparer.OrdinalIgnoreCase); + errors = new Dictionary(errors, StringComparer.OrdinalIgnoreCase); // match error by type and message - foreach ((string target, string errorMessage) in errors) + foreach ((string target, string? errorMessage) in errors) { if (!target.Contains(":")) continue; @@ -304,7 +302,7 @@ namespace StardewModdingAPI.Web.Controllers } // match by type - return errors.TryGetValue(error.ErrorType.ToString(), out string message) + return errors.TryGetValue(error.ErrorType.ToString(), out string? message) ? message?.Trim() : null; } @@ -317,7 +315,7 @@ namespace StardewModdingAPI.Web.Controllers /// The field type. /// The schema whose extension fields to search. /// The case-insensitive field key. - private T GetExtensionField(JSchema schema, string key) + private T? GetExtensionField(JSchema schema, string key) { foreach ((string curKey, JToken value) in schema.ExtensionData) { @@ -330,7 +328,7 @@ namespace StardewModdingAPI.Web.Controllers /// Format a schema value for display. /// The value to format. - private string FormatValue(object value) + private string FormatValue(object? value) { return value switch { diff --git a/src/SMAPI.Web/Controllers/LogParserController.cs b/src/SMAPI.Web/Controllers/LogParserController.cs index 93f2613e..33af5a81 100644 --- a/src/SMAPI.Web/Controllers/LogParserController.cs +++ b/src/SMAPI.Web/Controllers/LogParserController.cs @@ -69,7 +69,7 @@ namespace StardewModdingAPI.Web.Controllers case LogViewFormat.RawDownload: { - string content = file.Error ?? file.Content; + string content = file.Error ?? file.Content ?? string.Empty; return this.File(Encoding.UTF8.GetBytes(content), "plain/text", $"SMAPI log ({id}).txt"); } @@ -97,7 +97,7 @@ namespace StardewModdingAPI.Web.Controllers return this.View("Index", this.GetModel(null, uploadError: uploadResult.UploadError)); // redirect to view - return this.Redirect(this.Url.PlainAction("Index", "LogParser", new { id = uploadResult.ID })); + return this.Redirect(this.Url.PlainAction("Index", "LogParser", new { id = uploadResult.ID })!); } @@ -109,7 +109,7 @@ namespace StardewModdingAPI.Web.Controllers /// When the uploaded file will no longer be available. /// A non-blocking warning while uploading the log. /// An error which occurred while uploading the log. - private LogParserModel GetModel(string? pasteID, DateTime? expiry = null, string? uploadWarning = null, string? uploadError = null) + private LogParserModel GetModel(string? pasteID, DateTimeOffset? expiry = null, string? uploadWarning = null, string? uploadError = null) { Platform? platform = this.DetectClientPlatform(); diff --git a/src/SMAPI.Web/Controllers/ModsApiController.cs b/src/SMAPI.Web/Controllers/ModsApiController.cs index 3dc1e366..401bba4f 100644 --- a/src/SMAPI.Web/Controllers/ModsApiController.cs +++ b/src/SMAPI.Web/Controllers/ModsApiController.cs @@ -1,7 +1,6 @@ -#nullable disable - using System; using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; using System.IO; using System.Linq; using System.Threading.Tasks; @@ -78,7 +77,7 @@ namespace StardewModdingAPI.Web.Controllers /// The mod search criteria. /// The requested API version. [HttpPost] - public async Task> PostAsync([FromBody] ModSearchModel model, [FromRoute] string version) + public async Task> PostAsync([FromBody] ModSearchModel? model, [FromRoute] string version) { if (model?.Mods == null) return Array.Empty(); @@ -94,16 +93,16 @@ namespace StardewModdingAPI.Web.Controllers continue; // special case: if this is an update check for the official SMAPI repo, check the Nexus mod page for beta versions - if (mod.ID == config.SmapiInfo.ID && mod.UpdateKeys?.Any(key => key == config.SmapiInfo.DefaultUpdateKey) == true && mod.InstalledVersion?.IsPrerelease() == true) - mod.UpdateKeys = mod.UpdateKeys.Concat(config.SmapiInfo.AddBetaUpdateKeys).ToArray(); + if (mod.ID == config.SmapiInfo.ID && mod.UpdateKeys.Any(key => key == config.SmapiInfo.DefaultUpdateKey) && mod.InstalledVersion?.IsPrerelease() == true) + mod.AddUpdateKeys(config.SmapiInfo.AddBetaUpdateKeys); // fetch result ModEntryModel result = await this.GetModData(mod, wikiData, model.IncludeExtendedMetadata, model.ApiVersion); if (!model.IncludeExtendedMetadata && (model.ApiVersion == null || mod.InstalledVersion == null)) { - var errors = new List(result.Errors); - errors.Add($"This API can't suggest an update because {nameof(model.ApiVersion)} or {nameof(mod.InstalledVersion)} are null, and you didn't specify {nameof(model.IncludeExtendedMetadata)} to get other info. See the SMAPI technical docs for usage."); - result.Errors = errors.ToArray(); + result.Errors = result.Errors + .Concat(new[] { $"This API can't suggest an update because {nameof(model.ApiVersion)} or {nameof(mod.InstalledVersion)} are null, and you didn't specify {nameof(model.IncludeExtendedMetadata)} to get other info. See the SMAPI technical docs for usage." }) + .ToArray(); } mods[mod.ID] = result; @@ -123,26 +122,26 @@ namespace StardewModdingAPI.Web.Controllers /// Whether to include extended metadata for each mod. /// The SMAPI version installed by the player. /// Returns the mod data if found, else null. - private async Task GetModData(ModSearchEntryModel search, WikiModEntry[] wikiData, bool includeExtendedMetadata, ISemanticVersion apiVersion) + private async Task GetModData(ModSearchEntryModel search, WikiModEntry[] wikiData, bool includeExtendedMetadata, ISemanticVersion? apiVersion) { // cross-reference data - ModDataRecord record = this.ModDatabase.Get(search.ID); - WikiModEntry wikiEntry = wikiData.FirstOrDefault(entry => entry.ID.Contains(search.ID.Trim(), StringComparer.OrdinalIgnoreCase)); + ModDataRecord? record = this.ModDatabase.Get(search.ID); + WikiModEntry? wikiEntry = wikiData.FirstOrDefault(entry => entry.ID.Contains(search.ID.Trim(), StringComparer.OrdinalIgnoreCase)); UpdateKey[] updateKeys = this.GetUpdateKeys(search.UpdateKeys, record, wikiEntry).ToArray(); - ModOverrideConfig overrides = this.Config.Value.ModOverrides.FirstOrDefault(p => p.ID.Equals(search.ID?.Trim(), StringComparison.OrdinalIgnoreCase)); + ModOverrideConfig? overrides = this.Config.Value.ModOverrides.FirstOrDefault(p => p.ID.Equals(search.ID.Trim(), StringComparison.OrdinalIgnoreCase)); bool allowNonStandardVersions = overrides?.AllowNonStandardVersions ?? false; // SMAPI versions with a '-beta' tag indicate major changes that may need beta mod versions. // This doesn't apply to normal prerelease versions which have an '-alpha' tag. - bool isSmapiBeta = apiVersion.IsPrerelease() && apiVersion.PrereleaseTag.StartsWith("beta"); + bool isSmapiBeta = apiVersion != null && apiVersion.IsPrerelease() && apiVersion.PrereleaseTag.StartsWith("beta"); // get latest versions - ModEntryModel result = new() { ID = search.ID }; + ModEntryModel result = new(search.ID); IList errors = new List(); - ModEntryVersionModel main = null; - ModEntryVersionModel optional = null; - ModEntryVersionModel unofficial = null; - ModEntryVersionModel unofficialForBeta = null; + ModEntryVersionModel? main = null; + ModEntryVersionModel? optional = null; + ModEntryVersionModel? unofficial = null; + ModEntryVersionModel? unofficialForBeta = null; foreach (UpdateKey updateKey in updateKeys) { // validate update key @@ -162,9 +161,9 @@ namespace StardewModdingAPI.Web.Controllers // handle versions if (this.IsNewer(data.Version, main?.Version)) - main = new ModEntryVersionModel(data.Version, data.Url); + main = new ModEntryVersionModel(data.Version, data.Url!); if (this.IsNewer(data.PreviewVersion, optional?.Version)) - optional = new ModEntryVersionModel(data.PreviewVersion, data.Url); + optional = new ModEntryVersionModel(data.PreviewVersion, data.Url!); } // get unofficial version @@ -172,7 +171,7 @@ namespace StardewModdingAPI.Web.Controllers unofficial = new ModEntryVersionModel(wikiEntry.Compatibility.UnofficialVersion, $"{this.Url.PlainAction("Index", "Mods", absoluteUrl: true)}#{wikiEntry.Anchor}"); // get unofficial version for beta - if (wikiEntry?.HasBetaInfo == true) + if (wikiEntry is { HasBetaInfo: true }) { if (wikiEntry.BetaCompatibility.Status == WikiCompatibilityStatus.Unofficial) { @@ -198,13 +197,13 @@ namespace StardewModdingAPI.Web.Controllers if (overrides?.SetUrl != null) { if (main != null) - main.Url = overrides.SetUrl; + main = new(main.Version, overrides.SetUrl); if (optional != null) - optional.Url = overrides.SetUrl; + optional = new(optional.Version, overrides.SetUrl); } // get recommended update (if any) - ISemanticVersion installedVersion = this.ModSites.GetMappedVersion(search.InstalledVersion?.ToString(), wikiEntry?.Overrides?.ChangeLocalVersions, allowNonStandard: allowNonStandardVersions); + ISemanticVersion? installedVersion = this.ModSites.GetMappedVersion(search.InstalledVersion?.ToString(), wikiEntry?.Overrides?.ChangeLocalVersions, allowNonStandard: allowNonStandardVersions); if (apiVersion != null && installedVersion != null) { // get newer versions @@ -219,7 +218,7 @@ namespace StardewModdingAPI.Web.Controllers updates.Add(unofficialForBeta); // get newest version - ModEntryVersionModel newest = null; + ModEntryVersionModel? newest = null; foreach (ModEntryVersionModel update in updates) { if (newest == null || update.Version.IsNewerThan(newest.Version)) @@ -245,7 +244,7 @@ namespace StardewModdingAPI.Web.Controllers /// The current semantic version. /// The target semantic version. /// Whether the user enabled the beta channel and should be offered prerelease updates. - private bool IsRecommendedUpdate(ISemanticVersion currentVersion, ISemanticVersion newVersion, bool useBetaChannel) + private bool IsRecommendedUpdate(ISemanticVersion currentVersion, [NotNullWhen(true)] ISemanticVersion? newVersion, bool useBetaChannel) { return newVersion != null @@ -256,7 +255,7 @@ namespace StardewModdingAPI.Web.Controllers /// Get whether a version is newer than an version. /// The current version. /// The other version. - private bool IsNewer(ISemanticVersion current, ISemanticVersion other) + private bool IsNewer([NotNullWhen(true)] ISemanticVersion? current, ISemanticVersion? other) { return current != null && (other == null || other.IsOlderThan(current)); } @@ -265,17 +264,20 @@ namespace StardewModdingAPI.Web.Controllers /// The namespaced update key. /// Whether to allow non-standard versions. /// The changes to apply to remote versions for update checks. - private async Task GetInfoForUpdateKeyAsync(UpdateKey updateKey, bool allowNonStandardVersions, ChangeDescriptor mapRemoteVersions) + private async Task GetInfoForUpdateKeyAsync(UpdateKey updateKey, bool allowNonStandardVersions, ChangeDescriptor? mapRemoteVersions) { + if (!updateKey.LooksValid) + return new ModInfoModel().SetError(RemoteModStatus.DoesNotExist, $"Invalid update key '{updateKey}'."); + // get mod page IModPage page; { bool isCached = - this.ModCache.TryGetMod(updateKey.Site, updateKey.ID, out Cached cachedMod) + this.ModCache.TryGetMod(updateKey.Site, updateKey.ID, out Cached? cachedMod) && !this.ModCache.IsStale(cachedMod.LastUpdated, cachedMod.Data.Status == RemoteModStatus.TemporaryError ? this.Config.Value.ErrorCacheMinutes : this.Config.Value.SuccessCacheMinutes); if (isCached) - page = cachedMod.Data; + page = cachedMod!.Data; else { page = await this.ModSites.GetModPageAsync(updateKey); @@ -291,7 +293,7 @@ namespace StardewModdingAPI.Web.Controllers /// The specified update keys. /// The mod's entry in SMAPI's internal database. /// The mod's entry in the wiki list. - private IEnumerable GetUpdateKeys(string[] specifiedKeys, ModDataRecord record, WikiModEntry entry) + private IEnumerable GetUpdateKeys(string[]? specifiedKeys, ModDataRecord? record, WikiModEntry? entry) { // get unique update keys List updateKeys = this.GetUnfilteredUpdateKeys(specifiedKeys, record, entry) @@ -310,7 +312,7 @@ namespace StardewModdingAPI.Web.Controllers // if the list has both an update key (like "Nexus:2400") and subkey (like "Nexus:2400@subkey") for the same page, the subkey takes priority { var removeKeys = new HashSet(); - foreach (var key in updateKeys) + foreach (UpdateKey key in updateKeys) { if (key.Subkey != null) removeKeys.Add(new UpdateKey(key.Site, key.ID, null)); @@ -326,7 +328,7 @@ namespace StardewModdingAPI.Web.Controllers /// The specified update keys. /// The mod's entry in SMAPI's internal database. /// The mod's entry in the wiki list. - private IEnumerable GetUnfilteredUpdateKeys(string[] specifiedKeys, ModDataRecord record, WikiModEntry entry) + private IEnumerable GetUnfilteredUpdateKeys(string[]? specifiedKeys, ModDataRecord? record, WikiModEntry? entry) { // specified update keys foreach (string key in specifiedKeys ?? Array.Empty()) @@ -337,7 +339,7 @@ namespace StardewModdingAPI.Web.Controllers // default update key { - string defaultKey = record?.GetDefaultUpdateKey(); + string? defaultKey = record?.GetDefaultUpdateKey(); if (!string.IsNullOrWhiteSpace(defaultKey)) yield return defaultKey; } diff --git a/src/SMAPI.Web/Controllers/ModsController.cs b/src/SMAPI.Web/Controllers/ModsController.cs index 5292e1ce..919afa5b 100644 --- a/src/SMAPI.Web/Controllers/ModsController.cs +++ b/src/SMAPI.Web/Controllers/ModsController.cs @@ -1,5 +1,4 @@ -#nullable disable - +using System; using System.Linq; using System.Text.RegularExpressions; using Microsoft.AspNetCore.Mvc; @@ -54,8 +53,8 @@ namespace StardewModdingAPI.Web.Controllers public ModListModel FetchData() { // fetch cached data - if (!this.Cache.TryGetWikiMetadata(out Cached metadata)) - return new ModListModel(); + if (!this.Cache.TryGetWikiMetadata(out Cached? metadata)) + return new ModListModel(null, null, Array.Empty(), lastUpdated: DateTimeOffset.UtcNow, isStale: true); // build model return new ModListModel( diff --git a/src/SMAPI.Web/Framework/AllowLargePostsAttribute.cs b/src/SMAPI.Web/Framework/AllowLargePostsAttribute.cs index 108ceff7..bd414ea2 100644 --- a/src/SMAPI.Web/Framework/AllowLargePostsAttribute.cs +++ b/src/SMAPI.Web/Framework/AllowLargePostsAttribute.cs @@ -1,5 +1,3 @@ -#nullable disable - using System; using Microsoft.AspNetCore.Http.Features; using Microsoft.AspNetCore.Mvc.Filters; @@ -42,7 +40,7 @@ namespace StardewModdingAPI.Web.Framework public void OnAuthorization(AuthorizationFilterContext context) { IFeatureCollection features = context.HttpContext.Features; - IFormFeature formFeature = features.Get(); + IFormFeature? formFeature = features.Get(); if (formFeature?.Form == null) { diff --git a/src/SMAPI.Web/Framework/Caching/Cached.cs b/src/SMAPI.Web/Framework/Caching/Cached.cs index aabbf146..b393e1e1 100644 --- a/src/SMAPI.Web/Framework/Caching/Cached.cs +++ b/src/SMAPI.Web/Framework/Caching/Cached.cs @@ -1,5 +1,3 @@ -#nullable disable - using System; namespace StardewModdingAPI.Web.Framework.Caching @@ -12,21 +10,18 @@ namespace StardewModdingAPI.Web.Framework.Caching ** Accessors *********/ /// The cached data. - public T Data { get; set; } + public T Data { get; } /// When the data was last updated. - public DateTimeOffset LastUpdated { get; set; } + public DateTimeOffset LastUpdated { get; } /// When the data was last requested through the mod API. - public DateTimeOffset LastRequested { get; set; } + public DateTimeOffset LastRequested { get; internal set; } /********* ** Public methods *********/ - /// Construct an empty instance. - public Cached() { } - /// Construct an instance. /// The cached data. public Cached(T data) diff --git a/src/SMAPI.Web/Framework/Caching/Mods/IModCacheRepository.cs b/src/SMAPI.Web/Framework/Caching/Mods/IModCacheRepository.cs index 2020d747..fb74e9da 100644 --- a/src/SMAPI.Web/Framework/Caching/Mods/IModCacheRepository.cs +++ b/src/SMAPI.Web/Framework/Caching/Mods/IModCacheRepository.cs @@ -1,6 +1,5 @@ -#nullable disable - using System; +using System.Diagnostics.CodeAnalysis; using StardewModdingAPI.Toolkit.Framework.UpdateData; namespace StardewModdingAPI.Web.Framework.Caching.Mods @@ -16,7 +15,7 @@ namespace StardewModdingAPI.Web.Framework.Caching.Mods /// The mod's unique ID within the . /// The fetched mod. /// Whether to update the mod's 'last requested' date. - bool TryGetMod(ModSiteKey site, string id, out Cached mod, bool markRequested = true); + bool TryGetMod(ModSiteKey site, string id, [NotNullWhen(true)] out Cached? mod, bool markRequested = true); /// Save data fetched for a mod. /// The mod site on which the mod is found. diff --git a/src/SMAPI.Web/Framework/Caching/Mods/ModCacheMemoryRepository.cs b/src/SMAPI.Web/Framework/Caching/Mods/ModCacheMemoryRepository.cs index 338562d8..4ba0bd20 100644 --- a/src/SMAPI.Web/Framework/Caching/Mods/ModCacheMemoryRepository.cs +++ b/src/SMAPI.Web/Framework/Caching/Mods/ModCacheMemoryRepository.cs @@ -1,7 +1,6 @@ -#nullable disable - using System; using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; using System.Linq; using StardewModdingAPI.Toolkit.Framework.UpdateData; @@ -25,7 +24,7 @@ namespace StardewModdingAPI.Web.Framework.Caching.Mods /// The mod's unique ID within the . /// The fetched mod. /// Whether to update the mod's 'last requested' date. - public bool TryGetMod(ModSiteKey site, string id, out Cached mod, bool markRequested = true) + public bool TryGetMod(ModSiteKey site, string id, [NotNullWhen(true)] out Cached? mod, bool markRequested = true) { // get mod if (!this.Mods.TryGetValue(this.GetKey(site, id), out var cachedMod)) diff --git a/src/SMAPI.Web/Framework/Caching/Wiki/IWikiCacheRepository.cs b/src/SMAPI.Web/Framework/Caching/Wiki/IWikiCacheRepository.cs index 6edafddc..b8a0df34 100644 --- a/src/SMAPI.Web/Framework/Caching/Wiki/IWikiCacheRepository.cs +++ b/src/SMAPI.Web/Framework/Caching/Wiki/IWikiCacheRepository.cs @@ -1,7 +1,6 @@ -#nullable disable - using System; using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; using StardewModdingAPI.Toolkit.Framework.Clients.Wiki; namespace StardewModdingAPI.Web.Framework.Caching.Wiki @@ -14,16 +13,16 @@ namespace StardewModdingAPI.Web.Framework.Caching.Wiki *********/ /// Get the cached wiki metadata. /// The fetched metadata. - bool TryGetWikiMetadata(out Cached metadata); + bool TryGetWikiMetadata([NotNullWhen(true)] out Cached? metadata); /// Get the cached wiki mods. /// A filter to apply, if any. - IEnumerable> GetWikiMods(Func filter = null); + IEnumerable> GetWikiMods(Func? filter = null); /// Save data fetched from the wiki compatibility list. /// The current stable Stardew Valley version. /// The current beta Stardew Valley version. /// The mod data. - void SaveWikiData(string stableVersion, string betaVersion, IEnumerable mods); + void SaveWikiData(string? stableVersion, string? betaVersion, IEnumerable mods); } } diff --git a/src/SMAPI.Web/Framework/Caching/Wiki/WikiCacheMemoryRepository.cs b/src/SMAPI.Web/Framework/Caching/Wiki/WikiCacheMemoryRepository.cs index d1ccb9c7..8b4338e2 100644 --- a/src/SMAPI.Web/Framework/Caching/Wiki/WikiCacheMemoryRepository.cs +++ b/src/SMAPI.Web/Framework/Caching/Wiki/WikiCacheMemoryRepository.cs @@ -1,7 +1,6 @@ -#nullable disable - using System; using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; using System.Linq; using StardewModdingAPI.Toolkit.Framework.Clients.Wiki; @@ -14,7 +13,7 @@ namespace StardewModdingAPI.Web.Framework.Caching.Wiki ** Fields *********/ /// The saved wiki metadata. - private Cached Metadata; + private Cached? Metadata; /// The cached wiki data. private Cached[] Mods = Array.Empty>(); @@ -25,7 +24,7 @@ namespace StardewModdingAPI.Web.Framework.Caching.Wiki *********/ /// Get the cached wiki metadata. /// The fetched metadata. - public bool TryGetWikiMetadata(out Cached metadata) + public bool TryGetWikiMetadata([NotNullWhen(true)] out Cached? metadata) { metadata = this.Metadata; return metadata != null; @@ -33,7 +32,7 @@ namespace StardewModdingAPI.Web.Framework.Caching.Wiki /// Get the cached wiki mods. /// A filter to apply, if any. - public IEnumerable> GetWikiMods(Func filter = null) + public IEnumerable> GetWikiMods(Func? filter = null) { foreach (var mod in this.Mods) { @@ -46,7 +45,7 @@ namespace StardewModdingAPI.Web.Framework.Caching.Wiki /// The current stable Stardew Valley version. /// The current beta Stardew Valley version. /// The mod data. - public void SaveWikiData(string stableVersion, string betaVersion, IEnumerable mods) + public void SaveWikiData(string? stableVersion, string? betaVersion, IEnumerable mods) { this.Metadata = new Cached(new WikiMetadata(stableVersion, betaVersion)); this.Mods = mods.Select(mod => new Cached(mod)).ToArray(); diff --git a/src/SMAPI.Web/Framework/Caching/Wiki/WikiMetadata.cs b/src/SMAPI.Web/Framework/Caching/Wiki/WikiMetadata.cs index 6ae42488..f53ea201 100644 --- a/src/SMAPI.Web/Framework/Caching/Wiki/WikiMetadata.cs +++ b/src/SMAPI.Web/Framework/Caching/Wiki/WikiMetadata.cs @@ -1,5 +1,3 @@ -#nullable disable - namespace StardewModdingAPI.Web.Framework.Caching.Wiki { /// The model for cached wiki metadata. @@ -9,22 +7,19 @@ namespace StardewModdingAPI.Web.Framework.Caching.Wiki ** Accessors *********/ /// The current stable Stardew Valley version. - public string StableVersion { get; set; } + public string? StableVersion { get; } /// The current beta Stardew Valley version. - public string BetaVersion { get; set; } + public string? BetaVersion { get; } /********* ** Public methods *********/ - /// Construct an instance. - public WikiMetadata() { } - /// Construct an instance. /// The current stable Stardew Valley version. /// The current beta Stardew Valley version. - public WikiMetadata(string stableVersion, string betaVersion) + public WikiMetadata(string? stableVersion, string? betaVersion) { this.StableVersion = stableVersion; this.BetaVersion = betaVersion; diff --git a/src/SMAPI.Web/Framework/Clients/Chucklefish/ChucklefishClient.cs b/src/SMAPI.Web/Framework/Clients/Chucklefish/ChucklefishClient.cs index 4d041c1b..ce0f1122 100644 --- a/src/SMAPI.Web/Framework/Clients/Chucklefish/ChucklefishClient.cs +++ b/src/SMAPI.Web/Framework/Clients/Chucklefish/ChucklefishClient.cs @@ -1,5 +1,3 @@ -#nullable disable - using System; using System.Net; using System.Threading.Tasks; @@ -44,7 +42,7 @@ namespace StardewModdingAPI.Web.Framework.Clients.Chucklefish /// Get update check info about a mod. /// The mod ID. - public async Task GetModData(string id) + public async Task GetModData(string id) { IModPage page = new GenericModPage(this.SiteKey, id); @@ -53,7 +51,7 @@ namespace StardewModdingAPI.Web.Framework.Clients.Chucklefish return page.SetError(RemoteModStatus.DoesNotExist, $"The value '{id}' isn't a valid Chucklefish mod ID, must be an integer ID."); // fetch HTML - string html; + string? html; try { html = await this.Client @@ -69,7 +67,7 @@ namespace StardewModdingAPI.Web.Framework.Clients.Chucklefish // extract mod info string url = this.GetModUrl(parsedId); - string version = doc.DocumentNode.SelectSingleNode("//h1/span")?.InnerText; + string? version = doc.DocumentNode.SelectSingleNode("//h1/span")?.InnerText; string name = doc.DocumentNode.SelectSingleNode("//h1").ChildNodes[0].InnerText.Trim(); if (name.StartsWith("[SMAPI]")) name = name.Substring("[SMAPI]".Length).TrimStart(); @@ -81,7 +79,7 @@ namespace StardewModdingAPI.Web.Framework.Clients.Chucklefish /// Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources. public void Dispose() { - this.Client?.Dispose(); + this.Client.Dispose(); } @@ -92,7 +90,7 @@ namespace StardewModdingAPI.Web.Framework.Clients.Chucklefish /// The mod ID. private string GetModUrl(uint id) { - UriBuilder builder = new(this.Client.BaseClient.BaseAddress); + UriBuilder builder = new(this.Client.BaseClient.BaseAddress!); builder.Path += string.Format(this.ModPageUrlFormat, id); return builder.Uri.ToString(); } diff --git a/src/SMAPI.Web/Framework/Clients/CurseForge/CurseForgeClient.cs b/src/SMAPI.Web/Framework/Clients/CurseForge/CurseForgeClient.cs index 5ef369d5..d351b42d 100644 --- a/src/SMAPI.Web/Framework/Clients/CurseForge/CurseForgeClient.cs +++ b/src/SMAPI.Web/Framework/Clients/CurseForge/CurseForgeClient.cs @@ -1,5 +1,3 @@ -#nullable disable - using System.Collections.Generic; using System.Text.RegularExpressions; using System.Threading.Tasks; @@ -42,7 +40,7 @@ namespace StardewModdingAPI.Web.Framework.Clients.CurseForge /// Get update check info about a mod. /// The mod ID. - public async Task GetModData(string id) + public async Task GetModData(string id) { IModPage page = new GenericModPage(this.SiteKey, id); @@ -51,9 +49,9 @@ namespace StardewModdingAPI.Web.Framework.Clients.CurseForge return page.SetError(RemoteModStatus.DoesNotExist, $"The value '{id}' isn't a valid CurseForge mod ID, must be an integer ID."); // get raw data - ModModel mod = await this.Client + ModModel? mod = await this.Client .GetAsync($"addon/{parsedId}") - .As(); + .As(); if (mod == null) return page.SetError(RemoteModStatus.DoesNotExist, "Found no CurseForge mod with this ID."); @@ -73,7 +71,7 @@ namespace StardewModdingAPI.Web.Framework.Clients.CurseForge /// Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources. public void Dispose() { - this.Client?.Dispose(); + this.Client.Dispose(); } @@ -82,9 +80,9 @@ namespace StardewModdingAPI.Web.Framework.Clients.CurseForge *********/ /// Get a raw version string for a mod file, if available. /// The file whose version to get. - private string GetRawVersion(ModFileModel file) + private string? GetRawVersion(ModFileModel file) { - Match match = this.VersionInNamePattern.Match(file.DisplayName); + Match match = this.VersionInNamePattern.Match(file.DisplayName ?? ""); if (!match.Success) match = this.VersionInNamePattern.Match(file.FileName); diff --git a/src/SMAPI.Web/Framework/Clients/CurseForge/ResponseModels/ModFileModel.cs b/src/SMAPI.Web/Framework/Clients/CurseForge/ResponseModels/ModFileModel.cs index eabef9f0..e9adcf20 100644 --- a/src/SMAPI.Web/Framework/Clients/CurseForge/ResponseModels/ModFileModel.cs +++ b/src/SMAPI.Web/Framework/Clients/CurseForge/ResponseModels/ModFileModel.cs @@ -1,14 +1,28 @@ -#nullable disable - namespace StardewModdingAPI.Web.Framework.Clients.CurseForge.ResponseModels { /// Metadata from the CurseForge API about a mod file. public class ModFileModel { + /********* + ** Accessors + *********/ /// The file name as downloaded. - public string FileName { get; set; } + public string FileName { get; } /// The file display name. - public string DisplayName { get; set; } + public string? DisplayName { get; } + + + /********* + ** Public methods + *********/ + /// Construct an instance. + /// The file name as downloaded. + /// The file display name. + public ModFileModel(string fileName, string? displayName) + { + this.FileName = fileName; + this.DisplayName = displayName; + } } } diff --git a/src/SMAPI.Web/Framework/Clients/CurseForge/ResponseModels/ModModel.cs b/src/SMAPI.Web/Framework/Clients/CurseForge/ResponseModels/ModModel.cs index a95df7f1..fd7796f2 100644 --- a/src/SMAPI.Web/Framework/Clients/CurseForge/ResponseModels/ModModel.cs +++ b/src/SMAPI.Web/Framework/Clients/CurseForge/ResponseModels/ModModel.cs @@ -1,20 +1,38 @@ -#nullable disable - namespace StardewModdingAPI.Web.Framework.Clients.CurseForge.ResponseModels { /// An mod from the CurseForge API. public class ModModel { + /********* + ** Accessors + *********/ /// The mod's unique ID on CurseForge. - public int ID { get; set; } + public int ID { get; } /// The mod name. - public string Name { get; set; } + public string Name { get; } /// The web URL for the mod page. - public string WebsiteUrl { get; set; } + public string WebsiteUrl { get; } /// The available file downloads. - public ModFileModel[] LatestFiles { get; set; } + public ModFileModel[] LatestFiles { get; } + + + /********* + ** Public methods + *********/ + /// Construct an instance. + /// The mod's unique ID on CurseForge. + /// The mod name. + /// The web URL for the mod page. + /// The available file downloads. + public ModModel(int id, string name, string websiteUrl, ModFileModel[] latestFiles) + { + this.ID = id; + this.Name = name; + this.WebsiteUrl = websiteUrl; + this.LatestFiles = latestFiles; + } } } diff --git a/src/SMAPI.Web/Framework/Clients/GenericModDownload.cs b/src/SMAPI.Web/Framework/Clients/GenericModDownload.cs index 919072b0..548f17c3 100644 --- a/src/SMAPI.Web/Framework/Clients/GenericModDownload.cs +++ b/src/SMAPI.Web/Framework/Clients/GenericModDownload.cs @@ -1,5 +1,3 @@ -#nullable disable - namespace StardewModdingAPI.Web.Framework.Clients { /// Generic metadata about a file download on a mod page. @@ -9,26 +7,23 @@ namespace StardewModdingAPI.Web.Framework.Clients ** Accessors *********/ /// The download's display name. - public string Name { get; set; } + public string Name { get; } /// The download's description. - public string Description { get; set; } + public string? Description { get; } /// The download's file version. - public string Version { get; set; } + public string? Version { get; } /********* ** Public methods *********/ - /// Construct an empty instance. - public GenericModDownload() { } - /// Construct an instance. /// The download's display name. /// The download's description. /// The download's file version. - public GenericModDownload(string name, string description, string version) + public GenericModDownload(string name, string? description, string? version) { this.Name = name; this.Description = description; diff --git a/src/SMAPI.Web/Framework/Clients/GenericModPage.cs b/src/SMAPI.Web/Framework/Clients/GenericModPage.cs index 4788aa2a..5353c7e1 100644 --- a/src/SMAPI.Web/Framework/Clients/GenericModPage.cs +++ b/src/SMAPI.Web/Framework/Clients/GenericModPage.cs @@ -1,7 +1,6 @@ -#nullable disable - using System; using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; using System.Linq; using StardewModdingAPI.Toolkit.Framework.UpdateData; @@ -20,30 +19,31 @@ namespace StardewModdingAPI.Web.Framework.Clients public string Id { get; set; } /// The mod name. - public string Name { get; set; } + public string? Name { get; set; } /// The mod's semantic version number. - public string Version { get; set; } + public string? Version { get; set; } /// The mod's web URL. - public string Url { get; set; } + public string? Url { get; set; } /// The mod downloads. public IModDownload[] Downloads { get; set; } = Array.Empty(); /// The mod availability status on the remote site. - public RemoteModStatus Status { get; set; } = RemoteModStatus.Ok; + public RemoteModStatus Status { get; set; } = RemoteModStatus.InvalidData; /// A user-friendly error which indicates why fetching the mod info failed (if applicable). - public string Error { get; set; } + public string? Error { get; set; } + + /// Whether the mod data is valid. + [MemberNotNullWhen(true, nameof(IModPage.Name), nameof(IModPage.Url))] + public bool IsValid => this.Status == RemoteModStatus.Ok; /********* ** Public methods *********/ - /// Construct an empty instance. - public GenericModPage() { } - /// Construct an instance. /// The mod site containing the mod. /// The mod's unique ID within the site. @@ -58,12 +58,13 @@ namespace StardewModdingAPI.Web.Framework.Clients /// The mod's semantic version number. /// The mod's web URL. /// The mod downloads. - public IModPage SetInfo(string name, string version, string url, IEnumerable downloads) + public IModPage SetInfo(string name, string? version, string url, IEnumerable downloads) { this.Name = name; this.Version = version; this.Url = url; this.Downloads = downloads.ToArray(); + this.Status = RemoteModStatus.Ok; return this; } diff --git a/src/SMAPI.Web/Framework/Clients/GitHub/GitAsset.cs b/src/SMAPI.Web/Framework/Clients/GitHub/GitAsset.cs index 39ebf94e..dbce9368 100644 --- a/src/SMAPI.Web/Framework/Clients/GitHub/GitAsset.cs +++ b/src/SMAPI.Web/Framework/Clients/GitHub/GitAsset.cs @@ -1,5 +1,3 @@ -#nullable disable - using Newtonsoft.Json; namespace StardewModdingAPI.Web.Framework.Clients.GitHub @@ -7,16 +5,34 @@ namespace StardewModdingAPI.Web.Framework.Clients.GitHub /// A GitHub download attached to a release. internal class GitAsset { + /********* + ** Accessors + *********/ /// The file name. [JsonProperty("name")] - public string FileName { get; set; } + public string FileName { get; } /// The file content type. [JsonProperty("content_type")] - public string ContentType { get; set; } + public string ContentType { get; } /// The download URL. [JsonProperty("browser_download_url")] - public string DownloadUrl { get; set; } + public string DownloadUrl { get; } + + + /********* + ** Public methods + *********/ + /// Construct an instance. + /// The file name. + /// The file content type. + /// The download URL. + public GitAsset(string fileName, string contentType, string downloadUrl) + { + this.FileName = fileName; + this.ContentType = contentType; + this.DownloadUrl = downloadUrl; + } } } diff --git a/src/SMAPI.Web/Framework/Clients/GitHub/GitHubClient.cs b/src/SMAPI.Web/Framework/Clients/GitHub/GitHubClient.cs index 0e68e2c2..785979a5 100644 --- a/src/SMAPI.Web/Framework/Clients/GitHub/GitHubClient.cs +++ b/src/SMAPI.Web/Framework/Clients/GitHub/GitHubClient.cs @@ -1,5 +1,3 @@ -#nullable disable - using System; using System.Linq; using System.Net; @@ -35,26 +33,26 @@ namespace StardewModdingAPI.Web.Framework.Clients.GitHub /// The Accept header value expected by the GitHub API. /// The username with which to authenticate to the GitHub API. /// The password with which to authenticate to the GitHub API. - public GitHubClient(string baseUrl, string userAgent, string acceptHeader, string username, string password) + public GitHubClient(string baseUrl, string userAgent, string acceptHeader, string? username, string? password) { this.Client = new FluentClient(baseUrl) .SetUserAgent(userAgent) .AddDefault(req => req.WithHeader("Accept", acceptHeader)); if (!string.IsNullOrWhiteSpace(username)) - this.Client = this.Client.SetBasicAuthentication(username, password); + this.Client = this.Client.SetBasicAuthentication(username, password!); } /// Get basic metadata for a GitHub repository, if available. /// The repository key (like Pathoschild/SMAPI). /// Returns the repository info if it exists, else null. - public async Task GetRepositoryAsync(string repo) + public async Task GetRepositoryAsync(string repo) { this.AssertKeyFormat(repo); try { return await this.Client .GetAsync($"repos/{repo}") - .As(); + .As(); } catch (ApiException ex) when (ex.Status == HttpStatusCode.NotFound) { @@ -66,7 +64,7 @@ namespace StardewModdingAPI.Web.Framework.Clients.GitHub /// The repository key (like Pathoschild/SMAPI). /// Whether to return a prerelease version if it's latest. /// Returns the release if found, else null. - public async Task GetLatestReleaseAsync(string repo, bool includePrerelease = false) + public async Task GetLatestReleaseAsync(string repo, bool includePrerelease = false) { this.AssertKeyFormat(repo); try @@ -81,7 +79,7 @@ namespace StardewModdingAPI.Web.Framework.Clients.GitHub return await this.Client .GetAsync($"repos/{repo}/releases/latest") - .As(); + .As(); } catch (ApiException ex) when (ex.Status == HttpStatusCode.NotFound) { @@ -91,7 +89,7 @@ namespace StardewModdingAPI.Web.Framework.Clients.GitHub /// Get update check info about a mod. /// The mod ID. - public async Task GetModData(string id) + public async Task GetModData(string id) { IModPage page = new GenericModPage(this.SiteKey, id); @@ -99,15 +97,15 @@ namespace StardewModdingAPI.Web.Framework.Clients.GitHub return page.SetError(RemoteModStatus.DoesNotExist, $"The value '{id}' isn't a valid GitHub mod ID, must be a username and project name like 'Pathoschild/SMAPI'."); // fetch repo info - GitRepo repository = await this.GetRepositoryAsync(id); + GitRepo? repository = await this.GetRepositoryAsync(id); if (repository == null) return page.SetError(RemoteModStatus.DoesNotExist, "Found no GitHub repository for this ID."); string name = repository.FullName; string url = $"{repository.WebUrl}/releases"; // get releases - GitRelease latest; - GitRelease preview; + GitRelease? latest; + GitRelease? preview; { // get latest release (whether preview or stable) latest = await this.GetLatestReleaseAsync(id, includePrerelease: true); @@ -118,7 +116,7 @@ namespace StardewModdingAPI.Web.Framework.Clients.GitHub preview = null; if (latest.IsPrerelease) { - GitRelease release = await this.GetLatestReleaseAsync(id, includePrerelease: false); + GitRelease? release = await this.GetLatestReleaseAsync(id, includePrerelease: false); if (release != null) { preview = latest; @@ -129,8 +127,8 @@ namespace StardewModdingAPI.Web.Framework.Clients.GitHub // get downloads IModDownload[] downloads = new[] { latest, preview } - .Where(release => release != null) - .Select(release => (IModDownload)new GenericModDownload(release.Name, release.Body, release.Tag)) + .Where(release => release is not null) + .Select(release => (IModDownload)new GenericModDownload(release!.Name, release.Body, release.Tag)) .ToArray(); // return info @@ -140,7 +138,7 @@ namespace StardewModdingAPI.Web.Framework.Clients.GitHub /// Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources. public void Dispose() { - this.Client?.Dispose(); + this.Client.Dispose(); } diff --git a/src/SMAPI.Web/Framework/Clients/GitHub/GitLicense.cs b/src/SMAPI.Web/Framework/Clients/GitHub/GitLicense.cs index 275c775a..24d6c3c5 100644 --- a/src/SMAPI.Web/Framework/Clients/GitHub/GitLicense.cs +++ b/src/SMAPI.Web/Framework/Clients/GitHub/GitLicense.cs @@ -1,5 +1,3 @@ -#nullable disable - using Newtonsoft.Json; namespace StardewModdingAPI.Web.Framework.Clients.GitHub @@ -7,16 +5,34 @@ namespace StardewModdingAPI.Web.Framework.Clients.GitHub /// The license info for a GitHub project. internal class GitLicense { + /********* + ** Accessors + *********/ /// The license display name. [JsonProperty("name")] - public string Name { get; set; } + public string Name { get; } /// The SPDX ID for the license. [JsonProperty("spdx_id")] - public string SpdxId { get; set; } + public string SpdxId { get; } /// The URL for the license info. [JsonProperty("url")] - public string Url { get; set; } + public string Url { get; } + + + /********* + ** Public methods + *********/ + /// Construct an instance. + /// The license display name. + /// The SPDX ID for the license. + /// The URL for the license info. + public GitLicense(string name, string spdxId, string url) + { + this.Name = name; + this.SpdxId = spdxId; + this.Url = url; + } } } diff --git a/src/SMAPI.Web/Framework/Clients/GitHub/GitRelease.cs b/src/SMAPI.Web/Framework/Clients/GitHub/GitRelease.cs index 383775d2..9de6f020 100644 --- a/src/SMAPI.Web/Framework/Clients/GitHub/GitRelease.cs +++ b/src/SMAPI.Web/Framework/Clients/GitHub/GitRelease.cs @@ -1,5 +1,4 @@ -#nullable disable - +using System; using Newtonsoft.Json; namespace StardewModdingAPI.Web.Framework.Clients.GitHub @@ -12,24 +11,45 @@ namespace StardewModdingAPI.Web.Framework.Clients.GitHub *********/ /// The display name. [JsonProperty("name")] - public string Name { get; set; } + public string Name { get; } /// The semantic version string. [JsonProperty("tag_name")] - public string Tag { get; set; } + public string Tag { get; } /// The Markdown description for the release. - public string Body { get; set; } + public string Body { get; internal set; } /// Whether this is a draft version. [JsonProperty("draft")] - public bool IsDraft { get; set; } + public bool IsDraft { get; } /// Whether this is a prerelease version. [JsonProperty("prerelease")] - public bool IsPrerelease { get; set; } + public bool IsPrerelease { get; } /// The attached files. - public GitAsset[] Assets { get; set; } + public GitAsset[] Assets { get; } + + + /********* + ** Public methods + *********/ + /// Construct an instance. + /// The display name. + /// The semantic version string. + /// The Markdown description for the release. + /// Whether this is a draft version. + /// Whether this is a prerelease version. + /// The attached files. + public GitRelease(string name, string tag, string? body, bool isDraft, bool isPrerelease, GitAsset[]? assets) + { + this.Name = name; + this.Tag = tag; + this.Body = body ?? string.Empty; + this.IsDraft = isDraft; + this.IsPrerelease = isPrerelease; + this.Assets = assets ?? Array.Empty(); + } } } diff --git a/src/SMAPI.Web/Framework/Clients/GitHub/GitRepo.cs b/src/SMAPI.Web/Framework/Clients/GitHub/GitRepo.cs index 5b5ce6a6..879b5e49 100644 --- a/src/SMAPI.Web/Framework/Clients/GitHub/GitRepo.cs +++ b/src/SMAPI.Web/Framework/Clients/GitHub/GitRepo.cs @@ -1,5 +1,3 @@ -#nullable disable - using Newtonsoft.Json; namespace StardewModdingAPI.Web.Framework.Clients.GitHub @@ -7,16 +5,34 @@ namespace StardewModdingAPI.Web.Framework.Clients.GitHub /// Basic metadata about a GitHub project. internal class GitRepo { + /********* + ** Accessors + *********/ /// The full repository name, including the owner. [JsonProperty("full_name")] - public string FullName { get; set; } + public string FullName { get; } /// The URL to the repository web page, if any. [JsonProperty("html_url")] - public string WebUrl { get; set; } + public string? WebUrl { get; } /// The code license, if any. [JsonProperty("license")] - public GitLicense License { get; set; } + public GitLicense? License { get; } + + + /********* + ** Public methods + *********/ + /// Construct an instance. + /// The full repository name, including the owner. + /// The URL to the repository web page, if any. + /// The code license, if any. + public GitRepo(string fullName, string? webUrl, GitLicense? license) + { + this.FullName = fullName; + this.WebUrl = webUrl; + this.License = license; + } } } diff --git a/src/SMAPI.Web/Framework/Clients/GitHub/IGitHubClient.cs b/src/SMAPI.Web/Framework/Clients/GitHub/IGitHubClient.cs index e1961416..886e32d3 100644 --- a/src/SMAPI.Web/Framework/Clients/GitHub/IGitHubClient.cs +++ b/src/SMAPI.Web/Framework/Clients/GitHub/IGitHubClient.cs @@ -1,5 +1,3 @@ -#nullable disable - using System; using System.Threading.Tasks; @@ -14,12 +12,12 @@ namespace StardewModdingAPI.Web.Framework.Clients.GitHub /// Get basic metadata for a GitHub repository, if available. /// The repository key (like Pathoschild/SMAPI). /// Returns the repository info if it exists, else null. - Task GetRepositoryAsync(string repo); + Task GetRepositoryAsync(string repo); /// Get the latest release for a GitHub repository. /// The repository key (like Pathoschild/SMAPI). /// Whether to return a prerelease version if it's latest. /// Returns the release if found, else null. - Task GetLatestReleaseAsync(string repo, bool includePrerelease = false); + Task GetLatestReleaseAsync(string repo, bool includePrerelease = false); } } diff --git a/src/SMAPI.Web/Framework/Clients/IModSiteClient.cs b/src/SMAPI.Web/Framework/Clients/IModSiteClient.cs index 2cd1f635..3697ffae 100644 --- a/src/SMAPI.Web/Framework/Clients/IModSiteClient.cs +++ b/src/SMAPI.Web/Framework/Clients/IModSiteClient.cs @@ -1,5 +1,3 @@ -#nullable disable - using System.Threading.Tasks; using StardewModdingAPI.Toolkit.Framework.UpdateData; @@ -20,6 +18,6 @@ namespace StardewModdingAPI.Web.Framework.Clients *********/ /// Get update check info about a mod. /// The mod ID. - Task GetModData(string id); + Task GetModData(string id); } } diff --git a/src/SMAPI.Web/Framework/Clients/ModDrop/ModDropClient.cs b/src/SMAPI.Web/Framework/Clients/ModDrop/ModDropClient.cs index 1a11a606..c60b2c90 100644 --- a/src/SMAPI.Web/Framework/Clients/ModDrop/ModDropClient.cs +++ b/src/SMAPI.Web/Framework/Clients/ModDrop/ModDropClient.cs @@ -1,6 +1,5 @@ -#nullable disable - using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; using System.Threading.Tasks; using Pathoschild.Http.Client; using StardewModdingAPI.Toolkit.Framework.UpdateData; @@ -43,9 +42,10 @@ namespace StardewModdingAPI.Web.Framework.Clients.ModDrop /// Get update check info about a mod. /// The mod ID. - public async Task GetModData(string id) + [SuppressMessage("ReSharper", "ConstantConditionalAccessQualifier", Justification = "The nullability is validated in this method.")] + public async Task GetModData(string id) { - var page = new GenericModPage(this.SiteKey, id); + IModPage page = new GenericModPage(this.SiteKey, id); if (!long.TryParse(id, out long parsedId)) return page.SetError(RemoteModStatus.DoesNotExist, $"The value '{id}' isn't a valid ModDrop mod ID, must be an integer ID."); @@ -60,9 +60,11 @@ namespace StardewModdingAPI.Web.Framework.Clients.ModDrop Mods = true }) .As(); - ModModel mod = response.Mods[parsedId]; - if (mod.Mod?.Title == null || mod.Mod.ErrorCode.HasValue) - return null; + + if (!response.Mods.TryGetValue(parsedId, out ModModel? mod) || mod?.Mod is null) + return page.SetError(RemoteModStatus.DoesNotExist, "Found no ModDrop page with this ID."); + if (mod.Mod.ErrorCode is not null) + return page.SetError(RemoteModStatus.InvalidData, $"ModDrop returned error code {mod.Mod.ErrorCode} for mod ID '{id}'."); // get files var downloads = new List(); @@ -77,7 +79,7 @@ namespace StardewModdingAPI.Web.Framework.Clients.ModDrop } // return info - string name = mod.Mod?.Title; + string name = mod.Mod.Title; string url = string.Format(this.ModUrlFormat, id); return page.SetInfo(name: name, version: null, url: url, downloads: downloads); } @@ -85,7 +87,7 @@ namespace StardewModdingAPI.Web.Framework.Clients.ModDrop /// Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources. public void Dispose() { - this.Client?.Dispose(); + this.Client.Dispose(); } } } diff --git a/src/SMAPI.Web/Framework/Clients/ModDrop/ResponseModels/FileDataModel.cs b/src/SMAPI.Web/Framework/Clients/ModDrop/ResponseModels/FileDataModel.cs index dd6a95e0..31905338 100644 --- a/src/SMAPI.Web/Framework/Clients/ModDrop/ResponseModels/FileDataModel.cs +++ b/src/SMAPI.Web/Framework/Clients/ModDrop/ResponseModels/FileDataModel.cs @@ -1,5 +1,3 @@ -#nullable disable - using Newtonsoft.Json; namespace StardewModdingAPI.Web.Framework.Clients.ModDrop.ResponseModels @@ -7,27 +5,53 @@ namespace StardewModdingAPI.Web.Framework.Clients.ModDrop.ResponseModels /// Metadata from the ModDrop API about a mod file. public class FileDataModel { + /********* + ** Accessors + *********/ /// The file title. [JsonProperty("title")] - public string Name { get; set; } + public string Name { get; } /// The file description. [JsonProperty("desc")] - public string Description { get; set; } + public string Description { get; } /// The file version. - public string Version { get; set; } + public string Version { get; } /// Whether the file is deleted. - public bool IsDeleted { get; set; } + public bool IsDeleted { get; } /// Whether the file is hidden from users. - public bool IsHidden { get; set; } + public bool IsHidden { get; } /// Whether this is the default file for the mod. - public bool IsDefault { get; set; } + public bool IsDefault { get; } /// Whether this is an archived file. - public bool IsOld { get; set; } + public bool IsOld { get; } + + + /********* + ** Public methods + *********/ + /// Construct an instance. + /// The file title. + /// The file description. + /// The file version. + /// Whether the file is deleted. + /// Whether the file is hidden from users. + /// Whether this is the default file for the mod. + /// Whether this is an archived file. + public FileDataModel(string name, string description, string version, bool isDeleted, bool isHidden, bool isDefault, bool isOld) + { + this.Name = name; + this.Description = description; + this.Version = version; + this.IsDeleted = isDeleted; + this.IsHidden = isHidden; + this.IsDefault = isDefault; + this.IsOld = isOld; + } } } diff --git a/src/SMAPI.Web/Framework/Clients/ModDrop/ResponseModels/ModDataModel.cs b/src/SMAPI.Web/Framework/Clients/ModDrop/ResponseModels/ModDataModel.cs index 6cae16d9..0654b576 100644 --- a/src/SMAPI.Web/Framework/Clients/ModDrop/ResponseModels/ModDataModel.cs +++ b/src/SMAPI.Web/Framework/Clients/ModDrop/ResponseModels/ModDataModel.cs @@ -1,17 +1,33 @@ -#nullable disable - namespace StardewModdingAPI.Web.Framework.Clients.ModDrop.ResponseModels { /// Metadata about a mod from the ModDrop API. public class ModDataModel { + /********* + ** Accessors + *********/ /// The mod's unique ID on ModDrop. public int ID { get; set; } + /// The mod name. + public string Title { get; set; } + /// The error code, if any. public int? ErrorCode { get; set; } - /// The mod name. - public string Title { get; set; } + + /********* + ** Public methods + *********/ + /// Construct an instance. + /// The mod's unique ID on ModDrop. + /// The mod name. + /// The error code, if any. + public ModDataModel(int id, string title, int? errorCode) + { + this.ID = id; + this.Title = title; + this.ErrorCode = errorCode; + } } } diff --git a/src/SMAPI.Web/Framework/Clients/ModDrop/ResponseModels/ModListModel.cs b/src/SMAPI.Web/Framework/Clients/ModDrop/ResponseModels/ModListModel.cs index 445e25cb..cb4be35c 100644 --- a/src/SMAPI.Web/Framework/Clients/ModDrop/ResponseModels/ModListModel.cs +++ b/src/SMAPI.Web/Framework/Clients/ModDrop/ResponseModels/ModListModel.cs @@ -1,5 +1,3 @@ -#nullable disable - using System.Collections.Generic; namespace StardewModdingAPI.Web.Framework.Clients.ModDrop.ResponseModels @@ -7,7 +5,10 @@ namespace StardewModdingAPI.Web.Framework.Clients.ModDrop.ResponseModels /// A list of mods from the ModDrop API. public class ModListModel { + /********* + ** Accessors + *********/ /// The mod data. - public IDictionary Mods { get; set; } + public IDictionary Mods { get; } = new Dictionary(); } } diff --git a/src/SMAPI.Web/Framework/Clients/ModDrop/ResponseModels/ModModel.cs b/src/SMAPI.Web/Framework/Clients/ModDrop/ResponseModels/ModModel.cs index 8869193e..60b818d6 100644 --- a/src/SMAPI.Web/Framework/Clients/ModDrop/ResponseModels/ModModel.cs +++ b/src/SMAPI.Web/Framework/Clients/ModDrop/ResponseModels/ModModel.cs @@ -1,14 +1,28 @@ -#nullable disable - namespace StardewModdingAPI.Web.Framework.Clients.ModDrop.ResponseModels { /// An entry in a mod list from the ModDrop API. public class ModModel { + /********* + ** Accessors + *********/ /// The available file downloads. - public FileDataModel[] Files { get; set; } + public FileDataModel[] Files { get; } /// The mod metadata. - public ModDataModel Mod { get; set; } + public ModDataModel Mod { get; } + + + /********* + ** Public methods + *********/ + /// Construct an instance. + /// The available file downloads. + /// The mod metadata. + public ModModel(FileDataModel[] files, ModDataModel mod) + { + this.Files = files; + this.Mod = mod; + } } } diff --git a/src/SMAPI.Web/Framework/Clients/Nexus/NexusClient.cs b/src/SMAPI.Web/Framework/Clients/Nexus/NexusClient.cs index dd0bb94f..23b25f95 100644 --- a/src/SMAPI.Web/Framework/Clients/Nexus/NexusClient.cs +++ b/src/SMAPI.Web/Framework/Clients/Nexus/NexusClient.cs @@ -1,5 +1,3 @@ -#nullable disable - using System; using System.Collections.Generic; using System.Linq; @@ -61,7 +59,7 @@ namespace StardewModdingAPI.Web.Framework.Clients.Nexus /// Get update check info about a mod. /// The mod ID. - public async Task GetModData(string id) + public async Task GetModData(string id) { IModPage page = new GenericModPage(this.SiteKey, id); @@ -72,7 +70,7 @@ namespace StardewModdingAPI.Web.Framework.Clients.Nexus // adult content are hidden for anonymous users, so fall back to the API in that case. // Note that the API has very restrictive rate limits which means we can't just use it // for all cases. - NexusMod mod = await this.GetModFromWebsiteAsync(parsedId); + NexusMod? mod = await this.GetModFromWebsiteAsync(parsedId); if (mod?.Status == NexusModStatus.AdultContentForbidden) mod = await this.GetModFromApiAsync(parsedId); @@ -81,16 +79,16 @@ namespace StardewModdingAPI.Web.Framework.Clients.Nexus return page.SetError(RemoteModStatus.DoesNotExist, "Found no Nexus mod with this ID."); // return info - page.SetInfo(name: mod.Name, url: mod.Url, version: mod.Version, downloads: mod.Downloads); + page.SetInfo(name: mod.Name ?? parsedId.ToString(), url: mod.Url ?? this.GetModUrl(parsedId), version: mod.Version, downloads: mod.Downloads); if (mod.Status != NexusModStatus.Ok) - page.SetError(RemoteModStatus.TemporaryError, mod.Error); + page.SetError(RemoteModStatus.TemporaryError, mod.Error!); return page; } /// Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources. public void Dispose() { - this.WebClient?.Dispose(); + this.WebClient.Dispose(); } @@ -100,7 +98,7 @@ namespace StardewModdingAPI.Web.Framework.Clients.Nexus /// Get metadata about a mod by scraping the Nexus website. /// The Nexus mod ID. /// Returns the mod info if found, else null. - private async Task GetModFromWebsiteAsync(uint id) + private async Task GetModFromWebsiteAsync(uint id) { // fetch HTML string html; @@ -116,35 +114,38 @@ namespace StardewModdingAPI.Web.Framework.Clients.Nexus } // parse HTML - var doc = new HtmlDocument(); + HtmlDocument doc = new(); doc.LoadHtml(html); // handle Nexus error message - HtmlNode node = doc.DocumentNode.SelectSingleNode("//div[contains(@class, 'site-notice')][contains(@class, 'warning')]"); + HtmlNode? node = doc.DocumentNode.SelectSingleNode("//div[contains(@class, 'site-notice')][contains(@class, 'warning')]"); if (node != null) { string[] errorParts = node.InnerText.Trim().Split(new[] { '\n' }, 2, System.StringSplitOptions.RemoveEmptyEntries); string errorCode = errorParts[0]; - string errorText = errorParts.Length > 1 ? errorParts[1] : null; + string? errorText = errorParts.Length > 1 ? errorParts[1] : null; switch (errorCode.Trim().ToLower()) { case "not found": return null; default: - return new NexusMod { Error = $"Nexus error: {errorCode} ({errorText}).", Status = this.GetWebStatus(errorCode) }; + return new NexusMod( + status: this.GetWebStatus(errorCode), + error: $"Nexus error: {errorCode} ({errorText})." + ); } } // extract mod info string url = this.GetModUrl(id); - string name = doc.DocumentNode.SelectSingleNode("//div[@id='pagetitle']//h1")?.InnerText.Trim(); - string version = doc.DocumentNode.SelectSingleNode("//ul[contains(@class, 'stats')]//li[@class='stat-version']//div[@class='stat']")?.InnerText.Trim(); - SemanticVersion.TryParse(version, out ISemanticVersion parsedVersion); + string? name = doc.DocumentNode.SelectSingleNode("//div[@id='pagetitle']//h1")?.InnerText.Trim(); + string? version = doc.DocumentNode.SelectSingleNode("//ul[contains(@class, 'stats')]//li[@class='stat-version']//div[@class='stat']")?.InnerText.Trim(); + SemanticVersion.TryParse(version, out ISemanticVersion? parsedVersion); // extract files var downloads = new List(); - foreach (var fileSection in doc.DocumentNode.SelectNodes("//div[contains(@class, 'files-tabs')]")) + foreach (HtmlNode fileSection in doc.DocumentNode.SelectNodes("//div[contains(@class, 'files-tabs')]")) { string sectionName = fileSection.Descendants("h2").First().InnerText; if (sectionName != "Main files" && sectionName != "Optional files") @@ -154,7 +155,7 @@ namespace StardewModdingAPI.Web.Framework.Clients.Nexus { string fileName = container.GetDataAttribute("name").Value; string fileVersion = container.GetDataAttribute("version").Value; - string description = container.SelectSingleNode("following-sibling::*[1][self::dd]//div").InnerText?.Trim(); // get text of next
tag; derived from https://stackoverflow.com/a/25535623/262123 + string? description = container.SelectSingleNode("following-sibling::*[1][self::dd]//div").InnerText?.Trim(); // get text of next
tag; derived from https://stackoverflow.com/a/25535623/262123 downloads.Add( new GenericModDownload(fileName, description, fileVersion) @@ -163,13 +164,12 @@ namespace StardewModdingAPI.Web.Framework.Clients.Nexus } // yield info - return new NexusMod - { - Name = name, - Version = parsedVersion?.ToString() ?? version, - Url = url, - Downloads = downloads.ToArray() - }; + return new NexusMod( + name: name ?? id.ToString(), + version: parsedVersion?.ToString() ?? version, + url: url, + downloads: downloads.ToArray() + ); } /// Get metadata about a mod from the Nexus API. @@ -182,22 +182,21 @@ namespace StardewModdingAPI.Web.Framework.Clients.Nexus ModFileList files = await this.ApiClient.ModFiles.GetModFiles("stardewvalley", (int)id, FileCategory.Main, FileCategory.Optional); // yield info - return new NexusMod - { - Name = mod.Name, - Version = SemanticVersion.TryParse(mod.Version, out ISemanticVersion version) ? version?.ToString() : mod.Version, - Url = this.GetModUrl(id), - Downloads = files.Files + return new NexusMod( + name: mod.Name, + version: SemanticVersion.TryParse(mod.Version, out ISemanticVersion? version) ? version.ToString() : mod.Version, + url: this.GetModUrl(id), + downloads: files.Files .Select(file => (IModDownload)new GenericModDownload(file.Name, file.Description, file.FileVersion)) .ToArray() - }; + ); } /// Get the full mod page URL for a given ID. /// The mod ID. private string GetModUrl(uint id) { - UriBuilder builder = new(this.WebClient.BaseClient.BaseAddress); + UriBuilder builder = new(this.WebClient.BaseClient.BaseAddress!); builder.Path += string.Format(this.WebModUrlFormat, id); return builder.Uri.ToString(); } diff --git a/src/SMAPI.Web/Framework/Clients/Nexus/ResponseModels/NexusMod.cs b/src/SMAPI.Web/Framework/Clients/Nexus/ResponseModels/NexusMod.cs index 358c4633..3155cfda 100644 --- a/src/SMAPI.Web/Framework/Clients/Nexus/ResponseModels/NexusMod.cs +++ b/src/SMAPI.Web/Framework/Clients/Nexus/ResponseModels/NexusMod.cs @@ -1,5 +1,4 @@ -#nullable disable - +using System; using Newtonsoft.Json; namespace StardewModdingAPI.Web.Framework.Clients.Nexus.ResponseModels @@ -11,25 +10,53 @@ namespace StardewModdingAPI.Web.Framework.Clients.Nexus.ResponseModels ** Accessors *********/ /// The mod name. - public string Name { get; set; } + public string? Name { get; } /// The mod's semantic version number. - public string Version { get; set; } + public string? Version { get; } /// The mod's web URL. [JsonProperty("mod_page_uri")] - public string Url { get; set; } + public string? Url { get; } /// The mod's publication status. [JsonIgnore] - public NexusModStatus Status { get; set; } = NexusModStatus.Ok; + public NexusModStatus Status { get; } /// The files available to download. [JsonIgnore] - public IModDownload[] Downloads { get; set; } + public IModDownload[] Downloads { get; } /// A custom user-friendly error which indicates why fetching the mod info failed (if applicable). [JsonIgnore] - public string Error { get; set; } + public string? Error { get; } + + + /********* + ** Public methods + *********/ + /// Construct an instance. + /// The mod name + /// The mod's semantic version number. + /// The mod's web URL. + /// The files available to download. + public NexusMod(string name, string? version, string url, IModDownload[] downloads) + { + this.Name = name; + this.Version = version; + this.Url = url; + this.Status = NexusModStatus.Ok; + this.Downloads = downloads; + } + + /// Construct an instance. + /// The mod's publication status. + /// A custom user-friendly error which indicates why fetching the mod info failed (if applicable). + public NexusMod(NexusModStatus status, string error) + { + this.Status = status; + this.Error = error; + this.Downloads = Array.Empty(); + } } } diff --git a/src/SMAPI.Web/Framework/Clients/Pastebin/IPastebinClient.cs b/src/SMAPI.Web/Framework/Clients/Pastebin/IPastebinClient.cs index 03c78e01..431fed7b 100644 --- a/src/SMAPI.Web/Framework/Clients/Pastebin/IPastebinClient.cs +++ b/src/SMAPI.Web/Framework/Clients/Pastebin/IPastebinClient.cs @@ -1,5 +1,3 @@ -#nullable disable - using System; using System.Threading.Tasks; diff --git a/src/SMAPI.Web/Framework/Clients/Pastebin/PasteInfo.cs b/src/SMAPI.Web/Framework/Clients/Pastebin/PasteInfo.cs index 2d48a7ae..7f40e713 100644 --- a/src/SMAPI.Web/Framework/Clients/Pastebin/PasteInfo.cs +++ b/src/SMAPI.Web/Framework/Clients/Pastebin/PasteInfo.cs @@ -1,17 +1,35 @@ -#nullable disable +using System.Diagnostics.CodeAnalysis; namespace StardewModdingAPI.Web.Framework.Clients.Pastebin { /// The response for a get-paste request. internal class PasteInfo { + /********* + ** Accessors + *********/ /// Whether the log was successfully fetched. - public bool Success { get; set; } + [MemberNotNullWhen(true, nameof(PasteInfo.Content))] + [MemberNotNullWhen(false, nameof(PasteInfo.Error))] + public bool Success => this.Error == null || this.Content != null; /// The fetched paste content (if is true). - public string Content { get; set; } + public string? Content { get; internal set; } - /// The error message if saving failed. - public string Error { get; set; } + /// The error message (if is false). + public string? Error { get; } + + + /********* + ** Public methods + *********/ + /// Construct an instance. + /// The fetched paste content. + /// The error message, if it failed. + 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 d0cdf374..0e00f071 100644 --- a/src/SMAPI.Web/Framework/Clients/Pastebin/PastebinClient.cs +++ b/src/SMAPI.Web/Framework/Clients/Pastebin/PastebinClient.cs @@ -1,5 +1,3 @@ -#nullable disable - using System; using System.Net; using System.Threading.Tasks; @@ -35,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("Decompress a string. /// The compressed text. /// Derived from . - 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 diff --git a/src/SMAPI.Web/Framework/Compression/IGzipHelper.cs b/src/SMAPI.Web/Framework/Compression/IGzipHelper.cs index e1ec9b67..ef2d5696 100644 --- a/src/SMAPI.Web/Framework/Compression/IGzipHelper.cs +++ b/src/SMAPI.Web/Framework/Compression/IGzipHelper.cs @@ -1,4 +1,4 @@ -#nullable disable +using System.Diagnostics.CodeAnalysis; namespace StardewModdingAPI.Web.Framework.Compression { @@ -14,6 +14,7 @@ namespace StardewModdingAPI.Web.Framework.Compression /// Decompress a string. /// The compressed text. - 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 3730a9db..b582b2b0 100644 --- a/src/SMAPI.Web/Framework/ConfigModels/ApiClientsConfig.cs +++ b/src/SMAPI.Web/Framework/ConfigModels/ApiClientsConfig.cs @@ -1,5 +1,3 @@ -#nullable disable - namespace StardewModdingAPI.Web.Framework.ConfigModels { /// The config settings for the API clients. @@ -12,17 +10,17 @@ namespace StardewModdingAPI.Web.Framework.ConfigModels ** Generic ****/ /// The user agent for API clients, where {0} is the SMAPI version. - public string UserAgent { get; set; } + public string UserAgent { get; set; } = null!; /**** ** Azure ****/ /// The connection string for the Azure Blob storage account. - public string AzureBlobConnectionString { get; set; } + public string? AzureBlobConnectionString { get; set; } /// The Azure Blob container in which to store temporary uploaded logs. - public string AzureBlobTempContainer { get; set; } + public string AzureBlobTempContainer { get; set; } = null!; /// The number of days since the blob's last-modified date when it will be deleted. public int AzureBlobTempExpiryDays { get; set; } @@ -32,65 +30,65 @@ namespace StardewModdingAPI.Web.Framework.ConfigModels ** Chucklefish ****/ /// The base URL for the Chucklefish mod site. - public string ChucklefishBaseUrl { get; set; } + public string ChucklefishBaseUrl { get; set; } = null!; /// The URL for a mod page on the Chucklefish mod site excluding the , where {0} is the mod ID. - public string ChucklefishModPageUrlFormat { get; set; } + public string ChucklefishModPageUrlFormat { get; set; } = null!; /**** ** CurseForge ****/ /// The base URL for the CurseForge API. - public string CurseForgeBaseUrl { get; set; } + public string CurseForgeBaseUrl { get; set; } = null!; /**** ** GitHub ****/ /// The base URL for the GitHub API. - public string GitHubBaseUrl { get; set; } + public string GitHubBaseUrl { get; set; } = null!; /// The Accept header value expected by the GitHub API. - public string GitHubAcceptHeader { get; set; } + public string GitHubAcceptHeader { get; set; } = null!; /// The username with which to authenticate to the GitHub API (if any). - public string GitHubUsername { get; set; } + public string? GitHubUsername { get; set; } /// The password with which to authenticate to the GitHub API (if any). - public string GitHubPassword { get; set; } + public string? GitHubPassword { get; set; } /**** ** ModDrop ****/ /// The base URL for the ModDrop API. - public string ModDropApiUrl { get; set; } + public string ModDropApiUrl { get; set; } = null!; /// The URL for a ModDrop mod page for the user, where {0} is the mod ID. - public string ModDropModPageUrl { get; set; } + public string ModDropModPageUrl { get; set; } = null!; /**** ** Nexus Mods ****/ /// The base URL for the Nexus Mods API. - public string NexusBaseUrl { get; set; } + public string NexusBaseUrl { get; set; } = null!; /// The URL for a Nexus mod page for the user, excluding the , where {0} is the mod ID. - public string NexusModUrlFormat { get; set; } + public string NexusModUrlFormat { get; set; } = null!; /// The URL for a Nexus mod page to scrape for versions, excluding the , where {0} is the mod ID. - public string NexusModScrapeUrlFormat { get; set; } + public string NexusModScrapeUrlFormat { get; set; } = null!; /// The Nexus API authentication key. - public string NexusApiKey { get; set; } + public string? NexusApiKey { get; set; } /**** ** Pastebin ****/ /// The base URL for the Pastebin API. - 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 682c97e6..e46ecf2b 100644 --- a/src/SMAPI.Web/Framework/ConfigModels/ModOverrideConfig.cs +++ b/src/SMAPI.Web/Framework/ConfigModels/ModOverrideConfig.cs @@ -1,17 +1,15 @@ -#nullable disable - namespace StardewModdingAPI.Web.Framework.ConfigModels { /// Override update-check metadata for a mod. internal class ModOverrideConfig { /// The unique ID from the mod's manifest. - public string ID { get; set; } + public string ID { get; set; } = null!; /// Whether to allow non-standard versions. public bool AllowNonStandardVersions { get; set; } /// The mod page URL to use regardless of which site has the update, or null to use the site URL. - 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 e525e09a..c3b136e8 100644 --- a/src/SMAPI.Web/Framework/ConfigModels/ModUpdateCheckConfig.cs +++ b/src/SMAPI.Web/Framework/ConfigModels/ModUpdateCheckConfig.cs @@ -1,4 +1,4 @@ -#nullable disable +using System; namespace StardewModdingAPI.Web.Framework.ConfigModels { @@ -8,16 +8,16 @@ namespace StardewModdingAPI.Web.Framework.ConfigModels /********* ** Accessors *********/ - /// The number of minutes successful update checks should be cached before refetching them. + /// The number of minutes successful update checks should be cached before re-fetching them. public int SuccessCacheMinutes { get; set; } - /// The number of minutes failed update checks should be cached before refetching them. + /// The number of minutes failed update checks should be cached before re-fetching them. public int ErrorCacheMinutes { get; set; } /// Update-check metadata to override. - public ModOverrideConfig[] ModOverrides { get; set; } + public ModOverrideConfig[] ModOverrides { get; set; } = Array.Empty(); /// The update-check config for SMAPI's own update checks. - 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 ef6c2659..62685e47 100644 --- a/src/SMAPI.Web/Framework/ConfigModels/SiteConfig.cs +++ b/src/SMAPI.Web/Framework/ConfigModels/SiteConfig.cs @@ -1,5 +1,3 @@ -#nullable disable - namespace StardewModdingAPI.Web.Framework.ConfigModels { /// The site config settings. @@ -9,9 +7,9 @@ namespace StardewModdingAPI.Web.Framework.ConfigModels ** Accessors *********/ /// A message to show below the download button (e.g. for details on downloading a beta version), in Markdown format. - public string OtherBlurb { get; set; } + public string? OtherBlurb { get; set; } /// A list of supports to credit on the main page, in Markdown format. - 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 dbf58817..a95e0048 100644 --- a/src/SMAPI.Web/Framework/ConfigModels/SmapiInfoConfig.cs +++ b/src/SMAPI.Web/Framework/ConfigModels/SmapiInfoConfig.cs @@ -1,4 +1,4 @@ -#nullable disable +using System; namespace StardewModdingAPI.Web.Framework.ConfigModels { @@ -6,12 +6,12 @@ namespace StardewModdingAPI.Web.Framework.ConfigModels internal class SmapiInfoConfig { /// The mod ID used for SMAPI update checks. - public string ID { get; set; } + public string ID { get; set; } = null!; /// The default update key used for SMAPI update checks. - public string DefaultUpdateKey { get; set; } + public string DefaultUpdateKey { get; set; } = null!; /// The update keys to add for SMAPI update checks when the player has a beta version installed. - public string[] AddBetaUpdateKeys { get; set; } + public string[] AddBetaUpdateKeys { get; set; } = Array.Empty(); } } diff --git a/src/SMAPI.Web/Framework/Extensions.cs b/src/SMAPI.Web/Framework/Extensions.cs index a72c12c1..62a23155 100644 --- a/src/SMAPI.Web/Framework/Extensions.cs +++ b/src/SMAPI.Web/Framework/Extensions.cs @@ -1,5 +1,3 @@ -#nullable disable - using System; using JetBrains.Annotations; using Microsoft.AspNetCore.Html; @@ -28,7 +26,7 @@ namespace StardewModdingAPI.Web.Framework /// An object that contains route values. /// Get an absolute URL instead of a server-relative path/ /// The generated URL. - 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(values); @@ -39,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); @@ -59,7 +57,7 @@ namespace StardewModdingAPI.Web.Framework /// The value to serialize. /// The serialized JSON. /// This bypasses unnecessary validation (e.g. not allowing null values) in . - 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 b8d1f62c..fe171785 100644 --- a/src/SMAPI.Web/Framework/IModDownload.cs +++ b/src/SMAPI.Web/Framework/IModDownload.cs @@ -1,17 +1,18 @@ -#nullable disable - namespace StardewModdingAPI.Web.Framework { /// Generic metadata about a file download on a mod page. internal interface IModDownload { + /********* + ** Accessors + *********/ /// The download's display name. string Name { get; } /// The download's description. - string Description { get; } + string? Description { get; } /// The download's file version. - string Version { get; } + string? Version { get; } } } diff --git a/src/SMAPI.Web/Framework/IModPage.cs b/src/SMAPI.Web/Framework/IModPage.cs index 68220b49..4d0a8d61 100644 --- a/src/SMAPI.Web/Framework/IModPage.cs +++ b/src/SMAPI.Web/Framework/IModPage.cs @@ -1,6 +1,5 @@ -#nullable disable - using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; using StardewModdingAPI.Toolkit.Framework.UpdateData; namespace StardewModdingAPI.Web.Framework @@ -18,13 +17,13 @@ namespace StardewModdingAPI.Web.Framework string Id { get; } /// The mod name. - string Name { get; } + string? Name { get; } /// The mod's semantic version number. - string Version { get; } + string? Version { get; } /// The mod's web URL. - string Url { get; } + string? Url { get; } /// The mod downloads. IModDownload[] Downloads { get; } @@ -33,7 +32,12 @@ namespace StardewModdingAPI.Web.Framework RemoteModStatus Status { get; } /// A user-friendly error which indicates why fetching the mod info failed (if applicable). - string Error { get; } + string? Error { get; } + + /// Whether the mod data is valid. + [MemberNotNullWhen(true, nameof(IModPage.Name), nameof(IModPage.Url))] + [MemberNotNullWhen(false, nameof(IModPage.Error))] + bool IsValid { get; } /********* @@ -44,7 +48,7 @@ namespace StardewModdingAPI.Web.Framework /// The mod's semantic version number. /// The mod's web URL. /// The mod downloads. - IModPage SetInfo(string name, string version, string url, IEnumerable downloads); + IModPage SetInfo(string name, string? version, string url, IEnumerable downloads); /// Set a mod fetch error. /// The mod availability status on the remote site. diff --git a/src/SMAPI.Web/Framework/InternalControllerFeatureProvider.cs b/src/SMAPI.Web/Framework/InternalControllerFeatureProvider.cs index 98738a82..2c24c610 100644 --- a/src/SMAPI.Web/Framework/InternalControllerFeatureProvider.cs +++ b/src/SMAPI.Web/Framework/InternalControllerFeatureProvider.cs @@ -1,5 +1,3 @@ -#nullable disable - using System; using System.Reflection; using Microsoft.AspNetCore.Mvc; diff --git a/src/SMAPI.Web/Framework/JobDashboardAuthorizationFilter.cs b/src/SMAPI.Web/Framework/JobDashboardAuthorizationFilter.cs index 8db43dca..3c1405eb 100644 --- a/src/SMAPI.Web/Framework/JobDashboardAuthorizationFilter.cs +++ b/src/SMAPI.Web/Framework/JobDashboardAuthorizationFilter.cs @@ -1,5 +1,3 @@ -#nullable disable - using Hangfire.Dashboard; namespace StardewModdingAPI.Web.Framework diff --git a/src/SMAPI.Web/Framework/ModInfoModel.cs b/src/SMAPI.Web/Framework/ModInfoModel.cs index 021d14fb..e70b60bf 100644 --- a/src/SMAPI.Web/Framework/ModInfoModel.cs +++ b/src/SMAPI.Web/Framework/ModInfoModel.cs @@ -1,4 +1,5 @@ -#nullable disable +using System.Diagnostics.CodeAnalysis; +using Newtonsoft.Json; namespace StardewModdingAPI.Web.Framework { @@ -9,22 +10,22 @@ namespace StardewModdingAPI.Web.Framework ** Accessors *********/ /// The mod name. - public string Name { get; set; } - - /// The mod's latest version. - public ISemanticVersion Version { get; set; } - - /// The mod's latest optional or prerelease version, if newer than . - public ISemanticVersion PreviewVersion { get; set; } + public string? Name { get; private set; } /// The mod's web URL. - public string Url { get; set; } + public string? Url { get; private set; } + + /// The mod's latest version. + public ISemanticVersion? Version { get; private set; } + + /// The mod's latest optional or prerelease version, if newer than . + public ISemanticVersion? PreviewVersion { get; private set; } /// The mod availability status on the remote site. - public RemoteModStatus Status { get; set; } = RemoteModStatus.Ok; + public RemoteModStatus Status { get; private set; } /// The error message indicating why the mod is invalid (if applicable). - public string Error { get; set; } + public string? Error { get; private set; } /********* @@ -35,19 +36,24 @@ namespace StardewModdingAPI.Web.Framework /// Construct an instance. /// The mod name. + /// The mod's web URL. /// The semantic version for the mod's latest release. /// The semantic version for the mod's latest preview release, if available and different from . - /// The mod's web URL. - public ModInfoModel(string name, ISemanticVersion version, string url, ISemanticVersion previewVersion = null) + /// The mod availability status on the remote site. + /// The error message indicating why the mod is invalid (if applicable). + [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!); } /// Set the basic mod info. /// The mod name. /// The mod's web URL. + [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 /// Set the mod version info. /// The semantic version for the mod's latest release. /// The semantic version for the mod's latest preview release, if available and different from . - 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 2d6755d8..674b9ffc 100644 --- a/src/SMAPI.Web/Framework/ModSiteManager.cs +++ b/src/SMAPI.Web/Framework/ModSiteManager.cs @@ -1,7 +1,6 @@ -#nullable disable - using System; using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; using System.Linq; using System.Text.RegularExpressions; using System.Threading.Tasks; @@ -36,12 +35,15 @@ namespace StardewModdingAPI.Web.Framework /// The namespaced update key. public async Task 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); @@ -60,39 +62,42 @@ namespace StardewModdingAPI.Web.Framework /// The optional update subkey to match in available files. (If no file names or descriptions contain the subkey, it'll be ignored.) /// The changes to apply to remote versions for update checks. /// Whether to allow non-standard versions. - 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); } /// Get a semantic local version for update checks. /// The version to parse. /// Changes to apply to the raw version, if any. /// Whether to allow non-standard versions. - 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; } @@ -108,31 +113,31 @@ namespace StardewModdingAPI.Web.Framework /// The changes to apply to remote versions for update checks. /// The main mod version. /// The latest prerelease version, if newer than . - 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); } @@ -143,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()) @@ -160,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 @@ -183,8 +188,7 @@ namespace StardewModdingAPI.Web.Framework /// Get a semantic local version for update checks. /// The version to map. /// Changes to apply to the raw version, if any. - /// Whether to allow non-standard versions. - private string GetRawMappedVersion(string version, ChangeDescriptor map, bool allowNonStandard) + private string? GetRawMappedVersion(string? version, ChangeDescriptor? map) { if (version == null || map?.HasChanges != true) return version; @@ -197,7 +201,7 @@ namespace StardewModdingAPI.Web.Framework /// Normalize a version string. /// The version to normalize. - 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 fe601524..7b8f0ec9 100644 --- a/src/SMAPI.Web/Framework/RedirectRules/RedirectHostsToUrlsRule.cs +++ b/src/SMAPI.Web/Framework/RedirectRules/RedirectHostsToUrlsRule.cs @@ -1,5 +1,3 @@ -#nullable disable - using System; using System.Net; using Microsoft.AspNetCore.Rewrite; @@ -13,7 +11,7 @@ namespace StardewModdingAPI.Web.Framework.RedirectRules ** Fields *********/ /// Maps a lowercase hostname to the resulting redirect URL. - private readonly Func Map; + private readonly Func Map; /********* @@ -22,7 +20,7 @@ namespace StardewModdingAPI.Web.Framework.RedirectRules /// Construct an instance. /// The status code to use for redirects. /// Hostnames mapped to the resulting redirect URL. - public RedirectHostsToUrlsRule(HttpStatusCode statusCode, Func map) + public RedirectHostsToUrlsRule(HttpStatusCode statusCode, Func map) { this.StatusCode = statusCode; this.Map = map ?? throw new ArgumentNullException(nameof(map)); @@ -35,10 +33,10 @@ namespace StardewModdingAPI.Web.Framework.RedirectRules /// Get the new redirect URL. /// The rewrite context. /// Returns the redirect URL, or null if the redirect doesn't apply. - protected override string GetNewUrl(RewriteContext context) + protected override string? GetNewUrl(RewriteContext context) { // get requested host - string host = context.HttpContext.Request.Host.Host; + 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 81a265c9..b46e8f69 100644 --- a/src/SMAPI.Web/Framework/RedirectRules/RedirectMatchRule.cs +++ b/src/SMAPI.Web/Framework/RedirectRules/RedirectMatchRule.cs @@ -1,5 +1,3 @@ -#nullable disable - using System; using System.Net; using Microsoft.AspNetCore.Http; @@ -24,7 +22,7 @@ namespace StardewModdingAPI.Web.Framework.RedirectRules /// The rewrite context. public void ApplyRule(RewriteContext context) { - string newUrl = this.GetNewUrl(context); + string? newUrl = this.GetNewUrl(context); if (newUrl == null) return; @@ -41,7 +39,7 @@ namespace StardewModdingAPI.Web.Framework.RedirectRules /// Get the new redirect URL. /// The rewrite context. /// Returns the redirect URL, or null if the redirect doesn't apply. - protected abstract string GetNewUrl(RewriteContext context); + protected abstract string? GetNewUrl(RewriteContext context); /// Get the full request URL. /// The request. diff --git a/src/SMAPI.Web/Framework/RedirectRules/RedirectPathsToUrlsRule.cs b/src/SMAPI.Web/Framework/RedirectRules/RedirectPathsToUrlsRule.cs index cb3e53ef..e691ffba 100644 --- a/src/SMAPI.Web/Framework/RedirectRules/RedirectPathsToUrlsRule.cs +++ b/src/SMAPI.Web/Framework/RedirectRules/RedirectPathsToUrlsRule.cs @@ -1,5 +1,3 @@ -#nullable disable - using System.Collections.Generic; using System.Linq; using System.Net; @@ -39,9 +37,9 @@ namespace StardewModdingAPI.Web.Framework.RedirectRules /// Get the new redirect URL. /// The rewrite context. /// Returns the redirect URL, or null if the redirect doesn't apply. - 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 dd7c836f..01807608 100644 --- a/src/SMAPI.Web/Framework/RedirectRules/RedirectToHttpsRule.cs +++ b/src/SMAPI.Web/Framework/RedirectRules/RedirectToHttpsRule.cs @@ -1,5 +1,3 @@ -#nullable disable - using System; using System.Net; using Microsoft.AspNetCore.Http; @@ -22,7 +20,7 @@ namespace StardewModdingAPI.Web.Framework.RedirectRules *********/ /// Construct an instance. /// Matches requests which should be ignored. - public RedirectToHttpsRule(Func except = null) + public RedirectToHttpsRule(Func? except = null) { this.Except = except ?? (_ => false); this.StatusCode = HttpStatusCode.RedirectKeepVerb; @@ -35,7 +33,7 @@ namespace StardewModdingAPI.Web.Framework.RedirectRules /// Get the new redirect URL. /// The rewrite context. /// Returns the redirect URL, or null if the redirect doesn't apply. - 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/IStorageProvider.cs b/src/SMAPI.Web/Framework/Storage/IStorageProvider.cs index 2eca4845..dfc1fb47 100644 --- a/src/SMAPI.Web/Framework/Storage/IStorageProvider.cs +++ b/src/SMAPI.Web/Framework/Storage/IStorageProvider.cs @@ -1,5 +1,3 @@ -#nullable disable - using System.Threading.Tasks; namespace StardewModdingAPI.Web.Framework.Storage diff --git a/src/SMAPI.Web/Framework/Storage/StorageProvider.cs b/src/SMAPI.Web/Framework/Storage/StorageProvider.cs index 0177e602..effbbc9f 100644 --- a/src/SMAPI.Web/Framework/Storage/StorageProvider.cs +++ b/src/SMAPI.Web/Framework/Storage/StorageProvider.cs @@ -1,5 +1,3 @@ -#nullable disable - using System; using System.Collections.Generic; using System.IO; @@ -65,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); } } @@ -77,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); } } @@ -110,21 +108,15 @@ namespace StardewModdingAPI.Web.Framework.Storage 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})." - }; + ); } } @@ -137,10 +129,7 @@ namespace StardewModdingAPI.Web.Framework.Storage 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 @@ -151,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." + ); } } @@ -166,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); } } @@ -179,8 +161,8 @@ namespace StardewModdingAPI.Web.Framework.Storage /// The file ID. 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 cd941c94..bbbcf2a9 100644 --- a/src/SMAPI.Web/Framework/Storage/StoredFileInfo.cs +++ b/src/SMAPI.Web/Framework/Storage/StoredFileInfo.cs @@ -1,25 +1,52 @@ -#nullable disable - using System; +using System.Diagnostics.CodeAnalysis; namespace StardewModdingAPI.Web.Framework.Storage { /// The response for a get-file request. internal class StoredFileInfo { + /********* + ** Accessors + *********/ /// Whether the file was successfully fetched. - public bool Success { get; set; } + [MemberNotNullWhen(true, nameof(StoredFileInfo.Content))] + public bool Success => this.Content != null && this.Error == null; /// The fetched file content (if is true). - public string Content { get; set; } + public string? Content { get; } /// When the file will no longer be available. - public DateTime? Expiry { get; set; } + public DateTimeOffset? Expiry { get; } /// The error message if saving succeeded, but a non-blocking issue was encountered. - public string Warning { get; set; } + public string? Warning { get; } /// The error message if saving failed. - public string Error { get; set; } + public string? Error { get; } + + + /********* + ** Public methods + *********/ + /// Construct an instance. + /// The fetched file content (if is true). + /// When the file will no longer be available. + /// The error message if saving succeeded, but a non-blocking issue was encountered. + /// The error message if saving failed. + public StoredFileInfo(string? content, DateTimeOffset? expiry, string? warning = null, string? error = null) + { + this.Content = content; + this.Expiry = expiry; + this.Warning = warning; + this.Error = error; + } + + /// Construct an instance. + /// The error message if saving failed. + 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 b1eedd59..92993d42 100644 --- a/src/SMAPI.Web/Framework/Storage/UploadResult.cs +++ b/src/SMAPI.Web/Framework/Storage/UploadResult.cs @@ -1,4 +1,4 @@ -#nullable disable +using System.Diagnostics.CodeAnalysis; namespace StardewModdingAPI.Web.Framework.Storage { @@ -9,25 +9,25 @@ namespace StardewModdingAPI.Web.Framework.Storage ** Accessors *********/ /// Whether the file upload succeeded. - public bool Succeeded { get; } + [MemberNotNullWhen(true, nameof(UploadResult.ID))] + [MemberNotNullWhen(false, nameof(UploadResult.UploadError))] + public bool Succeeded => this.ID != null && this.UploadError == null; /// The file ID, if applicable. - public string ID { get; } + public string? ID { get; } /// The upload error, if any. - public string UploadError { get; } + public string? UploadError { get; } /********* ** Public methods *********/ /// Construct an instance. - /// Whether the file upload succeeded. /// The file ID, if applicable. /// The upload error, if any. - 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 f230a95b..1b1abd81 100644 --- a/src/SMAPI.Web/Framework/VersionConstraint.cs +++ b/src/SMAPI.Web/Framework/VersionConstraint.cs @@ -1,5 +1,3 @@ -#nullable disable - using System; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Routing; @@ -20,7 +18,7 @@ namespace StardewModdingAPI.Web.Framework /// A dictionary that contains the parameters for the URL. /// 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. /// true if the URL parameter contains a valid value; otherwise, false. - 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)); @@ -28,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 _); } diff --git a/src/SMAPI.Web/Program.cs b/src/SMAPI.Web/Program.cs index 5134791a..1fdd3185 100644 --- a/src/SMAPI.Web/Program.cs +++ b/src/SMAPI.Web/Program.cs @@ -1,5 +1,3 @@ -#nullable disable - using Microsoft.AspNetCore.Hosting; using Microsoft.Extensions.Hosting; diff --git a/src/SMAPI.Web/Startup.cs b/src/SMAPI.Web/Startup.cs index 0199938d..98dbca5e 100644 --- a/src/SMAPI.Web/Startup.cs +++ b/src/SMAPI.Web/Startup.cs @@ -1,5 +1,3 @@ -#nullable disable - using System.Collections.Generic; using System.Net; using Hangfire; @@ -102,7 +100,7 @@ namespace StardewModdingAPI.Web // init API clients { ApiClientsConfig api = this.Configuration.GetSection("ApiClients").Get(); - string version = this.GetType().Assembly.GetName().Version.ToString(3); + string version = this.GetType().Assembly.GetName().Version!.ToString(3); string userAgent = string.Format(api.UserAgent, version); services.AddSingleton(new ChucklefishClient( diff --git a/src/SMAPI.Web/ViewModels/IndexModel.cs b/src/SMAPI.Web/ViewModels/IndexModel.cs index 2283acd9..098f18cc 100644 --- a/src/SMAPI.Web/ViewModels/IndexModel.cs +++ b/src/SMAPI.Web/ViewModels/IndexModel.cs @@ -1,5 +1,3 @@ -#nullable disable - namespace StardewModdingAPI.Web.ViewModels { /// The view model for the index page. @@ -9,26 +7,23 @@ namespace StardewModdingAPI.Web.ViewModels ** Accessors *********/ /// The latest stable SMAPI version. - public IndexVersionModel StableVersion { get; set; } + public IndexVersionModel StableVersion { get; } /// A message to show below the download button (e.g. for details on downloading a beta version), in Markdown format. - public string OtherBlurb { get; set; } + public string? OtherBlurb { get; } /// A list of supports to credit on the main page, in Markdown format. - public string SupporterList { get; set; } + public string? SupporterList { get; } /********* ** Public methods *********/ - /// Construct an instance. - public IndexModel() { } - /// Construct an instance. /// The latest stable SMAPI version. /// A message to show below the download button (e.g. for details on downloading a beta version), in Markdown format. /// A list of supports to credit on the main page, in Markdown format. - internal IndexModel(IndexVersionModel stableVersion, string otherBlurb, string supporterList) + internal IndexModel(IndexVersionModel stableVersion, string? otherBlurb, string? supporterList) { this.StableVersion = stableVersion; this.OtherBlurb = otherBlurb; diff --git a/src/SMAPI.Web/ViewModels/IndexVersionModel.cs b/src/SMAPI.Web/ViewModels/IndexVersionModel.cs index 1f5d4ec0..a76a5924 100644 --- a/src/SMAPI.Web/ViewModels/IndexVersionModel.cs +++ b/src/SMAPI.Web/ViewModels/IndexVersionModel.cs @@ -1,5 +1,3 @@ -#nullable disable - namespace StardewModdingAPI.Web.ViewModels { /// The fields for a SMAPI version. @@ -9,30 +7,27 @@ namespace StardewModdingAPI.Web.ViewModels ** Accessors *********/ /// The release version. - public string Version { get; set; } + public string Version { get; } /// The Markdown description for the release. - public string Description { get; set; } + public string Description { get; } /// The main download URL. - public string DownloadUrl { get; set; } + public string DownloadUrl { get; } /// The for-developers download URL (not applicable for prerelease versions). - public string DevDownloadUrl { get; set; } + public string? DevDownloadUrl { get; } /********* ** Public methods *********/ - /// Construct an instance. - public IndexVersionModel() { } - /// Construct an instance. /// The release number. /// The Markdown description for the release. /// The main download URL. /// The for-developers download URL (not applicable for prerelease versions). - internal IndexVersionModel(string version, string description, string downloadUrl, string devDownloadUrl) + internal IndexVersionModel(string version, string description, string downloadUrl, string? devDownloadUrl) { this.Version = version; this.Description = description; diff --git a/src/SMAPI.Web/ViewModels/JsonValidator/JsonValidatorErrorModel.cs b/src/SMAPI.Web/ViewModels/JsonValidator/JsonValidatorErrorModel.cs index 3c63b730..4d37d449 100644 --- a/src/SMAPI.Web/ViewModels/JsonValidator/JsonValidatorErrorModel.cs +++ b/src/SMAPI.Web/ViewModels/JsonValidator/JsonValidatorErrorModel.cs @@ -1,5 +1,3 @@ -#nullable disable - using Newtonsoft.Json.Schema; namespace StardewModdingAPI.Web.ViewModels.JsonValidator @@ -11,30 +9,27 @@ namespace StardewModdingAPI.Web.ViewModels.JsonValidator ** Accessors *********/ /// The line number on which the error occurred. - public int Line { get; set; } + public int Line { get; } /// The field path in the JSON file where the error occurred. - public string Path { get; set; } + public string? Path { get; } /// A human-readable description of the error. - public string Message { get; set; } + public string Message { get; } /// The schema error type. - public ErrorType SchemaErrorType { get; set; } + public ErrorType SchemaErrorType { get; } /********* ** Public methods *********/ - /// Construct an instance. - public JsonValidatorErrorModel() { } - /// Construct an instance. /// The line number on which the error occurred. /// The field path in the JSON file where the error occurred. /// A human-readable description of the error. /// The schema error type. - public JsonValidatorErrorModel(int line, string path, string message, ErrorType schemaErrorType) + public JsonValidatorErrorModel(int line, string? path, string message, ErrorType schemaErrorType) { this.Line = line; this.Path = path; diff --git a/src/SMAPI.Web/ViewModels/JsonValidator/JsonValidatorModel.cs b/src/SMAPI.Web/ViewModels/JsonValidator/JsonValidatorModel.cs index 2543807f..85c2f44d 100644 --- a/src/SMAPI.Web/ViewModels/JsonValidator/JsonValidatorModel.cs +++ b/src/SMAPI.Web/ViewModels/JsonValidator/JsonValidatorModel.cs @@ -1,5 +1,3 @@ -#nullable disable - using System; using System.Collections.Generic; using System.Linq; @@ -13,51 +11,48 @@ namespace StardewModdingAPI.Web.ViewModels.JsonValidator ** Accessors *********/ /// Whether to show the edit view. - public bool IsEditView { get; set; } + public bool IsEditView { get; } /// The paste ID. - public string PasteID { get; set; } + public string? PasteID { get; } /// The schema name with which the JSON was validated. - public string SchemaName { get; set; } + public string? SchemaName { get; } /// The supported JSON schemas (names indexed by ID). - public readonly IDictionary SchemaFormats; + public IDictionary SchemaFormats { get; } /// The validated content. - public string Content { get; set; } + public string? Content { get; set; } /// The schema validation errors, if any. public JsonValidatorErrorModel[] Errors { get; set; } = Array.Empty(); /// A non-blocking warning while uploading the file. - public string UploadWarning { get; set; } + public string? UploadWarning { get; set; } /// When the uploaded file will no longer be available. - public DateTime? Expiry { get; set; } + public DateTimeOffset? Expiry { get; set; } /// An error which occurred while uploading the JSON. - public string UploadError { get; set; } + public string? UploadError { get; set; } /// An error which occurred while parsing the JSON. - public string ParseError { get; set; } + public string? ParseError { get; set; } /// A web URL to the user-facing format documentation. - public string FormatUrl { get; set; } + public string? FormatUrl { get; set; } /********* ** Public methods *********/ - /// Construct an instance. - public JsonValidatorModel() { } - /// Construct an instance. /// The stored file ID. /// The schema name with which the JSON was validated. /// The supported JSON schemas (names indexed by ID). /// Whether to show the edit view. - public JsonValidatorModel(string pasteID, string schemaName, IDictionary schemaFormats, bool isEditView) + public JsonValidatorModel(string? pasteID, string? schemaName, IDictionary schemaFormats, bool isEditView) { this.PasteID = pasteID; this.SchemaName = schemaName; @@ -69,7 +64,7 @@ namespace StardewModdingAPI.Web.ViewModels.JsonValidator /// The validated content. /// When the uploaded file will no longer be available. /// A non-blocking warning while uploading the log. - public JsonValidatorModel SetContent(string content, DateTime? expiry, string uploadWarning = null) + public JsonValidatorModel SetContent(string content, DateTimeOffset? expiry, string? uploadWarning = null) { this.Content = content; this.Expiry = expiry; diff --git a/src/SMAPI.Web/ViewModels/JsonValidator/JsonValidatorRequestModel.cs b/src/SMAPI.Web/ViewModels/JsonValidator/JsonValidatorRequestModel.cs index 43114d94..3edb58db 100644 --- a/src/SMAPI.Web/ViewModels/JsonValidator/JsonValidatorRequestModel.cs +++ b/src/SMAPI.Web/ViewModels/JsonValidator/JsonValidatorRequestModel.cs @@ -1,5 +1,3 @@ -#nullable disable - namespace StardewModdingAPI.Web.ViewModels.JsonValidator { /// The view model for a JSON validation request. @@ -9,9 +7,22 @@ namespace StardewModdingAPI.Web.ViewModels.JsonValidator ** Accessors *********/ /// The schema name with which to validate the JSON. - public string SchemaName { get; set; } + public string SchemaName { get; } /// The raw content to validate. - public string Content { get; set; } + public string Content { get; } + + + /********* + ** Accessors + *********/ + /// Construct an instance. + /// The schema name with which to validate the JSON. + /// The raw content to validate. + public JsonValidatorRequestModel(string schemaName, string content) + { + this.SchemaName = schemaName; + this.Content = content; + } } } diff --git a/src/SMAPI.Web/ViewModels/LogParserModel.cs b/src/SMAPI.Web/ViewModels/LogParserModel.cs index d7e4d810..c39a9b0a 100644 --- a/src/SMAPI.Web/ViewModels/LogParserModel.cs +++ b/src/SMAPI.Web/ViewModels/LogParserModel.cs @@ -44,7 +44,7 @@ namespace StardewModdingAPI.Web.ViewModels public string? ParseError => this.ParsedLog?.Error; /// When the uploaded file will no longer be available. - public DateTime? Expiry { get; set; } + public DateTimeOffset? Expiry { get; set; } /// Whether parsed log data is available. [MemberNotNullWhen(true, nameof(LogParserModel.PasteID), nameof(LogParserModel.ParsedLog))] diff --git a/src/SMAPI.Web/ViewModels/ModCompatibilityModel.cs b/src/SMAPI.Web/ViewModels/ModCompatibilityModel.cs index 2af30cc3..36ea891d 100644 --- a/src/SMAPI.Web/ViewModels/ModCompatibilityModel.cs +++ b/src/SMAPI.Web/ViewModels/ModCompatibilityModel.cs @@ -1,5 +1,4 @@ -#nullable disable - +using Newtonsoft.Json; using StardewModdingAPI.Toolkit.Framework.Clients.Wiki; namespace StardewModdingAPI.Web.ViewModels @@ -11,21 +10,35 @@ namespace StardewModdingAPI.Web.ViewModels ** Accessors *********/ /// The compatibility status, as a string like "Broken". - public string Status { get; set; } + public string Status { get; } /// The human-readable summary, as an HTML block. - public string Summary { get; set; } + public string? Summary { get; } /// The game or SMAPI version which broke this mod (if applicable). - public string BrokeIn { get; set; } + public string? BrokeIn { get; } /// A link to the unofficial version which fixes compatibility, if any. - public ModLinkModel UnofficialVersion { get; set; } + public ModLinkModel? UnofficialVersion { get; } /********* ** Public methods *********/ + /// Construct an instance. + /// The compatibility status, as a string like "Broken". + /// The human-readable summary, as an HTML block. + /// The game or SMAPI version which broke this mod (if applicable). + /// A link to the unofficial version which fixes compatibility, if any. + [JsonConstructor] + public ModCompatibilityModel(string status, string? summary, string? brokeIn, ModLinkModel? unofficialVersion) + { + this.Status = status; + this.Summary = summary; + this.BrokeIn = brokeIn; + this.UnofficialVersion = unofficialVersion; + } + /// Construct an instance. /// The mod metadata. public ModCompatibilityModel(WikiCompatibilityInfo info) @@ -36,7 +49,7 @@ namespace StardewModdingAPI.Web.ViewModels this.Summary = info.Summary; this.BrokeIn = info.BrokeIn; if (info.UnofficialVersion != null) - this.UnofficialVersion = new ModLinkModel(info.UnofficialUrl, info.UnofficialVersion.ToString()); + this.UnofficialVersion = new ModLinkModel(info.UnofficialUrl!, info.UnofficialVersion.ToString()); } } } diff --git a/src/SMAPI.Web/ViewModels/ModLinkModel.cs b/src/SMAPI.Web/ViewModels/ModLinkModel.cs index 3039702e..96f14d48 100644 --- a/src/SMAPI.Web/ViewModels/ModLinkModel.cs +++ b/src/SMAPI.Web/ViewModels/ModLinkModel.cs @@ -1,5 +1,3 @@ -#nullable disable - namespace StardewModdingAPI.Web.ViewModels { /// Metadata about a link. @@ -9,10 +7,10 @@ namespace StardewModdingAPI.Web.ViewModels ** Accessors *********/ /// The URL of the linked page. - public string Url { get; set; } + public string Url { get; } /// The suggested link text. - public string Text { get; set; } + public string Text { get; } /********* diff --git a/src/SMAPI.Web/ViewModels/ModListModel.cs b/src/SMAPI.Web/ViewModels/ModListModel.cs index f0cf0c3a..be9f973a 100644 --- a/src/SMAPI.Web/ViewModels/ModListModel.cs +++ b/src/SMAPI.Web/ViewModels/ModListModel.cs @@ -1,5 +1,3 @@ -#nullable disable - using System; using System.Collections.Generic; using System.Linq; @@ -13,37 +11,34 @@ namespace StardewModdingAPI.Web.ViewModels ** Accessors *********/ /// The current stable version of the game. - public string StableVersion { get; set; } + public string? StableVersion { get; } /// The current beta version of the game (if any). - public string BetaVersion { get; set; } + public string? BetaVersion { get; } /// The mods to display. - public ModModel[] Mods { get; set; } + public ModModel[] Mods { get; } /// When the data was last updated. - public DateTimeOffset LastUpdated { get; set; } + public DateTimeOffset LastUpdated { get; } /// Whether the data hasn't been updated in a while. - public bool IsStale { get; set; } + public bool IsStale { get; } /// Whether the mod metadata is available. - public bool HasData => this.Mods?.Any() == true; + public bool HasData => this.Mods.Any(); /********* ** Public methods *********/ - /// Construct an empty instance. - public ModListModel() { } - /// Construct an instance. /// The current stable version of the game. /// The current beta version of the game (if any). /// The mods to display. /// When the data was last updated. /// Whether the data hasn't been updated in a while. - public ModListModel(string stableVersion, string betaVersion, IEnumerable mods, DateTimeOffset lastUpdated, bool isStale) + public ModListModel(string? stableVersion, string? betaVersion, IEnumerable mods, DateTimeOffset lastUpdated, bool isStale) { this.StableVersion = stableVersion; this.BetaVersion = betaVersion; diff --git a/src/SMAPI.Web/ViewModels/ModModel.cs b/src/SMAPI.Web/ViewModels/ModModel.cs index d0d7373b..929bf682 100644 --- a/src/SMAPI.Web/ViewModels/ModModel.cs +++ b/src/SMAPI.Web/ViewModels/ModModel.cs @@ -1,7 +1,6 @@ -#nullable disable - using System.Collections.Generic; using System.Linq; +using Newtonsoft.Json; using StardewModdingAPI.Toolkit.Framework.Clients.Wiki; namespace StardewModdingAPI.Web.ViewModels @@ -13,43 +12,43 @@ namespace StardewModdingAPI.Web.ViewModels ** Accessors *********/ /// The mod name. - public string Name { get; set; } + public string? Name { get; } /// The mod's alternative names, if any. - public string AlternateNames { get; set; } + public string AlternateNames { get; } /// The mod author's name. - public string Author { get; set; } + public string? Author { get; } /// The mod author's alternative names, if any. - public string AlternateAuthors { get; set; } + public string AlternateAuthors { get; } /// The GitHub repo, if any. - public string GitHubRepo { get; set; } + public string? GitHubRepo { get; } /// The URL to the mod's source code, if any. - public string SourceUrl { get; set; } + public string? SourceUrl { get; } /// The compatibility status for the stable version of the game. - public ModCompatibilityModel Compatibility { get; set; } + public ModCompatibilityModel Compatibility { get; } /// The compatibility status for the beta version of the game. - public ModCompatibilityModel BetaCompatibility { get; set; } + public ModCompatibilityModel? BetaCompatibility { get; } /// Links to the available mod pages. - public ModLinkModel[] ModPages { get; set; } + public ModLinkModel[] ModPages { get; } /// The human-readable warnings for players about this mod. - public string[] Warnings { get; set; } + public string[] Warnings { get; } /// The URL of the pull request which submits changes for an unofficial update to the author, if any. - public string PullRequestUrl { get; set; } + public string? PullRequestUrl { get; } - /// Special notes intended for developers who maintain unofficial updates or submit pull requests. - public string DevNote { get; set; } + /// Special notes intended for developers who maintain unofficial updates or submit pull requests. + public string? DevNote { get; } /// A unique identifier for the mod that can be used in an anchor URL. - public string Slug { get; set; } + public string? Slug { get; } /// The sites where the mod can be downloaded. public string[] ModPageSites => this.ModPages.Select(p => p.Text).ToArray(); @@ -58,6 +57,38 @@ namespace StardewModdingAPI.Web.ViewModels /********* ** Public methods *********/ + /// Construct an instance. + /// The mod name. + /// The mod's alternative names, if any. + /// The mod author's name. + /// The mod author's alternative names, if any. + /// The GitHub repo, if any. + /// The URL to the mod's source code, if any. + /// The compatibility status for the stable version of the game. + /// The compatibility status for the beta version of the game. + /// Links to the available mod pages. + /// The human-readable warnings for players about this mod. + /// The URL of the pull request which submits changes for an unofficial update to the author, if any. + /// Special notes intended for developers who maintain unofficial updates or submit pull requests. + /// A unique identifier for the mod that can be used in an anchor URL. + [JsonConstructor] + public ModModel(string? name, string alternateNames, string author, string alternateAuthors, string gitHubRepo, string sourceUrl, ModCompatibilityModel compatibility, ModCompatibilityModel betaCompatibility, ModLinkModel[] modPages, string[] warnings, string pullRequestUrl, string devNote, string slug) + { + this.Name = name; + this.AlternateNames = alternateNames; + this.Author = author; + this.AlternateAuthors = alternateAuthors; + this.GitHubRepo = gitHubRepo; + this.SourceUrl = sourceUrl; + this.Compatibility = compatibility; + this.BetaCompatibility = betaCompatibility; + this.ModPages = modPages; + this.Warnings = warnings; + this.PullRequestUrl = pullRequestUrl; + this.DevNote = devNote; + this.Slug = slug; + } + /// Construct an instance. /// The mod metadata. public ModModel(WikiModEntry entry) @@ -84,7 +115,7 @@ namespace StardewModdingAPI.Web.ViewModels *********/ /// Get the web URL for the mod's source code repository, if any. /// The mod metadata. - private string GetSourceUrl(WikiModEntry entry) + private string? GetSourceUrl(WikiModEntry entry) { if (!string.IsNullOrWhiteSpace(entry.GitHubRepo)) return $"https://github.com/{entry.GitHubRepo}"; diff --git a/src/SMAPI.Web/Views/Index/Index.cshtml b/src/SMAPI.Web/Views/Index/Index.cshtml index 9841ca42..acb8df78 100644 --- a/src/SMAPI.Web/Views/Index/Index.cshtml +++ b/src/SMAPI.Web/Views/Index/Index.cshtml @@ -1,7 +1,3 @@ -@{ - #nullable disable -} - @using Microsoft.Extensions.Options @using StardewModdingAPI.Web.Framework @using StardewModdingAPI.Web.Framework.ConfigModels @@ -28,7 +24,7 @@
- Download SMAPI @Model.StableVersion.Version
+ Download SMAPI @Model.StableVersion.Version