Skip to content

Commit 591e917

Browse files
Dennis Doomendennisdoomen
authored andcommitted
Added FindFirst to find the first file or directory matching a specification.
1 parent 587940e commit 591e917

File tree

7 files changed

+264
-31
lines changed

7 files changed

+264
-31
lines changed

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

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,8 @@ namespace Pathy
2525
public System.IO.DirectoryInfo ToDirectoryInfo() { }
2626
public System.IO.FileInfo ToFileInfo() { }
2727
public override string ToString() { }
28+
public static Pathy.ChainablePath FindFirst(params Pathy.ChainablePath[] paths) { }
29+
public static Pathy.ChainablePath FindFirst(params string[] paths) { }
2830
public static Pathy.ChainablePath From(string path) { }
2931
public static string op_Implicit(Pathy.ChainablePath chainablePath) { }
3032
public static Pathy.ChainablePath op_Implicit(string path) { }

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

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,8 @@ namespace Pathy
2626
public System.IO.DirectoryInfo ToDirectoryInfo() { }
2727
public System.IO.FileInfo ToFileInfo() { }
2828
public override string ToString() { }
29+
public static Pathy.ChainablePath FindFirst(params Pathy.ChainablePath[] paths) { }
30+
public static Pathy.ChainablePath FindFirst(params string[] paths) { }
2931
public static Pathy.ChainablePath From(string path) { }
3032
public static string op_Implicit(Pathy.ChainablePath chainablePath) { }
3133
public static Pathy.ChainablePath op_Implicit(string path) { }

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

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,8 @@ namespace Pathy
2525
public System.IO.DirectoryInfo ToDirectoryInfo() { }
2626
public System.IO.FileInfo ToFileInfo() { }
2727
public override string ToString() { }
28+
public static Pathy.ChainablePath FindFirst(params Pathy.ChainablePath[] paths) { }
29+
public static Pathy.ChainablePath FindFirst(params string[] paths) { }
2830
public static Pathy.ChainablePath From(string path) { }
2931
public static string op_Implicit(Pathy.ChainablePath chainablePath) { }
3032
public static Pathy.ChainablePath op_Implicit(string path) { }

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

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,8 @@ namespace Pathy
2626
public System.IO.DirectoryInfo ToDirectoryInfo() { }
2727
public System.IO.FileInfo ToFileInfo() { }
2828
public override string ToString() { }
29+
public static Pathy.ChainablePath FindFirst(params Pathy.ChainablePath[] paths) { }
30+
public static Pathy.ChainablePath FindFirst(params string[] paths) { }
2931
public static Pathy.ChainablePath From(string path) { }
3032
public static string op_Implicit(Pathy.ChainablePath chainablePath) { }
3133
public static Pathy.ChainablePath op_Implicit(string path) { }

Pathy.Specs/ChainablePathSpecs.cs

Lines changed: 168 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,20 @@
11
using System;
22
using System.IO;
3+
using System.Linq;
34
using System.Reflection;
4-
using System.Runtime.InteropServices;
55
using FluentAssertions;
66
using Xunit;
77

88
namespace Pathy.Specs;
99

1010
public class ChainablePathSpecs
1111
{
12-
private readonly char slash = Path.DirectorySeparatorChar;
12+
private static readonly char Slash = Path.DirectorySeparatorChar;
1313
private readonly ChainablePath testFolder;
1414

1515
public ChainablePathSpecs()
1616
{
17-
testFolder = ChainablePath.Temp / nameof(ChainablePathSpecs) / Environment.Version.ToString();
17+
testFolder = ChainablePath.Temp / nameof(ChainablePathSpecs) / Environment.Version.ToString();
1818
testFolder.DeleteFileOrDirectory();
1919
testFolder.CreateDirectoryRecursively();
2020
}
@@ -57,7 +57,7 @@ public void Can_build_a_path_from_a_drive_letter(string drive)
5757
var path = ChainablePath.From(drive);
5858

5959
// Assert
60-
path.ToString().Should().BeEquivalentTo("C:" + slash);
60+
path.ToString().Should().BeEquivalentTo("C:" + Slash);
6161
path.IsRooted.Should().BeTrue();
6262
}
6363

