diff --git a/includes/Support/Path.php b/includes/Support/Path.php new file mode 100644 index 000000000..fa8c04ddb --- /dev/null +++ b/includes/Support/Path.php @@ -0,0 +1,68 @@ +children = $path; + } + + public static function of(string $path): Path + { + return new Path(\preg_split("#[\\\\/]#", $path)); + } + + public function append(string $child): Path + { + return new Path([...$this->children, $child]); + } + + public function toString(): string + { + return \join(\DIRECTORY_SEPARATOR, \iterator_to_array($this->children())); + } + + private function children(): Iterator + { + if (empty($this->children)) { + return new EmptyIterator(); + } + return $this->normalizedChildren(); + } + + private function normalizedChildren(): Iterator + { + [$root, $children] = $this->rootAndChildren(); + yield \rTrim($root, "/\\"); + foreach ($children as $child) { + if ($child === "") { + continue; + } + yield \trim($child, "/\\"); + } + } + + private function rootAndChildren(): array + { + $root = \reset($this->children); + return [$root, \array_slice($this->children, 1)]; + } +} diff --git a/tests/Psr4/Concerns/PhpunitConcern.php b/tests/Psr4/Concerns/PhpunitConcern.php new file mode 100644 index 000000000..19daabe17 --- /dev/null +++ b/tests/Psr4/Concerns/PhpunitConcern.php @@ -0,0 +1,23 @@ +expectException(Exception::class); + throw new Exception($message); + } +} diff --git a/tests/Psr4/PhpUnitPolyfill.php b/tests/Psr4/PhpUnitPolyfill.php new file mode 100644 index 000000000..6e0d9f844 --- /dev/null +++ b/tests/Psr4/PhpUnitPolyfill.php @@ -0,0 +1,16 @@ +toString(); + // then + $this->assertSame("", $asString); + } + + /** + * @test + */ + public function shouldIgnoreEmptyChildren() + { + // given + $path = new Path(["string", "", "", "string"]); + // when + $asString = $path->toString(); + // then + $this->assertSameWindows("string\string", $asString); + $this->assertSameUnix("string/string", $asString); + } + + /** + * @test + * @dataProvider fileNames + */ + public function shouldGetSingleFile(string $filename) + { + // given + $path = new Path([$filename]); + // when + $asString = $path->toString(); + // then + $this->assertSame("file.txt", $asString); + } + + public function fileNames(): array + { + return [["file.txt"], ["file.txt/"], ["file.txt\\"]]; + } + + /** + * @test + * @dataProvider pathPieces + */ + public function shouldGetManyFiles(array $pathPieces) + { + // given + $path = new Path($pathPieces); + // when + $asString = $path->toString(); + // then + $this->assertSameWindows('first\second\third\file.txt', $asString); + $this->assertSameUnix("first/second/third/file.txt", $asString); + } + + public function pathPieces(): array + { + return [ + [["first", "second", "third", "file.txt"]], + + [["first", "/second", "/third", "/file.txt"]], + [["first", "\second", '\third', '\file.txt']], + + [["first/", "second/", "third/", "file.txt"]], + [["first\\", "second\\", "third\\", "file.txt"]], + + [["first/", "/second/", "/third/", "/file.txt"]], + [["first\\", "\\second\\", '\\third\\', '\\file.txt']], + ]; + } + + /** + * @test + * @dataProvider paths + */ + public function shouldRepresentPath(string $stringPath) + { + // given + $path = Path::of($stringPath); + // when + $asString = $path->toString(); + // then + $this->assertSameWindows('one\two\three\file.txt', $asString); + $this->assertSameUnix("one/two/three/file.txt", $asString); + } + + public function paths(): array + { + return [['one\two\three\file.txt'], ["one/two/three/file.txt"]]; + } + + /** + * @test + * @dataProvider children + */ + public function shouldAppendPath(Path $path, string $appendant) + { + // when + $childPath = $path->append($appendant); + // then + $this->assertSameWindows('uno\dos\tres', $childPath->toString()); + $this->assertSameUnix("uno/dos/tres", $childPath->toString()); + } + + public function children(): array + { + return [ + [Path::of("uno/dos"), "tres"], + [Path::of("uno/dos"), '\tres'], + [Path::of("uno/dos"), "/tres"], + [Path::of("uno/dos"), "tres/"], + [Path::of("uno/dos"), "tres\\"], + + [Path::of("uno/dos/"), "tres"], + [Path::of("uno/dos/"), '\tres'], + [Path::of("uno/dos/"), "/tres"], + + [Path::of("uno/dos\\"), "tres"], + [Path::of("uno/dos\\"), '\tres'], + [Path::of("uno/dos\\"), "/tres"], + ]; + } + + /** + * @test + */ + public function shouldAcceptPathWithDriveOnWindows() + { + if ($this->isUnix()) { + $this->markTestUnnecessary("There are no drives on Unix"); + } + // given + $path = Path::of("C:\directory"); + // when + $child = $path->append("file.txt"); + // then + $this->assertSame('C:\directory\file.txt', $child->toString()); + } + + /** + * @test + */ + public function shouldRemainAbsolutePathOnUnix() + { + if (!$this->isUnix()) { + $this->markTestUnnecessary("There are no leading separators on Windows"); + } + // given + $path = Path::of("/usr/bin"); + // when + $child = $path->append("local"); + // then + $this->assertSame("/usr/bin/local", $child->toString()); + } + + /** + * @test + */ + public function shouldBeImmutable() + { + // given + $path = Path::of("one/two/three"); + // when + $this->assertSame($path->toString(), $path->toString()); + } +}