feat: add rename format registry
This commit is contained in:
1 parent
d6677b323b
commit
c0c6e0a38f
2 files changed
+282
No files matched your search
@@ -0,0 +1,146 @@
|
|||||||
|
using SongTidy.Models;
|
||||||
|
|
||||||
|
namespace SongTidy.Tests;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Contains tests for <see cref="RenameFormats"/>.
|
||||||
|
/// </summary>
|
||||||
|
[TestClass]
|
||||||
|
public sealed class RenameFormatsTests {
|
||||||
|
#region Artist-first format
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Verifies the artist-first layout when both optional fields are present.
|
||||||
|
/// </summary>
|
||||||
|
[TestMethod]
|
||||||
|
public void GeneratesArtistFirstWithArtistAndComment() {
|
||||||
|
AssertStem(RenameFormat.ArtistFirst, "Artist", "Title", "Live", "Artist - Title (Live)");
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Verifies the artist-first layout when only the artist is present.
|
||||||
|
/// </summary>
|
||||||
|
[TestMethod]
|
||||||
|
public void GeneratesArtistFirstWithArtistOnly() {
|
||||||
|
AssertStem(RenameFormat.ArtistFirst, "Artist", "Title", string.Empty, "Artist - Title");
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Verifies the artist-first layout when only the comment is present.
|
||||||
|
/// </summary>
|
||||||
|
[TestMethod]
|
||||||
|
public void GeneratesArtistFirstWithCommentOnly() {
|
||||||
|
AssertStem(RenameFormat.ArtistFirst, string.Empty, "Title", "Live", "Title (Live)");
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Verifies the artist-first layout when neither optional field is present.
|
||||||
|
/// </summary>
|
||||||
|
[TestMethod]
|
||||||
|
public void GeneratesArtistFirstWithTitleOnly() {
|
||||||
|
AssertStem(RenameFormat.ArtistFirst, string.Empty, "Title", string.Empty, "Title");
|
||||||
|
}
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
#region Title-first format
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Verifies the title-first layout when both optional fields are present.
|
||||||
|
/// </summary>
|
||||||
|
[TestMethod]
|
||||||
|
public void GeneratesTitleFirstWithArtistAndComment() {
|
||||||
|
AssertStem(RenameFormat.TitleFirst, "Artist", "Title", "Live", "Title (Live) - Artist");
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Verifies the title-first layout when only the artist is present.
|
||||||
|
/// </summary>
|
||||||
|
[TestMethod]
|
||||||
|
public void GeneratesTitleFirstWithArtistOnly() {
|
||||||
|
AssertStem(RenameFormat.TitleFirst, "Artist", "Title", string.Empty, "Title - Artist");
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Verifies the title-first layout when only the comment is present.
|
||||||
|
/// </summary>
|
||||||
|
[TestMethod]
|
||||||
|
public void GeneratesTitleFirstWithCommentOnly() {
|
||||||
|
AssertStem(RenameFormat.TitleFirst, string.Empty, "Title", "Live", "Title (Live)");
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Verifies the title-first layout when neither optional field is present.
|
||||||
|
/// </summary>
|
||||||
|
[TestMethod]
|
||||||
|
public void GeneratesTitleFirstWithTitleOnly() {
|
||||||
|
AssertStem(RenameFormat.TitleFirst, string.Empty, "Title", string.Empty, "Title");
|
||||||
|
}
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
#region Registry
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Verifies that every enum member has a registered format definition.
|
||||||
|
/// </summary>
|
||||||
|
[TestMethod]
|
||||||
|
public void RegistersEveryFormat() {
|
||||||
|
foreach (RenameFormat format in Enum.GetValues<RenameFormat>()) {
|
||||||
|
Assert.IsTrue(
|
||||||
|
RenameFormats.Definitions.ContainsKey(format),
|
||||||
|
$"Missing definition for {format}.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Verifies that an unregistered enum value is rejected.
|
||||||
|
/// </summary>
|
||||||
|
[TestMethod]
|
||||||
|
public void ThrowsForUnregisteredFormat() {
|
||||||
|
Assert.Throws<ArgumentOutOfRangeException>(
|
||||||
|
() => RenameFormats.GenerateStem((RenameFormat)999, "Artist", "Title", "Live"));
|
||||||
|
}
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
#region Edge cases
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Verifies that whitespace-only optional fields are treated as missing.
|
||||||
|
/// </summary>
|
||||||
|
[TestMethod]
|
||||||
|
public void TreatsWhitespaceOnlyOptionalFieldsAsMissing() {
|
||||||
|
AssertStem(RenameFormat.ArtistFirst, " ", "Title", "\t", "Title");
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Verifies that a null title is rejected.
|
||||||
|
/// </summary>
|
||||||
|
[TestMethod]
|
||||||
|
public void ThrowsForNullTitle() {
|
||||||
|
Assert.Throws<ArgumentNullException>(
|
||||||
|
() => RenameFormats.GenerateStem(RenameFormat.ArtistFirst, "Artist", null!, "Live"));
|
||||||
|
}
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Generates a file name stem and asserts the result.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="format">The format to apply.</param>
|
||||||
|
/// <param name="artist">The artist value.</param>
|
||||||
|
/// <param name="title">The title value.</param>
|
||||||
|
/// <param name="comment">The comment value.</param>
|
||||||
|
/// <param name="expected">The expected generated stem.</param>
|
||||||
|
private static void AssertStem(
|
||||||
|
RenameFormat format,
|
||||||
|
string artist,
|
||||||
|
string title,
|
||||||
|
string comment,
|
||||||
|
string expected) {
|
||||||
|
string actual = RenameFormats.GenerateStem(format, artist, title, comment);
|
||||||
|
|
||||||
|
Assert.AreEqual(expected, actual, $"Unexpected stem for {format}.");
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,136 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Collections.ObjectModel;
|
||||||
|
using System.Xml.Serialization;
|
||||||
|
|
||||||
|
namespace SongTidy.Models;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Identifies a supported layout for generated target file names.
|
||||||
|
/// </summary>
|
||||||
|
public enum RenameFormat {
|
||||||
|
/// <summary>
|
||||||
|
/// Places the artist before the title.
|
||||||
|
/// </summary>
|
||||||
|
[XmlEnum("ArtistFirst")]
|
||||||
|
ArtistFirst,
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Places the title before the artist.
|
||||||
|
/// </summary>
|
||||||
|
[XmlEnum("TitleFirst")]
|
||||||
|
TitleFirst,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Represents the artist, title, and comment values used to build a target file name.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="Artist">The artist value, which may be empty.</param>
|
||||||
|
/// <param name="Title">The title value.</param>
|
||||||
|
/// <param name="Comment">The comment value, which may be empty.</param>
|
||||||
|
public sealed record RenameFields(string Artist, string Title, string Comment);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Formats name fields into a target file name stem.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="fields">The fields to format.</param>
|
||||||
|
/// <returns>The formatted file name stem.</returns>
|
||||||
|
public delegate string RenameFormatFormatter(RenameFields fields);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Defines the formatter functions used to generate a target file name for one format.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="ArtistAndComment">The formatter used when both optional fields are present.</param>
|
||||||
|
/// <param name="ArtistOnly">The formatter used when only the artist is present.</param>
|
||||||
|
/// <param name="CommentOnly">The formatter used when only the comment is present.</param>
|
||||||
|
/// <param name="Plain">The formatter used when neither optional field is present.</param>
|
||||||
|
public sealed record RenameFormatDefinition(
|
||||||
|
RenameFormatFormatter ArtistAndComment,
|
||||||
|
RenameFormatFormatter ArtistOnly,
|
||||||
|
RenameFormatFormatter CommentOnly,
|
||||||
|
RenameFormatFormatter Plain);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Provides the registered target file name formats and generates file name stems from them.
|
||||||
|
/// </summary>
|
||||||
|
public static class RenameFormats {
|
||||||
|
#region Registry
|
||||||
|
|
||||||
|
private static readonly ReadOnlyDictionary<RenameFormat, RenameFormatDefinition> s_definitions =
|
||||||
|
new ReadOnlyDictionary<RenameFormat, RenameFormatDefinition>(
|
||||||
|
new Dictionary<RenameFormat, RenameFormatDefinition> {
|
||||||
|
[RenameFormat.ArtistFirst] = new RenameFormatDefinition(
|
||||||
|
ArtistAndComment: static fields => $"{fields.Artist} - {fields.Title} ({fields.Comment})",
|
||||||
|
ArtistOnly: static fields => $"{fields.Artist} - {fields.Title}",
|
||||||
|
CommentOnly: static fields => $"{fields.Title} ({fields.Comment})",
|
||||||
|
Plain: static fields => fields.Title),
|
||||||
|
[RenameFormat.TitleFirst] = new RenameFormatDefinition(
|
||||||
|
ArtistAndComment: static fields => $"{fields.Title} ({fields.Comment}) - {fields.Artist}",
|
||||||
|
ArtistOnly: static fields => $"{fields.Title} - {fields.Artist}",
|
||||||
|
CommentOnly: static fields => $"{fields.Title} ({fields.Comment})",
|
||||||
|
Plain: static fields => fields.Title),
|
||||||
|
});
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the registered format definitions keyed by format.
|
||||||
|
/// </summary>
|
||||||
|
public static IReadOnlyDictionary<RenameFormat, RenameFormatDefinition> Definitions => s_definitions;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Looks up the definition registered for a format.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="format">The format to look up.</param>
|
||||||
|
/// <returns>The registered definition.</returns>
|
||||||
|
/// <exception cref="ArgumentOutOfRangeException">The format is not registered.</exception>
|
||||||
|
private static RenameFormatDefinition GetDefinition(RenameFormat format) {
|
||||||
|
if (!s_definitions.TryGetValue(format, out RenameFormatDefinition? definition)) {
|
||||||
|
throw new ArgumentOutOfRangeException(nameof(format), format, "The format is not registered.");
|
||||||
|
}
|
||||||
|
|
||||||
|
return definition;
|
||||||
|
}
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
#region Generation
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Generates the target file name stem for the supplied fields and format.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="format">The naming format to apply.</param>
|
||||||
|
/// <param name="artist">The artist value, which may be empty.</param>
|
||||||
|
/// <param name="title">The title value, which is always inserted.</param>
|
||||||
|
/// <param name="comment">The comment value, which may be empty.</param>
|
||||||
|
/// <returns>
|
||||||
|
/// The generated file name stem. The caller appends the original extension because
|
||||||
|
/// extensions are not part of the format definition.
|
||||||
|
/// </returns>
|
||||||
|
/// <remarks>
|
||||||
|
/// Whitespace-only optional fields are treated as missing.
|
||||||
|
/// </remarks>
|
||||||
|
public static string GenerateStem(RenameFormat format, string artist, string title, string comment) {
|
||||||
|
ArgumentNullException.ThrowIfNull(artist);
|
||||||
|
ArgumentNullException.ThrowIfNull(title);
|
||||||
|
ArgumentNullException.ThrowIfNull(comment);
|
||||||
|
|
||||||
|
RenameFormatDefinition definition = GetDefinition(format);
|
||||||
|
bool hasArtist = !string.IsNullOrWhiteSpace(artist);
|
||||||
|
bool hasComment = !string.IsNullOrWhiteSpace(comment);
|
||||||
|
RenameFormatFormatter formatter;
|
||||||
|
|
||||||
|
// Select the formatter that matches which optional fields are present.
|
||||||
|
if (hasArtist && hasComment) {
|
||||||
|
formatter = definition.ArtistAndComment;
|
||||||
|
} else if (hasArtist) {
|
||||||
|
formatter = definition.ArtistOnly;
|
||||||
|
} else if (hasComment) {
|
||||||
|
formatter = definition.CommentOnly;
|
||||||
|
} else {
|
||||||
|
formatter = definition.Plain;
|
||||||
|
}
|
||||||
|
|
||||||
|
return formatter(new RenameFields(artist, title, comment));
|
||||||
|
}
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
}
|
||||||
Reference in new issue
Block a user