Skip to content

Commit

Permalink
Add Path
Browse files Browse the repository at this point in the history
  • Loading branch information
danon committed May 31, 2022
1 parent 6c2f999 commit 9909b3c
Show file tree
Hide file tree
Showing 3 changed files with 275 additions and 0 deletions.
68 changes: 68 additions & 0 deletions includes/Support/Path.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
<?php
namespace App\Support;

use EmptyIterator;
use Iterator;

/**
* Currently there is no notion of "relative" or "absolute"
* path as of {@see Path} object, because there is no functionality
* of joining paths together. Method {@see Path::toString}
* currently returns paths as absolute paths, but {@see Path}
* itself can be thought of as both absolute and relative.
* The distinction will have to be made when there are methods
* in this class that allow joining paths - those methods
* will have to decide whether to treat other {@see Path} as
* relative or absolute.
*/
class Path
{
/** @var string[] */
private array $children;

public function __construct(array $path)
{
$this->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)];
}
}
23 changes: 23 additions & 0 deletions tests/Psr4/Concerns/PhpunitConcern.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
<?php
namespace Tests\Psr4\Concerns;

use Exception;

/**
* @method expectException(string $message)
*/
trait PhpunitConcern
{
/**
* This test really is marked as unnecessary. If the condition is not met,
* marking it as unnecessary is preferable to marking it as risky, incomplete
* or skipped.
*
* This test simply doesn't make sense, if the condition is not met.
*/
public function markTestUnnecessary(string $message): void
{
$this->expectException(Exception::class);
throw new Exception($message);
}
}
184 changes: 184 additions & 0 deletions tests/Unit/Support/PathTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,184 @@
<?php
namespace Support;

use App\Support\Path;
use PHPUnit\Framework\TestCase;
use Tests\Psr4\Concerns\PhpunitConcern;
use Tests\Psr4\Concerns\SystemConcern;

class PathTest extends TestCase
{
use SystemConcern, PhpunitConcern;

/**
* @test
*/
public function shouldGetEmptyPath()
{
// given
$path = new Path([]);
// when
$asString = $path->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());
}
}

0 comments on commit 9909b3c

Please sign in to comment.