diff --git a/SabreTools.RedumpLib.Test/Data/ExtensionsTests.cs b/SabreTools.RedumpLib.Test/Data/ExtensionsTests.cs index 1e0e088..40a0a0f 100644 --- a/SabreTools.RedumpLib.Test/Data/ExtensionsTests.cs +++ b/SabreTools.RedumpLib.Test/Data/ExtensionsTests.cs @@ -921,6 +921,113 @@ namespace SabreTools.RedumpLib.Test.Data #endregion + #region List Subpath + + /// + /// Check that every ListSubpath has a long name provided + /// + /// ListSubpath value to check + /// True to expect a null value, false otherwise + [Theory] + [MemberData(nameof(GenerateListSubpathTestData))] + public void ListSubpath_LongName(ListSubpath? listSubpath, bool expectNull) + { + var actual = listSubpath.LongName(); + + if (expectNull) + Assert.Null(actual); + else + Assert.NotNull(actual); + } + + /// + /// Check that every ListSubpath has a short name provided + /// + /// ListSubpath value to check + /// True to expect a null value, false otherwise + [Theory] + [MemberData(nameof(GenerateListSubpathTestData))] + public void ListSubpath_ShortName(ListSubpath? listSubpath, bool expectNull) + { + var actual = listSubpath.ShortName(); + + if (expectNull) + Assert.Null(actual); + else + Assert.NotNull(actual); + } + + /// + /// Ensure that every ListSubpath that has a short name that is unique + /// + [Fact] + public void ListSubpath_ShortName_NoDuplicates() + { + var fullListSubpaths = Enum.GetValues().Cast().ToList(); + var filteredListSubpaths = new Dictionary(); + + int totalCount = 0; + foreach (ListSubpath? listSubpath in fullListSubpaths) + { + var code = listSubpath.ShortName(); + if (string.IsNullOrEmpty(code)) + continue; + + // Throw if the code already exists + if (filteredListSubpaths.ContainsKey(code)) + throw new DuplicateNameException($"Code {code} already in dictionary"); + + filteredListSubpaths[code] = listSubpath; + totalCount++; + } + + Assert.Equal(totalCount, filteredListSubpaths.Count); + } + + /// + /// Check that every ListSubpath can be mapped from a string + /// + /// ListSubpath value to check + /// True to expect a null value, false otherwise + [Theory] + [MemberData(nameof(GenerateListSubpathTestData))] + public void ListSubpath_ToListSubpath(ListSubpath? listSubpath, bool expectNull) + { + string? longName = listSubpath.LongName(); + string? longNameSpaceless = longName?.Replace(" ", string.Empty); + + var actualNormal = longName.ToListSubpath(); + var actualSpaceless = longNameSpaceless.ToListSubpath(); + + if (expectNull) + { + Assert.Null(actualNormal); + Assert.Null(actualSpaceless); + } + else + { + Assert.Equal(listSubpath, actualNormal); + Assert.Equal(listSubpath, actualSpaceless); + } + } + + /// + /// Generate a test set of ListSubpath values + /// + /// MemberData-compatible list of ListSubpath values + public static TheoryData GenerateListSubpathTestData() + { + var testData = new TheoryData() { { null, true } }; + foreach (ListSubpath? listSubpath in Enum.GetValues().Cast()) + { + testData.Add(listSubpath, false); + } + + return testData; + } + + #endregion + #region Media Type /// diff --git a/SabreTools.RedumpLib.Test/Endpoints/ListTests.cs b/SabreTools.RedumpLib.Test/Endpoints/ListTests.cs new file mode 100644 index 0000000..6fea384 --- /dev/null +++ b/SabreTools.RedumpLib.Test/Endpoints/ListTests.cs @@ -0,0 +1,75 @@ +using SabreTools.RedumpLib.Data; +using Xunit; + +namespace SabreTools.RedumpLib.Test.Endpoints +{ + public class ListTests + { + #region BuildUrl + + [Fact] + public void BuildUrl_MissingUsername_DefaultEmpty() + { + var endpoint = new RedumpLib.Endpoints.List(); + string actual = endpoint.BuildUrl(); + Assert.Equal($"https://redump.info/list/", actual); + } + + [Fact] + public void BuildUrl_Subpath_Builds() + { + // Create a base endpoint and then modify the fields + var endpoint = new RedumpLib.Endpoints.List { Username = "user" }; + + // Invalid subpath, no system + endpoint.Subpath = null; + endpoint.System = null; + string actual = endpoint.BuildUrl(); + Assert.Equal("https://redump.info/list/user", actual); + + // Invalid subpath, valid system + endpoint.Subpath = null; + endpoint.System = PhysicalSystem.AcornArchimedesAndRiscPC; + actual = endpoint.BuildUrl(); + Assert.Equal("https://redump.info/list/user/ARCH", actual); + + // Have subpath, no system + endpoint.Subpath = ListSubpath.Have; + endpoint.System = null; + actual = endpoint.BuildUrl(); + Assert.Equal("https://redump.info/list/have/user", actual); + + // Have subpath, valid system + endpoint.Subpath = ListSubpath.Have; + endpoint.System = PhysicalSystem.AcornArchimedesAndRiscPC; + actual = endpoint.BuildUrl(); + Assert.Equal("https://redump.info/list/have/user/ARCH", actual); + + // Cuesheets subpath, no system + endpoint.Subpath = ListSubpath.Cuesheets; + endpoint.System = null; + actual = endpoint.BuildUrl(); + Assert.Equal("https://redump.info/list/cues/user", actual); + + // Cuesheets subpath, valid system + endpoint.Subpath = ListSubpath.Cuesheets; + endpoint.System = PhysicalSystem.AcornArchimedesAndRiscPC; + actual = endpoint.BuildUrl(); + Assert.Equal("https://redump.info/list/cues/user/ARCH", actual); + + // Datfile subpath, no system + endpoint.Subpath = ListSubpath.Datfile; + endpoint.System = null; + actual = endpoint.BuildUrl(); + Assert.Equal("https://redump.info/list/datfile/user", actual); + + // Datfile subpath, valid system + endpoint.Subpath = ListSubpath.Datfile; + endpoint.System = PhysicalSystem.AcornArchimedesAndRiscPC; + actual = endpoint.BuildUrl(); + Assert.Equal("https://redump.info/list/datfile/user/ARCH", actual); + } + + #endregion + } +} diff --git a/SabreTools.RedumpLib/Data/Enumerations.cs b/SabreTools.RedumpLib/Data/Enumerations.cs index 50a8e8e..26cb0b7 100644 --- a/SabreTools.RedumpLib/Data/Enumerations.cs +++ b/SabreTools.RedumpLib/Data/Enumerations.cs @@ -87,6 +87,21 @@ namespace SabreTools.RedumpLib.Data VerifiedGreen = 5, } + /// + /// List of all list subpaths + /// + public enum ListSubpath + { + [HumanReadable(LongName = "Have", ShortName = "have")] + Have = 1, + + [HumanReadable(LongName = "Cuesheets", ShortName = "cues")] + Cuesheets = 2, + + [HumanReadable(LongName = "Datfile", ShortName = "datfile")] + Datfile = 3, + } + /// /// List of all site-supported media types /// diff --git a/SabreTools.RedumpLib/Data/Extensions.cs b/SabreTools.RedumpLib/Data/Extensions.cs index 601f453..0ee8cf8 100644 --- a/SabreTools.RedumpLib/Data/Extensions.cs +++ b/SabreTools.RedumpLib/Data/Extensions.cs @@ -241,10 +241,10 @@ namespace SabreTools.RedumpLib.Data => AttributeHelper.GetHumanReadableAttribute(discSubpath)?.ShortName; /// - /// Get the Region enum value for a given string + /// Get the DiscSubpath enum value for a given string /// /// String value to convert - /// Region represented by the string, if possible + /// DiscSubpath represented by the string, if possible public static DiscSubpath? ToDiscSubpath(this string? discSubpath) { // No value means no match @@ -374,6 +374,70 @@ namespace SabreTools.RedumpLib.Data #endregion + #region List Subpath + + /// + /// Get the human readable name for a ListSubpath + /// + /// + /// + public static string? LongName(this ListSubpath listSubpath) + => AttributeHelper.GetHumanReadableAttribute(listSubpath)?.LongName; + + /// + /// Get the human readable name for a ListSubpath + /// + /// + /// + public static string? LongName(this ListSubpath? listSubpath) + => AttributeHelper.GetHumanReadableAttribute(listSubpath)?.LongName; + + /// + /// Get the URL path part for a ListSubpath + /// + /// + /// + public static string? ShortName(this ListSubpath listSubpath) + => AttributeHelper.GetHumanReadableAttribute(listSubpath)?.ShortName; + + /// + /// Get the URL path part for a ListSubpath + /// + /// + /// + public static string? ShortName(this ListSubpath? listSubpath) + => AttributeHelper.GetHumanReadableAttribute(listSubpath)?.ShortName; + + /// + /// Get the ListSubpath enum value for a given string + /// + /// String value to convert + /// ListSubpath represented by the string, if possible + public static ListSubpath? ToListSubpath(this string? listSubpath) + { + // No value means no match + if (listSubpath is null || listSubpath.Length == 0) + return null; + + listSubpath = listSubpath.ToLowerInvariant(); + var listSubpaths = (ListSubpath[])Enum.GetValues(typeof(ListSubpath)); + + // Check short names + int index = Array.FindIndex(listSubpaths, s => listSubpath == s.ShortName()?.ToLowerInvariant()); + if (index > -1) + return listSubpaths[index]; + + // Check long names + index = Array.FindIndex(listSubpaths, s => listSubpath == s.LongName()?.ToLowerInvariant() + || listSubpath == s.LongName()?.Replace(" ", string.Empty)?.ToLowerInvariant()); + if (index > -1) + return listSubpaths[index]; + + return null; + } + + #endregion + #region Media Type /// diff --git a/SabreTools.RedumpLib/Endpoints/List.cs b/SabreTools.RedumpLib/Endpoints/List.cs new file mode 100644 index 0000000..391a8ba --- /dev/null +++ b/SabreTools.RedumpLib/Endpoints/List.cs @@ -0,0 +1,52 @@ +using System; +using Newtonsoft.Json; +using SabreTools.RedumpLib.Data; + +namespace SabreTools.RedumpLib.Endpoints +{ + // TODO: Implement download and client extensions for these paths + [JsonObject("list")] + public class List + { + [JsonProperty("subpath")] + public ListSubpath? Subpath { get; set; } + + [JsonProperty("username")] + public string Username { get; set; } = string.Empty; + + [JsonProperty("syscode")] + public PhysicalSystem? System { get; set; } + + /// + /// Build a /list/ path URL + /// + public string BuildUrl() + { + var ub = new UriBuilder + { + Scheme = "https", + Host = "redump.info", + Path = "list", + }; + + switch (Subpath) + { + case ListSubpath.Have: + case ListSubpath.Cuesheets: + case ListSubpath.Datfile: + ub.Path += $"/{Subpath.ShortName()}"; + break; + + // Nothing else is valid as a subpath + default: + break; + } + + ub.Path += $"/{Username}"; + if (System?.Code is not null) + ub.Path += $"/{System.Code}"; + + return ub.ToString(); + } + } +}