diff --git a/DataLayer/EfClasses/Book.cs b/DataLayer/EfClasses/Book.cs index 4da6a5bb..1c1d1c3e 100644 --- a/DataLayer/EfClasses/Book.cs +++ b/DataLayer/EfClasses/Book.cs @@ -185,6 +185,9 @@ namespace DataLayer { get { + if (_seriesLink is null) + return ""; + // first: alphabetical by name var withNames = _seriesLink .Where(s => !string.IsNullOrWhiteSpace(s.Series.Name)) diff --git a/LibationFileManager/Templates.cs b/LibationFileManager/Templates.cs index 481895e3..d33ae7fc 100644 --- a/LibationFileManager/Templates.cs +++ b/LibationFileManager/Templates.cs @@ -6,6 +6,19 @@ namespace LibationFileManager { public abstract class Templates { + protected static string[] Valid => Array.Empty(); + public const string ERROR_NULL_IS_INVALID = "Null template is invalid."; + public const string ERROR_FULL_PATH_IS_INVALID = @"No full paths allowed. Eg: should not start with C:\"; + public const string ERROR_INVALID_FILE_NAME_CHAR = @"Only file name friendly characters allowed. Eg: no colons or slashes"; + + public const string WARNING_EMPTY = "Template is empty."; + public const string WARNING_WHITE_SPACE = "Template is white space."; + public const string WARNING_NO_TAGS = "Should use tags. Eg: "; + public const string WARNING_HAS_CHAPTER_TAGS = "Chapter tags should only be used in the template used for naming files which are split by chapter. Eg: <ch title>"; + public const string WARNING_NO_CHAPTER_NUMBER_TAG = "Should include chapter number tag in template used for naming files which are split by chapter. Ie: <ch#> or <ch# 0>"; + // actual possible to se? + public const string WARNING_NO_CHAPTER_TAGS = "Should include chapter tags in template used for naming files which are split by chapter. Eg: <ch title>"; + public static Templates Folder { get; } = new FolderTemplate(); public static Templates File { get; } = new FileTemplate(); public static Templates ChapterFile { get; } = new ChapterFileTemplate(); @@ -14,8 +27,12 @@ namespace LibationFileManager public abstract string Description { get; } public abstract string DefaultTemplate { get; } - public abstract bool IsValid(string template); - public abstract bool IsRecommended(string template); + public abstract IEnumerable<string> GetErrors(string template); + public bool IsValid(string template) => !GetErrors(template).Any(); + + public abstract IEnumerable<string> GetWarnings(string template); + public bool HasWarnings(string template) => GetWarnings(template).Any(); + public abstract int TagCount(string template); public static bool ContainsChapterOnlyTags(string template) @@ -25,19 +42,45 @@ namespace LibationFileManager public static bool ContainsTag(string template, string tag) => template.Contains($"<{tag}>"); - protected static bool fileIsValid(string template) + protected static string[] getFileErrors(string template) + { // File name only; not path. all other path chars are valid enough to pass this check and will be handled on final save. - // null is invalid. whitespace is valid but not recommended - => template is not null - && !template.Contains(':') - && !template.Contains(System.IO.Path.DirectorySeparatorChar) - && !template.Contains(System.IO.Path.AltDirectorySeparatorChar); - protected bool isRecommended(string template, bool isChapter) - => IsValid(template) - && !string.IsNullOrWhiteSpace(template) - && TagCount(template) > 0 - && ContainsChapterOnlyTags(template) == isChapter; + // null is invalid. whitespace is valid but not recommended + if (template is null) + return new[] { ERROR_NULL_IS_INVALID }; + + if (template.Contains(':') + || template.Contains(System.IO.Path.DirectorySeparatorChar) + || template.Contains(System.IO.Path.AltDirectorySeparatorChar) + ) + return new[] { ERROR_INVALID_FILE_NAME_CHAR }; + + return Valid; + } + + protected IEnumerable<string> getWarnings(string template, bool isChapter) + { + var warnings = GetErrors(template).ToList(); + if (template is null) + return warnings; + + if (string.IsNullOrEmpty(template)) + warnings.Add(WARNING_EMPTY); + else if (string.IsNullOrWhiteSpace(template)) + warnings.Add(WARNING_WHITE_SPACE); + + if (TagCount(template) == 0) + warnings.Add(WARNING_NO_TAGS); + + var containsChapterOnlyTags = ContainsChapterOnlyTags(template); + if (isChapter && !containsChapterOnlyTags) + warnings.Add(WARNING_NO_CHAPTER_TAGS); + if (!isChapter && containsChapterOnlyTags) + warnings.Add(WARNING_HAS_CHAPTER_TAGS); + + return warnings; + } protected static int tagCount(string template, Func<TemplateTags, bool> func) => TemplateTags.GetAll() @@ -53,13 +96,20 @@ namespace LibationFileManager public override string Description => Configuration.GetDescription(nameof(Configuration.FolderTemplate)); public override string DefaultTemplate { get; } = "<title short> [<id>]"; - public override bool IsValid(string template) - // must be relative. no colons. all other path chars are valid enough to pass this check and will be handled on final save. + public override IEnumerable<string> GetErrors(string template) + { // null is invalid. whitespace is valid but not recommended - => template is not null - && !template.Contains(':'); + if (template is null) + return new[] { ERROR_NULL_IS_INVALID }; - public override bool IsRecommended(string template) => isRecommended(template, false); + // must be relative. no colons. all other path chars are valid enough to pass this check and will be handled on final save. + if (template.Contains(':')) + return new[] { ERROR_FULL_PATH_IS_INVALID }; + + return Valid; + } + + public override IEnumerable<string> GetWarnings(string template) => getWarnings(template, false); public override int TagCount(string template) => tagCount(template, t => !t.IsChapterOnly); } @@ -70,9 +120,9 @@ namespace LibationFileManager public override string Description => Configuration.GetDescription(nameof(Configuration.FileTemplate)); public override string DefaultTemplate { get; } = "<title> [<id>]"; - public override bool IsValid(string template) => fileIsValid(template); + public override IEnumerable<string> GetErrors(string template) => getFileErrors(template); - public override bool IsRecommended(string template) => isRecommended(template, false); + public override IEnumerable<string> GetWarnings(string template) => getWarnings(template, false); public override int TagCount(string template) => tagCount(template, t => !t.IsChapterOnly); } @@ -82,13 +132,24 @@ namespace LibationFileManager public override string Name => "Chapter File Template"; public override string Description => Configuration.GetDescription(nameof(Configuration.ChapterFileTemplate)); public override string DefaultTemplate { get; } = "<title> [<id>] - <ch# 0> - <ch title>"; + + public override IEnumerable<string> GetErrors(string template) => getFileErrors(template); - public override bool IsValid(string template) => fileIsValid(template); + public override IEnumerable<string> GetWarnings(string template) + { + var warnings = getWarnings(template, true).ToList(); + if (template is null) + return warnings; - public override bool IsRecommended(string template) - => isRecommended(template, true) // recommended to incl. <ch#> or <ch# 0> - && (ContainsTag(template, TemplateTags.ChNumber.TagName) || ContainsTag(template, TemplateTags.ChNumber0.TagName)); + if (!ContainsTag(template, TemplateTags.ChNumber.TagName) && !ContainsTag(template, TemplateTags.ChNumber0.TagName)) + { + warnings.Remove(WARNING_NO_CHAPTER_TAGS); + warnings.Add(WARNING_NO_CHAPTER_NUMBER_TAG); + } + + return warnings; + } public override int TagCount(string template) => tagCount(template, t => true); } diff --git a/LibationWinForms/Dialogs/EditTemplateDialog.cs b/LibationWinForms/Dialogs/EditTemplateDialog.cs index 5eb2c22f..aa317cef 100644 --- a/LibationWinForms/Dialogs/EditTemplateDialog.cs +++ b/LibationWinForms/Dialogs/EditTemplateDialog.cs @@ -60,12 +60,35 @@ namespace LibationWinForms.Dialogs while (path.Contains(dbl)) path = path.Replace(dbl, $"{Path.DirectorySeparatorChar}"); + var book = new DataLayer.Book( + new DataLayer.AudibleProductId("123456789"), + "A Study in Scarlet", + "Fake description", + 1234, + DataLayer.ContentType.Product, + new List<DataLayer.Contributor> + { + new("Arthur Conan Doyle"), + new("Stephen Fry - introductions") + }, + new List<DataLayer.Contributor> { new("Stephen Fry") }, + new DataLayer.Category(new DataLayer.AudibleCategoryId("cat12345"), "Mystery"), + "us" + ); + var libraryBook = new DataLayer.LibraryBook(book, DateTime.Now, "my account"); + outputTb.Text = @$" {books} {folderTemplate} {fileTemplate} {ext} {path} + +{book.AudibleProductId} +{book.Title} +{book.AuthorNames} +{book.NarratorNames} +series: {"Sherlock Holmes"} "; } diff --git a/LibationWinForms/Dialogs/SettingsDialog.cs b/LibationWinForms/Dialogs/SettingsDialog.cs index acab607b..1774af5c 100644 --- a/LibationWinForms/Dialogs/SettingsDialog.cs +++ b/LibationWinForms/Dialogs/SettingsDialog.cs @@ -108,8 +108,10 @@ namespace LibationWinForms.Dialogs private void chapterFileTemplateBtn_Click(object sender, EventArgs e) => editTemplate(Templates.ChapterFile, chapterFileTemplateTb); private static void editTemplate(Templates template, TextBox textBox) { +#if !DEBUG TEMP_TEMP_TEMP(); return; +#endif var form = new EditTemplateDialog(template, textBox.Text); if (form.ShowDialog() == DialogResult.OK) @@ -124,31 +126,34 @@ namespace LibationWinForms.Dialogs var newBooks = booksSelectControl.SelectedDirectory; #region validation + static void validationError(string text, string caption) + => MessageBox.Show(text, caption, MessageBoxButtons.OK, MessageBoxIcon.Error); if (string.IsNullOrWhiteSpace(newBooks)) { - MessageBox.Show("Cannot set Books Location to blank", "Location is blank", MessageBoxButtons.OK, MessageBoxIcon.Error); + validationError("Cannot set Books Location to blank", "Location is blank"); return; } if (!Directory.Exists(newBooks) && booksSelectControl.SelectedDirectoryIsCustom) { - MessageBox.Show($"Not saving change to Books location. This folder does not exist:\r\n{newBooks}", "Folder does not exist", MessageBoxButtons.OK, MessageBoxIcon.Error); + validationError($"Not saving change to Books location. This folder does not exist:\r\n{newBooks}", "Folder does not exist"); return; } + // these 3 should do nothing. Configuration will only init these with a valid value. EditTemplateDialog ensures valid before returning if (!Templates.Folder.IsValid(folderTemplateTb.Text)) { - MessageBox.Show($"Not saving change to folder naming template. Invalid format.", "Invalid folder template", MessageBoxButtons.OK, MessageBoxIcon.Error); + validationError($"Not saving change to folder naming template. Invalid format.", "Invalid folder template"); return; } if (!Templates.File.IsValid(fileTemplateTb.Text)) { - MessageBox.Show($"Not saving change to file naming template. Invalid format.", "Invalid file template", MessageBoxButtons.OK, MessageBoxIcon.Error); + validationError($"Not saving change to file naming template. Invalid format.", "Invalid file template"); return; } if (!Templates.ChapterFile.IsValid(chapterFileTemplateTb.Text)) { - MessageBox.Show($"Not saving change to chapter file naming template. Invalid format.", "Invalid chapter file template", MessageBoxButtons.OK, MessageBoxIcon.Error); + validationError($"Not saving change to chapter file naming template. Invalid format.", "Invalid chapter file template"); return; } #endregion diff --git a/_Tests/LibationFileManager.Tests/TemplatesTests.cs b/_Tests/LibationFileManager.Tests/TemplatesTests.cs index 9010744a..02c1b3ff 100644 --- a/_Tests/LibationFileManager.Tests/TemplatesTests.cs +++ b/_Tests/LibationFileManager.Tests/TemplatesTests.cs @@ -33,6 +33,38 @@ namespace TemplatesTests namespace Templates_Folder_Tests { + [TestClass] + public class GetErrors + { + [TestMethod] + public void null_is_invalid() => Tests(null, new[] { Templates.ERROR_NULL_IS_INVALID }); + + [TestMethod] + public void empty_is_valid() => valid_tests(""); + + [TestMethod] + public void whitespace_is_valid() => valid_tests(" "); + + [TestMethod] + [DataRow(@"foo")] + [DataRow(@"\foo")] + [DataRow(@"foo\")] + [DataRow(@"\foo\")] + [DataRow(@"foo\bar")] + [DataRow(@"<id>")] + [DataRow(@"<id>\<title>")] + public void valid_tests(string template) => Tests(template, Array.Empty<string>()); + + [TestMethod] + [DataRow(@"C:\", Templates.ERROR_FULL_PATH_IS_INVALID)] + public void Tests(string template, params string[] expected) + { + var result = Templates.Folder.GetErrors(template); + result.Count().Should().Be(expected.Length); + result.Should().BeEquivalentTo(expected); + } + } + [TestClass] public class IsValid { @@ -58,36 +90,64 @@ namespace Templates_Folder_Tests } [TestClass] - public class IsRecommended + public class GetWarnings { [TestMethod] - public void null_is_not_recommended() => Tests(null, false); + public void null_is_invalid() => Tests(null, new[] { Templates.ERROR_NULL_IS_INVALID }); [TestMethod] - public void empty_is_not_recommended() => Tests("", false); + public void empty_has_warnings() => Tests("", Templates.WARNING_EMPTY, Templates.WARNING_NO_TAGS); [TestMethod] - public void whitespace_is_not_recommended() => Tests(" ", false); + public void whitespace_has_warnings() => Tests(" ", Templates.WARNING_WHITE_SPACE, Templates.WARNING_NO_TAGS); [TestMethod] - [DataRow(@"no tags", false)] - [DataRow(@"<id>\foo\bar", true)] - [DataRow("<ch#> <id>", false)] - [DataRow("<ch#> chapter tag", false)] - public void Tests(string template, bool expected) => Templates.Folder.IsRecommended(template).Should().Be(expected); + [DataRow(@"<id>\foo\bar")] + public void valid_tests(string template) => Tests(template, Array.Empty<string>()); + + [TestMethod] + [DataRow(@"no tags", Templates.WARNING_NO_TAGS)] + [DataRow("<ch#> <id>", Templates.WARNING_HAS_CHAPTER_TAGS)] + [DataRow("<ch#> chapter tag", Templates.WARNING_NO_TAGS, Templates.WARNING_HAS_CHAPTER_TAGS)] + public void Tests(string template, params string[] expected) + { + var result = Templates.Folder.GetWarnings(template); + result.Count().Should().Be(expected.Length); + result.Should().BeEquivalentTo(expected); + } + } + + [TestClass] + public class HasWarnings + { + [TestMethod] + public void null_has_warnings() => Tests(null, true); + + [TestMethod] + public void empty_has_warnings() => Tests("", true); + + [TestMethod] + public void whitespace_has_warnings() => Tests(" ", true); + + [TestMethod] + [DataRow(@"no tags", true)] + [DataRow(@"<id>\foo\bar", false)] + [DataRow("<ch#> <id>", true)] + [DataRow("<ch#> chapter tag", true)] + public void Tests(string template, bool expected) => Templates.Folder.HasWarnings(template).Should().Be(expected); } [TestClass] public class TagCount { [TestMethod] - public void null_is_not_recommended() => Assert.ThrowsException<NullReferenceException>(() => Tests(null, -1)); + public void null_throws() => Assert.ThrowsException<NullReferenceException>(() => Templates.Folder.TagCount(null)); [TestMethod] - public void empty_is_not_recommended() => Tests("", 0); + public void empty() => Tests("", 0); [TestMethod] - public void whitespace_is_not_recommended() => Tests(" ", 0); + public void whitespace() => Tests(" ", 0); [TestMethod] [DataRow("no tags", 0)] @@ -105,6 +165,37 @@ namespace Templates_Folder_Tests namespace Templates_File_Tests { + [TestClass] + public class GetErrors + { + [TestMethod] + public void null_is_invalid() => Tests(null, new[] { Templates.ERROR_NULL_IS_INVALID }); + + [TestMethod] + public void empty_is_valid() => valid_tests(""); + + [TestMethod] + public void whitespace_is_valid() => valid_tests(" "); + + [TestMethod] + [DataRow(@"foo")] + [DataRow(@"<id>")] + public void valid_tests(string template) => Tests(template, Array.Empty<string>()); + + + [TestMethod] + [DataRow(@"C:\", Templates.ERROR_INVALID_FILE_NAME_CHAR)] + [DataRow(@"\foo", Templates.ERROR_INVALID_FILE_NAME_CHAR)] + [DataRow(@"/foo", Templates.ERROR_INVALID_FILE_NAME_CHAR)] + [DataRow(@"C:\", Templates.ERROR_INVALID_FILE_NAME_CHAR)] + public void Tests(string template, params string[] expected) + { + var result = Templates.File.GetErrors(template); + result.Count().Should().Be(expected.Length); + result.Should().BeEquivalentTo(expected); + } + } + [TestClass] public class IsValid { @@ -126,9 +217,13 @@ namespace Templates_File_Tests public void Tests(string template, bool expected) => Templates.File.IsValid(template).Should().Be(expected); } - // same as Templates.Folder.IsRecommended + // same as Templates.Folder.GetWarnings //[TestClass] - //public class IsRecommended { } + //public class GetWarnings { } + + // same as Templates.Folder.HasWarnings + //[TestClass] + //public class HasWarnings { } // same as Templates.Folder.TagCount //[TestClass] @@ -137,29 +232,62 @@ namespace Templates_File_Tests namespace Templates_ChapterFile_Tests { + // same as Templates.File.GetErrors + //[TestClass] + //public class GetErrors { } + // same as Templates.File.IsValid //[TestClass] //public class IsValid { } [TestClass] - public class IsRecommended + public class GetWarnings { [TestMethod] - public void null_is_not_recommended() => Tests(null, false); + public void null_is_invalid() => Tests(null, new[] { Templates.ERROR_NULL_IS_INVALID }); [TestMethod] - public void empty_is_not_recommended() => Tests("", false); + public void empty_has_warnings() => Tests("", Templates.WARNING_EMPTY, Templates.WARNING_NO_TAGS, Templates.WARNING_NO_CHAPTER_NUMBER_TAG); [TestMethod] - public void whitespace_is_not_recommended() => Tests(" ", false); + public void whitespace_has_warnings() => Tests(" ", Templates.WARNING_WHITE_SPACE, Templates.WARNING_NO_TAGS, Templates.WARNING_NO_CHAPTER_NUMBER_TAG); [TestMethod] - [DataRow(@"no tags", false)] - [DataRow(@"<id>\foo\bar", false)] - [DataRow("<ch#> <id>", true)] - [DataRow("<ch#> -- chapter tag", true)] - [DataRow("<chapter count> -- chapter tag but not ch# or ch_#", false)] - public void Tests(string template, bool expected) => Templates.ChapterFile.IsRecommended(template).Should().Be(expected); + [DataRow("<ch#>")] + [DataRow("<ch#> <id>")] + public void valid_tests(string template) => Tests(template, Array.Empty<string>()); + + [TestMethod] + [DataRow(@"no tags", Templates.WARNING_NO_TAGS, Templates.WARNING_NO_CHAPTER_NUMBER_TAG)] + [DataRow(@"<id>\foo\bar", Templates.ERROR_INVALID_FILE_NAME_CHAR, Templates.WARNING_NO_CHAPTER_NUMBER_TAG)] + [DataRow("<chapter count> -- chapter tag but not ch# or ch_#", Templates.WARNING_NO_TAGS, Templates.WARNING_NO_CHAPTER_NUMBER_TAG)] + public void Tests(string template, params string[] expected) + { + var result = Templates.ChapterFile.GetWarnings(template); + result.Count().Should().Be(expected.Length); + result.Should().BeEquivalentTo(expected); + } + } + + [TestClass] + public class HasWarnings + { + [TestMethod] + public void null_has_warnings() => Tests(null, true); + + [TestMethod] + public void empty_has_warnings() => Tests("", true); + + [TestMethod] + public void whitespace_has_warnings() => Tests(" ", true); + + [TestMethod] + [DataRow(@"no tags", true)] + [DataRow(@"<id>\foo\bar", true)] + [DataRow("<ch#> <id>", false)] + [DataRow("<ch#> -- chapter tag", false)] + [DataRow("<chapter count> -- chapter tag but not ch# or ch_#", true)] + public void Tests(string template, bool expected) => Templates.ChapterFile.HasWarnings(template).Should().Be(expected); } [TestClass]