@@ -93,15 +93,15 @@ public void A_trailing_slash_is_fine()
9393
public void Can_build_from_a_path_with_reverse_traversals()
9494
{
9595
// Arrange
96-
var nestedPath = Directory.CreateDirectory(testFolder.ToString() + "/dir1" + slash + "dir2" + slash + "dir3/");
96+
var nestedPath = Directory.CreateDirectory(testFolder.ToString() + "/dir1" + Slash + "dir2" + Slash + "dir3/");
9797

9898
string location = nestedPath.FullName + "/../../..";
9999

100100
// Act
101101
var path = ChainablePath.From(location);
102102

103103
// Assert
104-
path.ToString().Should().Be(testFolder.ToString().Trim(slash));
104+
path.ToString().Should().Be(testFolder.ToString().Trim(Slash));
105105
path.IsRooted.Should().BeTrue();
106106
}
107107

@@ -164,7 +164,7 @@ public void Can_start_with_an_empty_path()
164164
var path = ChainablePath.New / "c:" / "temp" / "somefile.txt";
165165

166166
// Assert
167-
path.ToString().Should().Be("c:" + slash + "temp" + slash + "somefile.txt");
167+
path.ToString().Should().Be("c:" + Slash + "temp" + Slash + "somefile.txt");
168168
}
169169

170170
[Fact]
@@ -178,7 +178,7 @@ public void Can_chain_a_relative_path_to_an_absolute_path()
178178
var path = absolutePath / relativePath;
179179

180180
// Assert
181-
path.ToString().Should().Be(testFolder + slash + "dir1" + slash + "somefile.txt");
181+
path.ToString().Should().Be(testFolder + Slash + "dir1" + Slash + "somefile.txt");
182182
}
183183

184184
[Fact]
@@ -188,7 +188,7 @@ public void Can_be_assigned_to_a_string()
188188
string result = ChainablePath.From("temp/somefile.txt");
189189

190190
// Assert
191-
result.Should().Be("temp" + slash + "somefile.txt");
191+
result.Should().Be("temp" + Slash + "somefile.txt");
192192
}
193193

194194
[Fact]
@@ -201,7 +201,7 @@ public void Can_chain_multiple_directories()
201201
var result = temp / "dir1" / "dir2" / "dir3";
202202

203203
// Assert
204-
result.DirectoryName.Should().Be(temp + slash + "dir1" + slash + "dir2");
204+
result.DirectoryName.Should().Be(temp + Slash + "dir1" + Slash + "dir2");
205205
result.Name.Should().Be("dir3");
206206
}
207207

@@ -235,7 +235,7 @@ public void Can_chain_directories_and_files()
235235
var result = testFolder / "dir1" / "dir2" / "dir3" / "file.txt";
236236

237237
// Assert
238-
result.DirectoryName.Should().Be(testFolder + slash + "dir1" + slash + "dir2" + slash + "dir3");
238+
result.DirectoryName.Should().Be(testFolder + Slash + "dir1" + Slash + "dir2" + Slash + "dir3");
239239
result.Name.Should().Be("file.txt");
240240
}
241241

@@ -246,7 +246,7 @@ public void Ignores_superfluous_slashes()
246246
var result = testFolder / "dir1" / "dir2/" / "dir3/" / "file.txt";
247247

248248
// Assert
249-
result.DirectoryName.Should().Be(testFolder + slash + "dir1" + slash + "dir2" + slash + "dir3");
249+
result.DirectoryName.Should().Be(testFolder + Slash + "dir1" + Slash + "dir2" + Slash + "dir3");
250250
result.Name.Should().Be("file.txt");
251251
}
252252

@@ -341,10 +341,9 @@ public void A_trailing_slash_does_not_affect_the_directory(string path)
341341
var result = ChainablePath.From(path);
342342

343343
// Assert
344-
result.Directory!.ToString().Should().Be("C:" + slash);
344+
result.Directory!.ToString().Should().Be("C:" + Slash);
345345
}
346346

