Skip to content

Commit abcf717

Browse files
authored
Add ResolveFile extension method for flexible file path resolution (#75)
1 parent 4124902 commit abcf717

File tree

8 files changed

+158
-1
lines changed

8 files changed

+158
-1
lines changed

.gitignore

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -191,4 +191,4 @@ Tests/FluentAssertions.Specs/FluentAssertions.Specs.xml
191191
Tests/Benchmarks/BenchmarkDotNet.Artifacts/
192192

193193
# Documentation spell check
194-
node_modules/
194+
node_modules/

Pathy.ApiVerificationTests/ApprovedApi/pathy.net47.verified.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,7 @@ namespace Pathy
4646
public static void DeleteFileOrDirectory(this System.Collections.Generic.IEnumerable<Pathy.ChainablePath> paths) { }
4747
public static void MoveFileOrDirectory(this System.Collections.Generic.IEnumerable<Pathy.ChainablePath> sourcePaths, Pathy.ChainablePath destinationDirectory) { }
4848
public static void MoveFileOrDirectory(this Pathy.ChainablePath sourcePath, Pathy.ChainablePath destinationDirectory, string newName = null) { }
49+
public static Pathy.ChainablePath ResolveFile(this Pathy.ChainablePath path, string fileName) { }
4950
}
5051
public static class StringExtensions
5152
{

Pathy.ApiVerificationTests/ApprovedApi/pathy.net8.0.verified.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,7 @@ namespace Pathy
4747
public static void DeleteFileOrDirectory(this System.Collections.Generic.IEnumerable<Pathy.ChainablePath> paths) { }
4848
public static void MoveFileOrDirectory(this System.Collections.Generic.IEnumerable<Pathy.ChainablePath> sourcePaths, Pathy.ChainablePath destinationDirectory) { }
4949
public static void MoveFileOrDirectory(this Pathy.ChainablePath sourcePath, Pathy.ChainablePath destinationDirectory, string newName = null) { }
50+
public static Pathy.ChainablePath ResolveFile(this Pathy.ChainablePath path, string fileName) { }
5051
}
5152
public static class StringExtensions
5253
{

Pathy.ApiVerificationTests/ApprovedApi/pathy.netstandard2.0.verified.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,7 @@ namespace Pathy
4646
public static void DeleteFileOrDirectory(this System.Collections.Generic.IEnumerable<Pathy.ChainablePath> paths) { }
4747
public static void MoveFileOrDirectory(this System.Collections.Generic.IEnumerable<Pathy.ChainablePath> sourcePaths, Pathy.ChainablePath destinationDirectory) { }
4848
public static void MoveFileOrDirectory(this Pathy.ChainablePath sourcePath, Pathy.ChainablePath destinationDirectory, string newName = null) { }
49+
public static Pathy.ChainablePath ResolveFile(this Pathy.ChainablePath path, string fileName) { }
4950
}
5051
public static class StringExtensions
5152
{

Pathy.ApiVerificationTests/ApprovedApi/pathy.netstandard2.1.verified.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,7 @@ namespace Pathy
4747
public static void DeleteFileOrDirectory(this System.Collections.Generic.IEnumerable<Pathy.ChainablePath> paths) { }
4848
public static void MoveFileOrDirectory(this System.Collections.Generic.IEnumerable<Pathy.ChainablePath> sourcePaths, Pathy.ChainablePath destinationDirectory) { }
4949
public static void MoveFileOrDirectory(this Pathy.ChainablePath sourcePath, Pathy.ChainablePath destinationDirectory, string newName = null) { }
50+
public static Pathy.ChainablePath ResolveFile(this Pathy.ChainablePath path, string fileName) { }
5051
}
5152
public static class StringExtensions
5253
{

Pathy.Specs/ChainablePathSpecs.cs

Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -948,6 +948,99 @@ public void Cannot_find_the_first_existing_path_from_an_empty_array()
948948
.WithParameterName("paths");
949949
}
950950

