diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 0c48d37..75d191e 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -4,15 +4,13 @@ on: [push] jobs: blackbox: - uses: innmind/github-workflows/.github/workflows/black-box-matrix.yml@main + uses: innmind/github-workflows/.github/workflows/black-box-matrix.yml@next with: scenarii: 20 coverage: - uses: innmind/github-workflows/.github/workflows/coverage-matrix.yml@main + uses: innmind/github-workflows/.github/workflows/coverage-matrix.yml@next secrets: inherit psalm: - uses: innmind/github-workflows/.github/workflows/psalm-matrix.yml@main + uses: innmind/github-workflows/.github/workflows/psalm-matrix.yml@next cs: - uses: innmind/github-workflows/.github/workflows/cs.yml@main - with: - php-version: '8.2' + uses: innmind/github-workflows/.github/workflows/cs.yml@next diff --git a/.github/workflows/extensive.yml b/.github/workflows/extensive.yml new file mode 100644 index 0000000..257f139 --- /dev/null +++ b/.github/workflows/extensive.yml @@ -0,0 +1,12 @@ +name: Extensive CI + +on: + push: + tags: + - '*' + paths: + - '.github/workflows/extensive.yml' + +jobs: + blackbox: + uses: innmind/github-workflows/.github/workflows/extensive.yml@main diff --git a/.gitignore b/.gitignore index b292da5..a696500 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,3 @@ composer.lock vendor -.phpunit.result.cache .cache diff --git a/CHANGELOG.md b/CHANGELOG.md index af9fb77..911b0c6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,30 @@ # Changelog +## [Unreleased] + +### Added + +- `Innmind\Filesystem\Recover` + +### Changed + +- Requires PHP `8.4` +- Links are not silently ignored when listing files/directories. An error is still returned when trying to remove a link. +- `Innmind\Filesystem\Adapter\Filesystem::mount()` now returns an `Innmind\Immutable\Attempt` +- `Innmind\Filesystem\Directory::removed()` is now flagged as internal +- `Innmind\Filesystem\Adapter` is now a final class +- `Innmind\Filesystem\Adapter\Filesystem` is now flagged as internal +- `Innmind\Filesystem\Adapter\InMemory` is now flagged as internal +- `Innmind\Filesystem\Adapter\Logger` is now flagged as internal +- `Innmind\Filesystem\Adapter::mount()` no longer automatically create the directory if it doesn't exist + +### Removed + +- `Innmind\Filesystem\Adapter\InMemory::new()` +- `Innmind\Filesystem\Adapter\Filesystem::withCaseSensitivity()`, case sensitivity can be specified as the second argument of `::mount()` +- `Innmind\Filesystem\Exception\*` +- `Innmind\Filesystem\File\Content::atPath()`, use `Content::io()` instead + ## 8.1.0 - 2025-05-09 ### Added diff --git a/blackbox.php b/blackbox.php index a8adaf5..a2e2be2 100644 --- a/blackbox.php +++ b/blackbox.php @@ -26,9 +26,6 @@ ) ->scenariiPerProof(1), ) - ->when( - \method_exists(Application::class, 'allowProofsToNotMakeAnyAssertions'), - static fn($app) => $app->allowProofsToNotMakeAnyAssertions(), - ) + ->allowProofsToNotMakeAnyAssertions() ->tryToProve(Load::everythingIn(__DIR__.'/proofs/')) ->exit(); diff --git a/composer.json b/composer.json index 0434cda..f54b67a 100644 --- a/composer.json +++ b/composer.json @@ -15,14 +15,16 @@ "issues": "http://github.com/Innmind/filesystem/issues" }, "require": { - "php": "~8.2", - "innmind/immutable": "~4.15|~5.0", - "symfony/filesystem": "~6.0|~7.0", - "innmind/media-type": "~2.1", - "innmind/url": "~4.2", + "php": "~8.4", + "innmind/immutable": "dev-next", + "innmind/media-type": "dev-next", + "innmind/url": "dev-next", "psr/log": "~3.0", - "innmind/io": "^3.0.1", - "innmind/validation": "~2.0" + "innmind/io": "dev-next", + "innmind/validation": "dev-next", + "innmind/time-continuum": "dev-next", + "innmind/ip": "dev-next", + "innmind/mutable": "dev-next" }, "autoload": { "psr-4": { @@ -38,8 +40,9 @@ }, "require-dev": { "innmind/static-analysis": "^1.2.1", - "innmind/black-box": "^6.0.2", + "innmind/black-box": "~6.5", "innmind/coding-standard": "~2.0", + "symfony/filesystem": "~6.0|~7.0", "ramsey/uuid": "^4.6" }, "conflict": { diff --git a/phpunit.xml.dist b/phpunit.xml.dist deleted file mode 100644 index 9d3a4b0..0000000 --- a/phpunit.xml.dist +++ /dev/null @@ -1,26 +0,0 @@ - - - - - - - - - - - - - - ./tests - - - - - . - - - ./tests - ./vendor - - - diff --git a/proofs/adapter/filesystem.php b/proofs/adapter/filesystem.php index f66a8a0..b6c4c47 100644 --- a/proofs/adapter/filesystem.php +++ b/proofs/adapter/filesystem.php @@ -2,67 +2,115 @@ declare(strict_types = 1); use Innmind\Filesystem\{ - Adapter\Filesystem, + Adapter, Directory, File, File\Content, CaseSensitivity, + Recover, +}; +use Innmind\IO\{ + IO, + Simulation\Disk, }; use Innmind\Url\Path; -use Properties\Innmind\Filesystem\Adapter; +use Properties\Innmind\Filesystem\Adapter as PAdapter; use Innmind\BlackBox\Set; use Symfony\Component\Filesystem\Filesystem as FS; return static function() { + $path = \rtrim(\sys_get_temp_dir(), '/').'/innmind/filesystem/'; + yield properties( 'Filesystem properties', - Adapter::properties(), - Set::call(static function() { - $path = \sys_get_temp_dir().'/innmind/filesystem/'; + PAdapter::properties(), + Set::call(static function() use ($path) { (new FS)->remove($path); - return Filesystem::mount(Path::of($path))->withCaseSensitivity(match (\PHP_OS) { - 'Darwin' => CaseSensitivity::insensitive, - default => CaseSensitivity::sensitive, - }); + return Adapter::mount( + Path::of($path), + match (\PHP_OS) { + 'Darwin' => CaseSensitivity::insensitive, + default => CaseSensitivity::sensitive, + }, + ) + ->recover(Recover::mount(...)) + ->unwrap(); }), ); - foreach (Adapter::alwaysApplicable() as $property) { + foreach (PAdapter::alwaysApplicable() as $property) { yield property( $property, - Set::call(static function() { - $path = \sys_get_temp_dir().'/innmind/filesystem/'; + Set::call(static function() use ($path) { (new FS)->remove($path); - return Filesystem::mount(Path::of($path))->withCaseSensitivity(match (\PHP_OS) { - 'Darwin' => CaseSensitivity::insensitive, - default => CaseSensitivity::sensitive, - }); + return Adapter::mount( + Path::of($path), + match (\PHP_OS) { + 'Darwin' => CaseSensitivity::insensitive, + default => CaseSensitivity::sensitive, + }, + ) + ->recover(Recover::mount(...)) + ->unwrap(); }), )->named('Filesystem'); } yield test( 'Regression adding file in directory due to case sensitivity', - static function($assert) { - $property = new Adapter\AddRemoveAddModificationsStillAddTheFile( + static function($assert) use ($path) { + $property = new PAdapter\AddRemoveAddModificationsStillAddTheFile( Directory::named('0') ->add($file = File::named('L', Content::none())) ->remove($file->name()), File::named('l', Content::none()), ); - $path = \sys_get_temp_dir().'/innmind/filesystem/'; (new FS)->remove($path); - $adapter = Filesystem::mount(Path::of($path))->withCaseSensitivity(match (\PHP_OS) { - 'Darwin' => CaseSensitivity::insensitive, - default => CaseSensitivity::sensitive, - }); + $adapter = Adapter::mount( + Path::of($path), + match (\PHP_OS) { + 'Darwin' => CaseSensitivity::insensitive, + default => CaseSensitivity::sensitive, + }, + ) + ->recover(Recover::mount(...)) + ->unwrap(); $property->ensureHeldBy($assert, $adapter); (new FS)->remove($path); }, ); + + foreach (CaseSensitivity::cases() as $case) { + yield properties( + 'Filesystem properties on simulated disk', + PAdapter::properties(), + Set::call(static fn() => Adapter::mount( + Path::of('/'), + $case, + IO::simulation( + IO::fromAmbientAuthority(), + Disk::new(), + ), + )->unwrap()), + ); + + foreach (PAdapter::alwaysApplicable() as $property) { + yield property( + $property, + Set::call(static fn() => Adapter::mount( + Path::of('/'), + $case, + IO::simulation( + IO::fromAmbientAuthority(), + Disk::new(), + ), + )->unwrap()), + )->named('Filesystem on simulated disk'); + } + } }; diff --git a/proofs/adapter/inMemory.php b/proofs/adapter/inMemory.php index c794480..7ba4045 100644 --- a/proofs/adapter/inMemory.php +++ b/proofs/adapter/inMemory.php @@ -1,30 +1,46 @@ named('InMemory'); + foreach (PAdapter::alwaysApplicable() as $property) { yield property( $property, - Set::call(InMemory::emulateFilesystem(...)), + Set::call(Adapter::inMemory(...)), )->named('InMemory emulating filesystem'); } + + yield test( + 'Adding a file in a directory should not remove other files starting with the same name', + static function($assert) { + $adapter = Adapter::inMemory(); + $property = new PAdapter\AddDirectoryFromAnotherAdapterWithFileAdded( + Name::of('0'), + File::named( + '+1', + Content::none(), + ), + File::named( + '+', + Content::none(), + ), + ); + + $property->ensureHeldBy($assert, $adapter); + }, + ); }; diff --git a/proofs/file/content.php b/proofs/file/content.php index 0b9ff5b..459196e 100644 --- a/proofs/file/content.php +++ b/proofs/file/content.php @@ -26,15 +26,6 @@ static fn($lines) => Model::ofString(\implode("\n", $lines)), ), ], - [ - 'Content::atPath()', - Set::of('LICENSE', 'CHANGELOG.md', 'composer.json') - ->map(Path::of(...)) - ->map(static fn($path) => Model::atPath( - $io, - $path, - )), - ], [ 'Content::io()', Set::of('LICENSE', 'CHANGELOG.md', 'composer.json') @@ -262,10 +253,12 @@ static function($assert, $a, $b) use ($io) { yield test( 'Content::ofChunks()->size() does not load the whole file in memory', static function($assert) use ($io) { - $atPath = Model::atPath( - $io, - Path::of('samples/sample.pdf'), - ); + $atPath = $io + ->files() + ->access(Path::of('samples/sample.pdf')) + ->map(static fn($file) => $file->read()) + ->map(Model::io(...)) + ->unwrap(); $content = Model::ofChunks($atPath->chunks()); $assert diff --git a/properties/Adapter/AddFileWithSameNameAsDirectoryDeleteTheDirectory.php b/properties/Adapter/AddFileWithSameNameAsDirectoryDeleteTheDirectory.php index cfa1594..91ffa89 100644 --- a/properties/Adapter/AddFileWithSameNameAsDirectoryDeleteTheDirectory.php +++ b/properties/Adapter/AddFileWithSameNameAsDirectoryDeleteTheDirectory.php @@ -51,7 +51,7 @@ public function applicableTo(object $adapter): bool public function ensureHeldBy(Assert $assert, object $adapter): object { - $adapter->remove($this->file->name())->unwrap(); + $_ = $adapter->remove($this->file->name())->unwrap(); $assert ->object($adapter->add($this->directory)->unwrap()) ->instance(SideEffect::class); diff --git a/properties/Adapter/AddRemoveAddModificationsStillAddTheFile.php b/properties/Adapter/AddRemoveAddModificationsStillAddTheFile.php index 33f484a..2a93d1f 100644 --- a/properties/Adapter/AddRemoveAddModificationsStillAddTheFile.php +++ b/properties/Adapter/AddRemoveAddModificationsStillAddTheFile.php @@ -48,7 +48,7 @@ public function applicableTo(object $adapter): bool public function ensureHeldBy(Assert $assert, object $adapter): object { - $adapter + $_ = $adapter ->add( $this ->directory diff --git a/properties/Adapter/AllRootFilesAreAccessible.php b/properties/Adapter/AllRootFilesAreAccessible.php index 3fe8a37..3c1b2b2 100644 --- a/properties/Adapter/AllRootFilesAreAccessible.php +++ b/properties/Adapter/AllRootFilesAreAccessible.php @@ -30,7 +30,7 @@ public function applicableTo(object $adapter): bool public function ensureHeldBy(Assert $assert, object $adapter): object { - $adapter + $_ = $adapter ->root() ->foreach(static function($file) use ($assert, $adapter) { $assert->true($adapter->contains($file->name())); diff --git a/properties/Adapter/ReAddingFilesHasNoSideEffect.php b/properties/Adapter/ReAddingFilesHasNoSideEffect.php index fd14a52..cd8daa2 100644 --- a/properties/Adapter/ReAddingFilesHasNoSideEffect.php +++ b/properties/Adapter/ReAddingFilesHasNoSideEffect.php @@ -30,10 +30,10 @@ public function applicableTo(object $adapter): bool public function ensureHeldBy(Assert $assert, object $adapter): object { - $adapter + $_ = $adapter ->root() ->foreach(static function($file) use ($assert, $adapter) { - $adapter->add($file)->unwrap(); + $_ = $adapter->add($file)->unwrap(); $assert->true($adapter->contains($file->name())); if ($file instanceof Directory) { diff --git a/properties/Adapter/RemoveAddRemoveModificationsDoesntAddTheFile.php b/properties/Adapter/RemoveAddRemoveModificationsDoesntAddTheFile.php index 5a74719..f7b0603 100644 --- a/properties/Adapter/RemoveAddRemoveModificationsDoesntAddTheFile.php +++ b/properties/Adapter/RemoveAddRemoveModificationsDoesntAddTheFile.php @@ -53,7 +53,7 @@ public function applicableTo(object $adapter): bool public function ensureHeldBy(Assert $assert, object $adapter): object { - $adapter + $_ = $adapter ->add( $this ->directory diff --git a/properties/Content/Lines.php b/properties/Content/Lines.php index 53a0a65..f9fef07 100644 --- a/properties/Content/Lines.php +++ b/properties/Content/Lines.php @@ -28,7 +28,7 @@ public function applicableTo(object $systemUnderTest): bool public function ensureHeldBy(Assert $assert, object $systemUnderTest): object { $content = $assert->string($systemUnderTest->toString()); - $systemUnderTest + $_ = $systemUnderTest ->lines() ->foreach(static fn($line) => $content->contains($line->toString())); diff --git a/properties/Content/Size.php b/properties/Content/Size.php index 9a7a786..ea86aa9 100644 --- a/properties/Content/Size.php +++ b/properties/Content/Size.php @@ -28,12 +28,13 @@ public function applicableTo(object $systemUnderTest): bool public function ensureHeldBy(Assert $assert, object $systemUnderTest): object { $expected = \mb_strlen($systemUnderTest->toString(), 'ascii'); - $systemUnderTest + $size = $systemUnderTest ->size() ->match( - static fn($size) => $assert->same($expected, $size->toInt()), + static fn($size) => $size->toInt(), static fn() => null, ); + $assert->same($expected, $size); return $systemUnderTest; } diff --git a/properties/Directory/ThrowWhenFlatMappingToSameFileTwice.php b/properties/Directory/ThrowWhenFlatMappingToSameFileTwice.php index 9bd1d10..4805bee 100644 --- a/properties/Directory/ThrowWhenFlatMappingToSameFileTwice.php +++ b/properties/Directory/ThrowWhenFlatMappingToSameFileTwice.php @@ -7,7 +7,6 @@ File, Directory, Name, - Exception\DuplicatedFile, }; use Innmind\Immutable\Sequence; use Innmind\BlackBox\{ @@ -71,7 +70,7 @@ public function ensureHeldBy(Assert $assert, object $directory): object } catch (\Exception $e) { $assert ->object($e) - ->instance(DuplicatedFile::class); + ->instance(\LogicException::class); } return $directory; diff --git a/properties/Directory/ThrowWhenMappingToSameFileTwice.php b/properties/Directory/ThrowWhenMappingToSameFileTwice.php index b61a87a..d1d382d 100644 --- a/properties/Directory/ThrowWhenMappingToSameFileTwice.php +++ b/properties/Directory/ThrowWhenMappingToSameFileTwice.php @@ -6,7 +6,6 @@ use Innmind\Filesystem\{ Directory, File, - Exception\DuplicatedFile, }; use Innmind\BlackBox\{ Property, @@ -54,7 +53,7 @@ public function ensureHeldBy(Assert $assert, object $directory): object } catch (\Exception $e) { $assert ->object($e) - ->instance(DuplicatedFile::class); + ->instance(\LogicException::class); } return $directory; diff --git a/src/Adapter.php b/src/Adapter.php index 01a9711..53a9c21 100644 --- a/src/Adapter.php +++ b/src/Adapter.php @@ -3,31 +3,223 @@ namespace Innmind\Filesystem; +use Innmind\Filesystem\{ + Adapter\Name as Name_, + Adapter\TreePath, + Adapter\Implementation, + Adapter\Filesystem, + Adapter\InMemory, + Adapter\Logger, + Exception\MountPathDoesntExist, + Exception\RecoverMount, +}; +use Innmind\IO\IO; +use Innmind\Url\Path; use Innmind\Immutable\{ - Maybe, Attempt, + Maybe, + Set, SideEffect, }; +use Psr\Log\LoggerInterface; /** * Layer between value objects and concrete implementation */ -interface Adapter +final class Adapter { + /** @var \WeakMap */ + private \WeakMap $loaded; + + private function __construct( + private Implementation $implementation, + private CaseSensitivity $case, + ) { + /** @var \WeakMap */ + $this->loaded = new \WeakMap; + } + + /** + * @return Attempt + */ + public static function mount( + Path $path, + CaseSensitivity $case = CaseSensitivity::sensitive, + ?IO $io = null, + ): Attempt { + return Filesystem::mount($path, $io) + ->map(static fn($implementation) => new self( + $implementation, + $case, + )) + ->mapError(static fn($e) => match (true) { + $e instanceof MountPathDoesntExist => new RecoverMount( + static fn() => $e + ->recover() + ->map(static fn($implementation) => new self( + $implementation, + $case, + )), + ), + default => $e, + }); + } + + public static function inMemory(): self + { + return new self( + InMemory::emulateFilesystem(), + CaseSensitivity::sensitive, + ); + } + + public static function logger( + self $adapter, + LoggerInterface $logger, + ): self { + return new self( + Logger::psr($adapter->implementation, $logger), + $adapter->case, + ); + } + + /** + * @return Attempt + */ + public function add(File|Directory $file): Attempt + { + return $this->write(TreePath::root(), $file); + } + + /** + * @return Maybe + */ + public function get(Name $file): Maybe + { + return $this->access( + TreePath::root(), + Name_\Unknown::of($file), + ); + } + + public function contains(Name $file): bool + { + return $this->implementation->exists(TreePath::of($file))->match( + static fn($exists) => $exists, + static fn() => false, + ); + } + /** * @return Attempt */ - public function add(File|Directory $file): Attempt; + public function remove(Name $file): Attempt + { + return $this->implementation->remove(TreePath::root(), $file); + } + + public function root(): Directory + { + $root = TreePath::root(); + + return Directory::named( + 'root', + $this + ->implementation + ->list($root) + ->map(fn($name) => $this->access($root, $name)) + ->flatMap(static fn($read) => $read->toSequence()), + ); + } /** * @return Maybe */ - public function get(Name $file): Maybe; - public function contains(Name $file): bool; + private function access( + TreePath $path, + Name_\File|Name_\Directory|Name_\Unknown $name, + ): Maybe { + $fullPath = TreePath::of($name->unwrap())->under($path); + + return $this + ->implementation + ->access($path, $name) + ->maybe() + ->map(fn($file) => match (true) { + $file instanceof File => $file, + default => Directory::of( + $file->unwrap(), + $this + ->implementation + ->list(TreePath::directory($name->unwrap())->under($path)) + ->map(fn($file) => $this->access( + $fullPath, + $file, + )) + ->flatMap(static fn($read) => $read->toSequence()), + ), + }) + ->map(function($file) use ($fullPath) { + $this->loaded[$file] = $fullPath; + + return $file; + }); + } /** * @return Attempt */ - public function remove(Name $file): Attempt; - public function root(): Directory; + private function write(TreePath $path, File|Directory $file): Attempt + { + $fullPath = TreePath::of($file)->under($path); + + /** @psalm-suppress PossiblyNullReference */ + if ( + $this->loaded->offsetExists($file) && + $this->loaded[$file]->equals($fullPath) + ) { + // no need to persist untouched file where it was loaded from + return Attempt::result(SideEffect::identity); + } + + $this->loaded[$file] = $fullPath; + + if ($file instanceof Directory) { + /** @var Set */ + $names = Set::of(); + + return $this + ->implementation + ->createDirectory($path, $file->name()) + ->flatMap( + fn() => $file + ->all() + ->sink($names) + ->attempt( + fn($persisted, $file) => $this + ->write($fullPath, $file) + ->map(static fn() => ($persisted)($file->name())), + ), + ) + ->flatMap( + fn($persisted) => $file + ->removed() + ->exclude(fn($file): bool => $this->case->contains( + $file, + $persisted, + )) + ->unsorted() + ->sink(SideEffect::identity) + ->attempt(fn($_, $file) => $this->implementation->remove($fullPath, $file)), + ); + } + + return $this + ->implementation + ->remove($path, $file->name()) + ->flatMap(fn() => $this->implementation->write( + $path, + $file, + )); + } } diff --git a/src/Adapter/Filesystem.php b/src/Adapter/Filesystem.php index cf01b47..633247c 100644 --- a/src/Adapter/Filesystem.php +++ b/src/Adapter/Filesystem.php @@ -4,263 +4,223 @@ namespace Innmind\Filesystem\Adapter; use Innmind\Filesystem\{ - Adapter, + Adapter\Name as Name_, File, + File\Content, Name, - Directory, - CaseSensitivity, - Exception\PathDoesntRepresentADirectory, - Exception\PathTooLong, - Exception\LinksAreNotSupported, + Exception\MountPathDoesntExist, +}; +use Innmind\IO\{ + IO, + Files, }; -use Innmind\IO\IO; use Innmind\MediaType\MediaType; use Innmind\Url\Path; use Innmind\Immutable\{ Sequence, Str, - Maybe, Attempt, SideEffect, - Set, }; -use Symfony\Component\Filesystem\Filesystem as FS; -final class Filesystem implements Adapter +final class Filesystem implements Implementation { - private const INVALID_FILES = ['.', '..']; - private IO $io; - private Path $path; - private CaseSensitivity $case; - private FS $filesystem; - /** @var \WeakMap */ - private \WeakMap $loaded; - private function __construct( - IO $io, - Path $path, - CaseSensitivity $case, + private IO $io, + private Path $path, ) { - if (!$path->directory()) { - throw new PathDoesntRepresentADirectory($path->toString()); - } - - $this->io = $io; - $this->path = $path; - $this->case = $case; - $this->filesystem = new FS; - /** @var \WeakMap */ - $this->loaded = new \WeakMap; - - if (!$this->filesystem->exists($this->path->toString())) { - $this->filesystem->mkdir($this->path->toString()); - } } + /** + * @return Attempt + */ public static function mount( Path $path, ?IO $io = null, - ): self { - return new self( - $io ?? IO::fromAmbientAuthority(), - $path, - CaseSensitivity::sensitive, - ); - } + ): Attempt { + if (!$path->directory()) { + return Attempt::error(new \LogicException(\sprintf( + "Path doesn't represent a directory '%s'", + $path->toString(), + ))); + } - public function withCaseSensitivity(CaseSensitivity $case): self - { - return new self($this->io, $this->path, $case); + $io ??= IO::fromAmbientAuthority(); + + return self::assert($path) + ->map($io->files()->exists(...)) + ->flatMap(static fn($exist) => match ($exist) { + false => Attempt::error(new MountPathDoesntExist( + static fn() => $io + ->files() + ->create($path) + ->map(static fn() => new self( + $io, + $path, + )), + )), + default => Attempt::result(SideEffect::identity), + }) + ->map(static fn() => new self( + $io, + $path, + )); } #[\Override] - public function add(File|Directory $file): Attempt + public function exists(TreePath $path): Attempt { - return $this->createFileAt($this->path, $file); + return self::assert($path->asPath($this->path))->map( + $this->io->files()->exists(...), + ); } #[\Override] - public function get(Name $file): Maybe - { - if (!$this->contains($file)) { - /** @var Maybe */ - return Maybe::nothing(); + public function access( + TreePath $parent, + Name_\File|Name_\Directory|Name_\Unknown $name, + ): Attempt { + if ($name instanceof Name_\Directory) { + return Attempt::result($name); } - return Maybe::just($this->open($this->path, $file)); + $name = $name->unwrap(); + $path = TreePath::of($name) + ->under($parent) + ->asPath($this->path); + + return self::assert($path) + ->flatMap($this->io->files()->access(...)) + ->flatMap(static fn($file) => match (true) { + $file instanceof Files\Link => Attempt::error(new \RuntimeException('Links are not supported')), + default => Attempt::result($file), + }) + ->map(static fn($file) => match (true) { + $file instanceof Files\Directory => Name_\Directory::of($name), + default => File::of( + $name, + Content::io($file->read()), + $file + ->mediaType() + ->maybe() + ->flatMap(MediaType::maybe(...)) + ->match( + static fn($mediaType) => $mediaType, + static fn() => null, + ), + ), + }); } #[\Override] - public function contains(Name $file): bool + public function list(TreePath $parent): Sequence { - return $this->filesystem->exists($this->path->toString().'/'.$file->toString()); + return $this + ->io + ->files() + ->access($parent->asPath($this->path)) + ->flatMap(static fn($file) => match (true) { + $file instanceof Files\Directory => Attempt::result($file), + default => Attempt::error(new \RuntimeException('Path is not a directory')), + }) + ->unwrap() // todo silently return an empty sequence ? + ->list() + ->map(static fn($name) => match ($name->kind()) { + Files\Kind::directory => Name_\Directory::of(Name::of($name->toString())), + default => Name_\File::of(Name::of($name->toString())), // let the read function handle the links + }); } + /** + * This method only relies on the returned boolean to know if the deletion + * was successful or not. It doesn't check afterward if the content is no + * longer there as it may lead to race conditions with other processes. + * + * Such race condition could be P1 removes a file, P2 creates the same file + * and then P1 check the file doesn't exist. This scenario would report a + * failure. + * + * This package doesn't want to bleed this global state between processes. + * If you end up here, know that you should design your app in a way that + * there is as little as possible race conditions like these. + */ #[\Override] - public function remove(Name $file): Attempt + public function remove(TreePath $parent, Name $name): Attempt { - return Attempt::of( - fn() => $this->filesystem->remove( - $this->path->toString().'/'.$file->toString(), - ), - )->map(static fn() => SideEffect::identity()); + $path = TreePath::of($name) + ->under($parent) + ->asPath($this->path); + + return self::assert($path) + ->map($this->io->files()->access(...)) + ->flatMap(static fn($file) => $file->eitherWay( + static fn($file) => match (true) { + $file instanceof Files\Link => Attempt::error(new \RuntimeException('Links are not supported')), + default => $file->remove(), + }, + static fn() => Attempt::result(SideEffect::identity), + )); } #[\Override] - public function root(): Directory + public function createDirectory(TreePath $parent, Name $name): Attempt { - return Directory::lazy( - Name::of('root'), - $this->list($this->path), - ); + $path = TreePath::directory($name) + ->under($parent) + ->asPath($this->path); + + return self::assert($path) + ->map($this->io->files()->access(...)) + ->flatMap(fn($file) => $file->eitherWay( + fn($file) => match (true) { + $file instanceof Files\Link => Attempt::error(new \RuntimeException('Links are not supported')), + $file instanceof Files\Directory => Attempt::result($file), + default => $this + ->remove($parent, $name) + ->flatMap( + fn() => $this + ->io + ->files() + ->create($path), + ), + }, + fn() => $this + ->io + ->files() + ->create($path), + )) + ->flatMap(static fn($file) => match (true) { + $file instanceof Files\Directory => Attempt::result(SideEffect::identity), + default => Attempt::error(new \RuntimeException('File created instead of a directory')), + }); } - /** - * Create the wished file at the given absolute path - * - * @return Attempt - */ - private function createFileAt(Path $path, File|Directory $file): Attempt + #[\Override] + public function write(TreePath $parent, File $file): Attempt { - $name = $file->name()->toString(); - - if ($file instanceof Directory) { - $name .= '/'; - } - - $path = $path->resolve(Path::of($name)); - - /** @psalm-suppress PossiblyNullReference */ - if ($this->loaded->offsetExists($file) && $this->loaded[$file]->equals($path)) { - // no need to persist untouched file where it was loaded from - return Attempt::result(SideEffect::identity()); - } - - $this->loaded[$file] = $path; - - if ($file instanceof Directory) { - /** @var Set */ - $names = Set::of(); - - return Attempt::of( - fn() => $this->filesystem->mkdir($path->toString()), - ) - ->flatMap( - fn() => $file - ->all() - ->sink($names) - ->attempt( - fn($persisted, $file) => $this - ->createFileAt($path, $file) - ->map(static fn() => ($persisted)($file->name())), - ), - ) - ->flatMap( - fn($persisted) => $file - ->removed() - ->filter(fn($file): bool => !$this->case->contains( - $file, - $persisted, - )) - ->unsorted() - ->sink(null) - ->attempt( - fn($_, $file) => Attempt::of( - fn() => $this->filesystem->remove( - $path->toString().$file->toString(), - ), - ), - ) - ->map(static fn() => SideEffect::identity()), - ); - } - - if (\is_dir($path->toString())) { - try { - $this->filesystem->remove($path->toString()); - } catch (\Throwable $e) { - return Attempt::error($e); - } - } - + $absolutePath = TreePath::of($file)->under($parent)->asPath($this->path); $chunks = $file->content()->chunks(); - try { - $this->filesystem->touch($path->toString()); - } catch (\Throwable $e) { - if (\PHP_OS === 'Darwin' && Str::of($path->toString(), Str\Encoding::ascii)->length() > 1014) { - return Attempt::error(new PathTooLong($path->toString(), 0, $e)); - } - - return Attempt::error($e); - } - - return $this - ->io - ->files() - ->write($path) - ->watch() - ->sink($chunks); + return self::assert($absolutePath) + ->flatMap($this->io->files()->create(...)) + ->flatMap(static fn($file) => match (true) { + $file instanceof Files\Directory => Attempt::error(new \RuntimeException('Directory created instead of a file')), + default => $file + ->write() + ->watch() + ->sink($chunks), + }); } /** - * Open the file in the given folder + * @return Attempt */ - private function open(Path $folder, Name $file): File|Directory + private static function assert(Path $path): Attempt { - $path = $folder->resolve(Path::of($file->toString())); - - if (\is_dir($path->toString())) { - $directoryPath = $folder->resolve(Path::of($file->toString().'/')); - $files = $this->list($directoryPath); - - $directory = Directory::lazy($file, $files); - $this->loaded[$directory] = $directoryPath; - - return $directory; + if (Str::of($path->toString())->length() > \PHP_MAXPATHLEN) { + return Attempt::error(new \RuntimeException('Path too long')); } - if (\is_link($path->toString())) { - throw new LinksAreNotSupported($path->toString()); - } - - $file = File::of( - $file, - File\Content::atPath( - $this->io, - $path, - ), - MediaType::maybe(match ($mediaType = @\mime_content_type($path->toString())) { - false => '', - default => $mediaType, - })->match( - static fn($mediaType) => $mediaType, - static fn() => MediaType::null(), - ), - ); - $this->loaded[$file] = $path; - - return $file; - } - - /** - * @return Sequence - */ - private function list(Path $path): Sequence - { - /** @var Sequence */ - return Sequence::lazy(function() use ($path): \Generator { - $files = new \FilesystemIterator($path->toString()); - - /** @var \SplFileInfo $file */ - foreach ($files as $file) { - if (\is_link($file->getPathname())) { - throw new LinksAreNotSupported($file->getPathname()); - } - - /** @psalm-suppress ArgumentTypeCoercion */ - yield $this->open($path, Name::of($file->getBasename())); - } - }); + return Attempt::result($path); } } diff --git a/src/Adapter/Implementation.php b/src/Adapter/Implementation.php new file mode 100644 index 0000000..5139275 --- /dev/null +++ b/src/Adapter/Implementation.php @@ -0,0 +1,54 @@ + + */ + public function exists(TreePath $path): Attempt; + + /** + * @return Attempt + */ + public function access( + TreePath $parent, + Name_\File|Name_\Directory|Name_\Unknown $name, + ): Attempt; + + /** + * @return Sequence + */ + public function list(TreePath $parent): Sequence; + + /** + * @return Attempt + */ + public function remove(TreePath $parent, Name $name): Attempt; + + /** + * @return Attempt + */ + public function createDirectory(TreePath $parent, Name $name): Attempt; + + /** + * @return Attempt + */ + public function write(TreePath $parent, File $file): Attempt; +} diff --git a/src/Adapter/InMemory.php b/src/Adapter/InMemory.php index 112746f..04d9895 100644 --- a/src/Adapter/InMemory.php +++ b/src/Adapter/InMemory.php @@ -4,106 +4,171 @@ namespace Innmind\Filesystem\Adapter; use Innmind\Filesystem\{ - Adapter, + Adapter\Name as Name_, File, Name, - Directory, }; +use Innmind\Url\Path; use Innmind\Immutable\{ - Maybe, + Str, + Map, Attempt, + Sequence, SideEffect, - Predicate\Instance, }; -final class InMemory implements Adapter +/** + * @internal + */ +final class InMemory implements Implementation { - private Directory $root; - - private function __construct() - { - $this->root = Directory::named('root'); - } - /** - * @deprecated Use self::emulateFilesystem() + * @param Map $files + * @param Map> $directories */ - public static function new(): self - { - return self::emulateFilesystem(); + private function __construct( + private Map $files, + private Map $directories, + ) { } public static function emulateFilesystem(): self { - return new self; + return new self( + Map::of(), + Map::of(), + ); } #[\Override] - public function add(File|Directory $file): Attempt + public function exists(TreePath $path): Attempt { - $this->root = $this->merge($this->root, $file); + $path = $this->path($path); - return Attempt::result(SideEffect::identity()); + return Attempt::result($this->files->contains($path) || $this->directories->contains("$path/")); } #[\Override] - public function get(Name $file): Maybe - { - return $this->root->get($file); + public function access( + TreePath $parent, + Name_\File|Name_\Directory|Name_\Unknown $name, + ): Attempt { + if ($name instanceof Name_\Directory) { + return $this + ->directories + ->get($this->path(TreePath::directory($name->unwrap())->under($parent))) + ->map(static fn() => $name) + ->attempt(static fn() => new \RuntimeException('Directory not found')); + } + + if ($name instanceof Name_\File) { + return $this + ->files + ->get($this->path(TreePath::of($name->unwrap())->under($parent))) + ->attempt(static fn() => new \RuntimeException('File not found')); + } + + return $this + ->access($parent, Name_\Directory::of($name->unwrap())) + ->recover(fn() => $this->access( + $parent, + Name_\File::of($name->unwrap()), + )); } #[\Override] - public function contains(Name $file): bool + public function list(TreePath $parent): Sequence { - return $this->root->contains($file); + $path = $this->path($parent); + + /** @psalm-suppress ArgumentTypeCoercion Due to Name::of() */ + return $this + ->directories + ->get($path) + ->toSequence() + ->flatMap(static fn($files) => $files) + ->map(fn($file) => match ($this->directories->contains("$path$file/")) { + true => Name_\Directory::of(Name::of($file)), + false => Name_\File::of(Name::of($file)), + }); } #[\Override] - public function remove(Name $file): Attempt + public function remove(TreePath $parent, Name $name): Attempt { - $this->root = $this->root->remove($file); + $asDirectory = $this->path(TreePath::directory($name)->under($parent)); + $this->files = $this + ->files + ->remove($this->path(TreePath::of($name)->under($parent))) + ->exclude(static fn($path) => Str::of($path)->startsWith($asDirectory)); + $parent = $this->path($parent); + $directories = $this + ->directories + ->exclude(static fn($path) => Str::of($path)->startsWith($asDirectory)); + $files = $directories + ->get($parent) + ->toSequence() + ->flatMap(static fn($files) => $files) + ->exclude(static fn($file) => $file === $name->toString()); + $this->directories = ($directories)( + $parent, + $files, + ); - return Attempt::result(SideEffect::identity()); + return Attempt::result(SideEffect::identity); } #[\Override] - public function root(): Directory + public function createDirectory(TreePath $parent, Name $name): Attempt { - return $this->root; - } + $path = $this->path(TreePath::directory($name)->under($parent)); + $asFile = Str::of($path) + ->dropEnd(1) // trailing / + ->toString(); - private function merge(Directory $parent, File|Directory $file): Directory - { - if (!$file instanceof Directory) { - return $parent->add($file); - } + $this->files = $this->files->remove($asFile); - $file = $parent - ->get($file->name()) - ->keep(Instance::of(Directory::class)) - ->match( - fn($existing) => $this->mergeDirectories($existing, $file), - static fn() => $file, + if (!$this->directories->contains($path)) { + $this->directories = ($this->directories)( + $path, + Sequence::strings(), ); + $parent = $this->path($parent); + $files = $this + ->directories + ->get($parent) + ->toSequence() + ->flatMap(static fn($files) => $files) + ->add($name->toString()); + $this->directories = ($this->directories)( + $parent, + $files, + ); + } - return $parent->add($file); + return Attempt::result(SideEffect::identity); } - private function mergeDirectories( - Directory $existing, - Directory $new, - ): Directory { - $existing = $new - ->removed() - ->filter(static fn($name) => !$new->contains($name)) - ->reduce( - $existing, - static fn(Directory $existing, $name) => $existing->remove($name), - ); + #[\Override] + public function write(TreePath $parent, File $file): Attempt + { + $fullPath = $this->path(TreePath::of($file)->under($parent)); + $parent = $this->path($parent); + + $this->files = ($this->files)($fullPath, $file); + $files = $this + ->directories + ->get($parent) + ->toSequence() + ->flatMap(static fn($files) => $files) + ->add($file->name()->toString()); + $this->directories = ($this->directories)($parent, $files); + + return Attempt::result(SideEffect::identity); + } - return $new->reduce( - $existing, - fn(Directory $directory, $file) => $this->merge($directory, $file), - ); + private function path(TreePath $path): string + { + return $path->asPath(Path::of('/'))->toString(); } } diff --git a/src/Adapter/Logger.php b/src/Adapter/Logger.php index 3f3f2c1..13e68dd 100644 --- a/src/Adapter/Logger.php +++ b/src/Adapter/Logger.php @@ -4,80 +4,124 @@ namespace Innmind\Filesystem\Adapter; use Innmind\Filesystem\{ - Adapter, + Adapter\Name as Name_, File, Name, - Directory, }; +use Innmind\Url\Path; use Innmind\Immutable\{ - Maybe, + Sequence, Attempt, }; use Psr\Log\LoggerInterface; -final class Logger implements Adapter +/** + * @internal + */ +final class Logger implements Implementation { - private Adapter $filesystem; - private LoggerInterface $logger; - - private function __construct(Adapter $filesystem, LoggerInterface $logger) - { - $this->filesystem = $filesystem; - $this->logger = $logger; + private function __construct( + private Implementation $implementation, + private LoggerInterface $logger, + ) { } - public static function psr(Adapter $filesystem, LoggerInterface $logger): self + public static function psr(Implementation $implementation, LoggerInterface $logger): self { - return new self($filesystem, $logger); + return new self($implementation, $logger); } #[\Override] - public function add(File|Directory $file): Attempt + public function exists(TreePath $path): Attempt { - $this->logger->debug('Adding file {file}', ['file' => $file->name()->toString()]); + return $this + ->implementation + ->exists($path) + ->map(function($exists) use ($path) { + $this->logger->debug('Cheking if filesystem contains {file}', [ + 'file' => self::path($path), + 'contains' => $exists, + ]); - return $this->filesystem->add($file); + return $exists; + }); } #[\Override] - public function get(Name $file): Maybe - { + public function access( + TreePath $parent, + Name_\File|Name_\Directory|Name_\Unknown $name, + ): Attempt { return $this - ->filesystem - ->get($file) - ->map(function($file) { - $this->logger->debug( - 'Accessing file {name}', - ['name' => $file->name()->toString()], - ); + ->implementation + ->access($parent, $name) + ->map(function($file) use ($parent, $name) { + $this->logger->debug('Accessing file {file}', [ + 'file' => self::path(TreePath::of($name->unwrap())->under($parent)), + ]); return $file; }); } #[\Override] - public function contains(Name $file): bool + public function list(TreePath $parent): Sequence { - $contains = $this->filesystem->contains($file); - $this->logger->debug('Cheking if filesystem contains {file}', [ - 'file' => $file->toString(), - 'contains' => $contains, + $this->logger->debug('Listing files in {directory}', [ + 'directory' => self::path($parent), ]); - return $contains; + return $this->implementation->list($parent); + } + + #[\Override] + public function remove(TreePath $parent, Name $name): Attempt + { + return $this + ->implementation + ->remove($parent, $name) + ->map(function($_) use ($parent, $name) { + $this->logger->debug('File removed {file}', [ + 'file' => self::path(TreePath::of($name)->under($parent)), + ]); + + return $_; + }); } #[\Override] - public function remove(Name $file): Attempt + public function createDirectory(TreePath $parent, Name $name): Attempt { - $this->logger->debug('Removing file {file}', ['file' => $file->toString()]); + return $this + ->implementation + ->createDirectory($parent, $name) + ->map(function($_) use ($parent, $name) { + $this->logger->debug('Directory created {directory}', [ + 'directory' => self::path(TreePath::directory($name)->under($parent)), + ]); - return $this->filesystem->remove($file); + return $_; + }); } #[\Override] - public function root(): Directory + public function write(TreePath $parent, File $file): Attempt + { + return $this + ->implementation + ->write($parent, $file) + ->map(function($_) use ($parent, $file) { + $this->logger->debug('File written {file}', [ + 'file' => self::path(TreePath::of($file)->under($parent)), + 'mediaType' => $file->mediaType()->toString(), + ]); + + return $_; + }); + } + + private static function path(TreePath $path): string { - return $this->filesystem->root(); + return $path->asPath(Path::of('/'))->toString(); } } diff --git a/src/Adapter/Name/Directory.php b/src/Adapter/Name/Directory.php new file mode 100644 index 0000000..c933d23 --- /dev/null +++ b/src/Adapter/Name/Directory.php @@ -0,0 +1,31 @@ +name; + } +} diff --git a/src/Adapter/Name/File.php b/src/Adapter/Name/File.php new file mode 100644 index 0000000..80b7fe3 --- /dev/null +++ b/src/Adapter/Name/File.php @@ -0,0 +1,31 @@ +name; + } +} diff --git a/src/Adapter/Name/Unknown.php b/src/Adapter/Name/Unknown.php new file mode 100644 index 0000000..7f9e45f --- /dev/null +++ b/src/Adapter/Name/Unknown.php @@ -0,0 +1,31 @@ +name; + } +} diff --git a/src/Adapter/TreePath.php b/src/Adapter/TreePath.php new file mode 100644 index 0000000..378c733 --- /dev/null +++ b/src/Adapter/TreePath.php @@ -0,0 +1,123 @@ + $path + */ + private function __construct( + private Sequence $path, + private bool $directory, + ) { + } + + /** + * @psalm-pure + */ + public static function of(Name|File|Directory $file): self + { + return new self( + Sequence::of(match (true) { + $file instanceof Name => $file, + default => $file->name(), + }), + $file instanceof Directory, + ); + } + + /** + * @psalm-pure + */ + public static function directory(Name $file): self + { + return new self( + Sequence::of($file), + true, + ); + } + + /** + * @psalm-pure + */ + public static function root(): self + { + return new self( + Sequence::of(), + true, + ); + } + + public function under(self $parent): self + { + return new self( + $this->path->append($parent->path), + $this->directory, + ); + } + + public function equals(self $other): bool + { + $root = Path::of('/'); + + return $this->asPath($root)->equals($other->asPath($root)); + } + + public function asPath(Path $root): Path + { + if ($this->path->empty()) { + return $root; + } + + $path = $this + ->path + ->reverse() + ->map(static fn($name) => $name->str()->append('/')) + ->fold(new Concat); + + if (!$this->directory) { + $path = $path->dropEnd(1); // remove trailing '/' + } + + return $root->resolve(Path::of($path->toString())); + } + + /** + * @template R + * + * @param callable(Name, self, bool): R $file + * @param callable(): R $root + * + * @return R + */ + public function match( + callable $file, + callable $root, + ): mixed { + return $this->path->match( + fn($name, $parent) => $file( + $name, + new self($parent, true), // since there's a child the parent is necessarily a directory + $this->directory, + ), + $root, + ); + } +} diff --git a/src/Directory.php b/src/Directory.php index 5638458..63ce2ca 100644 --- a/src/Directory.php +++ b/src/Directory.php @@ -3,7 +3,6 @@ namespace Innmind\Filesystem; -use Innmind\Filesystem\Exception\DuplicatedFile; use Innmind\Immutable\{ Set, Sequence, @@ -16,29 +15,21 @@ */ final class Directory { - private Name $name; - /** @var Sequence */ - private Sequence $files; - /** @var Set */ - private Set $removed; - /** * @param Sequence $files * @param Set $removed */ - private function __construct(Name $name, Sequence $files, Set $removed) - { - $this->name = $name; - $this->files = $files; - $this->removed = $removed; + private function __construct( + private Name $name, + private Sequence $files, + private Set $removed, + ) { } /** * @psalm-pure * * @param Sequence|null $files - * - * @throws DuplicatedFile */ public static function of(Name $name, ?Sequence $files = null): self { @@ -54,8 +45,6 @@ public static function of(Name $name, ?Sequence $files = null): self * * @param non-empty-string $name * @param Sequence|null $files - * - * @throws DuplicatedFile */ public static function named(string $name, ?Sequence $files = null): self { @@ -102,7 +91,7 @@ public function add(File|self $file): self $this->name, $this ->files - ->filter(static fn(File|self $known): bool => !$known->name()->equals($file->name())) + ->exclude(static fn(File|self $known): bool => $known->name()->equals($file->name())) ->add($file), $this->removed, ); @@ -128,7 +117,7 @@ public function remove(Name $name): self { return new self( $this->name, - $this->files->filter(static fn(File|self $file) => !$file->name()->equals($name)), + $this->files->exclude(static fn(File|self $file) => $file->name()->equals($name)), ($this->removed)($name), ); } @@ -160,8 +149,6 @@ public function filter(callable $predicate): self /** * @param callable(File|self): File $map - * - * @throws DuplicatedFile */ public function map(callable $map): self { @@ -174,8 +161,6 @@ public function map(callable $map): self /** * @param callable(File|self): self $map - * - * @throws DuplicatedFile */ public function flatMap(callable $map): self { @@ -206,6 +191,8 @@ public function reduce($carry, callable $reducer) * This method should only be used for implementations of the Adapter * interface, normal users should never have to use this method * + * @internal + * * @return Set */ public function removed(): Set @@ -226,8 +213,6 @@ public function all(): Sequence * * @param Sequence $files * - * @throws DuplicatedFile - * * @return Sequence */ private static function safeguard(Sequence $files): Sequence @@ -235,7 +220,10 @@ private static function safeguard(Sequence $files): Sequence return $files->safeguard( Set::strings(), static fn(Set $names, $file) => match ($names->contains($file->name()->toString())) { - true => throw new DuplicatedFile($file->name()), + true => throw new \LogicException(\sprintf( + "Same file '%s' found multiple times", + $file->name()->toString(), + )), false => ($names)($file->name()->toString()), }, ); diff --git a/src/Exception/CannotPersistClosedStream.php b/src/Exception/CannotPersistClosedStream.php deleted file mode 100644 index fbace93..0000000 --- a/src/Exception/CannotPersistClosedStream.php +++ /dev/null @@ -1,8 +0,0 @@ -toString()}' found multiple times"); - } -} diff --git a/src/Exception/Exception.php b/src/Exception/Exception.php deleted file mode 100644 index 549d910..0000000 --- a/src/Exception/Exception.php +++ /dev/null @@ -1,8 +0,0 @@ - $recover + */ + public function __construct( + private \Closure $recover, + ) { + } + + /** + * @return Attempt + */ + public function recover(): Attempt + { + return ($this->recover)(); + } +} diff --git a/src/Exception/PathDoesntRepresentADirectory.php b/src/Exception/PathDoesntRepresentADirectory.php deleted file mode 100644 index 116ac28..0000000 --- a/src/Exception/PathDoesntRepresentADirectory.php +++ /dev/null @@ -1,8 +0,0 @@ - $recover + */ + public function __construct( + private \Closure $recover, + ) { + } + + /** + * @return Attempt + */ + public function recover(): Attempt + { + return ($this->recover)(); + } +} diff --git a/src/Exception/RuntimeException.php b/src/Exception/RuntimeException.php deleted file mode 100644 index 8e9c8de..0000000 --- a/src/Exception/RuntimeException.php +++ /dev/null @@ -1,8 +0,0 @@ -name = $name; - $this->content = $content; - $this->mediaType = $mediaType ?? MediaType::null(); } /** @@ -33,7 +26,7 @@ public static function of( Content $content, ?MediaType $mediaType = null, ): self { - return new self($name, $content, $mediaType); + return new self($name, $content, $mediaType ?? MediaType::null()); } /** @@ -46,7 +39,7 @@ public static function named( Content $content, ?MediaType $mediaType = null, ): self { - return new self(Name::of($name), $content, $mediaType); + return self::of(Name::of($name), $content, $mediaType); } public function name(): Name diff --git a/src/File/Content.php b/src/File/Content.php index f60b1aa..22e0ba0 100644 --- a/src/File/Content.php +++ b/src/File/Content.php @@ -8,12 +8,10 @@ Line, }; use Innmind\IO\{ - IO, Streams\Stream, Files\Read, Stream\Size, }; -use Innmind\Url\Path; use Innmind\Immutable\{ Str, Sequence, @@ -26,21 +24,8 @@ */ final class Content { - private Implementation $implementation; - - private function __construct(Implementation $implementation) + private function __construct(private Implementation $implementation) { - $this->implementation = $implementation; - } - - /** - * @psalm-pure - */ - public static function atPath( - IO $io, - Path $path, - ): self { - return new self(Content\AtPath::of($io, $path)); } /** diff --git a/src/File/Content/AtPath.php b/src/File/Content/AtPath.php deleted file mode 100644 index 57181d0..0000000 --- a/src/File/Content/AtPath.php +++ /dev/null @@ -1,109 +0,0 @@ -lines()->foreach($function); - } - - #[\Override] - public function map(callable $map): Implementation - { - return Lines::of($this->lines()->map($map)); - } - - #[\Override] - public function flatMap(callable $map): Implementation - { - return Lines::of($this->lines())->flatMap($map); - } - - #[\Override] - public function filter(callable $filter): Implementation - { - return Lines::of($this->lines()->filter($filter)); - } - - #[\Override] - public function lines(): Sequence - { - /** @psalm-suppress ImpureMethodCall */ - return $this - ->io - ->files() - ->read($this->path) - ->watch() - ->lines() - ->map(Line::fromStream(...)); - } - - #[\Override] - public function reduce($carry, callable $reducer) - { - return $this->lines()->reduce($carry, $reducer); - } - - #[\Override] - public function size(): Maybe - { - /** @psalm-suppress ImpureMethodCall */ - return $this - ->io - ->files() - ->read($this->path) - ->size(); - } - - #[\Override] - public function toString(): string - { - return $this - ->chunks() - ->fold(new Concat) - ->toString(); - } - - #[\Override] - public function chunks(): Sequence - { - /** @psalm-suppress ImpureMethodCall */ - return $this - ->io - ->files() - ->read($this->path) - ->watch() - ->chunks(8192); - } -} diff --git a/src/File/Content/Chunks.php b/src/File/Content/Chunks.php index ea829aa..ba1b9c7 100644 --- a/src/File/Content/Chunks.php +++ b/src/File/Content/Chunks.php @@ -19,15 +19,11 @@ */ final class Chunks implements Implementation { - /** @var Sequence */ - private Sequence $chunks; - /** * @param Sequence $chunks */ - private function __construct(Sequence $chunks) + private function __construct(private Sequence $chunks) { - $this->chunks = $chunks->pad(1, Str::of('')); } /** @@ -37,7 +33,7 @@ private function __construct(Sequence $chunks) */ public static function of(Sequence $chunks): self { - return new self($chunks); + return new self($chunks->pad(1, Str::of(''))); } #[\Override] diff --git a/src/File/Content/Line.php b/src/File/Content/Line.php index f4c350f..5617680 100644 --- a/src/File/Content/Line.php +++ b/src/File/Content/Line.php @@ -3,7 +3,6 @@ namespace Innmind\Filesystem\File\Content; -use Innmind\Filesystem\Exception\DomainException; use Innmind\Immutable\Str; /** @@ -11,11 +10,8 @@ */ final class Line { - private Str $content; - - private function __construct(Str $content) + private function __construct(private Str $content) { - $this->content = $content; } /** @@ -24,7 +20,7 @@ private function __construct(Str $content) public static function of(Str $content): self { if ($content->contains("\n")) { - throw new DomainException('New line delimiter should not appear in the line content'); + throw new \DomainException('New line delimiter should not appear in the line content'); } return new self($content); diff --git a/src/File/Content/Lines.php b/src/File/Content/Lines.php index a932372..46d374a 100644 --- a/src/File/Content/Lines.php +++ b/src/File/Content/Lines.php @@ -17,15 +17,11 @@ */ final class Lines implements Implementation { - /** @var Sequence */ - private Sequence $lines; - /** * @param Sequence $lines */ - private function __construct(Sequence $lines) + private function __construct(private Sequence $lines) { - $this->lines = $lines->pad(1, Line::of(Str::of(''))); } /** @@ -35,7 +31,7 @@ private function __construct(Sequence $lines) */ public static function of(Sequence $lines): self { - return new self($lines); + return new self($lines->pad(1, Line::of(Str::of('')))); } #[\Override] @@ -53,7 +49,7 @@ public function map(callable $map): Implementation #[\Override] public function flatMap(callable $map): Implementation { - return new self($this->lines->flatMap( + return self::of($this->lines->flatMap( static fn($line) => $map($line)->lines(), )); } @@ -61,7 +57,7 @@ public function flatMap(callable $map): Implementation #[\Override] public function filter(callable $filter): Implementation { - return new self($this->lines->filter($filter)); + return self::of($this->lines->filter($filter)); } #[\Override] diff --git a/src/File/Content/OneShot.php b/src/File/Content/OneShot.php index 69509ec..df03fe9 100644 --- a/src/File/Content/OneShot.php +++ b/src/File/Content/OneShot.php @@ -3,7 +3,6 @@ namespace Innmind\Filesystem\File\Content; -use Innmind\Filesystem\Exception\LogicException; use Innmind\IO\{ Streams\Stream, Frame, @@ -21,12 +20,10 @@ */ final class OneShot implements Implementation { - private Stream $io; private bool $loaded = false; - private function __construct(Stream $io) + private function __construct(private Stream $io) { - $this->io = $io; } /** @@ -120,7 +117,7 @@ public function chunks(): Sequence private function guard(): void { if ($this->loaded) { - throw new LogicException("Content can't be loaded twice"); + throw new \LogicException("Content can't be loaded twice"); } /** @psalm-suppress InaccessibleProperty */ diff --git a/src/Name.php b/src/Name.php index 513bfa8..e285f1a 100644 --- a/src/Name.php +++ b/src/Name.php @@ -3,7 +3,6 @@ namespace Innmind\Filesystem; -use Innmind\Filesystem\Exception\DomainException; use Innmind\Immutable\Str; /** @@ -20,29 +19,29 @@ final class Name private function __construct(string $value) { if (Str::of($value)->empty()) { - throw new DomainException('A file name can\'t be empty'); + throw new \DomainException('A file name can\'t be empty'); } if (Str::of($value, Str\Encoding::ascii)->length() > 255) { - throw new DomainException($value); + throw new \DomainException($value); } if (Str::of($value)->contains('/')) { - throw new DomainException("A file name can't contain a slash, $value given"); + throw new \DomainException("A file name can't contain a slash, $value given"); } if (Str::of($value)->contains(\chr(0))) { - throw new DomainException("A file name can't contain the null control character, $value given"); + throw new \DomainException("A file name can't contain the null control character, $value given"); } // name with only _spaces_ are not accepted as it is not as valid path if (Str::of($value)->matches('~^\s+$~')) { - throw new DomainException($value); + throw new \DomainException($value); } if ($value === '.' || $value === '..') { // as they are special links on unix filesystems - throw new DomainException("'.' and '..' can't be used"); + throw new \DomainException("'.' and '..' can't be used"); } $this->value = $value; @@ -53,7 +52,7 @@ private function __construct(string $value) * * @param non-empty-string $value * - * @throws DomainException + * @throws \DomainException */ public static function of(string $value): self { diff --git a/src/Recover.php b/src/Recover.php new file mode 100644 index 0000000..ee183e1 --- /dev/null +++ b/src/Recover.php @@ -0,0 +1,25 @@ + + */ + public static function mount(\Throwable $e): Attempt + { + return match (true) { + $e instanceof RecoverMount => $e->recover(), + default => Attempt::error($e), + }; + } +} diff --git a/tests/Adapter/FilesystemTest.php b/tests/Adapter/FilesystemTest.php index 9284e79..423e380 100644 --- a/tests/Adapter/FilesystemTest.php +++ b/tests/Adapter/FilesystemTest.php @@ -4,16 +4,13 @@ namespace Tests\Innmind\Filesystem\Adapter; use Innmind\Filesystem\{ - Adapter\Filesystem, Adapter, File, File\Content, Name, Directory as DirectoryInterface, Directory, - Exception\PathDoesntRepresentADirectory, - Exception\PathTooLong, - Exception\LinksAreNotSupported, + Recover, }; use Innmind\Url\Path; use Innmind\Immutable\{ @@ -42,7 +39,7 @@ public function setUp(): void public function testInterface() { - $adapter = Filesystem::mount(Path::of('/tmp/')); + $adapter = Adapter::mount(Path::of('/tmp/'))->unwrap(); $this->assertInstanceOf(Adapter::class, $adapter); $this->assertFalse($adapter->contains(Name::of('foo'))); @@ -64,16 +61,17 @@ public function testInterface() public function testThrowWhenPathToMountIsNotADirectory() { - $this->expectException(PathDoesntRepresentADirectory::class); - $this->expectExceptionMessage('path/to/somewhere'); + $this->expectException(\LogicException::class); + $this->expectExceptionMessage("Path doesn't represent a directory 'path/to/somewhere'"); - Filesystem::mount(Path::of('path/to/somewhere')); + $_ = Adapter::mount(Path::of('path/to/somewhere'))->unwrap(); } public function testReturnNothingWhenGettingUnknownFile() { $this->assertNull( - Filesystem::mount(Path::of('/tmp/')) + Adapter::mount(Path::of('/tmp/')) + ->unwrap() ->get(Name::of('foo')) ->match( static fn($file) => $file, @@ -86,7 +84,8 @@ public function testRemovingUnknownFileDoesntThrow() { $this->assertInstanceOf( SideEffect::class, - Filesystem::mount(Path::of('/tmp/')) + Adapter::mount(Path::of('/tmp/')) + ->unwrap() ->remove(Name::of('foo')) ->unwrap(), ); @@ -94,7 +93,7 @@ public function testRemovingUnknownFileDoesntThrow() public function testCreateNestedStructure() { - $adapter = Filesystem::mount(Path::of('/tmp/')); + $adapter = Adapter::mount(Path::of('/tmp/'))->unwrap(); $directory = Directory::of(Name::of('foo')) ->add(File::of(Name::of('foo.md'), Content::ofString('# Foo'))) @@ -102,7 +101,7 @@ public function testCreateNestedStructure() Directory::of(Name::of('bar')) ->add(File::of(Name::of('bar.md'), Content::ofString('# Bar'))), ); - $adapter->add($directory)->unwrap(); + $_ = $adapter->add($directory)->unwrap(); $this->assertSame( '# Foo', $adapter @@ -127,7 +126,7 @@ public function testCreateNestedStructure() ), ); - $adapter = Filesystem::mount(Path::of('/tmp/')); + $adapter = Adapter::mount(Path::of('/tmp/'))->unwrap(); $this->assertTrue($adapter->contains(Name::of('foo'))); $this->assertSame( '# Foo', @@ -159,59 +158,59 @@ public function testCreateNestedStructure() ), ); - $adapter + $_ = $adapter ->remove(Name::of('foo')) ->unwrap(); } public function testRemoveFileWhenRemovedFromFolder() { - $a = Filesystem::mount(Path::of('/tmp/')); + $a = Adapter::mount(Path::of('/tmp/'))->unwrap(); $d = Directory::of(Name::of('foo')); $d = $d->add(File::of(Name::of('bar'), Content::ofString('some content'))); - $a->add($d)->unwrap(); + $_ = $a->add($d)->unwrap(); $d = $d->remove(Name::of('bar')); - $a->add($d)->unwrap(); - $this->assertSame(1, $d->removed()->count()); - $a = Filesystem::mount(Path::of('/tmp/')); + $_ = $a->add($d)->unwrap(); + $this->assertSame(1, $d->removed()->size()); + $a = Adapter::mount(Path::of('/tmp/'))->unwrap(); $this->assertFalse( $a->get(Name::of('foo'))->match( static fn($dir) => $dir->contains(Name::of('bar')), static fn() => true, ), ); - $a + $_ = $a ->remove(Name::of('foo')) ->unwrap(); } public function testDoesntFailWhenAddindSameDirectoryTwiceThatContainsARemovedFile() { - $a = Filesystem::mount(Path::of('/tmp/')); + $a = Adapter::mount(Path::of('/tmp/'))->unwrap(); $d = Directory::of(Name::of('foo')); $d = $d->add(File::of(Name::of('bar'), Content::ofString('some content'))); - $a->add($d)->unwrap(); + $_ = $a->add($d)->unwrap(); $d = $d->remove(Name::of('bar')); - $a->add($d)->unwrap(); - $a->add($d)->unwrap(); - $this->assertSame(1, $d->removed()->count()); - $a = Filesystem::mount(Path::of('/tmp/')); + $_ = $a->add($d)->unwrap(); + $_ = $a->add($d)->unwrap(); + $this->assertSame(1, $d->removed()->size()); + $a = Adapter::mount(Path::of('/tmp/'))->unwrap(); $this->assertFalse( $a->get(Name::of('foo'))->match( static fn($dir) => $dir->contains(Name::of('bar')), static fn() => true, ), ); - $a + $_ = $a ->remove(Name::of('foo')) ->unwrap(); } public function testLoadWithMediaType() { - $a = Filesystem::mount(Path::of('/tmp/')); + $a = Adapter::mount(Path::of('/tmp/'))->unwrap(); \file_put_contents( '/tmp/some_content.html', '', @@ -227,15 +226,17 @@ public function testLoadWithMediaType() static fn() => null, ), ); - $a + $_ = $a ->remove(Name::of('some_content.html')) ->unwrap(); } public function testRoot() { - $adapter = Filesystem::mount(Path::of('/tmp/test/')); - $adapter + $adapter = Adapter::mount(Path::of('/tmp/test/')) + ->recover(Recover::mount(...)) + ->unwrap(); + $_ = $adapter ->add(File::of( Name::of('foo'), Content::ofString('foo'), @@ -246,7 +247,7 @@ public function testRoot() \file_put_contents('/tmp/test/baz/foobar', 'baz'); $all = $adapter->root()->all(); - $this->assertCount(3, $all); + $this->assertSame(3, $all->size()); $all = Map::of( ...$all ->map(static fn($file) => [$file->name()->toString(), $file]) @@ -282,20 +283,22 @@ public function testRoot() static fn() => null, ), ); - $adapter + $_ = $adapter ->remove(Name::of('foo')) ->unwrap(); - $adapter + $_ = $adapter ->remove(Name::of('bar')) ->unwrap(); - $adapter + $_ = $adapter ->remove(Name::of('baz')) ->unwrap(); } public function testAddingTheSameFileTwiceDoesNothing() { - $adapter = Filesystem::mount(Path::of('/tmp/')); + $adapter = Adapter::mount(Path::of('/tmp/')) + ->recover(Recover::mount(...)) + ->unwrap(); $file = File::of( Name::of('foo'), Content::ofString('foo'), @@ -324,11 +327,14 @@ public function testPathTooLongThrowAnException() $path = \sys_get_temp_dir().'/innmind/filesystem/'; (new FS)->remove($path); - $filesystem = Filesystem::mount(Path::of($path)); + $filesystem = Adapter::mount(Path::of($path)) + ->recover(Recover::mount(...)) + ->unwrap(); - $this->expectException(PathTooLong::class); + $this->expectException(\RuntimeException::class); + $this->expectExceptionMessage('Path too long'); - $filesystem->add(Directory::of( + $_ = $filesystem->add(Directory::of( Name::of(\str_repeat('a', 255)), Sequence::of( Directory::of( @@ -363,7 +369,9 @@ public function testPersistedNameCanStartWithAnyAsciiCharacter() $path = \sys_get_temp_dir().'/innmind/filesystem/'; (new FS)->remove($path); - $filesystem = Filesystem::mount(Path::of($path)); + $filesystem = Adapter::mount(Path::of($path)) + ->recover(Recover::mount(...)) + ->unwrap(); $this->assertInstanceOf( SideEffect::class, @@ -396,7 +404,9 @@ public function testPersistedNameCanContainWithAnyAsciiCharacter() $path = \sys_get_temp_dir().'/innmind/filesystem/'; (new FS)->remove($path); - $filesystem = Filesystem::mount(Path::of($path)); + $filesystem = Adapter::mount(Path::of($path)) + ->recover(Recover::mount(...)) + ->unwrap(); $this->assertInstanceOf( SideEffect::class, @@ -431,7 +441,9 @@ public function testPersistedNameCanContainOnlyOneAsciiCharacter() $path = \sys_get_temp_dir().'/innmind/filesystem/'; (new FS)->remove($path); - $filesystem = Filesystem::mount(Path::of($path)); + $filesystem = Adapter::mount(Path::of($path)) + ->recover(Recover::mount(...)) + ->unwrap(); $this->assertInstanceOf( SideEffect::class, @@ -457,12 +469,16 @@ public function testThrowsWhenTryingToGetLink() (new FS)->dumpFile($path.'foo', 'bar'); \symlink($path.'foo', $path.'bar'); - $filesystem = Filesystem::mount(Path::of($path)); - - $this->expectException(LinksAreNotSupported::class); - $this->expectExceptionMessage($path.'bar'); + $filesystem = Adapter::mount(Path::of($path)) + ->recover(Recover::mount(...)) + ->unwrap(); - $filesystem->get(Name::of('bar')); + $this->assertNull( + $filesystem->get(Name::of('bar'))->match( + static fn($file) => $file, + static fn() => null, + ), + ); } public function testThrowsWhenListContainsALink() @@ -472,12 +488,18 @@ public function testThrowsWhenListContainsALink() (new FS)->dumpFile($path.'foo', 'bar'); \symlink($path.'foo', $path.'bar'); - $filesystem = Filesystem::mount(Path::of($path)); - - $this->expectException(LinksAreNotSupported::class); - $this->expectExceptionMessage($path.'bar'); + $filesystem = Adapter::mount(Path::of($path)) + ->recover(Recover::mount(...)) + ->unwrap(); - $filesystem->root()->all()->toList(); + $this->assertSame( + ['foo'], + $filesystem + ->root() + ->all() + ->map(static fn($file) => $file->name()->toString()) + ->toList(), + ); } public function testDotFilesAreListed() @@ -490,7 +512,9 @@ public function testDotFilesAreListed() (new FS)->mkdir($path); \file_put_contents($path.$name, 'bar'); - $filesystem = Filesystem::mount(Path::of($path)); + $filesystem = Adapter::mount(Path::of($path)) + ->recover(Recover::mount(...)) + ->unwrap(); $all = $filesystem->root()->all()->toList(); $this->assertCount(1, $all); diff --git a/tests/Adapter/InMemoryTest.php b/tests/Adapter/InMemoryTest.php index e132ea6..7fe7d86 100644 --- a/tests/Adapter/InMemoryTest.php +++ b/tests/Adapter/InMemoryTest.php @@ -4,7 +4,6 @@ namespace Tests\Innmind\Filesystem\Adapter; use Innmind\Filesystem\{ - Adapter\InMemory, Adapter, Directory, File, @@ -21,7 +20,7 @@ class InMemoryTest extends TestCase { public function testInterface() { - $a = InMemory::new(); + $a = Adapter::inMemory(); $this->assertInstanceOf(Adapter::class, $a); $this->assertFalse($a->contains(Name::of('foo'))); @@ -32,8 +31,8 @@ public function testInterface() ->unwrap(), ); $this->assertTrue($a->contains(Name::of('foo'))); - $this->assertSame( - $d, + $this->assertInstanceOf( + Directory::class, $a->get(Name::of('foo'))->match( static fn($file) => $file, static fn() => null, @@ -50,7 +49,7 @@ public function testInterface() public function testReturnNothingWhenGettingUnknownFile() { - $this->assertNull(InMemory::new()->get(Name::of('foo'))->match( + $this->assertNull(Adapter::inMemory()->get(Name::of('foo'))->match( static fn($file) => $file, static fn() => null, )); @@ -60,7 +59,7 @@ public function testRemovingUnknownFileDoesntThrow() { $this->assertInstanceOf( SideEffect::class, - InMemory::new() + Adapter::inMemory() ->remove(Name::of('foo')) ->unwrap(), ); @@ -68,14 +67,14 @@ public function testRemovingUnknownFileDoesntThrow() public function testRoot() { - $adapter = InMemory::new(); - $adapter + $adapter = Adapter::inMemory(); + $_ = $adapter ->add($foo = File::of( Name::of('foo'), Content::ofString('foo'), )) ->unwrap(); - $adapter + $_ = $adapter ->add($bar = File::of( Name::of('bar'), Content::ofString('bar'), @@ -91,15 +90,15 @@ public function testRoot() public function testEmulateFilesystem() { - $adapter = InMemory::emulateFilesystem(); - $adapter->add(Directory::of( + $adapter = Adapter::inMemory(); + $_ = $adapter->add(Directory::of( Name::of('foo'), Sequence::of( Directory::named('bar'), File::named('baz', Content::none()), ), ))->unwrap(); - $adapter->add(Directory::of( + $_ = $adapter->add(Directory::of( Name::of('foo'), Sequence::of( Directory::of( diff --git a/tests/Adapter/LoggerTest.php b/tests/Adapter/LoggerTest.php index ab43fdc..28c9679 100644 --- a/tests/Adapter/LoggerTest.php +++ b/tests/Adapter/LoggerTest.php @@ -5,8 +5,6 @@ use Innmind\Filesystem\{ Adapter, - Adapter\Logger, - Adapter\InMemory, File, File\Content, Name, @@ -21,8 +19,8 @@ public function testInterface() { $this->assertInstanceOf( Adapter::class, - Logger::psr( - InMemory::new(), + Adapter::logger( + Adapter::inMemory(), new NullLogger, ), ); @@ -30,8 +28,8 @@ public function testInterface() public function testAdd() { - $adapter = Logger::psr( - $inner = InMemory::new(), + $adapter = Adapter::logger( + $inner = Adapter::inMemory(), new NullLogger, ); $file = File::of(Name::of('foo'), Content::none()); @@ -47,13 +45,13 @@ public function testAdd() public function testGet() { - $adapter = Logger::psr( - $inner = InMemory::new(), + $adapter = Adapter::logger( + $inner = Adapter::inMemory(), new NullLogger, ); $name = Name::of('foo'); $file = File::of($name, Content::none()); - $inner + $_ = $inner ->add($file) ->unwrap(); @@ -68,12 +66,12 @@ public function testGet() public function testContains() { - $adapter = Logger::psr( - $inner = InMemory::new(), + $adapter = Adapter::logger( + $inner = Adapter::inMemory(), new NullLogger, ); $name = Name::of('foo'); - $inner + $_ = $inner ->add(File::of($name, Content::none())) ->unwrap(); @@ -82,12 +80,12 @@ public function testContains() public function testRemove() { - $adapter = Logger::psr( - $inner = InMemory::new(), + $adapter = Adapter::logger( + $inner = Adapter::inMemory(), new NullLogger, ); $name = Name::of('foo'); - $inner + $_ = $inner ->add(File::of($name, Content::none())) ->unwrap(); @@ -102,15 +100,15 @@ public function testRemove() public function testRoot() { - $adapter = Logger::psr( - $inner = InMemory::new(), + $adapter = Adapter::logger( + $inner = Adapter::inMemory(), new NullLogger, ); $file = File::named( 'watev', Content::none(), ); - $inner + $_ = $inner ->add($file) ->unwrap(); diff --git a/tests/CaseSensitivityTest.php b/tests/CaseSensitivityTest.php index 9954aa6..b413bfa 100644 --- a/tests/CaseSensitivityTest.php +++ b/tests/CaseSensitivityTest.php @@ -19,39 +19,31 @@ class CaseSensitivityTest extends TestCase { use BlackBox; - public function testContains() + public function testContainsSensitive(): BlackBox\Proof { - $this + return $this ->forAll( FName::strings(), - FName::strings(), + Set::sequence(FName::strings()), ) - ->filter(static fn($a, $b) => $a !== $b) - ->then(function($a, $b) { + ->filter(static fn($a, $b) => !\in_array($a, $b, true)) + ->prove(function($a, $b) { $this->assertTrue(CaseSensitivity::sensitive->contains( Name::of($a), ISet::of(Name::of($a)), )); - $this->assertFalse(CaseSensitivity::sensitive->contains( - Name::of($a), - ISet::of(Name::of($b)), - )); - }); - $this - ->forAll( - FName::strings(), - Set::sequence(FName::strings()), - ) - ->filter(static fn($a, $b) => !\in_array($a, $b, true)) - ->then(function($a, $b) { $this->assertFalse(CaseSensitivity::sensitive->contains( Name::of($a), ISet::of(...$b)->map(Name::of(...)), )); }); - $this + } + + public function testContainsInsensitive(): BlackBox\Proof + { + return $this ->forAll(FName::strings()) - ->then(function($a) { + ->prove(function($a) { $this->assertTrue(CaseSensitivity::insensitive->contains( Name::of($a), ISet::of($a)->map(\strtolower(...))->map(Name::of(...)), diff --git a/tests/DirectoryTest.php b/tests/DirectoryTest.php index 1d1fd75..72ce96f 100644 --- a/tests/DirectoryTest.php +++ b/tests/DirectoryTest.php @@ -8,7 +8,6 @@ File, Name, File\Content, - Exception\DuplicatedFile, }; use Innmind\Immutable\{ Set, @@ -51,8 +50,8 @@ public function testAdd() $this->assertInstanceOf(Directory::class, $d2); $this->assertNotSame($d, $d2); $this->assertSame($d->name(), $d2->name()); - $this->assertSame(0, $d->removed()->count()); - $this->assertSame(0, $d2->removed()->count()); + $this->assertSame(0, $d->removed()->size()); + $this->assertSame(0, $d2->removed()->size()); $this->assertFalse($d->contains($file->name())); $this->assertTrue($d2->contains($file->name())); $this->assertSame($file, $d2->get($file->name())->match( @@ -106,8 +105,8 @@ public function testRemove() $this->assertInstanceOf(Directory::class, $d2); $this->assertNotSame($d, $d2); $this->assertSame($d->name(), $d2->name()); - $this->assertSame(0, $d->removed()->count()); - $this->assertSame(1, $d2->removed()->count()); + $this->assertSame(0, $d->removed()->size()); + $this->assertSame(1, $d2->removed()->size()); $this->assertSame( 'bar', $d2 @@ -195,7 +194,7 @@ public function testDirectoryLoadedWithDifferentFilesWithTheSameNameThrows() FName::any(), ) ->then(function($directory, $file) { - $this->expectException(DuplicatedFile::class); + $this->expectException(\LogicException::class); $this->expectExceptionMessage("Same file '{$file->toString()}' found multiple times"); Directory::of( @@ -216,7 +215,7 @@ public function testNamedDirectoryLoadedWithDifferentFilesWithTheSameNameThrows( FName::any(), ) ->then(function($directory, $file) { - $this->expectException(DuplicatedFile::class); + $this->expectException(\LogicException::class); $this->expectExceptionMessage("Same file '{$file->toString()}' found multiple times"); Directory::named( diff --git a/tests/File/Content/LineTest.php b/tests/File/Content/LineTest.php index a88c2b6..992cced 100644 --- a/tests/File/Content/LineTest.php +++ b/tests/File/Content/LineTest.php @@ -3,10 +3,7 @@ namespace Tests\Innmind\Filesystem\File\Content; -use Innmind\Filesystem\{ - File\Content\Line, - Exception\DomainException, -}; +use Innmind\Filesystem\File\Content\Line; use Innmind\Immutable\Str; use Innmind\BlackBox\{ PHPUnit\BlackBox, @@ -31,7 +28,7 @@ public function testDoesntAcceptNewLineDelimiter() $this->fail('it should throw'); } catch (\Exception $e) { - $this->assertInstanceOf(DomainException::class, $e); + $this->assertInstanceOf(\DomainException::class, $e); } }); } @@ -90,7 +87,7 @@ public function testMappedLineCannotContainEndOfLineDelimiter() $this->fail('it should throw'); } catch (\Exception $e) { - $this->assertInstanceOf(DomainException::class, $e); + $this->assertInstanceOf(\DomainException::class, $e); } }); } diff --git a/tests/NameTest.php b/tests/NameTest.php index de20b19..1060ca6 100644 --- a/tests/NameTest.php +++ b/tests/NameTest.php @@ -3,10 +3,7 @@ namespace Innmind\Filesystem\Tests; -use Innmind\Filesystem\{ - Name, - Exception\DomainException, -}; +use Innmind\Filesystem\Name; use Innmind\Url\Path; use Innmind\Immutable\Str; use Innmind\BlackBox\{ @@ -29,7 +26,7 @@ public function testInterface() public function testThrowWhenABuildingNameWithASlash() { - $this->expectException(DomainException::class); + $this->expectException(\DomainException::class); $this->expectExceptionMessage('A file name can\'t contain a slash, foo/bar given'); Name::of('foo/bar'); @@ -43,7 +40,7 @@ public function testEquals() public function testEmptyNameIsNotAllowed() { - $this->expectException(DomainException::class); + $this->expectException(\DomainException::class); $this->expectExceptionMessage('A file name can\'t be empty'); Name::of(''); @@ -70,7 +67,7 @@ public function testNameContainingASlashIsNotAccepted() Fixture::strings(), ) ->then(function($a, $b) { - $this->expectException(DomainException::class); + $this->expectException(\DomainException::class); Name::of("$a/$b"); }); @@ -116,7 +113,7 @@ public function testDotFoldersAreNotAccepted() Name::of($name); $this->fail('it should throw'); - } catch (DomainException $e) { + } catch (\DomainException $e) { $this->assertSame("'.' and '..' can't be used", $e->getMessage()); } }); @@ -124,7 +121,7 @@ public function testDotFoldersAreNotAccepted() public function testChr0IsNotAccepted() { - $this->expectException(DomainException::class); + $this->expectException(\DomainException::class); Name::of('a'.\chr(0).'a'); } @@ -150,7 +147,7 @@ public function testNamesLongerThan255AreNotAccepted() $this->fail('it should throw'); } catch (\Throwable $e) { - $this->assertInstanceOf(DomainException::class, $e); + $this->assertInstanceOf(\DomainException::class, $e); } }); } @@ -167,7 +164,7 @@ public function testNameWithOnlyWhiteSpacesIsNotAccepted() Name::of(\chr($ord)); $this->fail('it should throw'); - } catch (DomainException $e) { + } catch (\DomainException $e) { $this->assertTrue(true); } });