347-
348347
[Fact]
349348
public void The_root_does_not_have_a_parent_directory()
350349
{
@@ -513,4 +512,159 @@ public void Can_determine_if_a_path_refers_to_a_directory()
513512
path.IsFile.Should().BeFalse();
514513
path.IsDirectory.Should().BeTrue();
515514
}
515+
516+
[Fact]
517+
public void Can_find_the_first_existing_file_using_a_string_path()
518+
{
519+
// Arrange
520+
var existingFile = testFolder / "existing.txt";
521+
var nonExistingFile = testFolder / "nonexisting.txt";
522+
File.WriteAllText(existingFile, "content");
523+
524+
// Act
525+
var result = ChainablePath.FindFirst(nonExistingFile.ToString(), existingFile.ToString());
526+
527+
// Assert
528+
result.ToString().Should().Be(existingFile.ToString());
529+
result.FileExists.Should().BeTrue();
530+
}
531+
532+
[Fact]
533+
public void Can_find_the_first_existing_file_using_a_chainable_path()
534+
{
535+
// Arrange
536+
var existingFile = testFolder / "existing.txt";
537+
var nonExistingFile = testFolder / "nonexisting.txt";
538+
File.WriteAllText(existingFile, "content");
539+
540+
// Act
541+
var result = ChainablePath.FindFirst(nonExistingFile, existingFile);
542+
543+
// Assert
544+
result.ToString().Should().Be(existingFile.ToString());
545+
result.FileExists.Should().BeTrue();
546+
}
547+
548+
[Fact]
549+
public void Can_find_the_first_existing_directory_using_a_string_path()
550+
{
551+
// Arrange
552+
var existingDir = testFolder / "existing-dir";
553+
var nonExistingDir = testFolder / "nonexisting-dir";
554+
existingDir.CreateDirectoryRecursively();
555+
556+
// Act
557+
var result = ChainablePath.FindFirst(nonExistingDir.ToString(), existingDir.ToString());
558+
559+
// Assert
560+
result.ToString().Should().Be(existingDir.ToString());
561+
result.DirectoryExists.Should().BeTrue();
562+
}
563+
564+
[Fact]
565+
public void Can_find_the_first_existing_directory_using_a_chainable_path()
566+
{
567+
// Arrange
568+
var existingDir = testFolder / "existing-dir";
569+
var nonExistingDir = testFolder / "nonexisting-dir";
570+
existingDir.CreateDirectoryRecursively();
571+
572+
// Act
573+
var result = ChainablePath.FindFirst(nonExistingDir, existingDir);
574+
575+
// Assert
576+
result.ToString().Should().Be(existingDir.ToString());
577+
result.DirectoryExists.Should().BeTrue();
578+
}
579+
580+
[Fact]
581+
public void Returns_empty_for_non_existing_string_paths()
582+
{
583+
// Arrange
584+
var nonExistingFile1 = testFolder / "nonexisting1.txt";
585+
var nonExistingFile2 = testFolder / "nonexisting2.txt";
586+
587+
// Act
588+
var result = ChainablePath.FindFirst(nonExistingFile1.ToString(), nonExistingFile2.ToString());
589+
590+
// Assert
591+
result.Should().Be(ChainablePath.Empty);
592+
}
593+
594+
[Fact]
595+
public void Returns_empty_for_non_existing_paths()
596+
{
597+
// Arrange
598+
var nonExistingFile1 = testFolder / "nonexisting1.txt";
599+
var nonExistingFile2 = testFolder / "nonexisting2.txt";
600+
601+
// Act
602+
var result = ChainablePath.FindFirst(nonExistingFile1, nonExistingFile2);
603+
604+
// Assert
605+
result.Should().Be(ChainablePath.Empty);
606+
}
607+
608+
[Fact]
609+
public void Cannot_find_the_first_existing_path_from_a_null_as_string_array()
610+
{
611+
// Act
612+
var act = () => ChainablePath.FindFirst((string[])null);
613+
614+
// Assert
615+
act.Should().Throw<ArgumentNullException>().WithParameterName("paths");
616+
}
617+
618+
[Fact]
619+
public void Cannot_find_the_first_existing_path_from_a_null_array()
620+
{
621+
// Act & Assert
622+
var act = () => ChainablePath.FindFirst((ChainablePath[])null);
623+
624+
// Assert
625+
act.Should().Throw<ArgumentNullException>().WithParameterName("paths");
626+
}
627+
628+
[Fact]
629+
public void Cannot_find_the_first_existing_path_from_an_empty_string_array()
630+
{
631+
// Act & Assert
632+
var act = () =>
633+
ChainablePath.FindFirst(new string[0]);
634+
635+
// Assert
636+
act.Should().Throw<ArgumentException>()
637+
.WithMessage("*At least one path must be provided*")
638+
.WithParameterName("paths");
639+
}
640+
641+
[Fact]
642+
public void Cannot_find_the_first_existing_path_from_an_empty_array()
643+
{
644+
// Act & Assert
645+
var act = () =>
646+
ChainablePath.FindFirst(new ChainablePath[0]);
647+
648+
act.Should().Throw<ArgumentException>()
649+
.WithMessage("*At least one path must be provided*")
650+
.WithParameterName("paths");
651+
}
652+
653+
[Theory]
654+
[InlineData("config.json", ".subdirectory/config.json")]
655+
[InlineData("app.config", "config/app.config", ".config/app.config")]
656+
public void FromFirstExisting_usage_examples_work_as_expected(params string[] paths)
657+
{
658+
// Arrange - create the second path in each case
659+
var testPath = testFolder / paths[1];
660+
testPath.Directory.CreateDirectoryRecursively();
661+
File.WriteAllText(testPath, "configuration content");
662+
663+
// Act
664+
var result = ChainablePath.FindFirst(paths.Select(p => testFolder / p).Select(p => p.ToString()).ToArray());
665+
666+
// Assert
667+
result.ToString().Should().Be(testPath.ToString());
668+
result.FileExists.Should().BeTrue();
669+
}
516670
}

