diff options
-rw-r--r-- | src/StardewModdingAPI.Models/ModSeachModel.cs | 30 | ||||
-rw-r--r-- | src/StardewModdingAPI.Models/StardewModdingAPI.Models.projitems | 1 | ||||
-rw-r--r-- | src/StardewModdingAPI.Web/Controllers/ModsController.cs | 17 | ||||
-rw-r--r-- | src/StardewModdingAPI.Web/Framework/VersionConstraint.cs | 15 | ||||
-rw-r--r-- | src/StardewModdingAPI.Web/Startup.cs | 19 | ||||
-rw-r--r-- | src/StardewModdingAPI/Framework/WebApiClient.cs | 29 |
6 files changed, 87 insertions, 24 deletions
diff --git a/src/StardewModdingAPI.Models/ModSeachModel.cs b/src/StardewModdingAPI.Models/ModSeachModel.cs new file mode 100644 index 00000000..526fbaf3 --- /dev/null +++ b/src/StardewModdingAPI.Models/ModSeachModel.cs @@ -0,0 +1,30 @@ +using System.Collections.Generic; +using System.Linq; + +namespace StardewModdingAPI.Models +{ + /// <summary>Specifies mods whose update-check info to fetch.</summary> + internal class ModSearchModel + { + /********* + ** Accessors + *********/ + /// <summary>The namespaced mod keys to search.</summary> + public string[] ModKeys { get; set; } + + + /********* + ** Public methods + *********/ + /// <summary>Construct an empty instance.</summary> + /// <remarks>This constructed is needed for JSON deserialisation.</remarks> + public ModSearchModel() { } + + /// <summary>Construct an valid instance.</summary> + /// <param name="modKeys">The namespaced mod keys to search.</param> + public ModSearchModel(IEnumerable<string> modKeys) + { + this.ModKeys = modKeys.ToArray(); + } + } +} diff --git a/src/StardewModdingAPI.Models/StardewModdingAPI.Models.projitems b/src/StardewModdingAPI.Models/StardewModdingAPI.Models.projitems index 2465760e..e2cb29e1 100644 --- a/src/StardewModdingAPI.Models/StardewModdingAPI.Models.projitems +++ b/src/StardewModdingAPI.Models/StardewModdingAPI.Models.projitems @@ -9,6 +9,7 @@ <Import_RootNamespace>StardewModdingAPI.Models</Import_RootNamespace> </PropertyGroup> <ItemGroup> + <Compile Include="$(MSBuildThisFileDirectory)ModSeachModel.cs" /> <Compile Include="$(MSBuildThisFileDirectory)ModInfoModel.cs" /> </ItemGroup> </Project>
\ No newline at end of file diff --git a/src/StardewModdingAPI.Web/Controllers/ModsController.cs b/src/StardewModdingAPI.Web/Controllers/ModsController.cs index 566577e4..f29de45a 100644 --- a/src/StardewModdingAPI.Web/Controllers/ModsController.cs +++ b/src/StardewModdingAPI.Web/Controllers/ModsController.cs @@ -14,6 +14,7 @@ namespace StardewModdingAPI.Web.Controllers { /// <summary>Provides an API to perform mod update checks.</summary> [Produces("application/json")] + [Route("api/{version:semanticVersion}/[controller]")] internal class ModsController : Controller { /********* @@ -80,15 +81,27 @@ namespace StardewModdingAPI.Web.Controllers [HttpGet] public async Task<IDictionary<string, ModInfoModel>> GetAsync(string modKeys) { + string[] modKeysArray = modKeys?.Split(',').Select(p => p.Trim()).ToArray(); + if (modKeysArray == null || !modKeysArray.Any()) + return new Dictionary<string, ModInfoModel>(); + + return await this.PostAsync(new ModSearchModel(modKeysArray)); + } + + /// <summary>Fetch version metadata for the given mods.</summary> + /// <param name="search">The mod search criteria.</param> + [HttpPost] + public async Task<IDictionary<string, ModInfoModel>> PostAsync([FromBody] ModSearchModel search) + { // sort & filter keys - string[] modKeysArray = (modKeys?.Split(',').Select(p => p.Trim()).ToArray() ?? new string[0]) + string[] modKeys = (search?.ModKeys?.ToArray() ?? new string[0]) .Distinct(StringComparer.CurrentCultureIgnoreCase) .OrderBy(p => p, StringComparer.CurrentCultureIgnoreCase) .ToArray(); // fetch mod info IDictionary<string, ModInfoModel> result = new Dictionary<string, ModInfoModel>(StringComparer.CurrentCultureIgnoreCase); - foreach (string modKey in modKeysArray) + foreach (string modKey in modKeys) { // parse mod key if (!this.TryParseModKey(modKey, out string vendorKey, out string modID)) diff --git a/src/StardewModdingAPI.Web/Framework/VersionConstraint.cs b/src/StardewModdingAPI.Web/Framework/VersionConstraint.cs new file mode 100644 index 00000000..be9c0918 --- /dev/null +++ b/src/StardewModdingAPI.Web/Framework/VersionConstraint.cs @@ -0,0 +1,15 @@ +using Microsoft.AspNetCore.Routing.Constraints; + +namespace StardewModdingAPI.Web.Framework +{ + /// <summary>Constrains a route value to a valid semantic version.</summary> + internal class VersionConstraint : RegexRouteConstraint + { + /********* + ** Public methods + *********/ + /// <summary>Construct an instance.</summary> + public VersionConstraint() + : base(@"^v(?>(?<major>0|[1-9]\d*))\.(?>(?<minor>0|[1-9]\d*))(?>(?:\.(?<patch>0|[1-9]\d*))?)(?:-(?<prerelease>(?>[a-z0-9]+[\-\.]?)+))?$") { } + } +} diff --git a/src/StardewModdingAPI.Web/Startup.cs b/src/StardewModdingAPI.Web/Startup.cs index d5b828b7..eaf14983 100644 --- a/src/StardewModdingAPI.Web/Startup.cs +++ b/src/StardewModdingAPI.Web/Startup.cs @@ -1,6 +1,7 @@ using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Hosting; using Microsoft.AspNetCore.Rewrite; +using Microsoft.AspNetCore.Routing; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; @@ -42,6 +43,7 @@ namespace StardewModdingAPI.Web { services .Configure<ModUpdateCheckConfig>(this.Configuration.GetSection("ModUpdateCheck")) + .Configure<RouteOptions>(options => options.ConstraintMap.Add("semanticVersion", typeof(VersionConstraint))) .AddMemoryCache() .AddMvc() .ConfigureApplicationPartManager(manager => manager.FeatureProviders.Add(new InternalControllerFeatureProvider())) @@ -62,22 +64,7 @@ namespace StardewModdingAPI.Web loggerFactory.AddDebug(); app .UseRewriter(new RewriteOptions().Add(new RewriteSubdomainRule())) // convert subdomain.smapi.io => smapi.io/subdomain for routing - .UseMvc(route => - { - route.MapRoute( - name: "API", - template: "api/{version}/{controller}/{action?}", - defaults: new - { - action = "GetAsync" - }, - constraints: new - { - // version regex from SMAPI's SemanticVersion implementation - version = @"^v(?>(?<major>0|[1-9]\d*))\.(?>(?<minor>0|[1-9]\d*))(?>(?:\.(?<patch>0|[1-9]\d*))?)(?:-(?<prerelease>(?>[a-z0-9]+[\-\.]?)+))?$" - } - ); - }); + .UseMvc(); } } } diff --git a/src/StardewModdingAPI/Framework/WebApiClient.cs b/src/StardewModdingAPI/Framework/WebApiClient.cs index 0ee57648..8f0b403d 100644 --- a/src/StardewModdingAPI/Framework/WebApiClient.cs +++ b/src/StardewModdingAPI/Framework/WebApiClient.cs @@ -2,6 +2,7 @@ using System; using System.Collections.Generic; using System.IO; using System.Net; +using System.Text; using System.Threading.Tasks; using Newtonsoft.Json; using StardewModdingAPI.Models; @@ -40,8 +41,10 @@ namespace StardewModdingAPI.Framework /// <param name="modKeys">The mod keys for which to fetch the latest version.</param> public async Task<IDictionary<string, ModInfoModel>> GetModInfoAsync(params string[] modKeys) { - string url = $"v{this.Version}/mods?modKeys={Uri.EscapeDataString(string.Join(",", modKeys))}"; - return await this.GetAsync<Dictionary<string, ModInfoModel>>(url); + return await this.PostAsync<ModSearchModel, Dictionary<string, ModInfoModel>>( + $"v{this.Version}/mods", + new ModSearchModel(modKeys) + ); } @@ -49,13 +52,27 @@ namespace StardewModdingAPI.Framework ** Private methods *********/ /// <summary>Fetch the response from the backend API.</summary> - /// <typeparam name="T">The expected response type.</typeparam> + /// <typeparam name="TBody">The body content type.</typeparam> + /// <typeparam name="TResult">The expected response type.</typeparam> /// <param name="url">The request URL, optionally excluding the base URL.</param> - private async Task<T> GetAsync<T>(string url) + /// <param name="content">The body content to post.</param> + private async Task<TResult> PostAsync<TBody, TResult>(string url, TBody content) { - // build request (avoid HttpClient for Mac compatibility) + /*** + ** Note: avoid HttpClient for Mac compatibility. + ***/ + + // serialise content + byte[] data = Encoding.UTF8.GetBytes(JsonConvert.SerializeObject(content)); + + // build request HttpWebRequest request = WebRequest.CreateHttp(new Uri(this.BaseUrl, url).ToString()); + request.Method = "POST"; request.UserAgent = $"SMAPI/{this.Version}"; + request.ContentType = "application/json"; + request.ContentLength = data.Length; + using (Stream bodyStream = request.GetRequestStream()) + bodyStream.Write(data, 0, data.Length); // fetch data using (WebResponse response = await request.GetResponseAsync()) @@ -63,7 +80,7 @@ namespace StardewModdingAPI.Framework using (StreamReader reader = new StreamReader(responseStream)) { string responseText = reader.ReadToEnd(); - return JsonConvert.DeserializeObject<T>(responseText); + return JsonConvert.DeserializeObject<TResult>(responseText); } } } |