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);
}
});