diff --git a/Semantics.Paths/CompatibilitySuppressions.xml b/Semantics.Paths/CompatibilitySuppressions.xml
index fd22e37..ec85be7 100644
--- a/Semantics.Paths/CompatibilitySuppressions.xml
+++ b/Semantics.Paths/CompatibilitySuppressions.xml
@@ -553,6 +553,36 @@
lib/net7.0/ktsu.Semantics.Paths.dll
lib/net8.0/ktsu.Semantics.Paths.dll
+
+ CP0016
+ M:ktsu.Semantics.Paths.DirectoryName.Equals(ktsu.Semantics.Paths.DirectoryName):[T:System.Runtime.CompilerServices.NullableContextAttribute]
+ lib/net7.0/ktsu.Semantics.Paths.dll
+ lib/net8.0/ktsu.Semantics.Paths.dll
+
+
+ CP0016
+ M:ktsu.Semantics.Paths.DirectoryName.Equals(ktsu.Semantics.Strings.SemanticString{ktsu.Semantics.Paths.DirectoryName})$0:[T:System.Runtime.CompilerServices.NullableAttribute]
+ lib/net7.0/ktsu.Semantics.Paths.dll
+ lib/net8.0/ktsu.Semantics.Paths.dll
+
+
+ CP0016
+ M:ktsu.Semantics.Paths.DirectoryName.Equals(System.Object):[T:System.Runtime.CompilerServices.NullableContextAttribute]
+ lib/net7.0/ktsu.Semantics.Paths.dll
+ lib/net8.0/ktsu.Semantics.Paths.dll
+
+
+ CP0016
+ M:ktsu.Semantics.Paths.DirectoryName.op_Equality(ktsu.Semantics.Paths.DirectoryName,ktsu.Semantics.Paths.DirectoryName):[T:System.Runtime.CompilerServices.NullableContextAttribute]
+ lib/net7.0/ktsu.Semantics.Paths.dll
+ lib/net8.0/ktsu.Semantics.Paths.dll
+
+
+ CP0016
+ M:ktsu.Semantics.Paths.DirectoryName.op_Inequality(ktsu.Semantics.Paths.DirectoryName,ktsu.Semantics.Paths.DirectoryName):[T:System.Runtime.CompilerServices.NullableContextAttribute]
+ lib/net7.0/ktsu.Semantics.Paths.dll
+ lib/net8.0/ktsu.Semantics.Paths.dll
+
CP0016
M:ktsu.Semantics.Paths.DirectoryPath.Equals(ktsu.Semantics.Paths.DirectoryPath):[T:System.Runtime.CompilerServices.NullableContextAttribute]
@@ -1363,6 +1393,18 @@
lib/net7.0/ktsu.Semantics.Paths.dll
lib/net8.0/ktsu.Semantics.Paths.dll
+
+ CP0016
+ T:ktsu.Semantics.Paths.DirectoryName:[T:System.Runtime.CompilerServices.NullableAttribute]
+ lib/net7.0/ktsu.Semantics.Paths.dll
+ lib/net8.0/ktsu.Semantics.Paths.dll
+
+
+ CP0016
+ T:ktsu.Semantics.Paths.DirectoryName:[T:System.Runtime.CompilerServices.NullableContextAttribute]
+ lib/net7.0/ktsu.Semantics.Paths.dll
+ lib/net8.0/ktsu.Semantics.Paths.dll
+
CP0016
T:ktsu.Semantics.Paths.DirectoryPath:[T:System.Runtime.CompilerServices.NullableAttribute]
@@ -1417,6 +1459,18 @@
lib/net7.0/ktsu.Semantics.Paths.dll
lib/net8.0/ktsu.Semantics.Paths.dll
+
+ CP0016
+ T:ktsu.Semantics.Paths.IsValidDirectoryNameAttribute:[T:System.Runtime.CompilerServices.NullableAttribute]
+ lib/net7.0/ktsu.Semantics.Paths.dll
+ lib/net8.0/ktsu.Semantics.Paths.dll
+
+
+ CP0016
+ T:ktsu.Semantics.Paths.IsValidDirectoryNameAttribute:[T:System.Runtime.CompilerServices.NullableContextAttribute]
+ lib/net7.0/ktsu.Semantics.Paths.dll
+ lib/net8.0/ktsu.Semantics.Paths.dll
+
CP0016
T:ktsu.Semantics.Paths.IsValidFileNameAttribute:[T:System.Runtime.CompilerServices.NullableAttribute]
diff --git a/Semantics.Paths/Implementations/AbsoluteDirectoryPath.cs b/Semantics.Paths/Implementations/AbsoluteDirectoryPath.cs
index d8e1223..3300d3a 100644
--- a/Semantics.Paths/Implementations/AbsoluteDirectoryPath.cs
+++ b/Semantics.Paths/Implementations/AbsoluteDirectoryPath.cs
@@ -128,6 +128,22 @@ protected override IDirectoryPath CreateDirectoryPath(string directoryPath) =>
return AbsoluteFilePath.Create(combinedPath);
}
+ ///
+ /// Combines an absolute directory path with a directory name using the '/' operator.
+ ///
+ /// The base absolute directory path.
+ /// The directory name to append.
+ /// A new representing the combined path.
+ [SuppressMessage("Usage", "CA2225:Operator overloads have named alternates", Justification = "Path combination is the semantic meaning, not mathematical division")]
+ public static AbsoluteDirectoryPath operator /(AbsoluteDirectoryPath left, DirectoryName right)
+ {
+ Guard.NotNull(left);
+ Guard.NotNull(right);
+
+ string combinedPath = PooledStringBuilder.CombinePaths(left.WeakString, right.WeakString);
+ return Create(combinedPath);
+ }
+
///
/// Combines an absolute directory path with a file name using the '/' operator.
///
diff --git a/Semantics.Paths/Implementations/DirectoryPath.cs b/Semantics.Paths/Implementations/DirectoryPath.cs
index b03fc86..50d5c30 100644
--- a/Semantics.Paths/Implementations/DirectoryPath.cs
+++ b/Semantics.Paths/Implementations/DirectoryPath.cs
@@ -107,6 +107,27 @@ public RelativeDirectoryPath AsRelative(AbsoluteDirectoryPath baseDirectory)
return FilePath.Create(combinedPath);
}
+ ///
+ /// Combines a directory path with a directory name using the '/' operator.
+ ///
+ /// The base directory path.
+ /// The directory name to append.
+ /// A new representing the combined path.
+ [SuppressMessage("Usage", "CA2225:Operator overloads have named alternates", Justification = "Path combination is the semantic meaning, not mathematical division")]
+ public static DirectoryPath operator /(DirectoryPath left, DirectoryName right)
+ {
+#if NET6_0_OR_GREATER
+ Guard.NotNull(left);
+ Guard.NotNull(right);
+#else
+ ArgumentNullExceptionPolyfill.ThrowIfNull(left);
+ ArgumentNullExceptionPolyfill.ThrowIfNull(right);
+#endif
+
+ string combinedPath = Path.Combine(left.WeakString, right.WeakString);
+ return Create(combinedPath);
+ }
+
///
/// Combines a directory path with a file name using the '/' operator.
///
diff --git a/Semantics.Paths/Implementations/RelativeDirectoryPath.cs b/Semantics.Paths/Implementations/RelativeDirectoryPath.cs
index 14e1cfd..c3ffa1e 100644
--- a/Semantics.Paths/Implementations/RelativeDirectoryPath.cs
+++ b/Semantics.Paths/Implementations/RelativeDirectoryPath.cs
@@ -13,7 +13,7 @@ namespace ktsu.Semantics.Paths;
///
/// Represents a relative directory path
///
-[IsPath, IsRelativePath, IsDirectoryPath]
+[IsPath, IsRelativePath]
public sealed record RelativeDirectoryPath : SemanticDirectoryPath, IRelativeDirectoryPath
{
// Cache for expensive parent directory computation
@@ -26,13 +26,13 @@ public sealed record RelativeDirectoryPath : SemanticDirectoryPath _cachedParent ??= Create(Path.GetDirectoryName(WeakString) ?? "");
// Cache for directory name
- private FileName? _cachedName;
+ private DirectoryName? _cachedName;
///
/// Gets the name of this directory (the last component of the path).
///
- /// A representing just the directory name.
- public FileName Name => _cachedName ??= FileName.Create(Path.GetFileName(WeakString) ?? "");
+ /// A representing just the directory name.
+ public DirectoryName Name => _cachedName ??= DirectoryName.Create(Path.GetFileName(WeakString) ?? "");
// Cache for depth calculation
private int? _cachedDepth;
diff --git a/Semantics.Paths/Implementations/RelativeFilePath.cs b/Semantics.Paths/Implementations/RelativeFilePath.cs
index b5e80f7..9098d6c 100644
--- a/Semantics.Paths/Implementations/RelativeFilePath.cs
+++ b/Semantics.Paths/Implementations/RelativeFilePath.cs
@@ -7,7 +7,7 @@ namespace ktsu.Semantics.Paths;
///
/// Represents a relative file path
///
-[IsPath, IsRelativePath, IsFilePath]
+[IsPath, IsRelativePath]
public sealed record RelativeFilePath : SemanticFilePath, IRelativeFilePath
{
// Cache for expensive directory path computation
diff --git a/Semantics.Paths/Interfaces/IDirectoryName.cs b/Semantics.Paths/Interfaces/IDirectoryName.cs
new file mode 100644
index 0000000..461c17e
--- /dev/null
+++ b/Semantics.Paths/Interfaces/IDirectoryName.cs
@@ -0,0 +1,12 @@
+// Copyright (c) ktsu.dev
+// All rights reserved.
+// Licensed under the MIT license.
+
+namespace ktsu.Semantics.Paths;
+
+///
+/// Interface for directory names
+///
+public interface IDirectoryName
+{
+}
diff --git a/Semantics.Paths/Primitives/DirectoryName.cs b/Semantics.Paths/Primitives/DirectoryName.cs
new file mode 100644
index 0000000..53ba4da
--- /dev/null
+++ b/Semantics.Paths/Primitives/DirectoryName.cs
@@ -0,0 +1,15 @@
+// Copyright (c) ktsu.dev
+// All rights reserved.
+// Licensed under the MIT license.
+
+namespace ktsu.Semantics.Paths;
+
+using ktsu.Semantics.Strings;
+
+///
+/// Represents a directory name
+///
+[IsValidDirectoryName]
+public sealed record DirectoryName : SemanticString, IDirectoryName
+{
+}
diff --git a/Semantics.Paths/Primitives/FileName.cs b/Semantics.Paths/Primitives/FileName.cs
index d435257..6e919a7 100644
--- a/Semantics.Paths/Primitives/FileName.cs
+++ b/Semantics.Paths/Primitives/FileName.cs
@@ -9,7 +9,7 @@ namespace ktsu.Semantics.Paths;
///
/// Represents a filename (without directory path)
///
-[IsFileName]
+[IsValidFileName]
public sealed record FileName : SemanticString, IFileName
{
}
diff --git a/Semantics.Paths/Validation/Attributes/Path/IsFileNameAttribute.cs b/Semantics.Paths/Validation/Attributes/Path/IsFileNameAttribute.cs
deleted file mode 100644
index 8b9e733..0000000
--- a/Semantics.Paths/Validation/Attributes/Path/IsFileNameAttribute.cs
+++ /dev/null
@@ -1,55 +0,0 @@
-// Copyright (c) ktsu.dev
-// All rights reserved.
-// Licensed under the MIT license.
-
-namespace ktsu.Semantics.Paths;
-
-using System;
-using System.IO;
-using System.Linq;
-using ktsu.Semantics.Strings;
-
-///
-/// Validates that a string represents a valid filename (no invalid filename characters, not a directory)
-///
-[AttributeUsage(AttributeTargets.Class, AllowMultiple = false, Inherited = true)]
-public sealed class IsFileNameAttribute : NativeSemanticStringValidationAttribute
-{
- ///
- /// Creates the validation adapter for filename validation.
- ///
- /// A validation adapter for filenames
- protected override ValidationAdapter CreateValidator() => new FileNameValidator();
-
- ///
- /// validation adapter for filenames.
- ///
- private sealed class FileNameValidator : ValidationAdapter
- {
- ///
- /// Validates that a string represents a valid filename.
- ///
- /// The string value to validate
- /// A validation result indicating success or failure
- ///
- /// A valid filename must meet the following criteria:
- ///
- /// - Must not contain any characters from
- /// - Must not be an existing directory path
- /// - Empty or null strings are considered valid
- ///
- ///
- protected override ValidationResult ValidateValue(string value)
- {
- if (string.IsNullOrEmpty(value))
- {
- return ValidationResult.Success();
- }
-
- bool isValidFileName = !Directory.Exists(value) && !value.Intersect(Path.GetInvalidFileNameChars()).Any();
- return isValidFileName
- ? ValidationResult.Success()
- : ValidationResult.Failure("The filename contains invalid characters or is an existing directory.");
- }
- }
-}
diff --git a/Semantics.Paths/Validation/Attributes/Path/IsValidDirectoryNameAttribute.cs b/Semantics.Paths/Validation/Attributes/Path/IsValidDirectoryNameAttribute.cs
new file mode 100644
index 0000000..68ef27d
--- /dev/null
+++ b/Semantics.Paths/Validation/Attributes/Path/IsValidDirectoryNameAttribute.cs
@@ -0,0 +1,63 @@
+// Copyright (c) ktsu.dev
+// All rights reserved.
+// Licensed under the MIT license.
+
+namespace ktsu.Semantics.Paths;
+
+using System;
+using System.IO;
+using ktsu.Semantics.Strings;
+
+///
+/// Validates that a path string contains valid directory name characters (no path separators) using span semantics.
+///
+[AttributeUsage(AttributeTargets.Class, AllowMultiple = false, Inherited = true)]
+public sealed class IsValidDirectoryNameAttribute : NativeSemanticStringValidationAttribute
+{
+ ///
+ /// Creates the validation adapter for valid directory name validation.
+ ///
+ /// A validation adapter for valid directory name strings
+ protected override ValidationAdapter CreateValidator() => new ValidDirectoryNameValidator();
+
+ ///
+ /// validation adapter for valid directory name strings.
+ ///
+ private sealed class ValidDirectoryNameValidator : ValidationAdapter
+ {
+ private static readonly char[] InvalidFileNameChars = Path.GetInvalidFileNameChars();
+
+ ///
+ /// Validates that a directory name string contains only valid characters and no path separators.
+ ///
+ /// The string value to validate
+ /// A validation result indicating success or failure
+ protected override ValidationResult ValidateValue(string value)
+ {
+ if (string.IsNullOrEmpty(value))
+ {
+ return ValidationResult.Success();
+ }
+
+ // Check for invalid filename characters
+#if NETSTANDARD2_0
+ bool hasInvalidChars = value.IndexOfAny(InvalidFileNameChars) != -1;
+#else
+ ReadOnlySpan valueSpan = value.AsSpan();
+ bool hasInvalidChars = valueSpan.IndexOfAny(InvalidFileNameChars) != -1;
+#endif
+ if (hasInvalidChars)
+ {
+ return ValidationResult.Failure("The directory name contains invalid characters.");
+ }
+
+ // Check for path separators (directory names shouldn't contain path separators)
+ if (value.Contains(Path.DirectorySeparatorChar) || value.Contains(Path.AltDirectorySeparatorChar))
+ {
+ return ValidationResult.Failure("The directory name contains path separators.");
+ }
+
+ return ValidationResult.Success();
+ }
+ }
+}
diff --git a/Semantics.Test/PathValidationAttributeTests.cs b/Semantics.Test/PathValidationAttributeTests.cs
index f3ee818..4ee6b31 100644
--- a/Semantics.Test/PathValidationAttributeTests.cs
+++ b/Semantics.Test/PathValidationAttributeTests.cs
@@ -326,6 +326,86 @@ public void IsFileNameAttribute_FileNameWithValidSpecialChars_ShouldPass()
// Act & Assert
Assert.IsTrue(specialCharsName.IsValid());
}
+
+ [TestMethod]
+ public void IsValidDirectoryNameAttribute_ValidDirectoryName_ShouldPass()
+ {
+ // Arrange
+ TestDirectoryName validName = TestDirectoryName.Create("MyFolder");
+
+ // Act & Assert
+ Assert.IsTrue(validName.IsValid());
+ }
+
+ [TestMethod]
+ public void IsValidDirectoryNameAttribute_DirectoryNameWithSpaces_ShouldPass()
+ {
+ // Arrange
+ TestDirectoryName nameWithSpaces = TestDirectoryName.Create("My Folder Name");
+
+ // Act & Assert
+ Assert.IsTrue(nameWithSpaces.IsValid());
+ }
+
+ [TestMethod]
+ public void IsValidDirectoryNameAttribute_DirectoryNameWithValidSpecialChars_ShouldPass()
+ {
+ // Arrange - using valid special chars
+ TestDirectoryName specialCharsName = TestDirectoryName.Create("valid-folder_name (1)");
+
+ // Act & Assert
+ Assert.IsTrue(specialCharsName.IsValid());
+ }
+
+ [TestMethod]
+ public void IsValidDirectoryNameAttribute_EmptyDirectoryName_ShouldPass()
+ {
+ // Arrange
+ TestDirectoryName emptyName = TestDirectoryName.Create("");
+
+ // Act & Assert
+ Assert.IsTrue(emptyName.IsValid());
+ }
+
+ [TestMethod]
+ public void IsValidDirectoryNameAttribute_DirectoryNameWithPathSeparator_ShouldFail()
+ {
+ // Arrange & Act & Assert - directory names shouldn't contain path separators
+ Assert.ThrowsExactly(() =>
+ TestDirectoryName.Create("folder\\subfolder"));
+ }
+
+ [TestMethod]
+ public void IsValidDirectoryNameAttribute_DirectoryNameWithForwardSlash_ShouldFail()
+ {
+ // Arrange & Act & Assert - directory names shouldn't contain forward slashes
+ Assert.ThrowsExactly(() =>
+ TestDirectoryName.Create("folder/subfolder"));
+ }
+
+ [TestMethod]
+ public void IsValidDirectoryNameAttribute_DirectoryNameWithInvalidChars_ShouldFail()
+ {
+ // Arrange & Act & Assert - test with invalid filename characters
+ Assert.ThrowsExactly(() =>
+ TestDirectoryName.Create("invalid<>name"));
+ }
+
+ [TestMethod]
+ public void IsValidDirectoryNameAttribute_DirectoryNameWithColon_ShouldFail()
+ {
+ // Arrange & Act & Assert - colon is invalid in directory names (except drive letters)
+ Assert.ThrowsExactly(() =>
+ TestDirectoryName.Create("invalid:name"));
+ }
+
+ [TestMethod]
+ public void IsValidDirectoryNameAttribute_DirectoryNameWithPipe_ShouldFail()
+ {
+ // Arrange & Act & Assert - pipe is an invalid character
+ Assert.ThrowsExactly(() =>
+ TestDirectoryName.Create("invalid|name"));
+ }
}
// Test record types for validation attribute testing
@@ -338,7 +418,7 @@ public record TestAbsolutePath : SemanticString { }
[IsRelativePath]
public record TestRelativePath : SemanticString { }
-[IsFileName]
+[IsValidFileName]
public record TestFileName : SemanticString { }
[IsDirectoryPath]
@@ -352,3 +432,6 @@ public record TestExistingPath : SemanticString { }
[IsExtension]
public record TestExtension : SemanticString { }
+
+[IsValidDirectoryName]
+public record TestDirectoryName : SemanticString { }
diff --git a/Semantics.Test/Paths/DirectoryNameTests.cs b/Semantics.Test/Paths/DirectoryNameTests.cs
new file mode 100644
index 0000000..4cc923b
--- /dev/null
+++ b/Semantics.Test/Paths/DirectoryNameTests.cs
@@ -0,0 +1,270 @@
+// Copyright (c) ktsu.dev
+// All rights reserved.
+// Licensed under the MIT license.
+
+namespace ktsu.Semantics.Test.Paths;
+
+using System.Collections.Generic;
+using ktsu.Semantics.Paths;
+using Microsoft.VisualStudio.TestTools.UnitTesting;
+
+[TestClass]
+public class DirectoryNameTests
+{
+ [TestMethod]
+ public void DirectoryName_Create_WithValidName_Succeeds()
+ {
+ // Test creating DirectoryName with valid name
+ DirectoryName name = DirectoryName.Create("MyFolder");
+
+ Assert.IsNotNull(name);
+ Assert.AreEqual("MyFolder", name.WeakString);
+ }
+
+ [TestMethod]
+ public void DirectoryName_Create_WithSpaces_Succeeds()
+ {
+ // Test creating DirectoryName with spaces
+ DirectoryName name = DirectoryName.Create("My Folder Name");
+
+ Assert.IsNotNull(name);
+ Assert.AreEqual("My Folder Name", name.WeakString);
+ }
+
+ [TestMethod]
+ public void DirectoryName_Create_WithSpecialCharacters_Succeeds()
+ {
+ // Test creating DirectoryName with valid special characters
+ DirectoryName name = DirectoryName.Create("folder-name_v2 (beta)");
+
+ Assert.IsNotNull(name);
+ Assert.AreEqual("folder-name_v2 (beta)", name.WeakString);
+ }
+
+ [TestMethod]
+ public void DirectoryName_Create_WithPathSeparator_ThrowsException()
+ {
+ // Test that DirectoryName rejects path separators
+ Assert.ThrowsExactly(() =>
+ DirectoryName.Create("folder\\subfolder"));
+ }
+
+ [TestMethod]
+ public void DirectoryName_Create_WithForwardSlash_ThrowsException()
+ {
+ // Test that DirectoryName rejects forward slashes
+ Assert.ThrowsExactly(() =>
+ DirectoryName.Create("folder/subfolder"));
+ }
+
+ [TestMethod]
+ public void DirectoryName_Create_WithInvalidCharacters_ThrowsException()
+ {
+ // Test that DirectoryName rejects invalid filename characters
+ Assert.ThrowsExactly(() =>
+ DirectoryName.Create("invalid<>name"));
+
+ Assert.ThrowsExactly(() =>
+ DirectoryName.Create("invalid:name"));
+
+ Assert.ThrowsExactly(() =>
+ DirectoryName.Create("invalid|name"));
+
+ Assert.ThrowsExactly(() =>
+ DirectoryName.Create("invalid\"name"));
+ }
+
+ [TestMethod]
+ public void DirectoryName_Create_WithEmptyString_Succeeds()
+ {
+ // Test that empty DirectoryName is valid
+ DirectoryName name = DirectoryName.Create("");
+
+ Assert.IsNotNull(name);
+ Assert.AreEqual("", name.WeakString);
+ }
+
+ [TestMethod]
+ public void DirectoryName_TryCreate_WithValidName_ReturnsTrue()
+ {
+ // Test TryCreate with valid directory name
+ bool success = DirectoryName.TryCreate("ValidFolder", out DirectoryName? result);
+
+ Assert.IsTrue(success);
+ Assert.IsNotNull(result);
+ Assert.AreEqual("ValidFolder", result.WeakString);
+ }
+
+ [TestMethod]
+ public void DirectoryName_TryCreate_WithInvalidName_ReturnsFalse()
+ {
+ // Test TryCreate with invalid directory name
+ bool success = DirectoryName.TryCreate("invalid\\name", out DirectoryName? result);
+
+ Assert.IsFalse(success);
+ Assert.IsNull(result);
+ }
+
+ [TestMethod]
+ public void DirectoryName_ExplicitCast_FromString_WorksCorrectly()
+ {
+ // Test explicit cast from string
+ DirectoryName name = DirectoryName.Create("MyFolder");
+
+ Assert.IsNotNull(name);
+ Assert.AreEqual("MyFolder", name.WeakString);
+ }
+
+ [TestMethod]
+ public void DirectoryName_ImplementsIDirectoryName_Interface()
+ {
+ // Test that DirectoryName implements IDirectoryName
+ DirectoryName name = DirectoryName.Create("TestFolder");
+ IDirectoryName interfaceReference = name;
+
+ Assert.IsNotNull(interfaceReference);
+ Assert.IsInstanceOfType(name);
+ }
+
+ [TestMethod]
+ public void DirectoryName_ImplementsISemanticString_Interface()
+ {
+ // Test that DirectoryName implements ISemanticString
+ DirectoryName name = DirectoryName.Create("TestFolder");
+ DirectoryName interfaceReference = name;
+
+ Assert.IsNotNull(interfaceReference);
+ Assert.AreEqual("TestFolder", interfaceReference.WeakString);
+ }
+
+ [TestMethod]
+ public void DirectoryName_Equality_WorksCorrectly()
+ {
+ // Test equality comparison
+ DirectoryName name1 = DirectoryName.Create("MyFolder");
+ DirectoryName name2 = DirectoryName.Create("MyFolder");
+ DirectoryName name3 = DirectoryName.Create("OtherFolder");
+
+ Assert.AreEqual(name1, name2);
+ Assert.AreNotEqual(name1, name3);
+ }
+
+ [TestMethod]
+ public void DirectoryName_GetHashCode_ConsistentForEqualValues()
+ {
+ // Test that hash codes are consistent
+ DirectoryName name1 = DirectoryName.Create("MyFolder");
+ DirectoryName name2 = DirectoryName.Create("MyFolder");
+
+ Assert.AreEqual(name1.GetHashCode(), name2.GetHashCode());
+ }
+
+ [TestMethod]
+ public void DirectoryName_ToString_ReturnsWeakString()
+ {
+ // Test ToString method
+ DirectoryName name = DirectoryName.Create("MyFolder");
+
+ string result = name.ToString();
+
+ Assert.AreEqual("MyFolder", result);
+ }
+
+ [TestMethod]
+ public void DirectoryName_UsedInDictionary_WorksCorrectly()
+ {
+ // Test that DirectoryName can be used as dictionary key
+ Dictionary dict = [];
+ DirectoryName key = DirectoryName.Create("MyFolder");
+
+ dict[key] = "some value";
+
+ Assert.AreEqual("some value", dict[key]);
+ Assert.IsTrue(dict.ContainsKey(DirectoryName.Create("MyFolder")));
+ }
+
+ [TestMethod]
+ public void DirectoryName_CombineWithAbsoluteDirectoryPath_CreatesValidPath()
+ {
+ // Test combining DirectoryName with AbsoluteDirectoryPath
+ AbsoluteDirectoryPath basePath = AbsoluteDirectoryPath.Create(@"C:\projects");
+ DirectoryName subDir = DirectoryName.Create("myapp");
+
+ AbsoluteDirectoryPath result = basePath / subDir;
+
+ Assert.IsNotNull(result);
+ Assert.Contains("myapp", result.WeakString);
+ }
+
+ [TestMethod]
+ public void DirectoryName_CombineWithDirectoryPath_CreatesValidPath()
+ {
+ // Test combining DirectoryName with DirectoryPath
+ DirectoryPath basePath = DirectoryPath.Create("projects");
+ DirectoryName subDir = DirectoryName.Create("myapp");
+
+ DirectoryPath result = basePath / subDir;
+
+ Assert.IsNotNull(result);
+ Assert.Contains("myapp", result.WeakString);
+ }
+
+ [TestMethod]
+ public void DirectoryName_MultipleInChain_CreatesDeepPath()
+ {
+ // Test chaining multiple DirectoryName combinations
+ DirectoryPath basePath = DirectoryPath.Create("root");
+ DirectoryName dir1 = DirectoryName.Create("level1");
+ DirectoryName dir2 = DirectoryName.Create("level2");
+ DirectoryName dir3 = DirectoryName.Create("level3");
+
+ DirectoryPath result = basePath / dir1 / dir2 / dir3;
+
+ Assert.IsNotNull(result);
+ Assert.Contains("root", result.WeakString);
+ Assert.Contains("level1", result.WeakString);
+ Assert.Contains("level2", result.WeakString);
+ Assert.Contains("level3", result.WeakString);
+ }
+
+ [TestMethod]
+ public void DirectoryName_WithUnicodeCharacters_WorksCorrectly()
+ {
+ // Test DirectoryName with Unicode characters
+ DirectoryName name = DirectoryName.Create("文件夹名称");
+
+ Assert.IsNotNull(name);
+ Assert.AreEqual("文件夹名称", name.WeakString);
+ }
+
+ [TestMethod]
+ public void DirectoryName_WithNumbers_WorksCorrectly()
+ {
+ // Test DirectoryName with numbers
+ DirectoryName name = DirectoryName.Create("folder123");
+
+ Assert.IsNotNull(name);
+ Assert.AreEqual("folder123", name.WeakString);
+ }
+
+ [TestMethod]
+ public void DirectoryName_WithDots_WorksCorrectly()
+ {
+ // Test DirectoryName with dots (but not as path traversal)
+ DirectoryName name = DirectoryName.Create("my.folder.name");
+
+ Assert.IsNotNull(name);
+ Assert.AreEqual("my.folder.name", name.WeakString);
+ }
+
+ [TestMethod]
+ public void DirectoryName_IsValid_WithValidName_ReturnsTrue()
+ {
+ // Test IsValid method with valid name
+ DirectoryName name = DirectoryName.Create("ValidFolder");
+
+ bool isValid = name.IsValid();
+
+ Assert.IsTrue(isValid);
+ }
+}
diff --git a/Semantics.Test/Paths/PathConversionTests.cs b/Semantics.Test/Paths/PathConversionTests.cs
index ac94d5d..4447ba1 100644
--- a/Semantics.Test/Paths/PathConversionTests.cs
+++ b/Semantics.Test/Paths/PathConversionTests.cs
@@ -4,7 +4,6 @@
namespace ktsu.Semantics.Test.Paths;
-using System;
using System.IO;
using ktsu.Semantics.Paths;
using Microsoft.VisualStudio.TestTools.UnitTesting;
@@ -42,24 +41,16 @@ public void AsRelative_WithNullBaseDirectory_ThrowsArgumentNullException()
public void AsAbsolute_WithCurrentWorkingDirectory_WorksCorrectly()
{
// Test AsAbsolute() without base directory (uses current working directory)
- RelativeFilePath relativeFile = RelativeFilePath.Create("test.txt");
- RelativeDirectoryPath relativeDir = RelativeDirectoryPath.Create("test");
FilePath genericFile = FilePath.Create("test.txt");
DirectoryPath genericDir = DirectoryPath.Create("test");
- AbsoluteFilePath absoluteFile1 = relativeFile.AsAbsolute();
- AbsoluteDirectoryPath absoluteDir1 = relativeDir.AsAbsolute();
AbsoluteFilePath absoluteFile2 = genericFile.AsAbsolute();
AbsoluteDirectoryPath absoluteDir2 = genericDir.AsAbsolute();
- Assert.IsNotNull(absoluteFile1);
- Assert.IsNotNull(absoluteDir1);
Assert.IsNotNull(absoluteFile2);
Assert.IsNotNull(absoluteDir2);
// Should be fully qualified paths
- Assert.IsTrue(Path.IsPathFullyQualified(absoluteFile1.WeakString));
- Assert.IsTrue(Path.IsPathFullyQualified(absoluteDir1.WeakString));
Assert.IsTrue(Path.IsPathFullyQualified(absoluteFile2.WeakString));
Assert.IsTrue(Path.IsPathFullyQualified(absoluteDir2.WeakString));
}
@@ -208,32 +199,24 @@ public void AsAbsolute_ReturnsCorrectConcreteTypes()
AbsolutePath absolutePath = AbsolutePath.Create(@"C:\test");
RelativePath relativePath = RelativePath.Create("test");
AbsoluteFilePath absoluteFile = AbsoluteFilePath.Create(@"C:\test\file.txt");
- RelativeFilePath relativeFile = RelativeFilePath.Create("file.txt");
AbsoluteDirectoryPath absoluteDir = AbsoluteDirectoryPath.Create(@"C:\test");
- RelativeDirectoryPath relativeDir = RelativeDirectoryPath.Create("test");
// Test AsAbsolute methods
AbsolutePath result1 = absolutePath.AsAbsolute();
AbsolutePath result2 = relativePath.AsAbsolute();
AbsoluteFilePath result3 = absoluteFile.AsAbsolute();
- AbsoluteFilePath result4 = relativeFile.AsAbsolute();
AbsoluteDirectoryPath result5 = absoluteDir.AsAbsolute();
- AbsoluteDirectoryPath result6 = relativeDir.AsAbsolute();
// Verify correct types are returned
Assert.IsInstanceOfType(result1);
Assert.IsInstanceOfType(result2);
Assert.IsInstanceOfType(result3);
- Assert.IsInstanceOfType(result4);
Assert.IsInstanceOfType(result5);
- Assert.IsInstanceOfType(result6);
// Verify they're not null and valid
Assert.IsNotNull(result1);
Assert.IsNotNull(result2);
Assert.IsNotNull(result3);
- Assert.IsNotNull(result4);
Assert.IsNotNull(result5);
- Assert.IsNotNull(result6);
}
}
diff --git a/Semantics.Test/Paths/PathIntegrationTests.cs b/Semantics.Test/Paths/PathIntegrationTests.cs
new file mode 100644
index 0000000..f7d5d20
--- /dev/null
+++ b/Semantics.Test/Paths/PathIntegrationTests.cs
@@ -0,0 +1,289 @@
+// Copyright (c) ktsu.dev
+// All rights reserved.
+// Licensed under the MIT license.
+
+namespace ktsu.Semantics.Test.Paths;
+
+using System.Collections.Generic;
+using ktsu.Semantics.Paths;
+using ktsu.Semantics.Strings;
+using Microsoft.VisualStudio.TestTools.UnitTesting;
+
+[TestClass]
+public class PathIntegrationTests
+{
+ [TestMethod]
+ public void MixedPathTypes_InCollection_WorkCorrectly()
+ {
+ // Test that different path types can coexist in polymorphic collections
+ List paths =
+ [
+ AbsoluteDirectoryPath.Create(@"C:\projects"),
+ AbsoluteFilePath.Create(@"C:\file.txt"),
+ RelativeDirectoryPath.Create("subfolder"),
+ RelativeFilePath.Create("file.txt"),
+ DirectoryPath.Create("any"),
+ FilePath.Create("any.txt")
+ ];
+
+ Assert.AreEqual(6, paths.Count);
+ foreach (IPath path in paths)
+ {
+ Assert.IsNotNull(path);
+ // All created paths are valid by construction
+ }
+ }
+
+ [TestMethod]
+ public void DirectoryNames_InCollection_WorkCorrectly()
+ {
+ // Test DirectoryName in collections
+ List names =
+ [
+ DirectoryName.Create("folder1"),
+ DirectoryName.Create("folder2"),
+ DirectoryName.Create("folder3")
+ ];
+
+ Assert.AreEqual(3, names.Count);
+ Assert.IsTrue(names.Contains(DirectoryName.Create("folder1")));
+ }
+
+ [TestMethod]
+ public void DirectoryNames_AsSet_WorkCorrectly()
+ {
+ // Test DirectoryName in HashSet
+ HashSet uniqueNames =
+ [
+ DirectoryName.Create("folder1"),
+ DirectoryName.Create("folder2"),
+ DirectoryName.Create("folder1") // Duplicate
+ ];
+
+ Assert.AreEqual(2, uniqueNames.Count); // Duplicate should be ignored
+ }
+
+ [TestMethod]
+ public void ComplexPathConstruction_WithAllTypes_WorksCorrectly()
+ {
+ // Test complex path construction scenario
+ AbsoluteDirectoryPath root = AbsoluteDirectoryPath.Create(@"C:\projects");
+ DirectoryName appDir = DirectoryName.Create("myapp");
+ DirectoryName srcDir = DirectoryName.Create("src");
+ FileName componentFile = FileName.Create("Component.tsx");
+
+ // Build path step by step
+ AbsoluteDirectoryPath appPath = root / appDir;
+ AbsoluteDirectoryPath srcPath = appPath / srcDir;
+ AbsoluteFilePath filePath = srcPath / componentFile;
+
+ Assert.IsNotNull(filePath);
+ Assert.Contains(@"C:\projects", filePath.WeakString);
+ Assert.Contains("myapp", filePath.WeakString);
+ Assert.Contains("src", filePath.WeakString);
+ Assert.Contains("Component.tsx", filePath.WeakString);
+ }
+
+ [TestMethod]
+ public void RelativeToAbsolute_RoundTrip_WorksCorrectly()
+ {
+ // Test converting between relative and absolute paths
+ RelativeDirectoryPath relative = RelativeDirectoryPath.Create("projects");
+ AbsoluteDirectoryPath baseDir = AbsoluteDirectoryPath.Create(@"C:\work");
+
+ // Convert to absolute with specific base
+ AbsoluteDirectoryPath absolute = relative.AsAbsolute(baseDir);
+
+ Assert.IsNotNull(absolute);
+ Assert.Contains(@"C:\work", absolute.WeakString);
+ Assert.Contains("projects", absolute.WeakString);
+ }
+
+ [TestMethod]
+ public void AbsoluteToRelative_RoundTrip_WorksCorrectly()
+ {
+ // Test converting from absolute to relative paths
+ AbsoluteDirectoryPath absolute = AbsoluteDirectoryPath.Create(@"C:\work\projects");
+ AbsoluteDirectoryPath baseDir = AbsoluteDirectoryPath.Create(@"C:\work");
+
+ // Convert to relative
+ RelativeDirectoryPath relative = absolute.AsRelative(baseDir);
+
+ Assert.IsNotNull(relative);
+ Assert.Contains("projects", relative.WeakString);
+ Assert.DoesNotContain(@"C:\work", relative.WeakString);
+ }
+
+ [TestMethod]
+ public void PathHierarchy_ParentTraversal_WorksCorrectly()
+ {
+ // Test traversing up the directory hierarchy
+ RelativeDirectoryPath deep = RelativeDirectoryPath.Create(@"a\b\c\d");
+
+ RelativeDirectoryPath parent1 = deep.Parent;
+ RelativeDirectoryPath parent2 = parent1.Parent;
+ RelativeDirectoryPath parent3 = parent2.Parent;
+ RelativeDirectoryPath parent4 = parent3.Parent;
+
+ Assert.Contains("c", parent1.WeakString);
+ Assert.Contains("b", parent2.WeakString);
+ Assert.AreEqual("a", parent3.WeakString);
+ Assert.AreEqual("", parent4.WeakString); // Should be empty at root
+ }
+
+ [TestMethod]
+ public void PathNormalization_WithDotComponents_ResolvesCorrectly()
+ {
+ // Test path normalization with . and .. components
+ RelativeDirectoryPath pathWithDots = RelativeDirectoryPath.Create(@"a\.\b\..\c");
+
+ RelativeDirectoryPath normalized = pathWithDots.Normalize();
+
+ Assert.IsNotNull(normalized);
+ // After normalization, . and .. should be resolved
+ // a\.\b\..\c -> a\c
+ Assert.Contains("a", normalized.WeakString);
+ Assert.Contains("c", normalized.WeakString);
+ }
+
+ [TestMethod]
+ public void PathNormalization_WithParentTraversal_ResolvesCorrectly()
+ {
+ // Test path normalization with parent directory traversal
+ RelativeDirectoryPath path = RelativeDirectoryPath.Create(@"a\b\..\..\c");
+
+ RelativeDirectoryPath normalized = path.Normalize();
+
+ Assert.IsNotNull(normalized);
+ // a\b\..\..\c -> c
+ Assert.AreEqual("c", normalized.WeakString);
+ }
+
+ [TestMethod]
+ public void FilePathExtensions_MultipleOperations_WorkCorrectly()
+ {
+ // Test multiple extension operations on same file
+ RelativeFilePath original = RelativeFilePath.Create("document.txt");
+ FileExtension mdExt = FileExtension.Create(".md");
+ FileExtension pdfExt = FileExtension.Create(".pdf");
+
+ RelativeFilePath md = original.ChangeExtension(mdExt);
+ RelativeFilePath pdf = md.ChangeExtension(pdfExt);
+ RelativeFilePath noExt = pdf.RemoveExtension();
+
+ Assert.Contains(".md", md.WeakString);
+ Assert.Contains(".pdf", pdf.WeakString);
+ Assert.DoesNotContain(".pdf", noExt.WeakString);
+ Assert.Contains("document", noExt.WeakString);
+ }
+
+ [TestMethod]
+ public void DirectoryDepth_Comparison_WorksCorrectly()
+ {
+ // Test comparing depths of different paths
+ RelativeDirectoryPath shallow = RelativeDirectoryPath.Create("a");
+ RelativeDirectoryPath medium = RelativeDirectoryPath.Create(@"a\b");
+ RelativeDirectoryPath deep = RelativeDirectoryPath.Create(@"a\b\c");
+
+ Assert.IsTrue(shallow.Depth < medium.Depth);
+ Assert.IsTrue(medium.Depth < deep.Depth);
+ Assert.AreEqual(0, shallow.Depth);
+ Assert.AreEqual(1, medium.Depth);
+ Assert.AreEqual(2, deep.Depth);
+ }
+
+ [TestMethod]
+ public void InterfaceBasedPathOperations_WorkPolymorphically()
+ {
+ // Test that interface-based operations work polymorphically
+ IDirectoryPath dir1 = AbsoluteDirectoryPath.Create(@"C:\test");
+ IDirectoryPath dir2 = RelativeDirectoryPath.Create("test");
+ IDirectoryPath dir3 = DirectoryPath.Create("test");
+
+ IPath[] paths = [dir1, dir2, dir3];
+
+ foreach (IPath path in paths)
+ {
+ Assert.IsNotNull(path);
+ ISemanticString semanticPath = (ISemanticString)path;
+ Assert.IsNotNull(semanticPath.WeakString);
+ // All paths are valid by construction
+ }
+ }
+
+ [TestMethod]
+ public void CombiningDirectoryNames_BuildsDeepHierarchy()
+ {
+ // Test building deep directory hierarchies with DirectoryName
+ DirectoryPath root = DirectoryPath.Create("root");
+ DirectoryName[] levels =
+ [
+ DirectoryName.Create("level1"),
+ DirectoryName.Create("level2"),
+ DirectoryName.Create("level3"),
+ DirectoryName.Create("level4"),
+ DirectoryName.Create("level5")
+ ];
+
+ DirectoryPath current = root;
+ foreach (DirectoryName level in levels)
+ {
+ current /= level;
+ }
+
+ // Verify all levels are in the final path
+ Assert.Contains("root", current.WeakString);
+ Assert.Contains("level1", current.WeakString);
+ Assert.Contains("level2", current.WeakString);
+ Assert.Contains("level3", current.WeakString);
+ Assert.Contains("level4", current.WeakString);
+ Assert.Contains("level5", current.WeakString);
+ }
+
+ [TestMethod]
+ public void EmptyPaths_HandleCorrectly()
+ {
+ // Test that empty paths are handled correctly
+ DirectoryPath emptyDir = DirectoryPath.Create("");
+ FilePath emptyFile = FilePath.Create("");
+ DirectoryName emptyName = DirectoryName.Create("");
+ FileName emptyFileName = FileName.Create("");
+
+ Assert.AreEqual("", emptyDir.WeakString);
+ Assert.AreEqual("", emptyFile.WeakString);
+ Assert.AreEqual("", emptyName.WeakString);
+ Assert.AreEqual("", emptyFileName.WeakString);
+ }
+
+ [TestMethod]
+ public void RelativePaths_WithDotDot_AreValid()
+ {
+ // Test that relative paths with .. components are valid
+ RelativeDirectoryPath parentRef = RelativeDirectoryPath.Create("..");
+ RelativeDirectoryPath multiParent = RelativeDirectoryPath.Create(@"..\..\..");
+ RelativeFilePath fileInParent = RelativeFilePath.Create(@"..\file.txt");
+
+ Assert.IsNotNull(parentRef);
+ Assert.IsNotNull(multiParent);
+ Assert.IsNotNull(fileInParent);
+ Assert.IsTrue(parentRef.IsValid());
+ Assert.IsTrue(multiParent.IsValid());
+ Assert.IsTrue(fileInParent.IsValid());
+ }
+
+ [TestMethod]
+ public void RelativePaths_WithDot_AreValid()
+ {
+ // Test that relative paths with . components are valid
+ RelativeDirectoryPath currentRef = RelativeDirectoryPath.Create(".");
+ RelativeDirectoryPath withCurrent = RelativeDirectoryPath.Create(@".\subfolder");
+ RelativeFilePath fileInCurrent = RelativeFilePath.Create(@".\file.txt");
+
+ Assert.IsNotNull(currentRef);
+ Assert.IsNotNull(withCurrent);
+ Assert.IsNotNull(fileInCurrent);
+ Assert.IsTrue(currentRef.IsValid());
+ Assert.IsTrue(withCurrent.IsValid());
+ Assert.IsTrue(fileInCurrent.IsValid());
+ }
+}
diff --git a/Semantics.Test/Paths/PathOperatorTests.cs b/Semantics.Test/Paths/PathOperatorTests.cs
index 2879a5c..6c60c16 100644
--- a/Semantics.Test/Paths/PathOperatorTests.cs
+++ b/Semantics.Test/Paths/PathOperatorTests.cs
@@ -16,36 +16,18 @@ public void PathOperators_NullArguments_ThrowArgumentNullException()
{
// Test all path combination operators with null arguments
AbsoluteDirectoryPath absoluteDir = AbsoluteDirectoryPath.Create(@"C:\test");
- RelativeDirectoryPath relativeDir = RelativeDirectoryPath.Create(@"test");
DirectoryPath genericDir = DirectoryPath.Create(@"test");
- RelativeDirectoryPath nullRelativeDir = null!;
- RelativeFilePath nullRelativeFile = null!;
FileName nullFileName = null!;
// AbsoluteDirectoryPath operators
- Assert.ThrowsExactly(() => absoluteDir / nullRelativeDir);
- Assert.ThrowsExactly(() => absoluteDir / nullRelativeFile);
Assert.ThrowsExactly(() => absoluteDir / nullFileName);
- AbsoluteDirectoryPath nullAbsoluteDir = null!;
- Assert.ThrowsExactly(() => nullAbsoluteDir / relativeDir);
-
- // RelativeDirectoryPath operators
- Assert.ThrowsExactly(() => relativeDir / nullRelativeDir);
- Assert.ThrowsExactly(() => relativeDir / nullRelativeFile);
- Assert.ThrowsExactly(() => relativeDir / nullFileName);
-
- RelativeDirectoryPath nullRelativeDir2 = null!;
- Assert.ThrowsExactly(() => nullRelativeDir2 / relativeDir);
-
// DirectoryPath operators
- Assert.ThrowsExactly(() => genericDir / nullRelativeDir);
- Assert.ThrowsExactly(() => genericDir / nullRelativeFile);
Assert.ThrowsExactly(() => genericDir / nullFileName);
DirectoryPath nullGenericDir = null!;
- Assert.ThrowsExactly(() => nullGenericDir / relativeDir);
+ Assert.ThrowsExactly(() => nullGenericDir / nullFileName);
}
[TestMethod]
@@ -53,18 +35,20 @@ public void PathOperators_ComplexCombinations_WorkCorrectly()
{
// Test complex path combinations
AbsoluteDirectoryPath baseDir = AbsoluteDirectoryPath.Create(@"C:\projects");
- RelativeDirectoryPath subDir1 = RelativeDirectoryPath.Create(@"app\src");
- RelativeDirectoryPath subDir2 = RelativeDirectoryPath.Create(@"components");
+ DirectoryPath subDir1 = DirectoryPath.Create(@"app\src");
+ DirectoryPath subDir2 = DirectoryPath.Create(@"components");
FileName fileName = FileName.Create("Component.tsx");
- // Chain multiple operations
- AbsoluteDirectoryPath combinedDir = baseDir / subDir1 / subDir2;
+ // Chain multiple operations - combine paths using Path.Combine
+ AbsoluteDirectoryPath combinedDir = AbsoluteDirectoryPath.Create(
+ Path.Combine(baseDir.WeakString, subDir1.WeakString, subDir2.WeakString));
AbsoluteFilePath finalFile = combinedDir / fileName;
Assert.IsNotNull(combinedDir);
Assert.IsNotNull(finalFile);
Assert.Contains(@"C:\projects", finalFile.WeakString);
- Assert.Contains(@"app\src", finalFile.WeakString);
+ Assert.Contains(@"app", finalFile.WeakString);
+ Assert.Contains(@"src", finalFile.WeakString);
Assert.Contains(@"components", finalFile.WeakString);
Assert.Contains("Component.tsx", finalFile.WeakString);
}
@@ -74,17 +58,11 @@ public void PathOperators_EmptyPaths_HandleCorrectly()
{
// Test operators with empty paths
AbsoluteDirectoryPath absoluteDir = AbsoluteDirectoryPath.Create(@"C:\test");
- RelativeDirectoryPath emptyRelativeDir = RelativeDirectoryPath.Create("");
- RelativeFilePath emptyRelativeFile = RelativeFilePath.Create("");
FileName emptyFileName = FileName.Create("");
// These should work without throwing
- AbsoluteDirectoryPath result1 = absoluteDir / emptyRelativeDir;
- AbsoluteFilePath result2 = absoluteDir / emptyRelativeFile;
AbsoluteFilePath result3 = absoluteDir / emptyFileName;
- Assert.IsNotNull(result1);
- Assert.IsNotNull(result2);
Assert.IsNotNull(result3);
}
@@ -93,10 +71,11 @@ public void PathOperators_SpecialCharacters_HandleCorrectly()
{
// Test with paths containing special characters
AbsoluteDirectoryPath baseDir = AbsoluteDirectoryPath.Create(@"C:\test folder");
- RelativeDirectoryPath specialDir = RelativeDirectoryPath.Create(@"sub folder (1)");
+ DirectoryPath specialDir = DirectoryPath.Create(@"sub folder (1)");
FileName specialFile = FileName.Create("file name with spaces.txt");
- AbsoluteDirectoryPath combinedDir = baseDir / specialDir;
+ AbsoluteDirectoryPath combinedDir = AbsoluteDirectoryPath.Create(
+ Path.Combine(baseDir.WeakString, specialDir.WeakString));
AbsoluteFilePath combinedFile = combinedDir / specialFile;
Assert.IsNotNull(combinedDir);
@@ -111,35 +90,17 @@ public void PathOperators_ReturnTypes_AreCorrect()
{
// Verify that operators return the correct types
AbsoluteDirectoryPath absoluteDir = AbsoluteDirectoryPath.Create(@"C:\test");
- RelativeDirectoryPath relativeDir = RelativeDirectoryPath.Create(@"sub");
DirectoryPath genericDir = DirectoryPath.Create(@"test");
- RelativeFilePath relativeFile = RelativeFilePath.Create(@"file.txt");
FileName fileName = FileName.Create("file.txt");
// AbsoluteDirectoryPath combinations
- AbsoluteDirectoryPath absDir = absoluteDir / relativeDir;
- AbsoluteFilePath absFile1 = absoluteDir / relativeFile;
AbsoluteFilePath absFile2 = absoluteDir / fileName;
- // RelativeDirectoryPath combinations
- RelativeDirectoryPath relDir = relativeDir / relativeDir;
- RelativeFilePath relFile1 = relativeDir / relativeFile;
- RelativeFilePath relFile2 = relativeDir / fileName;
-
// DirectoryPath combinations
- DirectoryPath genDir = genericDir / relativeDir;
- FilePath genFile1 = genericDir / relativeFile;
FilePath genFile2 = genericDir / fileName;
// Verify types
- Assert.IsInstanceOfType(absDir);
- Assert.IsInstanceOfType(absFile1);
Assert.IsInstanceOfType(absFile2);
- Assert.IsInstanceOfType(relDir);
- Assert.IsInstanceOfType(relFile1);
- Assert.IsInstanceOfType(relFile2);
- Assert.IsInstanceOfType(genDir);
- Assert.IsInstanceOfType(genFile1);
Assert.IsInstanceOfType(genFile2);
}
@@ -182,4 +143,79 @@ public void PathOperators_CrossPlatformSeparators_HandleCorrectly()
Assert.IsTrue(combinedDir.IsValid());
Assert.IsTrue(combinedFile.IsValid());
}
+
+ [TestMethod]
+ public void DirectoryNameOperator_WithAbsoluteDirectoryPath_CreatesCorrectPath()
+ {
+ // Test combining AbsoluteDirectoryPath with DirectoryName
+ AbsoluteDirectoryPath baseDir = AbsoluteDirectoryPath.Create(@"C:\projects");
+ DirectoryName subDir = DirectoryName.Create("myapp");
+
+ AbsoluteDirectoryPath result = baseDir / subDir;
+
+ Assert.IsNotNull(result);
+ Assert.IsTrue(result.IsValid());
+ Assert.Contains(@"C:\projects", result.WeakString);
+ Assert.Contains("myapp", result.WeakString);
+ }
+
+ [TestMethod]
+ public void DirectoryNameOperator_WithDirectoryPath_CreatesCorrectPath()
+ {
+ // Test combining DirectoryPath with DirectoryName
+ DirectoryPath baseDir = DirectoryPath.Create(@"projects");
+ DirectoryName subDir = DirectoryName.Create("myapp");
+
+ DirectoryPath result = baseDir / subDir;
+
+ Assert.IsNotNull(result);
+ Assert.IsTrue(result.IsValid());
+ Assert.Contains("projects", result.WeakString);
+ Assert.Contains("myapp", result.WeakString);
+ }
+
+ [TestMethod]
+ public void DirectoryNameOperator_ChainedCombinations_WorkCorrectly()
+ {
+ // Test chaining multiple DirectoryName combinations
+ DirectoryPath baseDir = DirectoryPath.Create(@"projects");
+ DirectoryName dir1 = DirectoryName.Create("app");
+ DirectoryName dir2 = DirectoryName.Create("src");
+ DirectoryName dir3 = DirectoryName.Create("components");
+
+ DirectoryPath result = baseDir / dir1 / dir2 / dir3;
+
+ Assert.IsNotNull(result);
+ Assert.IsTrue(result.IsValid());
+ Assert.Contains("projects", result.WeakString);
+ Assert.Contains("app", result.WeakString);
+ Assert.Contains("src", result.WeakString);
+ Assert.Contains("components", result.WeakString);
+ }
+
+ [TestMethod]
+ public void DirectoryNameOperator_WithNullDirectoryName_ThrowsArgumentNullException()
+ {
+ // Test null safety for DirectoryName operators
+ AbsoluteDirectoryPath absoluteDir = AbsoluteDirectoryPath.Create(@"C:\test");
+ DirectoryPath genericDir = DirectoryPath.Create(@"test");
+ DirectoryName nullDirName = null!;
+
+ Assert.ThrowsExactly(() => absoluteDir / nullDirName);
+ Assert.ThrowsExactly(() => genericDir / nullDirName);
+ }
+
+ [TestMethod]
+ public void DirectoryNameOperator_WithSpecialCharacters_HandlesCorrectly()
+ {
+ // Test DirectoryName with valid special characters
+ DirectoryPath baseDir = DirectoryPath.Create(@"projects");
+ DirectoryName specialDir = DirectoryName.Create("my-app_v2 (beta)");
+
+ DirectoryPath result = baseDir / specialDir;
+
+ Assert.IsNotNull(result);
+ Assert.IsTrue(result.IsValid());
+ Assert.Contains("my-app_v2 (beta)", result.WeakString);
+ }
}
diff --git a/Semantics.Test/Paths/PathUtilityTests.cs b/Semantics.Test/Paths/PathUtilityTests.cs
index 17b71e1..6a303a8 100644
--- a/Semantics.Test/Paths/PathUtilityTests.cs
+++ b/Semantics.Test/Paths/PathUtilityTests.cs
@@ -5,7 +5,6 @@
namespace ktsu.Semantics.Test.Paths;
using System;
-using System.IO;
using ktsu.Semantics.Paths;
using Microsoft.VisualStudio.TestTools.UnitTesting;
diff --git a/Semantics.Test/Paths/RelativePathPropertyTests.cs b/Semantics.Test/Paths/RelativePathPropertyTests.cs
new file mode 100644
index 0000000..7451788
--- /dev/null
+++ b/Semantics.Test/Paths/RelativePathPropertyTests.cs
@@ -0,0 +1,307 @@
+// Copyright (c) ktsu.dev
+// All rights reserved.
+// Licensed under the MIT license.
+
+namespace ktsu.Semantics.Test.Paths;
+
+using System;
+using ktsu.Semantics.Paths;
+using Microsoft.VisualStudio.TestTools.UnitTesting;
+
+[TestClass]
+public class RelativePathPropertyTests
+{
+ [TestMethod]
+ public void RelativeDirectoryPath_Name_ReturnsCorrectDirectoryName()
+ {
+ // Test that Name property returns the last component as DirectoryName
+ RelativeDirectoryPath path = RelativeDirectoryPath.Create(@"projects\app\src");
+
+ DirectoryName name = path.Name;
+
+ Assert.IsNotNull(name);
+ Assert.AreEqual("src", name.WeakString);
+ }
+
+ [TestMethod]
+ public void RelativeDirectoryPath_Name_WithSingleComponent_ReturnsComponent()
+ {
+ // Test Name property with single directory name
+ RelativeDirectoryPath path = RelativeDirectoryPath.Create("myapp");
+
+ DirectoryName name = path.Name;
+
+ Assert.IsNotNull(name);
+ Assert.AreEqual("myapp", name.WeakString);
+ }
+
+ [TestMethod]
+ public void RelativeDirectoryPath_Name_WithEmptyPath_ReturnsEmpty()
+ {
+ // Test Name property with empty path
+ RelativeDirectoryPath path = RelativeDirectoryPath.Create("");
+
+ DirectoryName name = path.Name;
+
+ Assert.IsNotNull(name);
+ Assert.AreEqual("", name.WeakString);
+ }
+
+ [TestMethod]
+ public void RelativeDirectoryPath_Parent_ReturnsCorrectParent()
+ {
+ // Test that Parent property returns parent directory
+ RelativeDirectoryPath path = RelativeDirectoryPath.Create(@"projects\app\src");
+
+ RelativeDirectoryPath parent = path.Parent;
+
+ Assert.IsNotNull(parent);
+ Assert.Contains("projects", parent.WeakString);
+ Assert.Contains("app", parent.WeakString);
+ Assert.DoesNotContain("src", parent.WeakString);
+ }
+
+ [TestMethod]
+ public void RelativeDirectoryPath_Parent_WithSingleComponent_ReturnsEmpty()
+ {
+ // Test Parent property with single directory
+ RelativeDirectoryPath path = RelativeDirectoryPath.Create("myapp");
+
+ RelativeDirectoryPath parent = path.Parent;
+
+ Assert.IsNotNull(parent);
+ Assert.AreEqual("", parent.WeakString);
+ }
+
+ [TestMethod]
+ public void RelativeDirectoryPath_Depth_CalculatesCorrectly()
+ {
+ // Test Depth property calculation
+ RelativeDirectoryPath shallow = RelativeDirectoryPath.Create("myapp");
+ RelativeDirectoryPath medium = RelativeDirectoryPath.Create(@"projects\myapp");
+ RelativeDirectoryPath deep = RelativeDirectoryPath.Create(@"projects\myapp\src\components");
+
+ Assert.AreEqual(0, shallow.Depth);
+ Assert.AreEqual(1, medium.Depth);
+ Assert.AreEqual(3, deep.Depth);
+ }
+
+ [TestMethod]
+ public void RelativeDirectoryPath_Depth_WithEmptyPath_ReturnsZero()
+ {
+ // Test Depth property with empty path
+ RelativeDirectoryPath empty = RelativeDirectoryPath.Create("");
+
+ Assert.AreEqual(0, empty.Depth);
+ }
+
+ [TestMethod]
+ public void RelativeFilePath_RelativeDirectoryPath_ReturnsCorrectDirectory()
+ {
+ // Test RelativeDirectoryPath property
+ RelativeFilePath file = RelativeFilePath.Create(@"projects\app\Component.tsx");
+
+ RelativeDirectoryPath dir = file.RelativeDirectoryPath;
+
+ Assert.IsNotNull(dir);
+ Assert.Contains("projects", dir.WeakString);
+ Assert.Contains("app", dir.WeakString);
+ Assert.DoesNotContain("Component", dir.WeakString);
+ }
+
+ [TestMethod]
+ public void RelativeFilePath_RelativeDirectoryPath_WithFileInRoot_ReturnsEmpty()
+ {
+ // Test RelativeDirectoryPath property with file in root
+ RelativeFilePath file = RelativeFilePath.Create("file.txt");
+
+ RelativeDirectoryPath dir = file.RelativeDirectoryPath;
+
+ Assert.IsNotNull(dir);
+ Assert.AreEqual("", dir.WeakString);
+ }
+
+ [TestMethod]
+ public void RelativeFilePath_FileNameWithoutExtension_ReturnsCorrectName()
+ {
+ // Test FileNameWithoutExtension property
+ RelativeFilePath file = RelativeFilePath.Create(@"projects\Component.tsx");
+
+ FileName name = file.FileNameWithoutExtension;
+
+ Assert.IsNotNull(name);
+ Assert.AreEqual("Component", name.WeakString);
+ }
+
+ [TestMethod]
+ public void RelativeFilePath_FileNameWithoutExtension_WithMultipleExtensions_RemovesLastOnly()
+ {
+ // Test FileNameWithoutExtension with multiple extensions
+ RelativeFilePath file = RelativeFilePath.Create("archive.tar.gz");
+
+ FileName name = file.FileNameWithoutExtension;
+
+ Assert.IsNotNull(name);
+ Assert.AreEqual("archive.tar", name.WeakString);
+ }
+
+ [TestMethod]
+ public void RelativeFilePath_ChangeExtension_ChangesCorrectly()
+ {
+ // Test ChangeExtension method
+ RelativeFilePath file = RelativeFilePath.Create(@"projects\file.txt");
+ FileExtension newExt = FileExtension.Create(".md");
+
+ RelativeFilePath result = file.ChangeExtension(newExt);
+
+ Assert.IsNotNull(result);
+ Assert.Contains("file.md", result.WeakString);
+ Assert.DoesNotContain(".txt", result.WeakString);
+ }
+
+ [TestMethod]
+ public void RelativeFilePath_ChangeExtension_WithNullExtension_ThrowsException()
+ {
+ // Test ChangeExtension with null extension
+ RelativeFilePath file = RelativeFilePath.Create("file.txt");
+
+ Assert.ThrowsExactly(() => file.ChangeExtension(null!));
+ }
+
+ [TestMethod]
+ public void RelativeFilePath_RemoveExtension_RemovesCorrectly()
+ {
+ // Test RemoveExtension method
+ RelativeFilePath file = RelativeFilePath.Create(@"projects\file.txt");
+
+ RelativeFilePath result = file.RemoveExtension();
+
+ Assert.IsNotNull(result);
+ Assert.Contains("file", result.WeakString);
+ Assert.DoesNotContain(".txt", result.WeakString);
+ }
+
+ [TestMethod]
+ public void RelativeFilePath_RemoveExtension_WithNoExtension_ReturnsUnchanged()
+ {
+ // Test RemoveExtension with file without extension
+ RelativeFilePath file = RelativeFilePath.Create(@"projects\README");
+
+ RelativeFilePath result = file.RemoveExtension();
+
+ Assert.IsNotNull(result);
+ Assert.Contains("README", result.WeakString);
+ }
+
+ [TestMethod]
+ public void RelativeDirectoryPath_CombineWithRelativeDirectory_WorksCorrectly()
+ {
+ // Test combining relative directories
+ RelativeDirectoryPath base1 = RelativeDirectoryPath.Create("projects");
+ RelativeDirectoryPath sub = RelativeDirectoryPath.Create("app");
+
+ RelativeDirectoryPath result = base1 / sub;
+
+ Assert.IsNotNull(result);
+ Assert.Contains("projects", result.WeakString);
+ Assert.Contains("app", result.WeakString);
+ }
+
+ [TestMethod]
+ public void RelativeDirectoryPath_CombineWithRelativeFile_WorksCorrectly()
+ {
+ // Test combining relative directory with relative file
+ RelativeDirectoryPath dir = RelativeDirectoryPath.Create("projects");
+ RelativeFilePath file = RelativeFilePath.Create("file.txt");
+
+ RelativeFilePath result = dir / file;
+
+ Assert.IsNotNull(result);
+ Assert.Contains("projects", result.WeakString);
+ Assert.Contains("file.txt", result.WeakString);
+ }
+
+ [TestMethod]
+ public void RelativeDirectoryPath_CombineWithFileName_WorksCorrectly()
+ {
+ // Test combining relative directory with file name
+ RelativeDirectoryPath dir = RelativeDirectoryPath.Create("projects");
+ FileName file = FileName.Create("Component.tsx");
+
+ RelativeFilePath result = dir / file;
+
+ Assert.IsNotNull(result);
+ Assert.Contains("projects", result.WeakString);
+ Assert.Contains("Component.tsx", result.WeakString);
+ }
+
+ [TestMethod]
+ public void RelativeDirectoryPath_AsRelative_ReturnsSelf()
+ {
+ // Test that AsRelative returns self for already relative paths
+ RelativeDirectoryPath path = RelativeDirectoryPath.Create("projects");
+ AbsoluteDirectoryPath baseDir = AbsoluteDirectoryPath.Create(@"C:\temp");
+
+ RelativeDirectoryPath result = path.AsRelative(baseDir);
+
+ Assert.AreSame(path, result);
+ }
+
+ [TestMethod]
+ public void RelativeFilePath_AsRelative_ReturnsSelf()
+ {
+ // Test that AsRelative returns self for already relative file paths
+ RelativeFilePath path = RelativeFilePath.Create("file.txt");
+ AbsoluteDirectoryPath baseDir = AbsoluteDirectoryPath.Create(@"C:\temp");
+
+ RelativeFilePath result = path.AsRelative(baseDir);
+
+ Assert.AreSame(path, result);
+ }
+
+ [TestMethod]
+ public void RelativeDirectoryPath_AsAbsoluteWithBase_ResolvesCorrectly()
+ {
+ // Test AsAbsolute with explicit base directory
+ RelativeDirectoryPath relative = RelativeDirectoryPath.Create("projects");
+ AbsoluteDirectoryPath baseDir = AbsoluteDirectoryPath.Create(@"C:\work");
+
+ AbsoluteDirectoryPath result = relative.AsAbsolute(baseDir);
+
+ Assert.IsNotNull(result);
+ Assert.Contains(@"C:\work", result.WeakString);
+ Assert.Contains("projects", result.WeakString);
+ }
+
+ [TestMethod]
+ public void RelativeFilePath_AsAbsoluteWithBase_ResolvesCorrectly()
+ {
+ // Test AsAbsolute with explicit base directory for files
+ RelativeFilePath relative = RelativeFilePath.Create("file.txt");
+ AbsoluteDirectoryPath baseDir = AbsoluteDirectoryPath.Create(@"C:\work");
+
+ AbsoluteFilePath result = relative.AsAbsolute(baseDir);
+
+ Assert.IsNotNull(result);
+ Assert.Contains(@"C:\work", result.WeakString);
+ Assert.Contains("file.txt", result.WeakString);
+ }
+
+ [TestMethod]
+ public void RelativeDirectoryPath_AsAbsoluteWithBase_WithNullBase_ThrowsException()
+ {
+ // Test AsAbsolute with null base directory
+ RelativeDirectoryPath relative = RelativeDirectoryPath.Create("projects");
+
+ Assert.ThrowsExactly(() => relative.AsAbsolute(null!));
+ }
+
+ [TestMethod]
+ public void RelativeFilePath_AsAbsoluteWithBase_WithNullBase_ThrowsException()
+ {
+ // Test AsAbsolute with null base directory for files
+ RelativeFilePath relative = RelativeFilePath.Create("file.txt");
+
+ Assert.ThrowsExactly(() => relative.AsAbsolute(null!));
+ }
+}
diff --git a/Semantics.Test/Paths/SemanticPathInterfaceTests.cs b/Semantics.Test/Paths/SemanticPathInterfaceTests.cs
index 6a16dc5..04bde51 100644
--- a/Semantics.Test/Paths/SemanticPathInterfaceTests.cs
+++ b/Semantics.Test/Paths/SemanticPathInterfaceTests.cs
@@ -17,21 +17,17 @@ public void DirectoryPath_CombineWithFileName_WorksCorrectly()
FileName fileName = FileName.Create("test.txt");
AbsoluteDirectoryPath absoluteDir = AbsoluteDirectoryPath.Create(@"C:\temp");
- RelativeDirectoryPath relativeDir = RelativeDirectoryPath.Create(@"temp");
DirectoryPath genericDir = DirectoryPath.Create(@"temp");
// Act
AbsoluteFilePath absoluteResult = absoluteDir / fileName;
- RelativeFilePath relativeResult = relativeDir / fileName;
FilePath genericResult = genericDir / fileName;
// Assert
Assert.IsNotNull(absoluteResult);
- Assert.IsNotNull(relativeResult);
Assert.IsNotNull(genericResult);
Assert.AreEqual(@"C:\temp\test.txt", absoluteResult.WeakString);
- Assert.AreEqual(@"temp\test.txt", relativeResult.WeakString);
Assert.AreEqual(@"temp\test.txt", genericResult.WeakString);
}
}
diff --git a/Semantics.Test/SemanticPathInterfaceTests.cs b/Semantics.Test/SemanticPathInterfaceTests.cs
index d59086f..a6f52ff 100644
--- a/Semantics.Test/SemanticPathInterfaceTests.cs
+++ b/Semantics.Test/SemanticPathInterfaceTests.cs
@@ -221,32 +221,26 @@ public void PolymorphicCollection_CanStoreAllPathTypes()
paths.Add(FilePath.Create("file.txt"));
paths.Add(DirectoryPath.Create("directory"));
paths.Add(AbsoluteFilePath.Create("C:\\file.txt"));
- paths.Add(RelativeFilePath.Create("relative\\file.txt"));
paths.Add(AbsoluteDirectoryPath.Create("C:\\directory"));
- paths.Add(RelativeDirectoryPath.Create("relative\\directory"));
filePaths.Add(FilePath.Create("file.txt"));
filePaths.Add(AbsoluteFilePath.Create("C:\\file.txt"));
- filePaths.Add(RelativeFilePath.Create("relative\\file.txt"));
directoryPaths.Add(DirectoryPath.Create("directory"));
directoryPaths.Add(AbsoluteDirectoryPath.Create