diff --git a/Source/FileManager/NamingTemplate/ConditionalTagClass[TClass].cs b/Source/FileManager/NamingTemplate/ConditionalTagClass[TClass].cs index bddfac26..7acb4fec 100644 --- a/Source/FileManager/NamingTemplate/ConditionalTagClass[TClass].cs +++ b/Source/FileManager/NamingTemplate/ConditionalTagClass[TClass].cs @@ -61,6 +61,6 @@ public class ConditionalTagClass : TagCollection return false; } - protected override Expression GetTagExpression(string exactName, string formatter) => formatter == "!" ? Expression.Not(ExpressionValue) : ExpressionValue; + protected override Expression GetTagExpression(string exactName, string formatter) => formatter == "!" ? Expression.Not(ValueExpression) : ValueExpression; } } diff --git a/Source/FileManager/NamingTemplate/NamingTemplate.cs b/Source/FileManager/NamingTemplate/NamingTemplate.cs index 1de60531..646166c5 100644 --- a/Source/FileManager/NamingTemplate/NamingTemplate.cs +++ b/Source/FileManager/NamingTemplate/NamingTemplate.cs @@ -27,7 +27,7 @@ public class NamingTemplate /// /// Invoke the to /// - /// Instances of the TClass used in and + /// Instances of the TClass used in and /// public TemplatePart Evaluate(params object[] propertyClasses) { diff --git a/Source/FileManager/NamingTemplate/PropertyTagClass[TClass].cs b/Source/FileManager/NamingTemplate/PropertyTagClass[TClass].cs deleted file mode 100644 index 56955cff..00000000 --- a/Source/FileManager/NamingTemplate/PropertyTagClass[TClass].cs +++ /dev/null @@ -1,88 +0,0 @@ -using System; -using System.Linq.Expressions; -using System.Reflection; -using System.Text.RegularExpressions; - -namespace FileManager.NamingTemplate; - -public delegate string PropertyFormatter(ITemplateTag templateTag, T value, string formatString); - -public class PropertyTagClass : TagCollection -{ - public PropertyTagClass(bool caseSensative = true) : base(typeof(TClass), caseSensative) { } - - /// - /// Register a nullable value type property. - /// - /// Type of the property from - /// A Func to get the property value from - /// Optional formatting function that accepts the property and a formatting string and returnes the value formatted to string - public void Add(ITemplateTag templateTag, Func propertyGetter, PropertyFormatter formatter = null) - where U : struct - => RegisterProperty(templateTag, propertyGetter, formatter); - - /// - /// Register a non-nullable value type property - /// - /// Type of the property from - /// A Func to get the property value from - /// Optional formatting function that accepts the property and a formatting string and returnes the value formatted to string - public void Add(ITemplateTag templateTag, Func propertyGetter, PropertyFormatter formatter = null) - where U : struct - => RegisterProperty(templateTag, propertyGetter, formatter); - - /// - /// Register a string type property. - /// - /// A Func to get the string property from - /// Optional formatting function that accepts the string property and a formatting string and returnes the value formatted to string - public void Add(ITemplateTag templateTag, Func propertyGetter, PropertyFormatter formatter = null) - => RegisterProperty(templateTag, propertyGetter, formatter); - - private void RegisterProperty(ITemplateTag templateTag, Delegate propertyGetter, Delegate formatter) - { - if (formatter?.Target is not null) - throw new ArgumentException($"{nameof(formatter)} must be a static method"); - - var expr = Expression.Call(Expression.Constant(propertyGetter.Target), propertyGetter.Method, Parameter); - - AddPropertyTag(new PropertyTag(templateTag, Options, expr, formatter?.Method)); - } - - private class PropertyTag : TagBase - { - private readonly Func createToStringExpression; - - public PropertyTag(ITemplateTag templateTag, RegexOptions options, Expression propertyExpression, MethodInfo formatter) - : base(templateTag, propertyExpression) - { - var regexStr = formatter is null ? @$"^<{TemplateTag.TagName}>" : @$"^<{TemplateTag.TagName.Replace(" ", "\\s*?")}\s*?(?:\[([^\[\]]*?)\]\s*?)?>"; - NameMatcher = new Regex(regexStr, options); - - //Create the ToString() expression for the TagBase.ExpressionValue's type. - //If a formatter delegate was registered for this property, use that. - //Otherwise use the object.Tostring() method. - createToStringExpression - = formatter is null - ? (expValue, retTyp, format) => Expression.Call(expValue, retTyp.GetMethod(nameof(object.ToString), Array.Empty())) - : (expValue, retTyp, format) => Expression.Call(null, formatter, Expression.Constant(templateTag), expValue, Expression.Constant(format)); - } - - protected override Expression GetTagExpression(string exactName, string formatString) - { - var underlyingType = Nullable.GetUnderlyingType(ReturnType); - - Expression toStringExpression - = ReturnType == typeof(string) - ? createToStringExpression(Expression.Coalesce(ExpressionValue, Expression.Constant("")), ReturnType, formatString) - : underlyingType is null - ? createToStringExpression(ExpressionValue, ReturnType, formatString) - : Expression.Condition( - Expression.PropertyOrField(ExpressionValue, "HasValue"), - createToStringExpression(Expression.PropertyOrField(ExpressionValue, "Value"), underlyingType, formatString), - Expression.Constant("")); - - return Expression.TryCatch(toStringExpression, Expression.Catch(typeof(Exception), Expression.Constant(exactName))); - } - } -} diff --git a/Source/FileManager/NamingTemplate/PropertyTagCollection[TClass].cs b/Source/FileManager/NamingTemplate/PropertyTagCollection[TClass].cs new file mode 100644 index 00000000..32d0ee0a --- /dev/null +++ b/Source/FileManager/NamingTemplate/PropertyTagCollection[TClass].cs @@ -0,0 +1,168 @@ +using Dinah.Core; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Linq.Expressions; +using System.Text.RegularExpressions; + +namespace FileManager.NamingTemplate; + +public delegate string PropertyFormatter(ITemplateTag templateTag, T value, string formatString); + +public class PropertyTagCollection : TagCollection +{ + private readonly Dictionary defaultFormatters = new(); + + public PropertyTagCollection(bool caseSensative = true, params Delegate[] defaultFormatters) : base(typeof(TClass), caseSensative) + { + foreach (var formatter in defaultFormatters) + { + var parameters = formatter.Method.GetParameters(); + + if (formatter.Method.ReturnType != typeof(string) + || parameters.Length != 3 + || parameters[0].ParameterType != typeof(ITemplateTag) + || parameters[2].ParameterType != typeof(string)) + throw new ArgumentException($"{nameof(defaultFormatters)} must have a signature of [{nameof(String)} PropertyFormatter({nameof(ITemplateTag)}, T, {nameof(String)})]"); + + this.defaultFormatters[parameters[1].ParameterType] = formatter; + } + } + + /// + /// Register a nullable value type property. + /// + /// Type of the property from + /// A Func to get the property value from + /// Optional formatting function that accepts the property + /// and a formatting string and returnes the value the formatted string. If , use the default + /// formatter if present, or + public void Add(ITemplateTag templateTag, Func propertyGetter, PropertyFormatter formatter = null) + where U : struct + => RegisterWithFormatter(templateTag, propertyGetter, formatter); + + /// + /// Register a nullable value type property. + /// + /// A Func to get the string property from + /// ToString function that accepts the property and returnes a string + public void Add(ITemplateTag templateTag, Func propertyGetter, Func toString) + where U : struct + => RegisterWithToString(templateTag, propertyGetter, toString); + + /// + /// Register a non-nullable value type property + /// + /// Type of the property from + /// A Func to get the property value from + /// Optional formatting function that accepts the property + /// and a formatting string and returnes the value formatted to string. If , use the default + /// formatter if present, or + public void Add(ITemplateTag templateTag, Func propertyGetter, PropertyFormatter formatter = null) + where U : struct + => RegisterWithFormatter(templateTag, propertyGetter, formatter); + + /// + /// Register a non-nullable value type property. + /// + /// A Func to get the string property from + /// ToString function that accepts the property and returnes a string + public void Add(ITemplateTag templateTag, Func propertyGetter, Func toString) + where U : struct + => RegisterWithToString(templateTag, propertyGetter, toString); + + /// + /// Register a string type property + /// + /// A Func to get the string property from + /// Optional formatting function that accepts the string property and a formatting + /// string and returnes the value formatted to string. If , use the default + /// formatter if present, or + public void Add(ITemplateTag templateTag, Func propertyGetter, PropertyFormatter formatter = null) + => RegisterWithFormatter(templateTag, propertyGetter, formatter); + + /// + /// Register a string type property. + /// + /// A Func to get the string property from + /// ToString function that accepts the string property and returnes a string + public void Add(ITemplateTag templateTag, Func propertyGetter, Func toString) + => RegisterWithToString(templateTag, propertyGetter, toString); + + private void RegisterWithFormatter(ITemplateTag templateTag, Func propertyGetter, PropertyFormatter formatter) + { + static string ToStringFunc(U value) => value is string str ? str : value.ToString(); + ArgumentValidator.EnsureNotNull(templateTag, nameof(templateTag)); + ArgumentValidator.EnsureNotNull(propertyGetter, nameof(propertyGetter)); + + var formatDelegate = formatter ?? defaultFormatters.FirstOrDefault(kvp => kvp.Key == typeof(U)).Value; + + var expr = Expression.Call(Expression.Constant(propertyGetter.Target), propertyGetter.Method, Parameter); + + if (formatDelegate is null) + AddPropertyTag(PropertyTag.CreateWithToString(templateTag, Options, expr, ToStringFunc)); + else + AddPropertyTag(PropertyTag.CreateWithFormatter(templateTag, Options, expr, formatDelegate)); + } + + private void RegisterWithToString(ITemplateTag templateTag, Func propertyGetter, Func toString) + { + static string ToStringFunc(U value) => value is string str ? str : value.ToString(); + ArgumentValidator.EnsureNotNull(templateTag, nameof(templateTag)); + ArgumentValidator.EnsureNotNull(propertyGetter, nameof(propertyGetter)); + + toString ??= ToStringFunc; + + var expr = Expression.Call(Expression.Constant(propertyGetter.Target), propertyGetter.Method, Parameter); + AddPropertyTag(PropertyTag.CreateWithToString(templateTag, Options, expr, toString)); + } + + private class PropertyTag : TagBase + { + private Func CreateToStringExpression { get; init; } + private PropertyTag(ITemplateTag templateTag, Expression propertyExpression) : base(templateTag, propertyExpression) { } + + public static PropertyTag CreateWithFormatter(ITemplateTag templateTag, RegexOptions options, Expression propertyExpression, Delegate formatter) + { + return new PropertyTag(templateTag, propertyExpression) + { + NameMatcher = new Regex(@$"^<{templateTag.TagName.Replace(" ", "\\s*?")}\s*?(?:\[([^\[\]]*?)\]\s*?)?>", options), + CreateToStringExpression = (expVal, format) => + Expression.Call( + formatter.Target is null ? null : Expression.Constant(formatter.Target), + formatter.Method, + Expression.Constant(templateTag), + expVal, + Expression.Constant(format)) + }; + } + + public static PropertyTag CreateWithToString(ITemplateTag templateTag, RegexOptions options, Expression propertyExpression, Delegate toString) + { + return new PropertyTag(templateTag, propertyExpression) + { + NameMatcher = new Regex(@$"^<{templateTag.TagName}>", options), + CreateToStringExpression = (expVal, _) => + Expression.Call( + toString.Target is null ? null : Expression.Constant(toString.Target), + toString.Method, + expVal) + }; + } + + protected override Expression GetTagExpression(string exactName, string formatString) + { + Expression toStringExpression + = ReturnType == typeof(string) + ? CreateToStringExpression(Expression.Coalesce(ValueExpression, Expression.Constant("")), formatString) + : Nullable.GetUnderlyingType(ReturnType) is null + ? CreateToStringExpression(ValueExpression, formatString) + : Expression.Condition( + Expression.PropertyOrField(ValueExpression, "HasValue"), + CreateToStringExpression(Expression.PropertyOrField(ValueExpression, "Value"), formatString), + Expression.Constant("")); + + return Expression.TryCatch(toStringExpression, Expression.Catch(typeof(Exception), Expression.Constant(exactName))); + } + } +} diff --git a/Source/FileManager/NamingTemplate/TagBase.cs b/Source/FileManager/NamingTemplate/TagBase.cs index 613d2a53..a6bf9864 100644 --- a/Source/FileManager/NamingTemplate/TagBase.cs +++ b/Source/FileManager/NamingTemplate/TagBase.cs @@ -29,13 +29,13 @@ internal abstract class TagBase : IPropertyTag { public ITemplateTag TemplateTag { get; } public Regex NameMatcher { get; protected init; } - public Type ReturnType => ExpressionValue.Type; - protected Expression ExpressionValue { get; } + public Type ReturnType => ValueExpression.Type; + protected Expression ValueExpression { get; } protected TagBase(ITemplateTag templateTag, Expression propertyExpression) { TemplateTag = templateTag; - ExpressionValue = propertyExpression; + ValueExpression = propertyExpression; } /// Create an that returns the property's value. diff --git a/Source/LibationFileManager/Templates.cs b/Source/LibationFileManager/Templates.cs index bd9ef37e..09393b55 100644 --- a/Source/LibationFileManager/Templates.cs +++ b/Source/LibationFileManager/Templates.cs @@ -12,7 +12,7 @@ namespace LibationFileManager public interface ITemplate { static abstract string DefaultTemplate { get; } - static abstract IEnumerable TagClass { get; } + static abstract IEnumerable TagClass { get; } } public abstract class Templates @@ -191,45 +191,48 @@ namespace LibationFileManager #region Registered Template Properties - private static readonly PropertyTagClass filePropertyTags = new() + private static readonly PropertyTagCollection filePropertyTags = + new(caseSensative: true, StringFormatter, DateTimeFormatter, IntegerFormatter) { - { TemplateTags.Id, lb => lb.AudibleProductId }, - { TemplateTags.Title, lb => lb.Title, StringFormatter }, - { TemplateTags.TitleShort, lb => lb.Title.IndexOf(':') < 1 ? lb.Title : lb.Title.Substring(0, lb.Title.IndexOf(':')), StringFormatter }, - { TemplateTags.Author, lb => lb.AuthorNames, StringFormatter }, - { TemplateTags.FirstAuthor, lb => lb.FirstAuthor, StringFormatter }, - { TemplateTags.Narrator, lb => lb.NarratorNames, StringFormatter }, - { TemplateTags.FirstNarrator, lb => lb.FirstNarrator, StringFormatter }, - { TemplateTags.Series, lb => lb.SeriesName, StringFormatter }, - { TemplateTags.SeriesNumber, lb => lb.SeriesNumber, IntegerFormatter }, - { TemplateTags.Language, lb => lb.Language, StringFormatter }, - { TemplateTags.LanguageShort, lb => getLanguageShort(lb.Language), StringFormatter }, - { TemplateTags.Bitrate, lb => lb.BitRate, IntegerFormatter }, - { TemplateTags.SampleRate, lb => lb.SampleRate, IntegerFormatter }, - { TemplateTags.Channels, lb => lb.Channels, IntegerFormatter }, - { TemplateTags.Account, lb => lb.Account, StringFormatter }, - { TemplateTags.Locale, lb => lb.Locale, StringFormatter }, - { TemplateTags.YearPublished, lb => lb.YearPublished, IntegerFormatter }, - { TemplateTags.DatePublished, lb => lb.DatePublished, DateTimeFormatter }, - { TemplateTags.DateAdded, lb => lb.DateAdded, DateTimeFormatter }, - { TemplateTags.FileDate, lb => lb.FileDate, DateTimeFormatter }, + //Don't allow formatting of Id + { TemplateTags.Id, lb => lb.AudibleProductId, v => v }, + { TemplateTags.Title, lb => lb.Title }, + { TemplateTags.TitleShort, lb => lb.Title.IndexOf(':') < 1 ? lb.Title : lb.Title.Substring(0, lb.Title.IndexOf(':')) }, + { TemplateTags.Author, lb => lb.AuthorNames }, + { TemplateTags.FirstAuthor, lb => lb.FirstAuthor }, + { TemplateTags.Narrator, lb => lb.NarratorNames }, + { TemplateTags.FirstNarrator, lb => lb.FirstNarrator }, + { TemplateTags.Series, lb => lb.SeriesName }, + { TemplateTags.SeriesNumber, lb => lb.SeriesNumber }, + { TemplateTags.Language, lb => lb.Language }, + //Don't allow formatting of LanguageShort + { TemplateTags.LanguageShort, lb =>lb.Language, getLanguageShort }, + { TemplateTags.Bitrate, lb => lb.BitRate }, + { TemplateTags.SampleRate, lb => lb.SampleRate }, + { TemplateTags.Channels, lb => lb.Channels }, + { TemplateTags.Account, lb => lb.Account }, + { TemplateTags.Locale, lb => lb.Locale }, + { TemplateTags.YearPublished, lb => lb.YearPublished }, + { TemplateTags.DatePublished, lb => lb.DatePublished }, + { TemplateTags.DateAdded, lb => lb.DateAdded }, + { TemplateTags.FileDate, lb => lb.FileDate }, }; - private static readonly List chapterPropertyTags = new() + private static readonly List chapterPropertyTags = new() { - new PropertyTagClass() + new PropertyTagCollection(caseSensative: true, StringFormatter) { - { TemplateTags.Title, lb => lb.Title, StringFormatter }, - { TemplateTags.TitleShort, lb => lb?.Title?.IndexOf(':') > 0 ? lb.Title.Substring(0, lb.Title.IndexOf(':')) : lb.Title, StringFormatter }, - { TemplateTags.Series, lb => lb.SeriesName, StringFormatter }, + { TemplateTags.Title, lb => lb.Title }, + { TemplateTags.TitleShort, lb => lb ?.Title ?.IndexOf(':') > 0 ? lb.Title.Substring(0, lb.Title.IndexOf(':')) : lb.Title }, + { TemplateTags.Series, lb => lb.SeriesName }, }, - new PropertyTagClass() + new PropertyTagCollection(caseSensative: true, StringFormatter, IntegerFormatter, DateTimeFormatter) { - { TemplateTags.ChCount, m => m.PartsTotal, IntegerFormatter }, - { TemplateTags.ChNumber, m => m.PartsPosition, IntegerFormatter }, + { TemplateTags.ChCount, m => m.PartsTotal }, + { TemplateTags.ChNumber, m => m.PartsPosition }, { TemplateTags.ChNumber0, m => m.PartsPosition.ToString("D" + ((int)Math.Log10(m.PartsTotal) + 1)) }, - { TemplateTags.ChTitle, m => m.Title, StringFormatter }, - { TemplateTags.FileDate, m => m.FileDate, DateTimeFormatter } + { TemplateTags.ChTitle, m => m.Title }, + { TemplateTags.FileDate, m => m.FileDate } } }; @@ -283,7 +286,7 @@ namespace LibationFileManager public override string Name => "Folder Template"; public override string Description => Configuration.GetDescription(nameof(Configuration.FolderTemplate)); public static string DefaultTemplate { get; } = " [<id>]"; - public static IEnumerable<TagClass> TagClass => new TagClass[] { filePropertyTags, conditionalTags }; + public static IEnumerable<TagCollection> TagClass => new TagCollection[] { filePropertyTags, conditionalTags }; public override IEnumerable<string> Errors => TemplateText?.Length >= 2 && Path.IsPathFullyQualified(TemplateText) ? base.Errors.Append(ERROR_FULL_PATH_IS_INVALID) : base.Errors; @@ -302,7 +305,7 @@ namespace LibationFileManager public override string Name => "File Template"; public override string Description => Configuration.GetDescription(nameof(Configuration.FileTemplate)); public static string DefaultTemplate { get; } = "<title> [<id>]"; - public static IEnumerable<TagClass> TagClass { get; } = new TagClass[] { filePropertyTags, conditionalTags }; + public static IEnumerable<TagCollection> TagClass { get; } = new TagCollection[] { filePropertyTags, conditionalTags }; } public class ChapterFileTemplate : Templates, ITemplate @@ -310,7 +313,7 @@ namespace LibationFileManager public override string Name => "Chapter File Template"; public override string Description => Configuration.GetDescription(nameof(Configuration.ChapterFileTemplate)); public static string DefaultTemplate { get; } = "<title> [<id>] - <ch# 0> - <ch title>"; - public static IEnumerable<TagClass> TagClass { get; } = chapterPropertyTags.Append(filePropertyTags).Append(conditionalTags); + public static IEnumerable<TagCollection> TagClass { get; } = chapterPropertyTags.Append(filePropertyTags).Append(conditionalTags); public override IEnumerable<string> Warnings => Template.TagsInUse.Any(t => t.TagName.In(TemplateTags.ChNumber.TagName, TemplateTags.ChNumber0.TagName)) @@ -323,7 +326,7 @@ namespace LibationFileManager public override string Name => "Chapter Title Template"; public override string Description => Configuration.GetDescription(nameof(Configuration.ChapterTitleTemplate)); public static string DefaultTemplate => "<ch#> - <title short>: <ch title>"; - public static IEnumerable<TagClass> TagClass { get; } = chapterPropertyTags.Append(conditionalTags); + public static IEnumerable<TagCollection> TagClass { get; } = chapterPropertyTags.Append(conditionalTags); protected override IEnumerable<string> GetTemplatePartsStrings(List<TemplatePart> parts, ReplacementCharacters replacements) => parts.Select(p => p.Value); diff --git a/Source/_Tests/FileManager.Tests/FileNamingTemplateTests.cs b/Source/_Tests/FileManager.Tests/FileNamingTemplateTests.cs index bbe4863d..6f9e70ac 100644 --- a/Source/_Tests/FileManager.Tests/FileNamingTemplateTests.cs +++ b/Source/_Tests/FileManager.Tests/FileNamingTemplateTests.cs @@ -41,21 +41,21 @@ namespace NamingTemplateTests [TestClass] public class GetPortionFilename { - PropertyTagClass<PropertyClass1> props1 = new() + PropertyTagCollection<PropertyClass1> props1 = new() { { new TemplateTag { TagName = "item1" }, i => i.Item1 }, { new TemplateTag { TagName = "item2" }, i => i.Item2 }, { new TemplateTag { TagName = "item3" }, i => i.Item3 } }; - PropertyTagClass<PropertyClass2> props2 = new() + PropertyTagCollection<PropertyClass2> props2 = new() { { new TemplateTag { TagName = "item1" }, i => i.Item1 }, { new TemplateTag { TagName = "item2" }, i => i.Item2 }, { new TemplateTag { TagName = "item3" }, i => i.Item3 }, { new TemplateTag { TagName = "item4" }, i => i.Item4 }, }; - PropertyTagClass<PropertyClass3> props3 = new() + PropertyTagCollection<PropertyClass3> props3 = new() { { new TemplateTag { TagName = "item3_1" }, i => i.Item1 }, { new TemplateTag { TagName = "item3_2" }, i => i.Item2 },