Pathy/ChainablePath.cs

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,9 @@
22

33
using System;
44
using System.IO;
5+
using System.Linq;
6+
7+
// ReSharper disable UseIndexFromEndExpression
58

69
#pragma warning disable
710

@@ -75,6 +78,69 @@ public static ChainablePath From(string path)
7578
}
7679
}
7780

81+
/// <summary>
82+
/// Creates a new instance of <see cref="ChainablePath"/> representing the first existing path from the specified list of paths.
83+
/// </summary>
84+
/// <param name="paths">
85+
/// An array of string representations of paths to check for existence. Paths are checked in the order provided.
86+
/// </param>
87+
/// <returns>
88+
/// A <see cref="ChainablePath"/> object representing the first path that exists or <see cref="Empty"/> if none exist.
89+
/// </returns>
90+
/// <exception cref="ArgumentNullException">
91+
/// Thrown if the <paramref name="paths"/> array is null.
92+
/// </exception>
93+
/// <exception cref="ArgumentException">
94+
/// Thrown if the <paramref name="paths"/> array is empty.
95+
/// </exception>
96+
public static ChainablePath FindFirst(params string[] paths)
97+
{
98+
if (paths == null)
99+
{
100+
throw new ArgumentNullException(nameof(paths));
101+
}
102+
103+
return FindFirst(paths.Select(From).ToArray());
104+
}
105+
106+
/// <summary>
107+
/// Creates a new instance of <see cref="ChainablePath"/> representing the first existing path from the specified list of paths.
108+
/// </summary>
109+
/// <param name="paths">
110+
/// An array of <see cref="ChainablePath"/> instances to check for existence. Paths are checked in the order provided.
111+
/// </param>
112+
/// <returns>
113+
/// A <see cref="ChainablePath"/> object representing the first path that exists or <see cref="Empty"/> if none exist.
114+
/// </returns>
115+
/// <exception cref="ArgumentNullException">
116+
/// Thrown if the <paramref name="paths"/> array is null.
117+
/// </exception>
118+
/// <exception cref="ArgumentException">
119+
/// Thrown if the <paramref name="paths"/> array is empty.
120+
/// </exception>
121+
public static ChainablePath FindFirst(params ChainablePath[] paths)
122+
{
123+
if (paths == null)
124+
{
125+
throw new ArgumentNullException(nameof(paths));
126+
}
127+
128+
if (paths.Length == 0)
129+
{
130+
throw new ArgumentException("At least one path must be provided", nameof(paths));
131+
}
132+
133+
foreach (var path in paths)
134+
{
135+
if (path.Exists)
136+
{
137+
return path;
138+
}
139+
}
140+
141+
return Empty;
142+
}
143+
78144
private static string NormalizeSlashes(string path)
79145
{
80146
return path.Replace(Path.AltDirectorySeparatorChar, Path.DirectorySeparatorChar);

0 commit comments

Comments
 (0)