951+
[Fact]
952+
public void Can_resolve_a_file_when_path_is_the_file_itself()
953+
{
954+
// Arrange
955+
var file = testFolder / "test.txt";
956+
File.WriteAllText(file, "content");
957+
958+
// Act
959+
var result = file.ResolveFile("test.txt");
960+
961+
// Assert
962+
result.ToString().Should().Be(file.ToString());
963+
result.FileExists.Should().BeTrue();
964+
}
965+
966+
[Fact]
967+
public void Can_resolve_a_file_when_path_is_a_directory_containing_the_file()
968+
{
969+
// Arrange
970+
var file = testFolder / "test.txt";
971+
File.WriteAllText(file, "content");
972+
973+
// Act
974+
var result = testFolder.ResolveFile("test.txt");
975+
976+
// Assert
977+
result.ToString().Should().Be(file.ToString());
978+
result.FileExists.Should().BeTrue();
979+
}
980+
981+
[Fact]
982+
public void ResolveFile_is_case_insensitive_when_path_is_the_file()
983+
{
984+
// Arrange
985+
var file = testFolder / "Test.txt";
986+
File.WriteAllText(file, "content");
987+
988+
// Act
989+
var result = file.ResolveFile("test.TXT");
990+
991+
// Assert
992+
result.ToString().Should().Be(file.ToString());
993+
result.FileExists.Should().BeTrue();
994+
}
995+
996+
[Fact]
997+
public void ResolveFile_returns_empty_when_file_does_not_exist_in_directory()
998+
{
999+
// Act
1000+
var result = testFolder.ResolveFile("nonexistent.txt");
1001+
1002+
// Assert
1003+
result.Should().Be(ChainablePath.Empty);
1004+
}
1005+
1006+
[Fact]
1007+
public void ResolveFile_returns_empty_when_path_is_a_file_with_different_name()
1008+
{
1009+
// Arrange
1010+
var file = testFolder / "actual.txt";
1011+
File.WriteAllText(file, "content");
1012+
1013+
// Act
1014+
var result = file.ResolveFile("different.txt");
1015+
1016+
// Assert
1017+
result.Should().Be(ChainablePath.Empty);
1018+
}
1019+
1020+
[Fact]
1021+
public void ResolveFile_throws_when_fileName_is_null()
1022+
{
1023+
// Act
1024+
var act = () => testFolder.ResolveFile(null);
1025+
1026+
// Assert
1027+
act.Should().Throw<ArgumentException>()
1028+
.WithMessage("*File name cannot be null or empty*")
1029+
.WithParameterName("fileName");
1030+
}
1031+
1032+
[Fact]
1033+
public void ResolveFile_throws_when_fileName_is_empty()
1034+
{
1035+
// Act
1036+
var act = () => testFolder.ResolveFile(string.Empty);
1037+
1038+
// Assert
1039+
act.Should().Throw<ArgumentException>()
1040+
.WithMessage("*File name cannot be null or empty*")
1041+
.WithParameterName("fileName");
1042+
}
1043+
9511044
[Fact]
9521045
public void Can_get_last_write_time_utc_for_file()
9531046
{

Pathy/ChainablePathExtensions.cs

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -87,4 +87,42 @@ public static void MoveFileOrDirectory(this System.Collections.Generic.IEnumerab
8787
sourcePath.MoveFileOrDirectory(destinationDirectory);
8888
}
8989
}
90+
91+
/// <summary>
92+
/// Resolves a file name within the current path.
93+
/// If the path represents a file with the specified name and that file exists, returns the path.
94+
/// If the path is a directory that contains a file with the specified name, returns the path to that file.
95+
/// Otherwise, returns <see cref="ChainablePath.Empty"/>.
96+
/// </summary>
97+
/// <param name="path">The base path to resolve from (can be a file or directory).</param>
98+
/// <param name="fileName">The file name to resolve.</param>
99+
/// <returns>
100+
/// A <see cref="ChainablePath"/> representing the resolved file if found; otherwise, <see cref="ChainablePath.Empty"/>.
101+
/// </returns>
102+
/// <exception cref="ArgumentException">Thrown if <paramref name="fileName"/> is null or empty.</exception>
103+
public static ChainablePath ResolveFile(this ChainablePath path, string fileName)
104+
{
105+
if (string.IsNullOrEmpty(fileName))
106+
{
107+
throw new ArgumentException("File name cannot be null or empty", nameof(fileName));
108+
}
109+
110+
// Case 1: If the path is a file with the specified name and exists, return it
111+
if (path.IsFile && string.Equals(path.Name, fileName, StringComparison.OrdinalIgnoreCase))
112+
{
113+
return path;
114+
}
115+
116+
// Case 2: If the path is a directory, check if it contains the file
117+
if (path.IsDirectory)
118+
{
119+
ChainablePath filePath = path / fileName;
120+
if (filePath.FileExists)
121+
{
122+
return filePath;
123+
}
124+
}
125+
126+
return ChainablePath.Empty;
127+
}
90128
}

README.md

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -164,6 +164,28 @@ var filesToMove = (ChainablePath.Current / "source").GlobFiles("*.txt");
164164
filesToMove.MoveFileOrDirectory(ChainablePath.Current / "destination");
165165
```
166166

167+
### Resolving files
168+
169+
If you have a `ChainablePath` that could represent either a file or a directory, and you want to resolve a specific file name, you can use the `ResolveFile` extension method:
170+
171+
```csharp
172+
// When the path is a directory containing the file
173+
var directory = ChainablePath.From("c:/projects/myapp");
174+
var configFile = directory.ResolveFile("appsettings.json");
175+
// Returns: c:/projects/myapp/appsettings.json (if it exists)
176+
177+
// When the path is already the file itself
178+
var filePath = ChainablePath.From("c:/projects/myapp/appsettings.json");
179+
var resolved = filePath.ResolveFile("appsettings.json");
180+
// Returns: c:/projects/myapp/appsettings.json (if it exists)
181+
182+
// When the file doesn't exist
183+
var missing = directory.ResolveFile("missing.txt");
184+
// Returns: ChainablePath.Empty
185+
```
186+
187+
The method performs case-insensitive file name matching, so `ResolveFile("CONFIG.JSON")` will match `config.json`.
188+
167189
## Building
168190

169191
To build this repository locally so you can contribute to it, you need the following:

0 commit comments

Comments
 (0)