diff --git a/CHANGELOG.md b/CHANGELOG.md index 49236f5..d968215 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,34 @@ # Changelog +## 7.0.0 - 2023-10-21 + +### Added + +- `Innmind\Filesystem\File\Content::chunks()` + +### Changed + +- `Innmind\Filesystem\Name` constructor is now private, use `::of()` named constructor instead +- `Innmind\Filesystem\File\File` constructor is now private, use `::of()` named constructor instead +- `Innmind\Filesystem\File\Content` is now a final class and its different implementations are declared internal, use the `Content` named constructors instead +- `Innmind\Filesystem\Directory` no longer extends `File`, all previous function typed against `File` are now typed `File|Directory` +- `Innmind\Filesystem\File` is now a final class instead of an interface +- `Innmind\Filesystem\Directory` is now a final class instead of an interface +- `Innmind\Filesystem\Directory::files()` has been renamed to `::all()` + +### Fixed + +- An inconsistency in `File\Content` that must contain at least one line but it wasn't applied after a `Content::filter()` + +### Removed + +- `Innmind\Filesystem\Adapter\HashedName` +- `Innmind\Filesystem\Adapter::all()` +- `Innmind\Filesystem\Chunk` +- `Innmind\Filesystem\File\Content\Chunkable` +- Possibility to use a `Innmind\Immutable\Set` of files inside a `Directory` +- `Innmind\Filesystem\Stream\LazyStream` + ## 6.6.0 - 2023-09-16 ### Added diff --git a/LICENSE b/LICENSE index 853b46d..f15008e 100644 --- a/LICENSE +++ b/LICENSE @@ -1,6 +1,6 @@ The MIT License (MIT) -Copyright (c) 2016 +Copyright (c) 2023 Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal diff --git a/README.md b/README.md index f207923..b8b96b6 100644 --- a/README.md +++ b/README.md @@ -21,9 +21,9 @@ The whole model is structured around files, directories, contents and adapters. Example: ```php use Innmind\Filesystem\{ - File\File, + File, File\Content, - Directory\Directory, + Directory, Adapter\Filesystem, }; use Innmind\Url\Path; @@ -31,7 +31,7 @@ use Innmind\Url\Path; $directory = Directory::named('uploads')->add( File::named( $_FILES['my_upload']['name'], - Content\AtPath::of(Path::of($_FILES['my_upload']['tmp_name'])), + Content::ofString(\file_get_contents($_FILES['my_upload']['tmp_name'])), ), ); $adapter = Filesystem::mount(Path::of('/var/www/web/')); diff --git a/blackbox.php b/blackbox.php index be63e73..c0aaac1 100644 --- a/blackbox.php +++ b/blackbox.php @@ -9,11 +9,8 @@ Runner\CodeCoverage, }; -// because the generated trees can be quite large -\ini_set('memory_limit', '-1'); - Application::new($argv) - ->disableMemoryLimit() + ->disableMemoryLimit() // because the generated trees can be quite large ->scenariiPerProof(20) ->when( \getenv('ENABLE_COVERAGE') !== false, diff --git a/codecov.yml b/codecov.yml index a36da49..7850bbe 100644 --- a/codecov.yml +++ b/codecov.yml @@ -1,4 +1,7 @@ ignore: + - proofs/* - fixtures/* - properties/* - properties/**/* + - .php-cs-fixer.dist.php + - blackbox.php diff --git a/composer.json b/composer.json index 5f9224c..b9b6c15 100644 --- a/composer.json +++ b/composer.json @@ -21,7 +21,9 @@ "innmind/stream": "~4.1", "innmind/media-type": "~2.1", "innmind/url": "~4.2", - "psr/log": "~3.0" + "psr/log": "~3.0", + "innmind/io": "~2.2", + "innmind/time-continuum": "~3.4" }, "autoload": { "psr-4": { diff --git a/documentation/testing/own_adapter.md b/documentation/testing/own_adapter.md index 7e1a9bd..1530c24 100644 --- a/documentation/testing/own_adapter.md +++ b/documentation/testing/own_adapter.md @@ -1,61 +1,27 @@ # Your own adapter -This library allows you to extend its behaviour by creating new implementations of the exposed interfaces (`File`, `Directory` and `Adapter`). The interfaces are strict enough to guide you through the expected behaviour but the type system can't express all of them, leaving the door open to inconsistencies between implementations. That's why the library expose a set of properties (as declared by [`innmind/black-box`](https://packagist.org/packages/innmind/black-box)) to help you make sure your implementations fulfill the expected behaviours. +This library allows you to extend its behaviour by creating new implementations of the exposed interface `Adapter`. The interface is strict enough to guide you through the expected behaviour but the type system can't express all of them, leaving the door open to inconsistencies between implementations. That's why the library expose a set of properties (as declared by [`innmind/black-box`](https://packagist.org/packages/innmind/black-box)) to help you make sure your implementations fulfill the expected behaviours. -You can test properties on your adapter as follow (with PHPUnit): +You can test properties on your adapter as follow: ```php use Properties\Innmind\Filesystem\Adapter; -use Innmind\BlackBox\PHPUnit\BlackBox; -use PHPUnit\Framework\TestCase; - -class MyAdapterTest extends TestCase -{ - use BlackBox; - - /** - * This test will make sure each property is held by your adapter - * - * @dataProvider properties - */ - public function testHoldProperty($property) - { - $this - ->forAll($property) - ->then(function($property) { - $adapter = /* instanciate your implementation here */; - - if (!$property->applicableTo($adapter)) { - $this->markTestSkipped(); - } - - $property->ensureHeldBy($adapter); - }); +use Innmind\BlackBox\Set; + +return static function() { + yield properties( + 'YourAdapter', + Adapter::properties(), + Set\Call::of(fn() => /* instanciate YourAdapter here */), + ); + + foreach (Adapter::alwaysApplicable() as $property) { + yield property( + $property, + Set\Call::of(fn() => /* instanciate YourAdapter here */), + )->named('YourAdapter'); } - - /** - * This test will try to prove your adapter hold any sequence of property - * - * This is useful to find bugs due to state mismanage - */ - public function testHoldProperties() - { - $this - ->forAll(Adapter::properties()) - ->then(function($properties) { - $properties->ensureHeldBy(/* instanciate your implementation here */); - }); - } - - public function properties(): iterable - { - foreach (Adapter::list() as $property) { - yield [$property]; - } - } -} +}; ``` -You can use the same logic to test `Directory` implementations with `Properties\Innmind\Filesystem\Directory`. - -**Note**: there is no properties for the `File` interface as it doesn't expose any behaviour. +Then you can [run your proofs](https://github.com/Innmind/BlackBox/blob/develop/documentation/organize.md) via BlackBox. diff --git a/documentation/use_cases/load_ftp_files.md b/documentation/use_cases/load_ftp_files.md index 4a7cde5..ee3a802 100644 --- a/documentation/use_cases/load_ftp_files.md +++ b/documentation/use_cases/load_ftp_files.md @@ -14,11 +14,11 @@ use Innmind\Immutable\Sequence; /** * @return Sequence */ -function flatten(File $file): Sequence +function flatten(File|Directory $file): Sequence { if ($file instanceof Directory) { // bring all the files from sub directories to the same level - return $file->files()->flatMap(flatten(...)); + return $file->all()->flatMap(flatten(...)); } return Sequence::of($file); @@ -26,7 +26,7 @@ function flatten(File $file): Sequence Filesystem::mount(Path::of('/path/to/ftp/directory/')) ->root() - ->files() + ->all() ->flatMap(flatten(...)) ->foreach(static fn(File $csv) => doYourStuff($csv)); ``` diff --git a/documentation/use_cases/modify_file.md b/documentation/use_cases/modify_file.md index 4947ac2..831c7a3 100644 --- a/documentation/use_cases/modify_file.md +++ b/documentation/use_cases/modify_file.md @@ -11,7 +11,10 @@ use Innmind\Filesystem\{ Name, }; use Innmind\Url\Path; -use Innmind\Immutable\Str; +use Innmind\Immutable\{ + Str, + Predicate\Instance, +}; // replace the "unreleased" title with the new version $insertRelease = static function(Str $line): Str { @@ -33,7 +36,8 @@ $filesystem = Filesystem::mount(Path::of('some/repository/')); $tmp = Filesystem::mount(Path::of('/tmp/')); $filesystem ->get(Name::of('CHANGELOG.md')) - ->map(static fn($changelog) => $release($changelog)) + ->keep(Instance::of(File::class)) + ->map($release) ->flatMap(static function($changelog) use ($tmp) { // this operation is due to the fact that you cannot read and write to // the same file at once @@ -63,18 +67,19 @@ use Innmind\Url\Path; use Innmind\Immutable\{ Sequence, Str, + Predicate\Instance, }; // Insert "Jane Doe" after the user "John Doe" $updateUser = static function(Line $user): Content { if ($user->toString() === 'John Doe') { - return Content\Lines::of(Sequence::of( + return Content::ofLines(Sequence::of( $user, Line::of(Str::of('Jane Doe')), )); } - return Content\Lines::of(Sequence::of($user)); + return Content::ofLines(Sequence::of($user)); }; $update = static function(File $users) use ($updateUser): File { return $users->withContent( @@ -85,7 +90,8 @@ $filesystem = Filesystem::mount(Path::of('/var/data/')); $tmp = Filesystem::mount(Path::of('/tmp/')); $filesystem ->get(Name::of('users.csv')) - ->map(static fn($users) => $update($users)) + ->keep(Instance::of(File::class)) + ->map($update) ->flatMap(static function($users) use ($tmp) { // this operation is due to the fact that you cannot read and write to // the same file at once @@ -111,19 +117,22 @@ use Innmind\Filesystem\{ Name, }; use Innmind\Url\Path; -use Innmind\Immutable\Maybe; +use Innmind\Immutable\{ + Maybe, + Predicate\Instance, +}; $merge = static function(File $file1, File $file2): File { - return File\File::named( + return File::named( 'all_users.csv', - Content\Lines::of( + Content::ofLines( $file1->content()->lines()->append($file2->content()->lines()), ), ); }; $filesystem = Filesystem::mount(Path::of('/var/data/')); -$users1 = $filesystem->get(Name::of('users1.csv')); -$users2 = $filesystem->get(Name::of('users2.csv')); +$users1 = $filesystem->get(Name::of('users1.csv'))->keep(Instance::of(File::class)); +$users2 = $filesystem->get(Name::of('users2.csv'))->keep(Instance::of(File::class)); Maybe::all($users1, $users2) ->map(static fn($file1, $file2) => $merge($file1, $file2)) ->match( diff --git a/documentation/use_cases/persist_hand_file.md b/documentation/use_cases/persist_hand_file.md index 0605be5..336452f 100644 --- a/documentation/use_cases/persist_hand_file.md +++ b/documentation/use_cases/persist_hand_file.md @@ -5,13 +5,13 @@ ```php use Innmind\Filesystem\{ Adapter\Filesystem, - File\File, + File, File\Content, }; use Innmind\Url\Path; $filesystem = Filesystem::mount(Path::of('/var/data/')); -$filesystem->add(File::named('some name'), Content\None::of()); +$filesystem->add(File::named('some name'), Content::none()); ``` This is equivalent of running the cli command `touch '/var/data/some name'`. @@ -21,8 +21,8 @@ This is equivalent of running the cli command `touch '/var/data/some name'`. ```php use Innmind\Filesystem\{ Adapter\Filesystem, - File\File, - File\Content\Lines, + File, + File\Content, File\Content\Line, }; use Innmind\Url\Path; @@ -34,7 +34,7 @@ use Innmind\Immutable\{ $filesystem = Filesystem::mount(Path::of('/var/data/')); $filesystem->add(File::named( 'some name', - Lines::of(Sequence::of( + Content::ofLines(Sequence::of( Line::of(Str::of('first line')), Line::of(Str::of('second line')), Line::of(Str::of('etc...')), @@ -49,15 +49,15 @@ When the file is persisted the _end of line_ character will automatically added ```php use Innmind\Filesystem\{ Adapter\Filesystem, - File\File, + File, File\Content, - Directory\Directory, + Directory, }; use Innmind\Url\Path; $filesystem = Filesystem::mount(Path::of('/var/data/')); $filesystem->add( - Directory::named('whatever')->add(File::named('some name'), Content\None::of()), + Directory::named('whatever')->add(File::named('some name'), Content::none()), ); ``` diff --git a/documentation/use_cases/persist_process_output.md b/documentation/use_cases/persist_process_output.md index 977c409..4f64c56 100644 --- a/documentation/use_cases/persist_process_output.md +++ b/documentation/use_cases/persist_process_output.md @@ -4,7 +4,7 @@ This example uses the [`innmind/operating-system`](https://packagist.org/package ```php use Innmind\Filesystem\{ - File\File, + File, File\Content, Adapter\Filesystem, }; @@ -14,7 +14,7 @@ use Innmind\Url\Path; $os = Factory::build(); $filesystem = Filesystem::mount(Path::of('/var/data/')); -$fileContent = Content\Chunks::of( +$fileContent = Content::ofChunks( $os ->control() ->processes() diff --git a/documentation/use_cases/persist_uploaded_file.md b/documentation/use_cases/persist_uploaded_file.md index 3a9eced..8bdd01e 100644 --- a/documentation/use_cases/persist_uploaded_file.md +++ b/documentation/use_cases/persist_uploaded_file.md @@ -2,20 +2,29 @@ ```php use Innmind\Filesystem\{ - File\File, + File, File\Content, - Directory\Directory, + Directory, Adapter\Filesystem, }; +use Innmind\IO\IO; +use Innmind\Stream\Streams; use Innmind\Url\Path; +$streams = Streams::fromAmbienAuthority(); +$io = IO::of($streams->watch()->waitForever(...)) + $filesystem = Filesystem::mount(Path::of('/var/data/')); $filesystem->add( Directory::named('uploads')->add( File::named( $_FILES['my_upload']['name'], - Content\AtPath::of(Path::of($_FILES['my_upload']['tmp_name'])), + Content::atPath( + $streams->readable(), + $io->readable(), + Path::of($_FILES['my_upload']['tmp_name']), + ), ), - ) + ), ); ``` diff --git a/documentation/use_cases/read_file.md b/documentation/use_cases/read_file.md index a1467bf..bc80136 100644 --- a/documentation/use_cases/read_file.md +++ b/documentation/use_cases/read_file.md @@ -9,6 +9,7 @@ use Innmind\Filesystem\{ Name, }; use Innmind\Url\Path; +use Innmind\Immutable\Predicate\Instance; $print = static function(File $file): void { $file @@ -21,6 +22,7 @@ $print = static function(File $file): void { $filesystem = Filesystem::mount(Path::of('/var/data/')); $filesystem ->get(Name::of('some file')) + ->keep(Instance::of(File::class)) ->match( static fn(File $file) => $print($file), static fn() => null, // the file doesn't exist diff --git a/fixtures/Directory.php b/fixtures/Directory.php index d07de3d..3187d7c 100644 --- a/fixtures/Directory.php +++ b/fixtures/Directory.php @@ -3,17 +3,14 @@ namespace Fixtures\Innmind\Filesystem; -use Innmind\Filesystem\{ - Directory\Directory as Model, - File as FileInterface, -}; +use Innmind\Filesystem\Directory as Model; use Properties\Innmind\Filesystem\Directory as Properties; use Innmind\BlackBox\{ Set as DataSet, Runner\Assert, Runner\Stats, }; -use Fixtures\Innmind\Immutable\Set; +use Fixtures\Innmind\Immutable\Sequence; final class Directory { @@ -38,14 +35,14 @@ public static function maxDepth(int $depth): DataSet private static function atDepth(int $depth, int $maxDepth): DataSet { if ($depth === $maxDepth) { - $files = Set::of( + $files = Sequence::of( DataSet\Randomize::of( File::any(), ), DataSet\Integers::between(0, 5), ); } else { - $files = Set::of( + $files = Sequence::of( DataSet\Either::any( DataSet\Randomize::of( File::any(), diff --git a/fixtures/File.php b/fixtures/File.php index f847dd9..989f98c 100644 --- a/fixtures/File.php +++ b/fixtures/File.php @@ -3,9 +3,9 @@ namespace Fixtures\Innmind\Filesystem; -use Innmind\Filesystem\File\{ +use Innmind\Filesystem\{ File as Model, - Content\Lines, + File\Content, }; use Innmind\BlackBox\Set; use Fixtures\Innmind\MediaType\MediaType; @@ -17,7 +17,7 @@ public static function any(): Set return Set\Composite::immutable( static fn($name, $content, $mediaType) => Model::of( $name, - Lines::ofContent($content), + Content::ofString($content), $mediaType, ), Name::any(), diff --git a/proofs/adapter/filesystem.php b/proofs/adapter/filesystem.php index ffdde06..c699076 100644 --- a/proofs/adapter/filesystem.php +++ b/proofs/adapter/filesystem.php @@ -3,9 +3,9 @@ use Innmind\Filesystem\{ Adapter\Filesystem, - Directory\Directory, - File\File, - File\Content\None, + Directory, + File, + File\Content, CaseSensitivity, }; use Innmind\Url\Path; @@ -48,9 +48,9 @@ function($assert) { $property = new Adapter\AddRemoveAddModificationsStillAddTheFile( Directory::named('0') - ->add($file = File::named('L', None::of())) + ->add($file = File::named('L', Content::none())) ->remove($file->name()), - File::named('l', None::of()), + File::named('l', Content::none()), ); $path = \sys_get_temp_dir().'/innmind/filesystem/'; diff --git a/proofs/directory/directory.php b/proofs/directory/directory.php index 886c126..0848f36 100644 --- a/proofs/directory/directory.php +++ b/proofs/directory/directory.php @@ -1,7 +1,7 @@ map(Directory::of(...)), ); yield properties( - 'Non EmptyDirectory properties', + 'Non empty Directory properties', PDirectory::properties(), Set\Composite::immutable( Directory::of(...), diff --git a/proofs/file/content.php b/proofs/file/content.php new file mode 100644 index 0000000..b67e31d --- /dev/null +++ b/proofs/file/content.php @@ -0,0 +1,231 @@ +watch()->waitForever(...))->readable(); + + $implementations = [ + [ + 'Content::ofString()', + Set\Sequence::of(Set\Strings::any())->map( + static fn($lines) => Model::ofString(\implode("\n", $lines)), + ), + ], + [ + 'Content::atPath()', + Set\Elements::of('LICENSE', 'CHANGELOG.md', 'composer.json') + ->map(Path::of(...)) + ->map(static fn($path) => Model::atPath( + $capabilities->readable(), + $io, + $path, + )), + ], + [ + 'Content::none()', + Set\Elements::of(Model::none()), + ], + [ + 'Content::ofLines()', + Set\Sequence::of( + Set\Strings::madeOf( + Set\Unicode::any()->filter(static fn($char) => $char !== "\n"), + ) + ->map(Str::of(...)) + ->map(Line::of(...)), + ) + ->map(static fn($lines) => Model::ofLines(Sequence::of(...$lines))), + ], + [ + 'Content::ofChunks()', + Set\Sequence::of( + Set\Strings::madeOf(Set\Unicode::any())->map(Str::of(...)), + ) + ->map(static fn($chunks) => Model::ofChunks(Sequence::of(...$chunks))), + ], + ]; + + foreach ($implementations as [$name, $content]) { + yield properties( + $name, + Content::properties(), + $content, + ); + + foreach (Content::all() as $property) { + yield property( + $property, + $content, + )->named($name); + } + } + + yield test( + 'Content::oneShot()->foreach()', + static function($assert) use ($io, $capabilities) { + $content = Model::oneShot($io->wrap( + $capabilities->readable()->open(Path::of('LICENSE')), + )); + + $count = 0; + $content->foreach(static function() use (&$count) { + $count++; + }); + + $assert->same(22, $count); + }, + ); + + yield proof( + 'Content::oneShot()->map()', + given( + Set\Strings::madeOf( + Set\Unicode::any()->filter(static fn($char) => $char !== "\n"), + ) + ->map(Str::of(...)) + ->map(Line::of(...)), + ), + static function($assert, $replacement) use ($io, $capabilities) { + $content = Model::oneShot($io->wrap( + $capabilities->readable()->open(Path::of('LICENSE')), + )); + + $lines = $content + ->map(static fn() => $replacement) + ->lines() + ->map(static fn($line) => $line->toString()) + ->distinct() + ->toList(); + + $assert->same([$replacement->toString()], $lines); + }, + ); + + yield proof( + 'Content::oneShot()->flatMap()', + given( + Set\Strings::madeOf( + Set\Unicode::any()->filter(static fn($char) => $char !== "\n"), + ), + Set\Strings::madeOf( + Set\Unicode::any()->filter(static fn($char) => $char !== "\n"), + ), + )->filter(static fn($a, $b) => $a !== $b), + static function($assert, $replacement1, $replacement2) use ($io, $capabilities) { + $content = Model::oneShot($io->wrap( + $capabilities->readable()->open(Path::of('LICENSE')), + )); + + $lines = $content + ->flatMap(static fn() => Model::ofString($replacement1."\n".$replacement2)) + ->lines() + ->map(static fn($line) => $line->toString()) + ->distinct() + ->toList(); + + $assert->same([$replacement1, $replacement2], $lines); + }, + ); + + yield test( + 'Content::oneShot()->filter()', + static function($assert) use ($io, $capabilities) { + $content = Model::oneShot($io->wrap( + $capabilities->readable()->open(Path::of('LICENSE')), + )); + + $size = $content + ->filter(static fn($line) => !$line->str()->empty()) + ->lines() + ->size(); + + $assert->same(17, $size); + }, + ); + + yield test( + 'Content::oneShot()->reduce()', + static function($assert) use ($io, $capabilities) { + $content = Model::oneShot($io->wrap( + $capabilities->readable()->open(Path::of('LICENSE')), + )); + + $size = $content->reduce( + 0, + static fn($i) => ++$i, + ); + + $assert->same(22, $size); + }, + ); + + yield test( + 'Content::oneShot()->toString()', + static function($assert) use ($io, $capabilities) { + $content = Model::oneShot($io->wrap( + $capabilities->readable()->open(Path::of('LICENSE')), + )); + + $assert->same(\file_get_contents('LICENSE'), $content->toString()); + }, + ); + + yield test( + 'Content::oneShot()->chunks()', + static function($assert) use ($io, $capabilities) { + $content = Model::oneShot($io->wrap( + $capabilities->readable()->open(Path::of('LICENSE')), + )); + + $assert->same( + \file_get_contents('LICENSE'), + $content + ->chunks() + ->fold(new Concat) + ->toString(), + ); + }, + ); + + $actions = Set\Elements::of( + static fn($content) => $content->foreach(static fn() => null), + static fn($content) => $content->toString(), + static fn($content) => $content->chunks()->toList(), + static fn($content) => $content->lines()->toList(), + static fn($content) => $content->reduce(null, static fn() => null), + static fn($content) => $content->filter(static fn() => true)->toString(), + static fn($content) => $content->map(static fn() => Str::of(''))->toString(), + static fn($content) => $content + ->flatMap(static fn() => Model::ofString('')) + ->toString(), + ); + + yield proof( + 'Content::oneShot() throws when loaded multiple times', + given($actions, $actions), + static function($assert, $a, $b) use ($io, $capabilities) { + $content = Model::oneShot($io->wrap( + $capabilities->readable()->open(Path::of('LICENSE')), + )); + + $a($content); + $assert->throws(static fn() => $b($content)); + }, + ); +}; diff --git a/proofs/stream/lazyStream.php b/proofs/stream/lazyStream.php deleted file mode 100644 index 75257be..0000000 --- a/proofs/stream/lazyStream.php +++ /dev/null @@ -1,24 +0,0 @@ -toString()); - - $properties->ensureHeldBy($assert, $stream); - }, - ); -}; diff --git a/properties/Adapter/AddDirectory.php b/properties/Adapter/AddDirectory.php index 1d8ce50..8b690db 100644 --- a/properties/Adapter/AddDirectory.php +++ b/properties/Adapter/AddDirectory.php @@ -54,16 +54,22 @@ public function ensureHeldBy(Assert $assert, object $adapter): object return $adapter; } - private function assertSame(Assert $assert, File $source, File $target): void - { + private function assertSame( + Assert $assert, + File|Directory $source, + File|Directory $target, + ): void { $assert->same( $source->name()->toString(), $target->name()->toString(), ); - $assert->same( - $source->content()->toString(), - $target->content()->toString(), - ); + + if ($source instanceof File) { + $assert->same( + $source->content()->toString(), + $target->content()->toString(), + ); + } if ($target instanceof Directory) { $target->foreach(function($file) use ($assert, $source) { diff --git a/properties/Adapter/AddDirectoryFromAnotherAdapter.php b/properties/Adapter/AddDirectoryFromAnotherAdapter.php index d1ab0e7..9c2117b 100644 --- a/properties/Adapter/AddDirectoryFromAnotherAdapter.php +++ b/properties/Adapter/AddDirectoryFromAnotherAdapter.php @@ -5,14 +5,14 @@ use Innmind\Filesystem\{ Adapter, - Directory\Directory, + Directory, File, Name, }; -use Innmind\Immutable\Set; +use Innmind\Immutable\Sequence; use Innmind\BlackBox\{ Property, - Set as DataSet, + Set, Runner\Assert, }; use Fixtures\Innmind\Filesystem\{ @@ -34,9 +34,9 @@ public function __construct(Name $name, File $file) $this->file = $file; } - public static function any(): DataSet + public static function any(): Set { - return DataSet\Composite::immutable( + return Set\Composite::immutable( static fn(...$args) => new self(...$args), FName::any(), FFile::any(), @@ -54,7 +54,7 @@ public function ensureHeldBy(Assert $assert, object $adapter): object // construct time (so there is no modifications()) $directory = Directory::of( $this->name, - Set::of($this->file), + Sequence::of($this->file), ); $assert->false($adapter->contains($directory->name())); diff --git a/properties/Adapter/AddDirectoryFromAnotherAdapterWithFileAdded.php b/properties/Adapter/AddDirectoryFromAnotherAdapterWithFileAdded.php index b7ae3d8..4bf5582 100644 --- a/properties/Adapter/AddDirectoryFromAnotherAdapterWithFileAdded.php +++ b/properties/Adapter/AddDirectoryFromAnotherAdapterWithFileAdded.php @@ -5,14 +5,14 @@ use Innmind\Filesystem\{ Adapter, - Directory\Directory, + Directory, File, Name, }; -use Innmind\Immutable\Set; +use Innmind\Immutable\Sequence; use Innmind\BlackBox\{ Property, - Set as DataSet, + Set, Runner\Assert, }; use Fixtures\Innmind\Filesystem\{ @@ -36,9 +36,9 @@ public function __construct(Name $name, File $file, File $added) $this->added = $added; } - public static function any(): DataSet + public static function any(): Set { - return DataSet\Composite::immutable( + return Set\Composite::immutable( static fn(...$args) => new self(...$args), FName::any(), FFile::any(), @@ -57,7 +57,7 @@ public function ensureHeldBy(Assert $assert, object $adapter): object // construct time (so there is no modifications()) $directory = Directory::of( $this->name, - Set::of($this->file), + Sequence::of($this->file), ); $directory = $directory->add($this->added); diff --git a/properties/Adapter/AddDirectoryFromAnotherAdapterWithFileRemoved.php b/properties/Adapter/AddDirectoryFromAnotherAdapterWithFileRemoved.php index 8043b2e..aa57c89 100644 --- a/properties/Adapter/AddDirectoryFromAnotherAdapterWithFileRemoved.php +++ b/properties/Adapter/AddDirectoryFromAnotherAdapterWithFileRemoved.php @@ -5,14 +5,14 @@ use Innmind\Filesystem\{ Adapter, - Directory\Directory, + Directory, File, Name, }; -use Innmind\Immutable\Set; +use Innmind\Immutable\Sequence; use Innmind\BlackBox\{ Property, - Set as DataSet, + Set, Runner\Assert, }; use Fixtures\Innmind\Filesystem\{ @@ -36,9 +36,9 @@ public function __construct(Name $name, File $file, File $removed) $this->removed = $removed; } - public static function any(): DataSet + public static function any(): Set { - return DataSet\Composite::immutable( + return Set\Composite::immutable( static fn(...$args) => new self(...$args), FName::any(), FFile::any(), @@ -57,7 +57,7 @@ public function ensureHeldBy(Assert $assert, object $adapter): object // construct time (so there is no modifications()) $directory = Directory::of( $this->name, - Set::of( + Sequence::of( $this->removed, $this->file, ), diff --git a/properties/Adapter/AddEmptyDirectory.php b/properties/Adapter/AddEmptyDirectory.php index b0645cc..6590ff3 100644 --- a/properties/Adapter/AddEmptyDirectory.php +++ b/properties/Adapter/AddEmptyDirectory.php @@ -5,7 +5,7 @@ use Innmind\Filesystem\{ Adapter, - Directory\Directory, + Directory, Name, }; use Innmind\BlackBox\{ diff --git a/properties/Adapter/AddFileWithSameNameAsDirectoryDeleteTheDirectory.php b/properties/Adapter/AddFileWithSameNameAsDirectoryDeleteTheDirectory.php index 475bbb0..8a0d516 100644 --- a/properties/Adapter/AddFileWithSameNameAsDirectoryDeleteTheDirectory.php +++ b/properties/Adapter/AddFileWithSameNameAsDirectoryDeleteTheDirectory.php @@ -31,7 +31,7 @@ public function __construct(File $file, File $fileInDirectory) { $this->file = $file; // the extra file is here to make sure we can delete non empty directories - $this->directory = Directory\Directory::of($file->name())->add($fileInDirectory); + $this->directory = Directory::of($file->name())->add($fileInDirectory); } public static function any(): Set diff --git a/properties/Adapter/AllRootFilesAreAccessible.php b/properties/Adapter/AllRootFilesAreAccessible.php index 24f23d8..6d7ead0 100644 --- a/properties/Adapter/AllRootFilesAreAccessible.php +++ b/properties/Adapter/AllRootFilesAreAccessible.php @@ -3,7 +3,10 @@ namespace Properties\Innmind\Filesystem\Adapter; -use Innmind\Filesystem\Adapter; +use Innmind\Filesystem\{ + Adapter, + Directory, +}; use Innmind\BlackBox\{ Property, Set, @@ -29,9 +32,13 @@ public function ensureHeldBy(Assert $assert, object $adapter): object { $adapter ->root() - ->files() ->foreach(static function($file) use ($assert, $adapter) { $assert->true($adapter->contains($file->name())); + + if ($file instanceof Directory) { + return; + } + $assert->same( $file->content()->toString(), $adapter @@ -43,22 +50,6 @@ public function ensureHeldBy(Assert $assert, object $adapter): object ), ); }); - $assert->same($adapter->root()->files()->size(), $adapter->all()->size()); - $adapter - ->all() - ->foreach(static function($file) use ($assert, $adapter) { - $assert->same( - $file->content()->toString(), - $adapter - ->root() - ->get($file->name()) - ->map(static fn($file) => $file->content()) - ->match( - static fn($content) => $content->toString(), - static fn() => null, - ), - ); - }); return $adapter; } diff --git a/properties/Adapter/ReAddingFilesHasNoSideEffect.php b/properties/Adapter/ReAddingFilesHasNoSideEffect.php index 89fc667..1d1fc44 100644 --- a/properties/Adapter/ReAddingFilesHasNoSideEffect.php +++ b/properties/Adapter/ReAddingFilesHasNoSideEffect.php @@ -3,7 +3,10 @@ namespace Properties\Innmind\Filesystem\Adapter; -use Innmind\Filesystem\Adapter; +use Innmind\Filesystem\{ + Adapter, + Directory, +}; use Innmind\BlackBox\{ Property, Set, @@ -28,10 +31,15 @@ public function applicableTo(object $adapter): bool public function ensureHeldBy(Assert $assert, object $adapter): object { $adapter - ->all() + ->root() ->foreach(static function($file) use ($assert, $adapter) { $adapter->add($file); $assert->true($adapter->contains($file->name())); + + if ($file instanceof Directory) { + return; + } + $assert->same( $file->content()->toString(), $adapter diff --git a/properties/Adapter/RemoveFileInDirectory.php b/properties/Adapter/RemoveFileInDirectory.php index 61c1459..2cd3129 100644 --- a/properties/Adapter/RemoveFileInDirectory.php +++ b/properties/Adapter/RemoveFileInDirectory.php @@ -7,7 +7,7 @@ Adapter, File, Name, - Directory\Directory, + Directory, }; use Innmind\BlackBox\{ Property, diff --git a/properties/Content.php b/properties/Content.php new file mode 100644 index 0000000..d36e845 --- /dev/null +++ b/properties/Content.php @@ -0,0 +1,45 @@ + + */ + public static function properties(): Set + { + return Set\Properties::any( + ...\array_map( + static fn($class) => $class::any(), + self::all(), + ), + ); + } + + /** + * @return class-string> + */ + public static function all(): array + { + return [ + Content\ForeachExposeAllLines::class, + Content\ForeachExposeAtLeastOneLine::class, + Content\Map::class, + Content\FlatMap::class, + Content\Filter::class, + Content\Lines::class, + Content\Chunks::class, + Content\Reduce::class, + Content\Size::class, + ]; + } +} diff --git a/properties/Content/Chunks.php b/properties/Content/Chunks.php new file mode 100644 index 0000000..68cee5e --- /dev/null +++ b/properties/Content/Chunks.php @@ -0,0 +1,44 @@ + + */ +final class Chunks implements Property +{ + public static function any(): Set + { + return Set\Elements::of(new self); + } + + public function applicableTo(object $systemUnderTest): bool + { + return true; + } + + public function ensureHeldBy(Assert $assert, object $systemUnderTest): object + { + $assert->same( + $systemUnderTest->toString(), + $systemUnderTest + ->chunks() + ->fold(new Concat) + ->toString(), + ); + $assert + ->number($systemUnderTest->chunks()->size()) + ->greaterThanOrEqual(1); + + return $systemUnderTest; + } +} diff --git a/properties/Content/Filter.php b/properties/Content/Filter.php new file mode 100644 index 0000000..105db50 --- /dev/null +++ b/properties/Content/Filter.php @@ -0,0 +1,63 @@ + + */ +final class Filter implements Property +{ + public static function any(): Set + { + return Set\Elements::of(new self); + } + + public function applicableTo(object $systemUnderTest): bool + { + return true; + } + + public function ensureHeldBy(Assert $assert, object $systemUnderTest): object + { + $none = $systemUnderTest->filter(static fn() => false); + $all = $systemUnderTest->filter(static fn() => true); + + $assert->same(1, $none->lines()->size()); + $assert->same(1, $none->chunks()->size()); + $assert->same( + [''], + $none + ->lines() + ->map(static fn($line) => $line->toString()) + ->toList(), + ); + $assert->same( + [''], + $none + ->chunks() + ->map(static fn($line) => $line->toString()) + ->toList(), + ); + $assert->same( + $systemUnderTest->lines()->size(), + $all->lines()->size(), + ); + $assert->same( + $systemUnderTest->toString(), + $all->toString(), + ); + + return $all; + } +} diff --git a/properties/Content/FlatMap.php b/properties/Content/FlatMap.php new file mode 100644 index 0000000..5eb5fb7 --- /dev/null +++ b/properties/Content/FlatMap.php @@ -0,0 +1,65 @@ + + */ +final class FlatMap implements Property +{ + private Content $content; + + public function __construct(Content $content) + { + $this->content = $content; + } + + public static function any(): Set + { + return Set\Strings::madeOf(Set\Unicode::any()) + ->map(Content::ofString(...)) + ->map(static fn($line) => new self($line)); + } + + public function applicableTo(object $systemUnderTest): bool + { + return true; + } + + public function ensureHeldBy(Assert $assert, object $systemUnderTest): object + { + $content = $systemUnderTest->flatMap(fn() => $this->content); + + $size = $systemUnderTest->lines()->size(); + + $assert->same( + $size * $this->content->lines()->size(), + $content->lines()->size(), + ); + + $assert->string($content->toString())->contains($this->content->toString()); + $assert->same( + $this + ->content + ->lines() + ->map(static fn($line) => $line->toString()) + ->distinct() + ->toList(), + $content + ->lines() + ->map(static fn($line) => $line->toString()) + ->distinct() + ->toList(), + ); + + return $content; + } +} diff --git a/properties/Content/ForeachExposeAllLines.php b/properties/Content/ForeachExposeAllLines.php new file mode 100644 index 0000000..63ab6aa --- /dev/null +++ b/properties/Content/ForeachExposeAllLines.php @@ -0,0 +1,44 @@ + + */ +final class ForeachExposeAllLines implements Property +{ + public static function any(): Set + { + return Set\Elements::of(new self); + } + + public function applicableTo(object $systemUnderTest): bool + { + return true; + } + + public function ensureHeldBy(Assert $assert, object $systemUnderTest): object + { + $count = 0; + $systemUnderTest->foreach(static function($line) use ($assert, &$count) { + $assert->object($line)->instance(Line::class); + $count++; + }); + $all = \count(\explode("\n", $systemUnderTest->toString())); + + $assert->same($all, $count); + + return $systemUnderTest; + } +} diff --git a/properties/Content/ForeachExposeAtLeastOneLine.php b/properties/Content/ForeachExposeAtLeastOneLine.php new file mode 100644 index 0000000..239fe62 --- /dev/null +++ b/properties/Content/ForeachExposeAtLeastOneLine.php @@ -0,0 +1,41 @@ + + */ +final class ForeachExposeAtLeastOneLine implements Property +{ + public static function any(): Set + { + return Set\Elements::of(new self); + } + + public function applicableTo(object $systemUnderTest): bool + { + return true; + } + + public function ensureHeldBy(Assert $assert, object $systemUnderTest): object + { + $count = 0; + $return = $systemUnderTest->foreach(static function($line) use (&$count) { + $count++; + }); + + $assert->object($return)->instance(SideEffect::class); + $assert->number($count)->greaterThanOrEqual(1); + + return $systemUnderTest; + } +} diff --git a/properties/Content/Lines.php b/properties/Content/Lines.php new file mode 100644 index 0000000..ef2b0bc --- /dev/null +++ b/properties/Content/Lines.php @@ -0,0 +1,37 @@ + + */ +final class Lines implements Property +{ + public static function any(): Set + { + return Set\Elements::of(new self); + } + + public function applicableTo(object $systemUnderTest): bool + { + return true; + } + + public function ensureHeldBy(Assert $assert, object $systemUnderTest): object + { + $content = $assert->string($systemUnderTest->toString()); + $systemUnderTest + ->lines() + ->foreach(static fn($line) => $content->contains($line->toString())); + + return $systemUnderTest; + } +} diff --git a/properties/Content/Map.php b/properties/Content/Map.php new file mode 100644 index 0000000..e03b6fb --- /dev/null +++ b/properties/Content/Map.php @@ -0,0 +1,63 @@ + + */ +final class Map implements Property +{ + private Line $replacement; + + public function __construct(Line $replacement) + { + $this->replacement = $replacement; + } + + public static function any(): Set + { + return Set\Strings::madeOf( + Set\Unicode::any()->filter(static fn($char) => $char !== "\n"), + ) + ->map(Str::of(...)) + ->map(Line::of(...)) + ->map(static fn($line) => new self($line)); + } + + public function applicableTo(object $systemUnderTest): bool + { + return true; + } + + public function ensureHeldBy(Assert $assert, object $systemUnderTest): object + { + $content = $systemUnderTest->map(fn() => $this->replacement); + + $size = $systemUnderTest->lines()->size(); + + $assert->same($size, $content->lines()->size()); + + $assert->same( + [$this->replacement->toString()], + $content + ->lines() + ->map(static fn($line) => $line->toString()) + ->distinct() + ->toList(), + ); + + return $content; + } +} diff --git a/properties/Content/Reduce.php b/properties/Content/Reduce.php new file mode 100644 index 0000000..ddc2105 --- /dev/null +++ b/properties/Content/Reduce.php @@ -0,0 +1,45 @@ + + */ +final class Reduce implements Property +{ + public static function any(): Set + { + return Set\Elements::of(new self); + } + + public function applicableTo(object $systemUnderTest): bool + { + return true; + } + + public function ensureHeldBy(Assert $assert, object $systemUnderTest): object + { + $content = $assert->string($systemUnderTest->toString()); + + $count = $systemUnderTest->reduce( + 0, + static function(int $count, $line) use ($content) { + $content->contains($line->toString()); + + return ++$count; + }, + ); + + $assert->same($systemUnderTest->lines()->size(), $count); + + return $systemUnderTest; + } +} diff --git a/properties/Content/Size.php b/properties/Content/Size.php new file mode 100644 index 0000000..a675dcc --- /dev/null +++ b/properties/Content/Size.php @@ -0,0 +1,40 @@ + + */ +final class Size implements Property +{ + public static function any(): Set + { + return Set\Elements::of(new self); + } + + public function applicableTo(object $systemUnderTest): bool + { + return true; + } + + public function ensureHeldBy(Assert $assert, object $systemUnderTest): object + { + $expected = \mb_strlen($systemUnderTest->toString(), 'ascii'); + $systemUnderTest + ->size() + ->match( + static fn($size) => $assert->same($expected, $size->toInt()), + static fn() => null, + ); + + return $systemUnderTest; + } +} diff --git a/properties/Directory.php b/properties/Directory.php index dfdb66d..aec7d37 100644 --- a/properties/Directory.php +++ b/properties/Directory.php @@ -35,14 +35,12 @@ public static function list(): array public static function all(): array { return [ - Directory\MediaTypeIsAlwaysTheSame::class, Directory\ContainsMethodAlwaysReturnTrueForFilesInTheDirectory::class, Directory\AllFilesInTheDirectoryAreAccessible::class, Directory\AccessingUnknownFileReturnsNothing::class, Directory\RemovingAnUnknownFileHasNoEffect::class, Directory\RemoveFile::class, Directory\RemoveDirectory::class, - Directory\ContentHoldsNothing::class, Directory\AddFile::class, Directory\AddDirectory::class, Directory\FilteringDoesntAffectTheDirectory::class, @@ -62,10 +60,8 @@ public static function all(): array public static function alwaysApplicable(): array { return [ - Directory\MediaTypeIsAlwaysTheSame::class, Directory\ContainsMethodAlwaysReturnTrueForFilesInTheDirectory::class, Directory\AllFilesInTheDirectoryAreAccessible::class, - Directory\ContentHoldsNothing::class, Directory\AddFile::class, Directory\AddDirectory::class, Directory\FilteringDoesntAffectTheDirectory::class, diff --git a/properties/Directory/AddDirectory.php b/properties/Directory/AddDirectory.php index 4dd9837..2df20ce 100644 --- a/properties/Directory/AddDirectory.php +++ b/properties/Directory/AddDirectory.php @@ -38,7 +38,7 @@ public function applicableTo(object $directory): bool public function ensureHeldBy(Assert $assert, object $directory): object { - $file = Directory\Directory::of($this->name); + $file = Directory::of($this->name); $assert->false($directory->contains($file->name())); $newDirectory = $directory->add($file); diff --git a/properties/Directory/AllFilesAreAccessible.php b/properties/Directory/AllFilesAreAccessible.php index 9b11e07..2cc730b 100644 --- a/properties/Directory/AllFilesAreAccessible.php +++ b/properties/Directory/AllFilesAreAccessible.php @@ -32,7 +32,7 @@ public function ensureHeldBy(Assert $assert, object $directory): object [], static fn($all, $file) => \array_merge($all, [$file]), ), - $directory->files()->toList(), + $directory->all()->toList(), ); return $directory; diff --git a/properties/Directory/ContentHoldsNothing.php b/properties/Directory/ContentHoldsNothing.php deleted file mode 100644 index 6c213b0..0000000 --- a/properties/Directory/ContentHoldsNothing.php +++ /dev/null @@ -1,34 +0,0 @@ - - */ -final class ContentHoldsNothing implements Property -{ - public static function any(): Set - { - return Set\Elements::of(new self); - } - - public function applicableTo(object $directory): bool - { - return true; - } - - public function ensureHeldBy(Assert $assert, object $directory): object - { - $assert->same('', $directory->content()->toString()); - - return $directory; - } -} diff --git a/properties/Directory/FlatMapFiles.php b/properties/Directory/FlatMapFiles.php index 15ecd8f..4ae4e80 100644 --- a/properties/Directory/FlatMapFiles.php +++ b/properties/Directory/FlatMapFiles.php @@ -8,10 +8,10 @@ Directory, Name, }; -use Innmind\Immutable\Set; +use Innmind\Immutable\Sequence; use Innmind\BlackBox\{ Property, - Set as DataSet, + Set, Runner\Assert, }; use Fixtures\Innmind\Filesystem\File as FFile; @@ -31,26 +31,26 @@ public function __construct(File $file1, File $file2) $this->file2 = $file2; } - public static function any(): DataSet + public static function any(): Set { - return DataSet\Composite::immutable( + return Set\Composite::immutable( static fn(...$args) => new self(...$args), - DataSet\Randomize::of(FFile::any()), - DataSet\Randomize::of(FFile::any()), + Set\Randomize::of(FFile::any()), + Set\Randomize::of(FFile::any()), ); } public function applicableTo(object $directory): bool { - return !$directory->files()->empty(); + return !$directory->all()->empty(); } public function ensureHeldBy(Assert $assert, object $directory): object { // we use uuids to avoid duplicates - $directory2 = $directory->flatMap(fn($file) => Directory\Directory::of( + $directory2 = $directory->flatMap(fn($file) => Directory::of( Name::of('doesntmatter'), - Set::of( + Sequence::of( $this->file1->rename(Name::of(Uuid::uuid4()->toString())), $this->file2->rename(Name::of(Uuid::uuid4()->toString())), ), @@ -62,14 +62,14 @@ public function ensureHeldBy(Assert $assert, object $directory): object ->same($directory2); $assert->same($directory->name()->toString(), $directory2->name()->toString()); $assert - ->expected($directory->files()) + ->expected($directory->all()) ->not() - ->same($directory2->files()); - $assert->same($directory->files()->size() * 2, $directory2->files()->size()); + ->same($directory2->all()); + $assert->same($directory->all()->size() * 2, $directory2->all()->size()); $assert->same( [$this->file1->content(), $this->file2->content()], $directory2 - ->files() + ->all() ->map(static fn($file) => $file->content()) ->distinct() ->toList(), diff --git a/properties/Directory/MapFiles.php b/properties/Directory/MapFiles.php index 9a9441a..48958f9 100644 --- a/properties/Directory/MapFiles.php +++ b/properties/Directory/MapFiles.php @@ -33,7 +33,7 @@ public static function any(): Set public function applicableTo(object $directory): bool { - return !$directory->files()->empty(); + return !$directory->all()->empty(); } public function ensureHeldBy(Assert $assert, object $directory): object @@ -46,14 +46,14 @@ public function ensureHeldBy(Assert $assert, object $directory): object ->same($directory2); $assert->same($directory->name()->toString(), $directory2->name()->toString()); $assert - ->expected($directory->files()) + ->expected($directory->all()) ->not() - ->same($directory2->files()); - $assert->same($directory->files()->size(), $directory2->files()->size()); + ->same($directory2->all()); + $assert->same($directory->all()->size(), $directory2->all()->size()); $assert->same( [$this->file->content()], $directory2 - ->files() + ->all() ->map(static fn($file) => $file->content()) ->distinct() ->toList(), diff --git a/properties/Directory/MediaTypeIsAlwaysTheSame.php b/properties/Directory/MediaTypeIsAlwaysTheSame.php deleted file mode 100644 index 06ae149..0000000 --- a/properties/Directory/MediaTypeIsAlwaysTheSame.php +++ /dev/null @@ -1,37 +0,0 @@ - - */ -final class MediaTypeIsAlwaysTheSame implements Property -{ - public static function any(): Set - { - return Set\Elements::of(new self); - } - - public function applicableTo(object $directory): bool - { - return true; - } - - public function ensureHeldBy(Assert $assert, object $directory): object - { - $assert->same( - 'text/directory', - $directory->mediaType()->toString(), - ); - - return $directory; - } -} diff --git a/properties/Directory/Rename.php b/properties/Directory/Rename.php index 8a32a87..f2e6b00 100644 --- a/properties/Directory/Rename.php +++ b/properties/Directory/Rename.php @@ -50,8 +50,8 @@ public function ensureHeldBy(Assert $assert, object $directory): object ->same($directory->name()); $assert->same($this->name, $directory2->name()); $assert->same( - $directory->files(), - $directory2->files(), + $directory->all(), + $directory2->all(), ); $assert->same( $directory->removed(), diff --git a/properties/Directory/ThrowWhenFlatMappingToSameFileTwice.php b/properties/Directory/ThrowWhenFlatMappingToSameFileTwice.php index bb3e937..c1f7e01 100644 --- a/properties/Directory/ThrowWhenFlatMappingToSameFileTwice.php +++ b/properties/Directory/ThrowWhenFlatMappingToSameFileTwice.php @@ -9,10 +9,10 @@ Name, Exception\DuplicatedFile, }; -use Innmind\Immutable\Set; +use Innmind\Immutable\Sequence; use Innmind\BlackBox\{ Property, - Set as DataSet, + Set, Runner\Assert, }; use Fixtures\Innmind\Filesystem\File as FFile; @@ -31,40 +31,40 @@ public function __construct(File $file1, File $file2) $this->file2 = $file2; } - public static function any(): DataSet + public static function any(): Set { - return DataSet\Composite::immutable( + return Set\Composite::immutable( static fn(...$args) => new self(...$args), - DataSet\Randomize::of(FFile::any()), - DataSet\Randomize::of(FFile::any()), + Set\Randomize::of(FFile::any()), + Set\Randomize::of(FFile::any()), ); } public function applicableTo(object $directory): bool { - return $directory->files()->size() >= 2; + return $directory->all()->size() >= 2; } public function ensureHeldBy(Assert $assert, object $directory): object { try { - // calling toList in case it uses a lazy Set of files, so we need to - // unwrap the list to trigger the safeguard + // calling toList in case it uses a lazy Sequence of files, so we + // need to unwrap the list to trigger the safeguard $directory - ->flatMap(fn() => Directory\Directory::of( + ->flatMap(fn() => Directory::of( Name::of('doesntmatter'), - Set::of( - File\File::of( + Sequence::of( + File::of( $this->file1->name(), $this->file1->content(), ), - File\File::of( + File::of( $this->file2->name(), $this->file2->content(), ), ), )) - ->files() + ->all() ->toList(); $assert->fail('It should throw'); diff --git a/properties/Directory/ThrowWhenMappingToSameFileTwice.php b/properties/Directory/ThrowWhenMappingToSameFileTwice.php index d168122..b61a87a 100644 --- a/properties/Directory/ThrowWhenMappingToSameFileTwice.php +++ b/properties/Directory/ThrowWhenMappingToSameFileTwice.php @@ -34,20 +34,20 @@ public static function any(): Set public function applicableTo(object $directory): bool { - return $directory->files()->size() >= 2; + return $directory->all()->size() >= 2; } public function ensureHeldBy(Assert $assert, object $directory): object { try { - // calling toList in case it uses a lazy Set of files, so we need to - // unwrap the list to trigger the safeguard + // calling toList in case it uses a lazy Sequence of files, so we + // need to unwrap the list to trigger the safeguard $directory - ->map(fn() => File\File::of( + ->map(fn() => File::of( $this->file->name(), $this->file->content(), )) - ->files() + ->all() ->toList(); $assert->fail('It should throw'); diff --git a/src/Adapter.php b/src/Adapter.php index 40eec2c..57defa7 100644 --- a/src/Adapter.php +++ b/src/Adapter.php @@ -13,20 +13,13 @@ */ interface Adapter { - public function add(File $file): void; + public function add(File|Directory $file): void; /** - * @return Maybe + * @return Maybe */ public function get(Name $file): Maybe; public function contains(Name $file): bool; public function remove(Name $file): void; - - /** - * @deprecated Use self::root() instead - * - * @return Set - */ - public function all(): Set; public function root(): Directory; } diff --git a/src/Adapter/Filesystem.php b/src/Adapter/Filesystem.php index 44c56fd..48c5ffc 100644 --- a/src/Adapter/Filesystem.php +++ b/src/Adapter/Filesystem.php @@ -8,7 +8,6 @@ File, Name, Directory, - Chunk, CaseSensitivity, Exception\PathDoesntRepresentADirectory, Exception\PathTooLong, @@ -17,6 +16,11 @@ Exception\LinksAreNotSupported, Exception\FailedToWriteFile, }; +use Innmind\TimeContinuum\ElapsedPeriod; +use Innmind\IO\{ + IO, + Readable, +}; use Innmind\Stream\{ Capabilities, Streams, @@ -40,15 +44,16 @@ final class Filesystem implements Adapter { private const INVALID_FILES = ['.', '..']; private Capabilities $capabilities; + private Readable $io; private Path $path; private CaseSensitivity $case; private FS $filesystem; - private Chunk $chunk; - /** @var \WeakMap */ + /** @var \WeakMap */ private \WeakMap $loaded; private function __construct( Capabilities $capabilities, + Readable $io, Path $path, CaseSensitivity $case, ) { @@ -57,11 +62,11 @@ private function __construct( } $this->capabilities = $capabilities; + $this->io = $io; $this->path = $path; $this->case = $case; $this->filesystem = new FS; - $this->chunk = new Chunk; - /** @var \WeakMap */ + /** @var \WeakMap */ $this->loaded = new \WeakMap; if (!$this->filesystem->exists($this->path->toString())) { @@ -69,10 +74,20 @@ private function __construct( } } - public static function mount(Path $path, Capabilities $capabilities = null): self - { + public static function mount( + Path $path, + Capabilities $capabilities = null, + IO $io = null, + ): self { + $capabilities ??= Streams::fromAmbientAuthority(); + $io ??= IO::of(static fn(?ElapsedPeriod $period) => match ($period) { + null => $capabilities->watch()->waitForever(), + default => $capabilities->watch()->timeoutAfter($period), + }); + return new self( - $capabilities ?? Streams::fromAmbientAuthority(), + $capabilities, + $io->readable(), $path, CaseSensitivity::sensitive, ); @@ -80,10 +95,10 @@ public static function mount(Path $path, Capabilities $capabilities = null): sel public function withCaseSensitivity(CaseSensitivity $case): self { - return new self($this->capabilities, $this->path, $case); + return new self($this->capabilities, $this->io, $this->path, $case); } - public function add(File $file): void + public function add(File|Directory $file): void { $this->createFileAt($this->path, $file); } @@ -91,7 +106,7 @@ public function add(File $file): void public function get(Name $file): Maybe { if (!$this->contains($file)) { - /** @var Maybe */ + /** @var Maybe */ return Maybe::nothing(); } @@ -108,14 +123,9 @@ public function remove(Name $file): void $this->filesystem->remove($this->path->toString().'/'.$file->toString()); } - public function all(): Set - { - return Set::of(...$this->root()->files()->toList()); - } - public function root(): Directory { - return Directory\Directory::lazy( + return Directory::lazy( Name::of('root'), $this->list($this->path), ); @@ -124,7 +134,7 @@ public function root(): Directory /** * Create the wished file at the given absolute path */ - private function createFileAt(Path $path, File $file): void + private function createFileAt(Path $path, File|Directory $file): void { $name = $file->name()->toString(); @@ -144,7 +154,7 @@ private function createFileAt(Path $path, File $file): void if ($file instanceof Directory) { $this->filesystem->mkdir($path->toString()); $persisted = $file - ->files() + ->all() ->map(function($file) use ($path) { $this->createFileAt($path, $file); @@ -170,7 +180,7 @@ private function createFileAt(Path $path, File $file): void $this->filesystem->remove($path->toString()); } - $chunks = ($this->chunk)($file->content()); + $chunks = $file->content()->chunks(); try { $this->filesystem->touch($path->toString()); @@ -208,7 +218,7 @@ private function createFileAt(Path $path, File $file): void /** * Open the file in the given folder */ - private function open(Path $folder, Name $file): File + private function open(Path $folder, Name $file): File|Directory { $path = $folder->resolve(Path::of($file->toString())); @@ -216,7 +226,7 @@ private function open(Path $folder, Name $file): File $directoryPath = $folder->resolve(Path::of($file->toString().'/')); $files = $this->list($directoryPath); - $directory = Directory\Directory::lazy($file, $files); + $directory = Directory::lazy($file, $files); $this->loaded[$directory] = $directoryPath; return $directory; @@ -226,9 +236,13 @@ private function open(Path $folder, Name $file): File throw new LinksAreNotSupported($path->toString()); } - $file = File\File::of( + $file = File::of( $file, - File\Content\AtPath::of($path, $this->capabilities->readable()), + File\Content::atPath( + $this->capabilities->readable(), + $this->io, + $path, + ), MediaType::maybe(@\mime_content_type($path->toString()) ?: '')->match( static fn($mediaType) => $mediaType, static fn() => MediaType::null(), @@ -240,7 +254,7 @@ private function open(Path $folder, Name $file): File } /** - * @return Sequence + * @return Sequence */ private function list(Path $path): Sequence { diff --git a/src/Adapter/HashedName.php b/src/Adapter/HashedName.php deleted file mode 100644 index 18900dd..0000000 --- a/src/Adapter/HashedName.php +++ /dev/null @@ -1,155 +0,0 @@ -filesystem = $filesystem; - } - - public static function of(Adapter $filesystem): self - { - return new self($filesystem); - } - - public function add(File $file): void - { - if ($file instanceof Directory) { - throw new LogicException('A directory can\'t be hashed'); - } - - $hashes = $this->hash($file->name()); - [$first, $second] = $this->fetch($hashes[0], $hashes[1], $hashes[2]); - - $first = $first->otherwise(static fn() => Maybe::just(Directory\Directory::of($hashes[0]))); - $second = $second - ->otherwise(static fn() => Maybe::just(Directory\Directory::of($hashes[1]))) - ->map(static fn($second) => $second->add(File\File::of( - $hashes[2], - $file->content(), - ))); - $persist = $second - ->flatMap(static fn($second) => $first->map( - static fn($first) => $first->add($second), - )) - ->match( - fn($first) => fn() => $this->filesystem->add($first), - static fn() => static fn() => null, - ); - $persist(); - } - - public function get(Name $file): Maybe - { - $originalName = $file; - $hashes = $this->hash($file); - [, , $concreteFile] = $this->fetch($hashes[0], $hashes[1], $hashes[2]); - - /** @var Maybe */ - return $concreteFile->map(static fn($file) => File\File::of( - $originalName, - $file->content(), - $file->mediaType(), - )); - } - - public function contains(Name $file): bool - { - return $this->get($file)->match( - static fn() => true, - static fn() => false, - ); - } - - public function remove(Name $file): void - { - $hashes = $this->hash($file); - [$first, $second] = $this->fetch($hashes[0], $hashes[1], $hashes[2]); - - $second = $second->map(static fn($second) => $second->remove($hashes[2])); - $first = $second->flatMap(static fn($second) => $first->map( - static fn($first) => $first->add($second), - )); - $persist = $first->match( - fn($first) => fn() => $this->filesystem->add($first), - static fn() => static fn() => null, - ); - $persist(); - } - - public function all(): Set - { - return Set::of(...$this->root()->files()->toList()); - } - - public function root(): Directory - { - //this is not ideal but the names can't be determined from the hashes - return $this->filesystem->root(); - } - - /** - * @return array{0: Name, 1: Name, 2: Name} - */ - private function hash(Name $name): array - { - $extension = \pathinfo($name->toString(), \PATHINFO_EXTENSION); - $hash = Str::of(\sha1(\pathinfo($name->toString(), \PATHINFO_BASENAME))); - - /** @psalm-suppress ArgumentTypeCoercion */ - $first = Name::of($hash->substring(0, 2)->toString()); - /** @psalm-suppress ArgumentTypeCoercion */ - $second = Name::of($hash->substring(2, 2)->toString()); - /** @psalm-suppress ArgumentTypeCoercion */ - $remaining = Name::of($hash->substring(4)->toString().($extension ? '.'.$extension : '')); - - return [$first, $second, $remaining]; - } - - /** - * @return array{0: Maybe, 1: Maybe, 2: Maybe} - */ - private function fetch(Name $first, Name $second, Name $file): array - { - /** @var Maybe */ - $firstDirectory = $this - ->filesystem - ->get($first) - ->filter(static fn($first) => $first instanceof Directory); - /** @var Maybe */ - $secondDirectory = $firstDirectory - ->flatMap(static fn($first) => $first->get($second)) - ->filter(static fn($second) => $second instanceof Directory); - $concreteFile = $secondDirectory->flatMap( - static fn($second) => $second->get($file), - ); - - return [$firstDirectory, $secondDirectory, $concreteFile]; - } -} diff --git a/src/Adapter/InMemory.php b/src/Adapter/InMemory.php index 473cf75..7692c6f 100644 --- a/src/Adapter/InMemory.php +++ b/src/Adapter/InMemory.php @@ -24,7 +24,7 @@ final class InMemory implements Adapter private function __construct(Overwrite|Merge $behaviour) { - $this->root = Directory\Directory::named('root'); + $this->root = Directory::named('root'); $this->behaviour = $behaviour; } @@ -38,7 +38,7 @@ public static function emulateFilesystem(): self return new self(new Merge); } - public function add(File $file): void + public function add(File|Directory $file): void { $this->root = ($this->behaviour)($this->root, $file); } @@ -58,11 +58,6 @@ public function remove(Name $file): void $this->root = $this->root->remove($file); } - public function all(): Set - { - return $this->root()->files()->toSet(); - } - public function root(): Directory { return $this->root; diff --git a/src/Adapter/InMemory/Merge.php b/src/Adapter/InMemory/Merge.php index 8688141..afc87ec 100644 --- a/src/Adapter/InMemory/Merge.php +++ b/src/Adapter/InMemory/Merge.php @@ -16,7 +16,7 @@ */ final class Merge { - public function __invoke(Directory $parent, File $file): Directory + public function __invoke(Directory $parent, File|Directory $file): Directory { if (!$file instanceof Directory) { return $parent->add($file); diff --git a/src/Adapter/InMemory/Overwrite.php b/src/Adapter/InMemory/Overwrite.php index f27ae04..450ae9f 100644 --- a/src/Adapter/InMemory/Overwrite.php +++ b/src/Adapter/InMemory/Overwrite.php @@ -13,7 +13,7 @@ */ final class Overwrite { - public function __invoke(Directory $parent, File $file): Directory + public function __invoke(Directory $parent, File|Directory $file): Directory { return $parent->add($file); } diff --git a/src/Adapter/Logger.php b/src/Adapter/Logger.php index df8eb38..2072939 100644 --- a/src/Adapter/Logger.php +++ b/src/Adapter/Logger.php @@ -31,7 +31,7 @@ public static function psr(Adapter $filesystem, LoggerInterface $logger): self return new self($filesystem, $logger); } - public function add(File $file): void + public function add(File|Directory $file): void { $this->logger->debug('Adding file {file}', ['file' => $file->name()->toString()]); $this->filesystem->add($file); @@ -69,11 +69,6 @@ public function remove(Name $file): void $this->filesystem->remove($file); } - public function all(): Set - { - return Set::of(...$this->root()->files()->toList()); - } - public function root(): Directory { return $this->filesystem->root(); diff --git a/src/Chunk.php b/src/Chunk.php deleted file mode 100644 index 876520f..0000000 --- a/src/Chunk.php +++ /dev/null @@ -1,37 +0,0 @@ - - */ - public function __invoke(File\Content $content): Sequence - { - if ($content instanceof File\Content\Chunkable) { - return $content->chunks(); - } - - $firstLineRead = false; - - return $content->lines()->map(static function($line) use (&$firstLineRead) { - if (!$firstLineRead) { - $firstLineRead = true; - - return $line->str(); - } - - return $line->str()->prepend("\n"); - }); - } -} diff --git a/src/Directory.php b/src/Directory.php index 76b5270..dc6d5b7 100644 --- a/src/Directory.php +++ b/src/Directory.php @@ -14,50 +14,190 @@ /** * @psalm-immutable */ -interface Directory extends File +final class Directory { - public function add(File $file): self; + private Name $name; + /** @var Sequence */ + private Sequence $files; + /** @var Set */ + private Set $removed; /** - * @return Maybe + * @param Sequence $files + * @param Set $removed */ - public function get(Name $name): Maybe; - public function contains(Name $name): bool; - public function remove(Name $name): self; + private function __construct(Name $name, Sequence $files, Set $removed) + { + $this->name = $name; + $this->files = $files; + $this->removed = $removed; + } /** - * @param callable(File): void $function + * @psalm-pure + * + * @param Sequence|null $files + * + * @throws DuplicatedFile + */ + public static function of(Name $name, Sequence $files = null): self + { + return new self( + $name, + self::safeguard($files ?? Sequence::of()), + Set::of(), + ); + } + + /** + * @psalm-pure + * + * @param non-empty-string $name + */ + public static function named(string $name): self + { + return new self( + Name::of($name), + Sequence::of(), + Set::of(), + ); + } + + /** + * @internal + * @psalm-pure + * + * @param Sequence $files + */ + public static function lazy(Name $name, Sequence $files): self + { + // we prevent the contrusctor from checking for duplicates when + // using a lazy set of files as it will trigger to load the whole + // directory tree, it's kinda safe to do this as this method should + // only be used within the filesystem adapter and there should be no + // duplicates on a concrete filesystem + return new self($name, $files, Set::of()); + } + + public function name(): Name + { + return $this->name; + } + + public function rename(Name $name): self + { + return new self( + $name, + $this->files, + $this->removed, + ); + } + + public function add(File|self $file): self + { + return new self( + $this->name, + $this + ->files + ->filter(static fn(File|self $known): bool => !$known->name()->equals($file->name())) + ->add($file), + $this->removed, + ); + } + + /** + * @return Maybe + */ + public function get(Name $name): Maybe + { + return $this->files->find(static fn($file) => $file->name()->equals($name)); + } + + public function contains(Name $name): bool + { + return $this->get($name)->match( + static fn() => true, + static fn() => false, + ); + } + + public function remove(Name $name): self + { + return new self( + $this->name, + $this->files->filter(static fn(File|self $file) => !$file->name()->equals($name)), + ($this->removed)($name), + ); + } + + /** + * @param callable(File|self): void $function */ - public function foreach(callable $function): SideEffect; + public function foreach(callable $function): SideEffect + { + return $this->files->foreach($function); + } /** - * @param callable(File): bool $predicate + * @param callable(File|self): bool $predicate */ - public function filter(callable $predicate): self; + public function filter(callable $predicate): self + { + // it is safe to not check for duplicates here as either the current + // directory comes from the filesystem thus can't have duplicates or + // comes from a user and must have used the standard constructor that + // validates for duplicates, so they're can't be any duplicates after + // a filter + return new self( + $this->name, + $this->files->filter($predicate), + $this->removed, + ); + } /** - * @param callable(File): File $map + * @param callable(File|self): File $map * * @throws DuplicatedFile */ - public function map(callable $map): self; + public function map(callable $map): self + { + return new self( + $this->name, + self::safeguard($this->files->map($map)), + $this->removed, + ); + } /** - * @param callable(File): self $map + * @param callable(File|self): self $map * * @throws DuplicatedFile */ - public function flatMap(callable $map): self; + public function flatMap(callable $map): self + { + /** @var callable(File|self): Sequence */ + $map = static fn(File|self $file): Sequence => $map($file)->all(); + + return new self( + $this->name, + self::safeguard($this->files->flatMap($map)), + $this->removed, + ); + } /** * @template R * * @param R $carry - * @param callable(R, File): R $reducer + * @param callable(R, File|self): R $reducer * * @return R */ - public function reduce($carry, callable $reducer); + public function reduce($carry, callable $reducer) + { + return $this->files->reduce($carry, $reducer); + } /** * This method should only be used for implementations of the Adapter @@ -65,10 +205,36 @@ public function reduce($carry, callable $reducer); * * @return Set */ - public function removed(): Set; + public function removed(): Set + { + return $this->removed; + } /** - * @return Sequence + * @return Sequence + */ + public function all(): Sequence + { + return $this->files; + } + + /** + * @psalm-pure + * + * @param Sequence $files + * + * @throws DuplicatedFile + * + * @return Sequence */ - public function files(): Sequence; + 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()), + false => ($names)($file->name()->toString()), + }, + ); + } } diff --git a/src/Directory/Directory.php b/src/Directory/Directory.php deleted file mode 100644 index 03369e6..0000000 --- a/src/Directory/Directory.php +++ /dev/null @@ -1,233 +0,0 @@ - */ - 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; - } - - /** - * @psalm-pure - * - * @param Set|Sequence|null $files - * - * @throws DuplicatedFile - */ - public static function of(Name $name, Set|Sequence $files = null): self - { - if ($files instanceof Set) { - $files = Sequence::of(...$files->toList()); - } - - return new self( - $name, - self::safeguard($files ?? Sequence::of()), - Set::of(), - ); - } - - /** - * @psalm-pure - * - * @param non-empty-string $name - */ - public static function named(string $name): self - { - return new self( - Name::of($name), - Sequence::of(), - Set::of(), - ); - } - - /** - * @internal - * @psalm-pure - * - * @param Sequence $files - */ - public static function lazy(Name $name, Sequence $files): self - { - // we prevent the contrusctor from checking for duplicates when - // using a lazy set of files as it will trigger to load the whole - // directory tree, it's kinda safe to do this as this method should - // only be used within the filesystem adapter and there should be no - // duplicates on a concrete filesystem - return new self($name, $files, Set::of()); - } - - public function name(): Name - { - return $this->name; - } - - public function content(): Content - { - return Content\None::of(); - } - - public function mediaType(): MediaType - { - return new MediaType( - 'text', - 'directory', - ); - } - - public function rename(Name $name): self - { - return new self( - $name, - $this->files, - $this->removed, - ); - } - - public function withContent(Content $content, MediaType $mediaType = null): self - { - return $this; - } - - public function add(File $file): DirectoryInterface - { - return new self( - $this->name, - $this - ->files - ->filter(static fn(File $known): bool => !$known->name()->equals($file->name())) - ->add($file), - $this->removed, - ); - } - - public function get(Name $name): Maybe - { - return $this->files->find(static fn($file) => $file->name()->equals($name)); - } - - public function contains(Name $name): bool - { - return $this->get($name)->match( - static fn() => true, - static fn() => false, - ); - } - - public function remove(Name $name): DirectoryInterface - { - return new self( - $this->name, - $this->files->filter(static fn(File $file) => !$file->name()->equals($name)), - ($this->removed)($name), - ); - } - - public function foreach(callable $function): SideEffect - { - return $this->files->foreach($function); - } - - public function filter(callable $predicate): self - { - // it is safe to not check for duplicates here as either the current - // directory comes from the filesystem thus can't have duplicates or - // comes from a user and must have used the standard constructor that - // validates for duplicates, so they're can't be any duplicates after - // a filter - return new self( - $this->name, - $this->files->filter($predicate), - $this->removed, - ); - } - - public function map(callable $map): self - { - return new self( - $this->name, - self::safeguard($this->files->map($map)), - $this->removed, - ); - } - - public function flatMap(callable $map): self - { - /** @var callable(File): Sequence */ - $map = static fn(File $file): Sequence => $map($file)->files(); - - return new self( - $this->name, - self::safeguard($this->files->flatMap($map)), - $this->removed, - ); - } - - public function reduce($carry, callable $reducer) - { - return $this->files->reduce($carry, $reducer); - } - - public function removed(): Set - { - return $this->removed; - } - - public function files(): Sequence - { - return $this->files; - } - - /** - * @psalm-pure - * - * @param Sequence $files - * - * @throws DuplicatedFile - * - * @return Sequence - */ - 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()), - false => ($names)($file->name()->toString()), - }, - ); - } -} diff --git a/src/File.php b/src/File.php index 0daeb6d..53baaad 100644 --- a/src/File.php +++ b/src/File.php @@ -9,15 +9,72 @@ /** * @psalm-immutable */ -interface File +final class File { - public function name(): Name; - public function content(): Content; - public function mediaType(): MediaType; - public function rename(Name $name): self; + private Name $name; + private Content $content; + private MediaType $mediaType; + + private function __construct( + Name $name, + Content $content, + MediaType $mediaType = null, + ) { + $this->name = $name; + $this->content = $content; + $this->mediaType = $mediaType ?? MediaType::null(); + } /** - * This method called on a directory DOES NOTHING + * @psalm-pure */ - public function withContent(Content $content, MediaType $mediaType = null): self; + public static function of( + Name $name, + Content $content, + MediaType $mediaType = null, + ): self { + return new self($name, $content, $mediaType); + } + + /** + * @psalm-pure + * + * @param non-empty-string $name + */ + public static function named( + string $name, + Content $content, + MediaType $mediaType = null, + ): self { + return new self(Name::of($name), $content, $mediaType); + } + + public function name(): Name + { + return $this->name; + } + + public function content(): Content + { + return $this->content; + } + + public function mediaType(): MediaType + { + return $this->mediaType; + } + + public function rename(Name $name): self + { + return new self($name, $this->content, $this->mediaType); + } + + public function withContent(Content $content, MediaType $mediaType = null): self + { + return new self( + $this->name, + $content, + $mediaType ?? $this->mediaType, + ); + } } diff --git a/src/File/Content.php b/src/File/Content.php index 216b6fa..5da3478 100644 --- a/src/File/Content.php +++ b/src/File/Content.php @@ -3,9 +3,19 @@ namespace Innmind\Filesystem\File; -use Innmind\Filesystem\File\Content\Line; -use Innmind\Stream\Stream\Size; +use Innmind\Filesystem\File\Content\{ + Implementation, + Line, +}; +use Innmind\IO; +use Innmind\Stream\{ + Stream\Size, + Readable as Stream, + Capabilities, +}; +use Innmind\Url\Path; use Innmind\Immutable\{ + Str, Sequence, SideEffect, Maybe, @@ -14,32 +24,123 @@ /** * @psalm-immutable */ -interface Content +final class Content { + private Implementation $implementation; + + private function __construct(Implementation $implementation) + { + $this->implementation = $implementation; + } + + /** + * @psalm-pure + */ + public static function atPath( + Capabilities\Readable $capabilities, + IO\Readable $io, + Path $path, + ): self { + return new self(Content\AtPath::of($capabilities, $io, $path)); + } + + /** + * @psalm-pure + * + * This method is to be used with sockets that can't be read twice + */ + public static function oneShot(IO\Readable\Stream $io): self + { + return new self(Content\OneShot::of($io)); + } + + /** + * @psalm-pure + * + * @param Sequence $chunks + */ + public static function ofChunks(Sequence $chunks): self + { + return new self(Content\Chunks::of($chunks)); + } + + /** + * @psalm-pure + * + * @param Sequence $lines + */ + public static function ofLines(Sequence $lines): self + { + return new self(Content\Lines::of($lines)); + } + + /** + * @psalm-pure + */ + public static function ofString(string $content): self + { + return self::ofLines( + Str::of($content) + ->split("\n") + ->map(static fn($line) => Line::fromStream($line)), + ); + } + + /** + * @psalm-pure + */ + public static function none(): self + { + return new self(Content\Lines::of(Sequence::of(Line::of(Str::of(''))))); + } + /** * @param callable(Line): void $function */ - public function foreach(callable $function): SideEffect; + public function foreach(callable $function): SideEffect + { + return $this->implementation->foreach($function); + } /** * @param callable(Line): Line $map */ - public function map(callable $map): self; + public function map(callable $map): self + { + return new self($this->implementation->map($map)); + } /** * @param callable(Line): self $map */ - public function flatMap(callable $map): self; + public function flatMap(callable $map): self + { + return new self($this->implementation->flatMap($map)); + } /** * @param callable(Line): bool $filter */ - public function filter(callable $filter): self; + public function filter(callable $filter): self + { + return new self($this->implementation->filter($filter)); + } /** * @return Sequence */ - public function lines(): Sequence; + public function lines(): Sequence + { + return $this->implementation->lines(); + } + + /** + * @return Sequence + */ + public function chunks(): Sequence + { + return $this->implementation->chunks(); + } /** * @template T @@ -49,11 +150,21 @@ public function lines(): Sequence; * * @return T */ - public function reduce($carry, callable $reducer); + public function reduce($carry, callable $reducer) + { + return $this->implementation->reduce($carry, $reducer); + } /** * @return Maybe */ - public function size(): Maybe; - public function toString(): string; + public function size(): Maybe + { + return $this->implementation->size(); + } + + public function toString(): string + { + return $this->implementation->toString(); + } } diff --git a/src/File/Content/AtPath.php b/src/File/Content/AtPath.php index a65009c..3fb25b5 100644 --- a/src/File/Content/AtPath.php +++ b/src/File/Content/AtPath.php @@ -3,80 +3,133 @@ namespace Innmind\Filesystem\File\Content; -use Innmind\Filesystem\{ - File\Content, - Stream\LazyStream, -}; -use Innmind\Stream\Capabilities\Readable; +use Innmind\Filesystem\Exception\FailedToLoadFile; +use Innmind\IO; +use Innmind\Stream\Capabilities; use Innmind\Url\Path; use Innmind\Immutable\{ Sequence, SideEffect, Maybe, + Monoid\Concat, }; /** + * @internal * @psalm-immutable */ -final class AtPath implements Content, Chunkable +final class AtPath implements Implementation { - private OfStream $content; + private Capabilities\Readable $capabilities; + private IO\Readable $io; + private Path $path; - private function __construct(OfStream $content) - { - $this->content = $content; + private function __construct( + Capabilities\Readable $capabilities, + IO\Readable $io, + Path $path, + ) { + $this->capabilities = $capabilities; + $this->io = $io; + $this->path = $path; } /** * @psalm-pure */ - public static function of(Path $path, Readable $capabilities = null): self - { - return new self(OfStream::lazy(static fn() => new LazyStream($path, $capabilities))); + public static function of( + Capabilities\Readable $capabilities, + IO\Readable $io, + Path $path, + ): self { + return new self($capabilities, $io, $path); } public function foreach(callable $function): SideEffect { - return $this->content->foreach($function); + return $this->lines()->foreach($function); } - public function map(callable $map): Content + public function map(callable $map): Implementation { - return $this->content->map($map); + return Lines::of($this->lines()->map($map)); } - public function flatMap(callable $map): Content + public function flatMap(callable $map): Implementation { - return $this->content->flatMap($map); + return Lines::of($this->lines())->flatMap($map); } - public function filter(callable $filter): Content + public function filter(callable $filter): Implementation { - return $this->content->filter($filter); + return Lines::of($this->lines()->filter($filter)); } public function lines(): Sequence { - return $this->content->lines(); + return Sequence::lazy(function($register) { + $stream = $this->capabilities->open($this->path); + $register(static fn() => $stream->close()->match( + static fn() => null, + static fn() => throw new FailedToLoadFile, + )); + $io = $this->io->wrap($stream); + + yield $io + ->lines() + ->lazy() + ->sequence(); + + $stream->close()->match( + static fn() => null, + static fn() => throw new FailedToLoadFile, + ); + }) + ->flatMap(static fn($lines) => $lines) + ->map(static fn($line) => Line::fromStream($line)); } public function reduce($carry, callable $reducer) { - return $this->content->reduce($carry, $reducer); + return $this->lines()->reduce($carry, $reducer); } public function size(): Maybe { - return $this->content->size(); + /** @psalm-suppress ImpureMethodCall */ + return $this + ->capabilities + ->open($this->path) + ->size(); } public function toString(): string { - return $this->content->toString(); + return $this + ->chunks() + ->fold(new Concat) + ->toString(); } public function chunks(): Sequence { - return $this->content->chunks(); + return Sequence::lazy(function($register) { + $stream = $this->capabilities->open($this->path); + $register(static fn() => $stream->close()->match( + static fn() => null, + static fn() => throw new FailedToLoadFile, + )); + $io = $this->io->wrap($stream); + + yield $io + ->chunks(8192) + ->lazy() + ->sequence(); + + $stream->close()->match( + static fn() => null, + static fn() => throw new FailedToLoadFile, + ); + })->flatMap(static fn($chunks) => $chunks); } } diff --git a/src/File/Content/Chunkable.php b/src/File/Content/Chunkable.php deleted file mode 100644 index 5fb4692..0000000 --- a/src/File/Content/Chunkable.php +++ /dev/null @@ -1,22 +0,0 @@ - - */ - public function chunks(): Sequence; -} diff --git a/src/File/Content/Chunks.php b/src/File/Content/Chunks.php index 1e5e94f..a9a5b0b 100644 --- a/src/File/Content/Chunks.php +++ b/src/File/Content/Chunks.php @@ -3,7 +3,6 @@ namespace Innmind\Filesystem\File\Content; -use Innmind\Filesystem\File\Content; use Innmind\Immutable\{ Sequence, SideEffect, @@ -13,9 +12,10 @@ }; /** + * @internal * @psalm-immutable */ -final class Chunks implements Content, Chunkable +final class Chunks implements Implementation { /** @var Sequence */ private Sequence $chunks; @@ -25,7 +25,7 @@ final class Chunks implements Content, Chunkable */ private function __construct(Sequence $chunks) { - $this->chunks = $chunks; + $this->chunks = $chunks->pad(1, Str::of('')); } /** @@ -43,17 +43,17 @@ public function foreach(callable $function): SideEffect return $this->content()->foreach($function); } - public function map(callable $map): Content + public function map(callable $map): Implementation { return $this->content()->map($map); } - public function flatMap(callable $map): Content + public function flatMap(callable $map): Implementation { return $this->content()->flatMap($map); } - public function filter(callable $filter): Content + public function filter(callable $filter): Implementation { return $this->content()->filter($filter); } @@ -92,7 +92,7 @@ public function toString(): string ->toString(); } - private function content(): Content + private function content(): Implementation { return Lines::of($this->lines()); } diff --git a/src/File/Content/Implementation.php b/src/File/Content/Implementation.php new file mode 100644 index 0000000..9b1cfdd --- /dev/null +++ b/src/File/Content/Implementation.php @@ -0,0 +1,66 @@ + + */ + public function lines(): Sequence; + + /** + * @return Sequence + */ + public function chunks(): Sequence; + + /** + * @template T + * + * @param T $carry + * @param callable(T, Line): T $reducer + * + * @return T + */ + public function reduce($carry, callable $reducer); + + /** + * @return Maybe + */ + public function size(): Maybe; + public function toString(): string; +} diff --git a/src/File/Content/Lines.php b/src/File/Content/Lines.php index 97c3f74..5d84511 100644 --- a/src/File/Content/Lines.php +++ b/src/File/Content/Lines.php @@ -3,7 +3,6 @@ namespace Innmind\Filesystem\File\Content; -use Innmind\Filesystem\File\Content; use Innmind\Stream\Stream\Size; use Innmind\Immutable\{ Sequence, @@ -13,9 +12,10 @@ }; /** + * @internal * @psalm-immutable */ -final class Lines implements Content +final class Lines implements Implementation { /** @var Sequence */ private Sequence $lines; @@ -25,7 +25,7 @@ final class Lines implements Content */ private function __construct(Sequence $lines) { - $this->lines = $lines; + $this->lines = $lines->pad(1, Line::of(Str::of(''))); } /** @@ -38,36 +38,24 @@ public static function of(Sequence $lines): self return new self($lines); } - /** - * @psalm-pure - */ - public static function ofContent(string $content): self - { - return new self( - Str::of($content) - ->split("\n") - ->map(static fn($line) => Line::fromStream($line)), - ); - } - public function foreach(callable $function): SideEffect { return $this->lines->foreach($function); } - public function map(callable $map): Content + public function map(callable $map): Implementation { return new self($this->lines->map($map)); } - public function flatMap(callable $map): Content + public function flatMap(callable $map): Implementation { return new self($this->lines->flatMap( static fn($line) => $map($line)->lines(), )); } - public function filter(callable $filter): Content + public function filter(callable $filter): Implementation { return new self($this->lines->filter($filter)); } @@ -77,6 +65,21 @@ public function lines(): Sequence return $this->lines; } + public function chunks(): Sequence + { + $firstLineRead = false; + + return $this->lines->map(static function($line) use (&$firstLineRead) { + if (!$firstLineRead) { + $firstLineRead = true; + + return $line->str(); + } + + return $line->str()->prepend("\n"); + }); + } + public function reduce($carry, callable $reducer) { return $this->lines->reduce($carry, $reducer); diff --git a/src/File/Content/None.php b/src/File/Content/None.php deleted file mode 100644 index 8202bdf..0000000 --- a/src/File/Content/None.php +++ /dev/null @@ -1,26 +0,0 @@ -load = $load; - } - - /** - * @psalm-pure - */ - public static function of(Readable $stream): self - { - return new self(static fn() => $stream); - } - - /** - * @psalm-pure - * - * @param callable(): Readable $load - */ - public static function lazy(callable $load): self - { - return new self($load); - } - - public function foreach(callable $function): SideEffect - { - return $this->lines()->foreach($function); - } - - public function map(callable $map): Content - { - return Lines::of($this->lines()->map($map)); - } - - public function flatMap(callable $map): Content - { - return Lines::of($this->lines())->flatMap($map); - } - - public function filter(callable $filter): Content - { - return Lines::of($this->lines()->filter($filter)); - } - - public function lines(): Sequence - { - return $this - ->sequence() - ->map(static fn($line) => Line::fromStream($line)); - } - - public function reduce($carry, callable $reducer) - { - return $this->lines()->reduce($carry, $reducer); - } - - public function size(): Maybe - { - return $this->load()->size(); - } - - public function toString(): string - { - return $this - ->chunks() - ->fold(new Concat) - ->toString(); - } - - public function chunks(): Sequence - { - return $this->sequence(static fn(Readable $stream) => $stream->read(8192)); - } - - /** - * @param ?callable(Readable): Maybe $read - * - * @return Sequence - */ - private function sequence(callable $read = null): Sequence - { - /** @var callable(Readable): Maybe */ - $read ??= static fn(Readable $stream): Maybe => $stream->readLine(); - - return Sequence::lazy(function($cleanup) use ($read) { - $stream = $this->load(); - $rewind = static function() use ($stream): void { - $_ = $stream->rewind()->match( - static fn() => null, // rewind successfull - static fn() => throw new FailedToLoadFile, - ); - }; - $cleanup($rewind); - /** @var Readable */ - $stream = $stream->rewind()->match( - static fn($stream) => $stream, - static fn() => throw new FailedToLoadFile, - ); - - while (!$stream->end()) { - // we yield an empty line when the readLine() call doesn't return - // anything otherwise it will fail to load empty files or files - // ending with the "end of line" character - yield $read($stream)->match( - static fn($line) => $line, - static fn() => match ($stream->end()) { - true => Str::of(''), - false => throw new FailedToLoadFile, - }, - ); - } - - $rewind(); - }); - } - - private function load(): Readable - { - /** @psalm-suppress ImpureFunctionCall */ - return ($this->load)(); - } -} diff --git a/src/File/Content/OneShot.php b/src/File/Content/OneShot.php new file mode 100644 index 0000000..b1412fc --- /dev/null +++ b/src/File/Content/OneShot.php @@ -0,0 +1,113 @@ +io = $io; + } + + /** + * @psalm-pure + */ + public static function of(Stream $io): self + { + return new self($io); + } + + public function foreach(callable $function): SideEffect + { + return $this->lines()->foreach($function); + } + + public function map(callable $map): Implementation + { + return Lines::of($this->lines()->map($map)); + } + + public function flatMap(callable $map): Implementation + { + return Lines::of($this->lines())->flatMap($map); + } + + public function filter(callable $filter): Implementation + { + return Lines::of($this->lines()->filter($filter)); + } + + public function lines(): Sequence + { + return Sequence::lazy(function() { + $this->guard(); + + yield $this + ->io + ->lines() + ->lazy() + ->sequence(); + }) + ->flatMap(static fn($lines) => $lines) + ->map(static fn($line) => Line::fromStream($line)); + } + + public function reduce($carry, callable $reducer) + { + return $this->lines()->reduce($carry, $reducer); + } + + public function size(): Maybe + { + /** @psalm-suppress ImpureMethodCall */ + return $this->io->size(); + } + + public function toString(): string + { + return $this + ->chunks() + ->fold(new Concat) + ->toString(); + } + + public function chunks(): Sequence + { + return Sequence::lazy(function() { + $this->guard(); + + yield $this + ->io + ->chunks(8192) + ->lazy() + ->sequence(); + })->flatMap(static fn($chunks) => $chunks); + } + + private function guard(): void + { + if ($this->loaded) { + throw new LogicException("Content can't be loaded twice"); + } + + /** @psalm-suppress InaccessibleProperty */ + $this->loaded = true; + } +} diff --git a/src/File/File.php b/src/File/File.php deleted file mode 100644 index e051457..0000000 --- a/src/File/File.php +++ /dev/null @@ -1,86 +0,0 @@ -name = $name; - $this->content = $content; - $this->mediaType = $mediaType ?? MediaType::null(); - } - - /** - * @psalm-pure - */ - public static function of( - Name $name, - Content $content, - MediaType $mediaType = null, - ): self { - return new self($name, $content, $mediaType); - } - - /** - * @psalm-pure - * - * @param non-empty-string $name - */ - public static function named( - string $name, - Content $content, - MediaType $mediaType = null, - ): self { - return new self(Name::of($name), $content, $mediaType); - } - - public function name(): Name - { - return $this->name; - } - - public function content(): Content - { - return $this->content; - } - - public function mediaType(): MediaType - { - return $this->mediaType; - } - - public function rename(Name $name): self - { - return new self($name, $this->content, $this->mediaType); - } - - public function withContent(Content $content, MediaType $mediaType = null): self - { - return new self( - $this->name, - $content, - $mediaType ?? $this->mediaType, - ); - } -} diff --git a/src/Name.php b/src/Name.php index 0b10b04..513bfa8 100644 --- a/src/Name.php +++ b/src/Name.php @@ -11,12 +11,13 @@ */ final class Name { + /** @var non-empty-string */ private string $value; /** - * @deprecated Use Name::of() instead + * @param non-empty-string $value */ - public function __construct(string $value) + private function __construct(string $value) { if (Str::of($value)->empty()) { throw new DomainException('A file name can\'t be empty'); @@ -69,6 +70,9 @@ public function str(): Str return Str::of($this->value); } + /** + * @return non-empty-string + */ public function toString(): string { return $this->value; diff --git a/src/Stream/LazyStream.php b/src/Stream/LazyStream.php deleted file mode 100644 index 226572d..0000000 --- a/src/Stream/LazyStream.php +++ /dev/null @@ -1,139 +0,0 @@ -path = $path; - $this->capabilities = $capabilities; - } - - /** - * @psalm-mutation-free - */ - public function resource() - { - /** @psalm-suppress ImpureMethodCall */ - return $this->stream()->resource(); - } - - public function close(): Either - { - return $this->stream()->close(); - } - - /** - * @psalm-mutation-free - */ - public function closed(): bool - { - /** @psalm-suppress ImpureMethodCall */ - return $this->stream()->closed(); - } - - public function position(): Position - { - return $this->stream()->position(); - } - - public function seek(Position $position, Mode $mode = null): Either - { - /** @var Either */ - return $this->stream()->seek($position, $mode)->map(fn() => $this); - } - - public function rewind(): Either - { - if ($this->stream && !$this->stream->closed()) { - // this trick allows to automatically close the opened files on the - // system in order to avoid a fatal error when too many files are - // opened. This is possible because of the rewind done in - // File\Content\OfStream::chunks() after persisting a file. - // This does not break be behaviour of the streams as once the stream - // is manually closed we won't reopen it here - /** - * @psalm-suppress InvalidArgument - * @var Either - */ - return $this - ->stream - ->close() - ->map(function() { - $this->stream = null; - - return $this; - }) - ->leftMap(static fn() => new PositionNotSeekable); - } - - /** @var Either */ - return Either::right($this); - } - - /** - * @psalm-mutation-free - */ - public function end(): bool - { - /** @psalm-suppress ImpureMethodCall */ - return $this->stream()->end(); - } - - /** - * @psalm-mutation-free - */ - public function size(): Maybe - { - /** @psalm-suppress ImpureMethodCall */ - return $this->stream()->size(); - } - - public function read(int $length = null): Maybe - { - return $this->stream()->read($length); - } - - public function readLine(): Maybe - { - return $this->stream()->readLine(); - } - - public function toString(): Maybe - { - return $this->stream()->toString(); - } - - private function stream(): Readable - { - return $this->stream ??= match ($this->capabilities) { - null => Readable\Stream::open($this->path), - default => $this->capabilities->open($this->path), - }; - } -} diff --git a/tests/Adapter/FilesystemTest.php b/tests/Adapter/FilesystemTest.php index fd2f60a..65934d2 100644 --- a/tests/Adapter/FilesystemTest.php +++ b/tests/Adapter/FilesystemTest.php @@ -7,26 +7,25 @@ Adapter\Filesystem, Adapter, CaseSensitivity, - File\File, - File\Content\None, - File\Content\Lines, + File, + File\Content, Name, Directory as DirectoryInterface, - Directory\Directory, + Directory, Exception\PathDoesntRepresentADirectory, Exception\PathTooLong, Exception\LinksAreNotSupported, }; use Innmind\Url\Path; use Innmind\Immutable\{ - Set, + Sequence, Map, }; use Symfony\Component\Filesystem\Filesystem as FS; use PHPUnit\Framework\TestCase; use Innmind\BlackBox\{ PHPUnit\BlackBox, - Set as DataSet, + Set, }; use Fixtures\Innmind\Filesystem\Name as FName; @@ -83,10 +82,10 @@ public function testCreateNestedStructure() $adapter = Filesystem::mount(Path::of('/tmp/')); $directory = Directory::of(Name::of('foo')) - ->add(File::of(Name::of('foo.md'), Lines::ofContent('# Foo'))) + ->add(File::of(Name::of('foo.md'), Content::ofString('# Foo'))) ->add( Directory::of(Name::of('bar')) - ->add(File::of(Name::of('bar.md'), Lines::ofContent('# Bar'))), + ->add(File::of(Name::of('bar.md'), Content::ofString('# Bar'))), ); $adapter->add($directory); $this->assertSame( @@ -153,7 +152,7 @@ public function testRemoveFileWhenRemovedFromFolder() $a = Filesystem::mount(Path::of('/tmp/')); $d = Directory::of(Name::of('foo')); - $d = $d->add(File::of(Name::of('bar'), Lines::ofContent('some content'))); + $d = $d->add(File::of(Name::of('bar'), Content::ofString('some content'))); $a->add($d); $d = $d->remove(Name::of('bar')); $a->add($d); @@ -173,7 +172,7 @@ public function testDoesntFailWhenAddindSameDirectoryTwiceThatContainsARemovedFi $a = Filesystem::mount(Path::of('/tmp/')); $d = Directory::of(Name::of('foo')); - $d = $d->add(File::of(Name::of('bar'), Lines::ofContent('some content'))); + $d = $d->add(File::of(Name::of('bar'), Content::ofString('some content'))); $a->add($d); $d = $d->remove(Name::of('bar')); $a->add($d); @@ -210,19 +209,18 @@ public function testLoadWithMediaType() $a->remove(Name::of('some_content.html')); } - public function testAll() + public function testRoot() { $adapter = Filesystem::mount(Path::of('/tmp/test/')); $adapter->add(File::of( Name::of('foo'), - Lines::ofContent('foo'), + Content::ofString('foo'), )); \file_put_contents('/tmp/test/bar', 'bar'); \mkdir('/tmp/test/baz'); \file_put_contents('/tmp/test/baz/foobar', 'baz'); - $all = $adapter->all(); - $this->assertInstanceOf(Set::class, $all); + $all = $adapter->root()->all(); $this->assertCount(3, $all); $all = Map::of( ...$all @@ -269,7 +267,7 @@ public function testAddingTheSameFileTwiceDoesNothing() $adapter = Filesystem::mount(Path::of('/tmp/')); $file = File::of( Name::of('foo'), - Lines::ofContent('foo'), + Content::ofString('foo'), ); $this->assertNull($adapter->add($file)); @@ -291,16 +289,16 @@ public function testPathTooLongThrowAnException() $filesystem->add(Directory::of( Name::of(\str_repeat('a', 255)), - Set::of( + Sequence::of( Directory::of( Name::of(\str_repeat('a', 255)), - Set::of( + Sequence::of( Directory::of( Name::of(\str_repeat('a', 255)), - Set::of( + Sequence::of( File::of( Name::of(\str_repeat('a', 255)), - None::of(), + Content::none(), ), ), ), @@ -314,11 +312,11 @@ public function testPersistedNameCanStartWithAnyAsciiCharacter() { $this ->forAll( - DataSet\Either::any( - DataSet\Integers::between(1, 46), - DataSet\Integers::between(48, 127), + Set\Either::any( + Set\Integers::between(1, 46), + Set\Integers::between(48, 127), ), - DataSet\Strings::any(), + Set\Strings::any(), ) ->then(function($ord, $content) { $path = \sys_get_temp_dir().'/innmind/filesystem/'; @@ -328,10 +326,10 @@ public function testPersistedNameCanStartWithAnyAsciiCharacter() $this->assertNull($filesystem->add(Directory::of( Name::of(\chr($ord).'a'), - Set::of( + Sequence::of( File::of( Name::of('a'), - Lines::ofContent($content), + Content::ofString($content), ), ), ))); @@ -344,11 +342,11 @@ public function testPersistedNameCanContainWithAnyAsciiCharacter() { $this ->forAll( - DataSet\Either::any( - DataSet\Integers::between(1, 46), - DataSet\Integers::between(48, 127), + Set\Either::any( + Set\Integers::between(1, 46), + Set\Integers::between(48, 127), ), - DataSet\Strings::any(), + Set\Strings::any(), ) ->then(function($ord, $content) { $path = \sys_get_temp_dir().'/innmind/filesystem/'; @@ -358,10 +356,10 @@ public function testPersistedNameCanContainWithAnyAsciiCharacter() $this->assertNull($filesystem->add(Directory::of( Name::of('a'.\chr($ord).'a'), - Set::of( + Sequence::of( File::of( Name::of('a'), - Lines::ofContent($content), + Content::ofString($content), ), ), ))); @@ -374,13 +372,13 @@ public function testPersistedNameCanContainOnlyOneAsciiCharacter() { $this ->forAll( - DataSet\Either::any( - DataSet\Integers::between(1, 8), - DataSet\Integers::between(14, 31), - DataSet\Integers::between(33, 45), - DataSet\Integers::between(48, 127), + Set\Either::any( + Set\Integers::between(1, 8), + Set\Integers::between(14, 31), + Set\Integers::between(33, 45), + Set\Integers::between(48, 127), ), - DataSet\Strings::any(), + Set\Strings::any(), ) ->then(function($ord, $content) { $path = \sys_get_temp_dir().'/innmind/filesystem/'; @@ -390,10 +388,10 @@ public function testPersistedNameCanContainOnlyOneAsciiCharacter() $this->assertNull($filesystem->add(Directory::of( Name::of(\chr($ord)), - Set::of( + Sequence::of( File::of( Name::of('a'), - Lines::ofContent($content), + Content::ofString($content), ), ), ))); @@ -429,7 +427,7 @@ public function testThrowsWhenListContainsALink() $this->expectException(LinksAreNotSupported::class); $this->expectExceptionMessage($path.'bar'); - $filesystem->all()->toList(); + $filesystem->root()->all()->toList(); } public function testDotFilesAreListed() @@ -444,7 +442,7 @@ public function testDotFilesAreListed() $filesystem = Filesystem::mount(Path::of($path)); - $all = $filesystem->all()->toList(); + $all = $filesystem->root()->all()->toList(); $this->assertCount(1, $all); $this->assertSame($name, $all[0]->name()->toString()); }); diff --git a/tests/Adapter/HashedNameTest.php b/tests/Adapter/HashedNameTest.php deleted file mode 100644 index d173b3c..0000000 --- a/tests/Adapter/HashedNameTest.php +++ /dev/null @@ -1,139 +0,0 @@ -remove('/tmp/hashed/'); - } - - public function testInterface() - { - $this->assertInstanceOf( - Adapter::class, - HashedName::of($this->createMock(Adapter::class)), - ); - } - - public function testThrowWhenAddingADirectory() - { - $filesystem = HashedName::of($this->createMock(Adapter::class)); - - $this->expectException(LogicException::class); - $this->expectExceptionMessage('A directory can\'t be hashed'); - - $filesystem->add($this->createMock(Directory::class)); - } - - public function testFileLifecycle() - { - $filesystem = HashedName::of( - $inner = Filesystem::mount(Path::of('/tmp/hashed/')), - ); - - $file = File\File::of(Name::of('foo'), Lines::ofContent('content')); - - $this->assertFalse($filesystem->contains(Name::of('foo'))); - $this->assertNull($filesystem->add($file)); - $this->assertTrue($filesystem->contains(Name::of('foo'))); - $this->assertSame( - 'content', - (string) $inner - ->get(Name::of('0b')) - ->flatMap(static fn($directory) => $directory->get(Name::of('ee'))) - ->flatMap(static fn($directory) => $directory->get(Name::of('c7b5ea3f0fdbc95d0dd47f3c5bc275da8a33'))) - ->map(static fn($file) => $file->content()) - ->match( - static fn($content) => $content->toString(), - static fn() => null, - ), - ); - $this->assertSame( - 'content', - $filesystem - ->get(Name::of('foo')) - ->map(static fn($file) => $file->content()) - ->match( - static fn($content) => $content->toString(), - static fn() => null, - ), - ); - - $file = File\File::of(Name::of('foo'), Lines::ofContent('content bis')); - - $this->assertNull($filesystem->add($file)); - $this->assertSame( - 'content bis', - (string) $inner - ->get(Name::of('0b')) - ->flatMap(static fn($directory) => $directory->get(Name::of('ee'))) - ->flatMap(static fn($directory) => $directory->get(Name::of('c7b5ea3f0fdbc95d0dd47f3c5bc275da8a33'))) - ->map(static fn($file) => $file->content()) - ->match( - static fn($content) => $content->toString(), - static fn() => null, - ), - ); - - $this->assertNull($filesystem->remove(Name::of('foo'))); - $this->assertFalse($filesystem->contains(Name::of('foo'))); - } - - public function testReturnNothingWhenGettingUnknownFile() - { - $filesystem = HashedName::of( - Filesystem::mount(Path::of('/tmp/hashed/')), - ); - - $this->assertNull($filesystem->get(Name::of('foo'))->match( - static fn($file) => $file, - static fn() => null, - )); - } - - public function testAll() - { - $filesystem = HashedName::of( - Filesystem::mount(Path::of('/tmp/hashed/')), - ); - - $filesystem->add(File\File::of(Name::of('foo'), Lines::ofContent('content'))); - $filesystem->add(File\File::of(Name::of('bar'), Lines::ofContent('content'))); - - $all = $filesystem->all(); - - $this->assertInstanceOf(Set::class, $all); - $this->assertCount(2, $all); - //as described in the method comment we return the inner structure instead of the files - $files = $all->toList(); - $this->assertInstanceOf(Directory::class, $files[0]); - $this->assertInstanceOf(Directory::class, $files[1]); - } - - public function testRemovingUnknownFileDoesntThrow() - { - $filesystem = HashedName::of( - Filesystem::mount(Path::of('/tmp/hashed/')), - ); - - $this->assertNull($filesystem->remove(Name::of('foo'))); - } -} diff --git a/tests/Adapter/InMemoryTest.php b/tests/Adapter/InMemoryTest.php index 034713b..f537b54 100644 --- a/tests/Adapter/InMemoryTest.php +++ b/tests/Adapter/InMemoryTest.php @@ -6,10 +6,9 @@ use Innmind\Filesystem\{ Adapter\InMemory, Adapter, - Directory\Directory, - File\File, - File\Content\Lines, - File\Content\None, + Directory, + File, + File\Content, Name, }; use Innmind\Immutable\{ @@ -54,20 +53,19 @@ public function testRemovingUnknownFileDoesntThrow() $this->assertNull(InMemory::new()->remove(Name::of('foo'))); } - public function testAll() + public function testRoot() { $adapter = InMemory::new(); $adapter->add($foo = File::of( Name::of('foo'), - Lines::ofContent('foo'), + Content::ofString('foo'), )); $adapter->add($bar = File::of( Name::of('bar'), - Lines::ofContent('bar'), + Content::ofString('bar'), )); - $all = $adapter->all(); - $this->assertInstanceOf(Set::class, $all); + $all = $adapter->root()->all(); $this->assertSame( [$foo, $bar], $all->toList(), @@ -81,7 +79,7 @@ public function testEmulateFilesystem() Name::of('foo'), Sequence::of( Directory::named('bar'), - File::named('baz', None::of()), + File::named('baz', Content::none()), ), )); $adapter->add(Directory::of( @@ -89,11 +87,11 @@ public function testEmulateFilesystem() Sequence::of( Directory::of( Name::of('bar'), - Sequence::of(File::named('baz', None::of())), + Sequence::of(File::named('baz', Content::none())), ), Directory::of( Name::of('foo'), - Sequence::of(File::named('foo', None::of())), + Sequence::of(File::named('foo', Content::none())), ), ), )); diff --git a/tests/Adapter/LoggerTest.php b/tests/Adapter/LoggerTest.php index 687702d..7d5f511 100644 --- a/tests/Adapter/LoggerTest.php +++ b/tests/Adapter/LoggerTest.php @@ -8,10 +8,10 @@ Adapter, File, Name, - Directory\Directory, + Directory, }; use Innmind\Immutable\{ - Set, + Sequence, Maybe, }; use Psr\Log\LoggerInterface; @@ -36,10 +36,7 @@ public function testAdd() $inner = $this->createMock(Adapter::class), $logger = $this->createMock(LoggerInterface::class), ); - $file = $this->createMock(File::class); - $file - ->method('name') - ->willReturn(Name::of('foo')); + $file = $file = File::of(Name::of('foo'), File\Content::none()); $logger ->expects($this->once()) ->method('debug'); @@ -58,10 +55,7 @@ public function testGet() $logger = $this->createMock(LoggerInterface::class), ); $name = Name::of('foo'); - $file = $this->createMock(File::class); - $file - ->method('name') - ->willReturn($name); + $file = File::of($name, File\Content::none()); $logger ->expects($this->once()) ->method('debug'); @@ -117,15 +111,15 @@ public function testRemove() $this->assertNull($adapter->remove($name)); } - public function testAll() + public function testRoot() { $adapter = Logger::psr( $inner = $this->createMock(Adapter::class), $this->createMock(LoggerInterface::class), ); - $all = Set::of($file = File\File::named( + $all = Sequence::of($file = File::named( 'watev', - $this->createMock(File\Content::class), + File\Content::none(), )); $inner ->expects($this->once()) @@ -135,6 +129,6 @@ public function testAll() $all, )); - $this->assertSame([$file], $adapter->all()->toList()); + $this->assertSame([$file], $adapter->root()->all()->toList()); } } diff --git a/tests/Directory/DirectoryTest.php b/tests/DirectoryTest.php similarity index 71% rename from tests/Directory/DirectoryTest.php rename to tests/DirectoryTest.php index fe532e2..a2b68b2 100644 --- a/tests/Directory/DirectoryTest.php +++ b/tests/DirectoryTest.php @@ -1,15 +1,13 @@ assertInstanceOf(DirectoryInterface::class, $d); $this->assertSame('foo', $d->name()->toString()); - $this->assertSame('', $d->content()->toString()); - $this->assertSame('text/directory', $d->mediaType()->toString()); - $this->assertEquals($d->mediaType(), $d->mediaType()); } public function testNamed() @@ -47,24 +41,28 @@ public function testNamed() public function testAdd() { $d = Directory::of(Name::of('foo')); - $d->content(); //force generation of files list, to be sure it's not cloned $d2 = $d->add( - $file = File\File::of(Name::of('foo'), Lines::ofContent('bar')), + $file = File::of(Name::of('foo'), Content::ofString('bar')), ); - $this->assertInstanceOf(DirectoryInterface::class, $d2); + $this->assertInstanceOf(Directory::class, $d2); $this->assertNotSame($d, $d2); $this->assertSame($d->name(), $d2->name()); - $this->assertNotSame($d->content(), $d2->content()); $this->assertSame(0, $d->removed()->count()); $this->assertSame(0, $d2->removed()->count()); + $this->assertFalse($d->contains($file->name())); + $this->assertTrue($d2->contains($file->name())); + $this->assertSame($file, $d2->get($file->name())->match( + static fn($file) => $file, + static fn() => null, + )); } public function testGet() { $d = Directory::of(Name::of('foo')) - ->add($f = File\File::of(Name::of('bar'), Lines::ofContent('baz'))); + ->add($f = File::of(Name::of('bar'), Content::ofString('baz'))); $this->assertSame( $f, @@ -90,7 +88,7 @@ public function testReturnNothingWhenGettingUnknownFile() public function testContains() { $d = Directory::of(Name::of('foo')) - ->add(File\File::of(Name::of('bar'), Lines::ofContent('baz'))); + ->add(File::of(Name::of('bar'), Content::ofString('baz'))); $this->assertFalse($d->contains(Name::of('baz'))); $this->assertTrue($d->contains(Name::of('bar'))); @@ -99,15 +97,13 @@ public function testContains() public function testRemove() { $d = Directory::of(Name::of('foo')) - ->add(File\File::of(Name::of('bar'), Lines::ofContent('baz'))); - $d->content(); //force generation of files list, to be sure it's not cloned + ->add(File::of(Name::of('bar'), Content::ofString('baz'))); $d2 = $d->remove(Name::of('bar')); - $this->assertInstanceOf(DirectoryInterface::class, $d2); + $this->assertInstanceOf(Directory::class, $d2); $this->assertNotSame($d, $d2); $this->assertSame($d->name(), $d2->name()); - $this->assertNotSame($d->content(), $d2->content()); $this->assertSame(0, $d->removed()->count()); $this->assertSame(1, $d2->removed()->count()); $this->assertSame( @@ -127,9 +123,9 @@ public function testForeach() $directory = Directory::of( Name::of('foo'), Sequence::lazy(static function() { - yield File\File::of(Name::of('foo'), Lines::ofContent('foo')); - yield File\File::of(Name::of('bar'), Lines::ofContent('bar')); - yield File\File::of(Name::of('foobar'), Lines::ofContent('foobar')); + yield File::of(Name::of('foo'), Content::ofString('foo')); + yield File::of(Name::of('bar'), Content::ofString('bar')); + yield File::of(Name::of('foobar'), Content::ofString('foobar')); yield Directory::of(Name::of('sub')); }), ); @@ -149,9 +145,9 @@ public function testReduce() $directory = Directory::of( Name::of('foo'), Sequence::lazy(static function() { - yield File\File::of(Name::of('foo'), Lines::ofContent('foo')); - yield File\File::of(Name::of('bar'), Lines::ofContent('bar')); - yield File\File::of(Name::of('foobar'), Lines::ofContent('foobar')); + yield File::of(Name::of('foo'), Content::ofString('foo')); + yield File::of(Name::of('bar'), Content::ofString('bar')); + yield File::of(Name::of('foobar'), Content::ofString('foobar')); yield Directory::of(Name::of('sub')); }), ); @@ -169,9 +165,9 @@ public function testFilter() $directory = Directory::of( Name::of('foo'), Sequence::lazy(static function() { - yield File\File::of(Name::of('foo'), Lines::ofContent('foo')); - yield File\File::of(Name::of('bar'), Lines::ofContent('bar')); - yield File\File::of(Name::of('foobar'), Lines::ofContent('foobar')); + yield File::of(Name::of('foo'), Content::ofString('foo')); + yield File::of(Name::of('bar'), Content::ofString('bar')); + yield File::of(Name::of('foobar'), Content::ofString('foobar')); yield Directory::of(Name::of('sub')); }), ); @@ -180,7 +176,7 @@ public function testFilter() static fn($file) => \strpos($file->name()->toString(), 'foo') === 0, ); - $this->assertInstanceOf(DirectoryInterface::class, $filtered); + $this->assertInstanceOf(Directory::class, $filtered); $files = $filtered->reduce( Set::objects(), static fn($files, $file) => ($files)($file), @@ -203,8 +199,8 @@ public function testDirectoryLoadedWithDifferentFilesWithTheSameNameThrows() Directory::of( $directory, Sequence::of( - File\File::named($file->toString(), None::of()), - File\File::named($file->toString(), None::of()), + File::named($file->toString(), Content::none()), + File::named($file->toString(), Content::none()), ), ); }); diff --git a/tests/File/Content/AtPathTest.php b/tests/File/Content/AtPathTest.php deleted file mode 100644 index c373678..0000000 --- a/tests/File/Content/AtPathTest.php +++ /dev/null @@ -1,273 +0,0 @@ -assertInstanceOf(Content::class, AtPath::of(Path::of('/dev/null'))); - } - - public function testForeach() - { - $this - ->forAll(Set\Sequence::of($this->strings())->between(1, 10)) - ->then(function($lines) { - \file_put_contents('/tmp/test_content', \implode("\n", $lines)); - $content = AtPath::of(Path::of('/tmp/test_content')); - $called = 0; - - $this->assertInstanceOf( - SideEffect::class, - $content->foreach(function($line) use ($lines, &$called) { - $this->assertSame($lines[$called], $line->toString()); - $called++; - }), - ); - $this->assertSame(\count($lines), $called); - }); - } - - public function testForeachCalledOnceWhenEmptyContent() - { - \file_put_contents('/tmp/test_content', ''); - $content = AtPath::of(Path::of('/tmp/test_content')); - $called = 0; - - $this->assertInstanceOf( - SideEffect::class, - $content->foreach(function($line) use (&$called) { - $this->assertSame('', $line->toString()); - $called++; - }), - ); - $this->assertSame(1, $called); - } - - public function testMap() - { - $this - ->forAll( - Set\Sequence::of($this->strings())->between(1, 10), - $this->strings(), - ) - ->then(function($lines, $replacement) { - $replacement = Line::of(Str::of($replacement)); - \file_put_contents('/tmp/test_content', \implode("\n", $lines)); - $content = AtPath::of(Path::of('/tmp/test_content')); - $mapped = $content->map(static fn() => $replacement); - $called = 0; - - $this->assertNotSame($content, $mapped); - $this->assertInstanceOf( - SideEffect::class, - $mapped->foreach(function($line) use ($replacement, &$called) { - $this->assertSame($replacement, $line); - $called++; - }), - ); - $this->assertSame(\count($lines), $called); - }); - } - - public function testDoesntThrowWhenLastLineIsEmpty() - { - $lines = [ - 'foo', - 'foo', - 'foo', - '', - ]; - $replacement = Line::of(Str::of('')); - \file_put_contents('/tmp/test_content', \implode("\n", $lines)); - $content = AtPath::of(Path::of('/tmp/test_content')); - $mapped = $content->map(static fn() => $replacement); - $called = 0; - - $this->assertNotSame($content, $mapped); - $this->assertInstanceOf( - SideEffect::class, - $mapped->foreach(function($line) use ($replacement, &$called) { - $this->assertSame($replacement, $line); - $called++; - }), - ); - $this->assertSame(\count($lines), $called); - } - - public function testFlatMap() - { - $this - ->forAll( - Set\Sequence::of($this->strings())->between(1, 10), - $this->strings(), - ) - ->then(function($lines, $newLine) { - $newLine = Line::of(Str::of($newLine)); - \file_put_contents('/tmp/test_content', \implode("\n", $lines)); - $content = AtPath::of(Path::of('/tmp/test_content')); - $empty = $content->flatMap(static fn() => Lines::of(Sequence::of())); - $extra = $content->flatMap(static fn($line) => Lines::of(Sequence::of( - $line, - $newLine, - ))); - - $called = 0; - $empty->foreach(static function() use (&$called) { - ++$called; - }); - $this->assertSame(0, $called); - - $called = 0; - $extra->foreach(static function() use (&$called) { - ++$called; - }); - $this->assertSame(\count($lines) * 2, $called); - $newContent = ''; - - foreach ($lines as $line) { - $newContent .= $line."\n".$newLine->toString()."\n"; - } - - $this->assertSame(\substr($newContent, 0, -1), $extra->toString()); - }); - } - - public function testFilter() - { - $this - ->forAll(Set\Sequence::of($this->strings())->between(1, 10)) - ->then(function($lines) { - \file_put_contents('/tmp/test_content', \implode("\n", $lines)); - $content = AtPath::of(Path::of('/tmp/test_content')); - $shouldBeEmpty = $content->filter(static fn() => false); - $shouldBeTheSame = $content->filter(static fn() => true); - - $called = 0; - $shouldBeEmpty->foreach(static function() use (&$called) { - ++$called; - }); - $this->assertSame(0, $called); - - $called = 0; - $shouldBeTheSame->foreach(static function() use (&$called) { - ++$called; - }); - $this->assertSame(\count($lines), $called); - }); - } - - public function testLines() - { - $this - ->forAll( - Set\Sequence::of($this->strings())->between(1, 10), - $this->strings(), - ) - ->then(function($lines, $replacement) { - $replacement = Line::of(Str::of($replacement)); - \file_put_contents('/tmp/test_content', \implode("\n", $lines)); - $content = AtPath::of(Path::of('/tmp/test_content')); - - $called = 0; - $sequence = $content->lines()->map(function($line) use ($lines, $replacement, &$called) { - $this->assertSame($lines[$called], $line->toString()); - $called++; - - return $replacement; - }); - - $this->assertInstanceOf(Sequence::class, $sequence); - $size = 0; - $sequence->foreach(function($line) use ($replacement, &$size) { - $this->assertSame($replacement, $line); - $size++; - }); - // we don't call $sequence->size() as the sequence is lazy so it - // would perform the transformation above twice resulting in an - // error due to &$called being incremented above the number of - // lines - $this->assertSame(\count($lines), $size); - }); - } - - public function testReduce() - { - $this - ->forAll(Set\Sequence::of($this->strings())->between(1, 10)) - ->then(function($lines) { - \file_put_contents('/tmp/test_content', \implode("\n", $lines)); - $content = AtPath::of(Path::of('/tmp/test_content')); - - $this->assertSame( - \count($lines), - $content->reduce( - 0, - static fn($carry, $_) => $carry + 1, - ), - ); - }); - } - - public function testToString() - { - $this - ->forAll(Set\Sequence::of($this->strings())->between(1, 10)) - ->then(function($lines) { - \file_put_contents('/tmp/test_content', \implode("\n", $lines)); - $content = AtPath::of(Path::of('/tmp/test_content')); - - $this->assertSame(\implode("\n", $lines), $content->toString()); - }); - } - - public function testSize() - { - $this - ->forAll(Set\Sequence::of($this->strings())->between(0, 10)) - ->then(function($lines) { - $expectedSize = Str::of(\implode("\n", $lines))->toEncoding(Str\Encoding::ascii)->length(); - \file_put_contents('/tmp/test_content', \implode("\n", $lines)); - $content = AtPath::of(Path::of('/tmp/test_content')); - - $this->assertSame( - $expectedSize, - $content->size()->match( - static fn($size) => $size->toInt(), - static fn() => null, - ), - ); - }); - } - - private function strings(): Set - { - return Set\Decorate::immutable( - static fn($line) => \rtrim($line, "\n"), - Set\Unicode::strings(), - )->filter(static fn($line) => !\str_contains($line, "\n")); - } -} diff --git a/tests/File/Content/ChunksTest.php b/tests/File/Content/ChunksTest.php deleted file mode 100644 index 17c8ed2..0000000 --- a/tests/File/Content/ChunksTest.php +++ /dev/null @@ -1,315 +0,0 @@ -assertInstanceOf(Content::class, Chunks::of(Sequence::of())); - $this->assertInstanceOf(Chunkable::class, Chunks::of(Sequence::of())); - } - - public function testForeach() - { - $this - ->forAll($this->chunks()) - ->then(function($chunks) { - $content = Chunks::of(Sequence::of(...$chunks)->map(Str::of(...))); - $lines = $this->lines($chunks); - $called = 0; - - $this->assertInstanceOf( - SideEffect::class, - $content->foreach(function($line) use ($lines, &$called) { - $this->assertSame($lines[$called], $line->toString()); - $called++; - }), - ); - }); - } - - public function testForeachCalledOnceWhenEmptyContent() - { - $content = Chunks::of(Sequence::of(Str::of(''))); - $called = 0; - - $this->assertInstanceOf( - SideEffect::class, - $content->foreach(function($line) use (&$called) { - $this->assertSame('', $line->toString()); - $called++; - }), - ); - $this->assertSame(1, $called); - } - - public function testMap() - { - $this - ->forAll( - $this->chunks(), - $this->strings(), - ) - ->then(function($chunks, $replacement) { - $content = Chunks::of(Sequence::of(...$chunks)->map(Str::of(...))); - $lines = $this->lines($chunks); - $mapped = $content->map(static fn() => $replacement); - $called = 0; - - $this->assertNotSame($content, $mapped); - $this->assertInstanceOf( - SideEffect::class, - $mapped->foreach(function($line) use ($replacement, &$called) { - $this->assertSame($replacement, $line); - $called++; - }), - ); - $this->assertSame(\count($lines), $called); - }); - } - - public function testFlatMap() - { - $this - ->forAll( - $this->chunks(), - $this->strings(), - ) - ->then(function($chunks, $newLine) { - $content = Chunks::of(Sequence::of(...$chunks)->map(Str::of(...))); - $lines = $this->lines($chunks); - $empty = $content->flatMap(static fn() => Lines::of(Sequence::of())); - $extra = $content->flatMap(static fn($line) => Lines::of(Sequence::of( - $line, - $newLine, - ))); - - $called = 0; - $empty->foreach(static function() use (&$called) { - ++$called; - }); - $this->assertSame(0, $called); - - $called = 0; - $extra->foreach(static function() use (&$called) { - ++$called; - }); - $this->assertSame(\count($lines) * 2, $called); - $newContent = ''; - - foreach ($lines as $line) { - $newContent .= $line."\n".$newLine->toString()."\n"; - } - - $this->assertSame(\substr($newContent, 0, -1), $extra->toString()); - }); - } - - public function testFilter() - { - $this - ->forAll($this->chunks()) - ->then(function($chunks) { - $content = Chunks::of(Sequence::of(...$chunks)->map(Str::of(...))); - $lines = $this->lines($chunks); - $shouldBeEmpty = $content->filter(static fn() => false); - $shouldBeTheSame = $content->filter(static fn() => true); - - $called = 0; - $shouldBeEmpty->foreach(static function() use (&$called) { - ++$called; - }); - $this->assertSame(0, $called); - - $called = 0; - $shouldBeTheSame->foreach(static function() use (&$called) { - ++$called; - }); - $this->assertSame(\count($lines), $called); - }); - } - - public function testLines() - { - $this - ->forAll( - $this->chunks(), - $this->strings(), - ) - ->then(function($chunks, $replacement) { - $content = Chunks::of(Sequence::of(...$chunks)->map(Str::of(...))); - $lines = $this->lines($chunks); - - $called = 0; - $sequence = $content->lines()->map(function($line) use ($lines, $replacement, &$called) { - $this->assertSame($lines[$called], $line->toString()); - $called++; - - return $replacement; - }); - - $this->assertInstanceOf(Sequence::class, $sequence); - $sequence->foreach(function($line) use ($replacement) { - $this->assertSame($replacement, $line); - }); - $this->assertSame(\count($lines), $sequence->size()); - }); - } - - public function testReduce() - { - $this - ->forAll($this->chunks()) - ->then(function($chunks) { - $content = Chunks::of(Sequence::of(...$chunks)->map(Str::of(...))); - $lines = $this->lines($chunks); - - $this->assertSame( - \count($lines), - $content->reduce( - 0, - static fn($carry, $_) => $carry + 1, - ), - ); - }); - } - - public function testToString() - { - $this - ->forAll($this->chunks()) - ->then(function($chunks) { - $content = Chunks::of(Sequence::of(...$chunks)->map(Str::of(...))); - - $this->assertSame( - \implode('', $chunks), - $content->toString(), - ); - }); - } - - public function testSize() - { - $content = Chunks::of(Sequence::of( - Str::of(''), - )); - $this->assertSame(0, $content->size()->match( - static fn($size) => $size->toInt(), - static fn() => null, - )); - - $content = Chunks::of(Sequence::of( - Str::of('foo'), - )); - $this->assertSame(3, $content->size()->match( - static fn($size) => $size->toInt(), - static fn() => null, - )); - - $content = Chunks::of(Sequence::of( - Str::of("foo\n"), - Str::of('foo'), - )); - $this->assertSame(7, $content->size()->match( - static fn($size) => $size->toInt(), - static fn() => null, - )); - - $content = Chunks::of(Sequence::of( - Str::of("foo\n"), - Str::of("foo\n"), - Str::of(''), - )); - $this->assertSame(8, $content->size()->match( - static fn($size) => $size->toInt(), - static fn() => null, - )); - - $content = Chunks::of(Sequence::of( - Str::of("foo\n"), - Str::of("\n"), - Str::of('foo'), - )); - $this->assertSame(8, $content->size()->match( - static fn($size) => $size->toInt(), - static fn() => null, - )); - - $this - ->forAll(Set\Sequence::of( - Set\Unicode::strings(), - )->between(0, 10)) - ->then(function($chunks) { - $expectedSize = Str::of(\implode('', $chunks), Str\Encoding::ascii)->length(); - $content = Chunks::of(Sequence::of(...$chunks)->map(Str::of(...))); - - $this->assertSame($expectedSize, $content->size()->match( - static fn($size) => $size->toInt(), - static fn() => null, - )); - }); - } - - public function testFunctional() - { - $this - ->forAll(Set\Integers::between(1, 10)) - ->then(function($size) { - $source = "foo\nbar\nbaz\nwatev"; - $content = Chunks::of( - Str::of($source)->chunk($size), - ); - - $this->assertSame($source, $content->toString()); - $this->assertSame( - ['foo', 'bar', 'baz', 'watev'], - $content - ->lines() - ->map(static fn($line) => $line->toString()) - ->toList(), - ); - }); - } - - private function lines(array $chunks): array - { - return \explode("\n", \implode('', $chunks)); - } - - private function chunks(): Set - { - return Set\Sequence::of( - Set\Unicode::strings(), - )->between(1, 10); - } - - private function strings(): Set - { - return Set\Decorate::immutable( - Str::of(...), - Set\Unicode::strings(), - ); - } -} diff --git a/tests/File/Content/LinesTest.php b/tests/File/Content/LinesTest.php deleted file mode 100644 index 9f08a01..0000000 --- a/tests/File/Content/LinesTest.php +++ /dev/null @@ -1,282 +0,0 @@ -assertInstanceOf(Content::class, Lines::of(Sequence::of())); - } - - public function testForeach() - { - $this - ->forAll(Set\Sequence::of($this->strings())->between(1, 10)) - ->then(function($lines) { - $content = Lines::of(Sequence::of(...$lines)); - $called = 0; - - $this->assertInstanceOf( - SideEffect::class, - $content->foreach(function($line) use ($lines, &$called) { - $this->assertSame($lines[$called], $line); - $called++; - }), - ); - $this->assertSame(\count($lines), $called); - }); - } - - public function testForeachCalledOnceWhenEmptyContent() - { - $content = Lines::of(Sequence::of(Line::of(Str::of('')))); - $called = 0; - - $this->assertInstanceOf( - SideEffect::class, - $content->foreach(function($line) use (&$called) { - $this->assertSame('', $line->toString()); - $called++; - }), - ); - $this->assertSame(1, $called); - } - - public function testMap() - { - $this - ->forAll( - Set\Sequence::of($this->strings())->between(1, 10), - $this->strings(), - ) - ->then(function($lines, $replacement) { - $content = Lines::of(Sequence::of(...$lines)); - $mapped = $content->map(static fn() => $replacement); - $called = 0; - - $this->assertNotSame($content, $mapped); - $this->assertInstanceOf( - SideEffect::class, - $mapped->foreach(function($line) use ($replacement, &$called) { - $this->assertSame($replacement, $line); - $called++; - }), - ); - $this->assertSame(\count($lines), $called); - }); - } - - public function testFlatMap() - { - $this - ->forAll( - Set\Sequence::of($this->strings())->between(1, 10), - $this->strings(), - ) - ->then(function($lines, $newLine) { - $content = Lines::of(Sequence::of(...$lines)); - $empty = $content->flatMap(static fn() => Lines::of(Sequence::of())); - $extra = $content->flatMap(static fn($line) => Lines::of(Sequence::of( - $line, - $newLine, - ))); - - $called = 0; - $empty->foreach(static function() use (&$called) { - ++$called; - }); - $this->assertSame(0, $called); - - $called = 0; - $extra->foreach(static function() use (&$called) { - ++$called; - }); - $this->assertSame(\count($lines) * 2, $called); - $newContent = ''; - - foreach ($lines as $line) { - $newContent .= $line->toString()."\n".$newLine->toString()."\n"; - } - - $this->assertSame(\substr($newContent, 0, -1), $extra->toString()); - }); - } - - public function testFilter() - { - $this - ->forAll(Set\Sequence::of($this->strings())->between(1, 10)) - ->then(function($lines) { - $content = Lines::of(Sequence::of(...$lines)); - $shouldBeEmpty = $content->filter(static fn() => false); - $shouldBeTheSame = $content->filter(static fn() => true); - - $called = 0; - $shouldBeEmpty->foreach(static function() use (&$called) { - ++$called; - }); - $this->assertSame(0, $called); - - $called = 0; - $shouldBeTheSame->foreach(static function() use (&$called) { - ++$called; - }); - $this->assertSame(\count($lines), $called); - }); - } - - public function testLines() - { - $this - ->forAll( - Set\Sequence::of($this->strings())->between(1, 10), - $this->strings(), - ) - ->then(function($lines, $replacement) { - $content = Lines::of(Sequence::of(...$lines)); - - $called = 0; - $sequence = $content->lines()->map(function($line) use ($lines, $replacement, &$called) { - $this->assertSame($lines[$called], $line); - $called++; - - return $replacement; - }); - - $this->assertInstanceOf(Sequence::class, $sequence); - $sequence->foreach(function($line) use ($replacement) { - $this->assertSame($replacement, $line); - }); - $this->assertSame(\count($lines), $sequence->size()); - }); - } - - public function testReduce() - { - $this - ->forAll(Set\Sequence::of($this->strings())->between(1, 10)) - ->then(function($lines) { - $content = Lines::of(Sequence::of(...$lines)); - - $this->assertSame( - \count($lines), - $content->reduce( - 0, - static fn($carry, $_) => $carry + 1, - ), - ); - }); - } - - public function testToString() - { - $this - ->forAll(Set\Sequence::of($this->strings())->between(1, 10)) - ->then(function($lines) { - $content = Lines::of(Sequence::of(...$lines)); - - $this->assertSame( - \implode( - "\n", - \array_map(static fn($line) => $line->toString(), $lines), - ), - $content->toString(), - ); - }); - } - - public function testSize() - { - $content = Lines::of(Sequence::of( - Line::of(Str::of('')), - )); - $this->assertSame(0, $content->size()->match( - static fn($size) => $size->toInt(), - static fn() => null, - )); - - $content = Lines::of(Sequence::of( - Line::of(Str::of('foo')), - )); - $this->assertSame(3, $content->size()->match( - static fn($size) => $size->toInt(), - static fn() => null, - )); - - $content = Lines::of(Sequence::of( - Line::of(Str::of('foo')), - Line::of(Str::of('foo')), - )); - $this->assertSame(7, $content->size()->match( - static fn($size) => $size->toInt(), - static fn() => null, - )); - - $content = Lines::of(Sequence::of( - Line::of(Str::of('foo')), - Line::of(Str::of('foo')), - Line::of(Str::of('')), - )); - $this->assertSame(8, $content->size()->match( - static fn($size) => $size->toInt(), - static fn() => null, - )); - - $content = Lines::of(Sequence::of( - Line::of(Str::of('foo')), - Line::of(Str::of('')), - Line::of(Str::of('foo')), - )); - $this->assertSame(8, $content->size()->match( - static fn($size) => $size->toInt(), - static fn() => null, - )); - - $this - ->forAll(Set\Sequence::of($this->strings())->between(0, 10)) - ->then(function($lines) { - $raw = \array_map( - static fn($line) => $line->toString(), - $lines, - ); - $expectedSize = Str::of(\implode("\n", $raw), Str\Encoding::ascii)->length(); - $content = Lines::of(Sequence::of(...$lines)); - - $this->assertSame($expectedSize, $content->size()->match( - static fn($size) => $size->toInt(), - static fn() => null, - )); - }); - } - - private function strings(): Set - { - return Set\Decorate::immutable( - static fn($string) => Line::of(Str::of($string)), - Set\Decorate::immutable( - static fn($line) => \rtrim($line, "\n"), - Set\Unicode::strings(), - )->filter(static fn($line) => !\str_contains($line, "\n")), - ); - } -} diff --git a/tests/File/Content/NoneTest.php b/tests/File/Content/NoneTest.php deleted file mode 100644 index 4c8d5b4..0000000 --- a/tests/File/Content/NoneTest.php +++ /dev/null @@ -1,136 +0,0 @@ -assertInstanceOf(Content::class, None::of()); - } - - public function testForeachCalledOnce() - { - $content = None::of(); - $called = 0; - - $this->assertInstanceOf( - SideEffect::class, - $content->foreach(function($line) use (&$called) { - $this->assertSame('', $line->toString()); - $called++; - }), - ); - $this->assertSame(1, $called); - } - - public function testMap() - { - $this - ->forAll($this->strings()) - ->then(function($replacement) { - $replacement = Line::of(Str::of($replacement)); - $content = None::of(); - $mapped = $content->map(static fn() => $replacement); - $called = 0; - - $this->assertNotSame($content, $mapped); - $this->assertInstanceOf( - SideEffect::class, - $mapped->foreach(function($line) use ($replacement, &$called) { - $this->assertSame($replacement, $line); - $called++; - }), - ); - $this->assertSame(1, $called); - }); - } - - public function testFilter() - { - $content = None::of(); - $shouldBeEmpty = $content->filter(static fn() => false); - $shouldBeTheSame = $content->filter(static fn() => true); - - $called = 0; - $shouldBeEmpty->foreach(static function() use (&$called) { - ++$called; - }); - $this->assertSame(0, $called); - - $called = 0; - $shouldBeTheSame->foreach(static function() use (&$called) { - ++$called; - }); - $this->assertSame(1, $called); - } - - public function testLines() - { - $this - ->forAll($this->strings()) - ->then(function($replacement) { - $replacement = Line::of(Str::of($replacement)); - $content = None::of(); - - $called = 0; - $sequence = $content->lines()->map(function($line) use ($replacement, &$called) { - $this->assertSame('', $line->toString()); - $called++; - - return $replacement; - }); - - $this->assertInstanceOf(Sequence::class, $sequence); - $sequence->foreach(function($line) use ($replacement) { - $this->assertSame($replacement, $line); - }); - $this->assertSame(1, $sequence->size()); - }); - } - - public function testReduce() - { - $content = None::of(); - - $this->assertSame( - 1, - $content->reduce( - 0, - static fn($carry, $_) => $carry + 1, - ), - ); - } - - public function testToString() - { - $this->assertSame('', None::of()->toString()); - } - - private function strings(): Set - { - return Set\Decorate::immutable( - static fn($line) => \rtrim($line, "\n"), - Set\Unicode::strings(), - )->filter(static fn($line) => !\str_contains($line, "\n")); - } -} diff --git a/tests/File/Content/OfStreamTest.php b/tests/File/Content/OfStreamTest.php deleted file mode 100644 index bb945e8..0000000 --- a/tests/File/Content/OfStreamTest.php +++ /dev/null @@ -1,267 +0,0 @@ -assertInstanceOf(Content::class, OfStream::of(Stream::open(Path::of('/dev/null')))); - } - - public function testForeach() - { - $this - ->forAll(Set\Sequence::of($this->strings())->between(1, 10)) - ->then(function($lines) { - \file_put_contents('/tmp/test_content', \implode("\n", $lines)); - $content = OfStream::of(Stream::open(Path::of('/tmp/test_content'))); - $called = 0; - - $this->assertInstanceOf( - SideEffect::class, - $content->foreach(function($line) use ($lines, &$called) { - $this->assertSame($lines[$called], $line->toString()); - $called++; - }), - ); - $this->assertSame(\count($lines), $called); - }); - } - - public function testForeachCalledOnceWhenEmptyContent() - { - \file_put_contents('/tmp/test_content', ''); - $content = OfStream::of(Stream::open(Path::of('/tmp/test_content'))); - $called = 0; - - $this->assertInstanceOf( - SideEffect::class, - $content->foreach(function($line) use (&$called) { - $this->assertSame('', $line->toString()); - $called++; - }), - ); - $this->assertSame(1, $called); - } - - public function testMap() - { - $this - ->forAll( - Set\Sequence::of($this->strings())->between(1, 10), - $this->strings(), - ) - ->then(function($lines, $replacement) { - $replacement = Line::of(Str::of($replacement)); - \file_put_contents('/tmp/test_content', \implode("\n", $lines)); - $content = OfStream::of(Stream::open(Path::of('/tmp/test_content'))); - $mapped = $content->map(static fn() => $replacement); - $called = 0; - - $this->assertNotSame($content, $mapped); - $this->assertInstanceOf( - SideEffect::class, - $mapped->foreach(function($line) use ($replacement, &$called) { - $this->assertSame($replacement, $line); - $called++; - }), - ); - $this->assertSame(\count($lines), $called); - }); - } - - public function testFlatMap() - { - $this - ->forAll( - Set\Sequence::of($this->strings())->between(1, 10), - $this->strings(), - ) - ->then(function($lines, $newLine) { - $newLine = Line::of(Str::of($newLine)); - \file_put_contents('/tmp/test_content', \implode("\n", $lines)); - $content = OfStream::of(Stream::open(Path::of('/tmp/test_content'))); - $empty = $content->flatMap(static fn() => Lines::of(Sequence::of())); - $extra = $content->flatMap(static fn($line) => Lines::of(Sequence::of( - $line, - $newLine, - ))); - - $called = 0; - $empty->foreach(static function() use (&$called) { - ++$called; - }); - $this->assertSame(0, $called); - - $called = 0; - $extra->foreach(static function() use (&$called) { - ++$called; - }); - $this->assertSame(\count($lines) * 2, $called); - $newContent = ''; - - foreach ($lines as $line) { - $newContent .= $line."\n".$newLine->toString()."\n"; - } - - $this->assertSame(\substr($newContent, 0, -1), $extra->toString()); - }); - } - - public function testFilter() - { - $this - ->forAll(Set\Sequence::of($this->strings())->between(1, 10)) - ->then(function($lines) { - \file_put_contents('/tmp/test_content', \implode("\n", $lines)); - $content = OfStream::of(Stream::open(Path::of('/tmp/test_content'))); - $shouldBeEmpty = $content->filter(static fn() => false); - $shouldBeTheSame = $content->filter(static fn() => true); - - $called = 0; - $shouldBeEmpty->foreach(static function() use (&$called) { - ++$called; - }); - $this->assertSame(0, $called); - - $called = 0; - $shouldBeTheSame->foreach(static function() use (&$called) { - ++$called; - }); - $this->assertSame(\count($lines), $called); - }); - } - - public function testLines() - { - $this - ->forAll( - Set\Sequence::of($this->strings())->between(1, 10), - $this->strings(), - ) - ->then(function($lines, $replacement) { - $replacement = Line::of(Str::of($replacement)); - \file_put_contents('/tmp/test_content', \implode("\n", $lines)); - $content = OfStream::of(Stream::open(Path::of('/tmp/test_content'))); - - $called = 0; - $sequence = $content->lines()->map(function($line) use ($lines, $replacement, &$called) { - $this->assertSame($lines[$called], $line->toString()); - $called++; - - return $replacement; - }); - - $this->assertInstanceOf(Sequence::class, $sequence); - $size = 0; - $sequence->foreach(function($line) use ($replacement, &$size) { - $this->assertSame($replacement, $line); - $size++; - }); - // we don't call $sequence->size() as the sequence is lazy so it - // would perform the transformation above twice resulting in an - // error due to &$called being incremented above the number of - // lines - $this->assertSame(\count($lines), $size); - }); - } - - public function testReduce() - { - $this - ->forAll(Set\Sequence::of($this->strings())->between(1, 10)) - ->then(function($lines) { - \file_put_contents('/tmp/test_content', \implode("\n", $lines)); - $content = OfStream::of(Stream::open(Path::of('/tmp/test_content'))); - - $this->assertSame( - \count($lines), - $content->reduce( - 0, - static fn($carry, $_) => $carry + 1, - ), - ); - }); - } - - public function testToString() - { - $this - ->forAll(Set\Sequence::of($this->strings())->between(1, 10)) - ->then(function($lines) { - \file_put_contents('/tmp/test_content', \implode("\n", $lines)); - $content = OfStream::of(Stream::open(Path::of('/tmp/test_content'))); - - $this->assertSame(\implode("\n", $lines), $content->toString()); - }); - } - - public function testSize() - { - $this - ->forAll(Set\Sequence::of($this->strings())->between(0, 10)) - ->then(function($lines) { - $expectedSize = Str::of(\implode("\n", $lines))->toEncoding(Str\Encoding::ascii)->length(); - \file_put_contents('/tmp/test_content', \implode("\n", $lines)); - $content = OfStream::of(Stream::open(Path::of('/tmp/test_content'))); - - $this->assertSame( - $expectedSize, - $content->size()->match( - static fn($size) => $size->toInt(), - static fn() => null, - ), - ); - }); - } - - public function testLoadingFilesWithVariousEnds() - { - \touch('/tmp/test_empty_file'); - $this->assertSame('', OfStream::of(Stream::open(Path::of('/tmp/test_empty_file')))->toString()); - - \file_put_contents( - '/tmp/test_single_line', - 'foo', - ); - $this->assertSame('foo', OfStream::of(Stream::open(Path::of('/tmp/test_single_line')))->toString()); - - \file_put_contents( - '/tmp/test_end_new_line', - "foo\n", - ); - $this->assertSame("foo\n", OfStream::of(Stream::open(Path::of('/tmp/test_end_new_line')))->toString()); - } - - private function strings(): Set - { - return Set\Decorate::immutable( - static fn($line) => \rtrim($line, "\n"), - Set\Unicode::strings(), - )->filter(static fn($line) => !\str_contains($line, "\n")); - } -} diff --git a/tests/File/FileTest.php b/tests/FileTest.php similarity index 84% rename from tests/File/FileTest.php rename to tests/FileTest.php index 6e9c76e..ca145a3 100644 --- a/tests/File/FileTest.php +++ b/tests/FileTest.php @@ -1,10 +1,10 @@ createMock(Content::class)); + $f = File::of($name = Name::of('foo'), $c = Content::none()); $this->assertInstanceOf(FileInterface::class, $f); $this->assertSame($name, $f->name()); @@ -34,7 +34,7 @@ public function testInterface() public function testNamed() { - $file = File::named('foo', $this->createMock(Content::class)); + $file = File::named('foo', Content::none()); $this->assertInstanceOf(File::class, $file); $this->assertSame('foo', $file->name()->toString()); @@ -44,7 +44,7 @@ public function testMediaType() { $f = File::of( Name::of('foo'), - $this->createMock(Content::class), + Content::none(), $mt = MediaType::of('application/json'), ); @@ -62,7 +62,7 @@ public function testContentIsNeverAltered() ->then(function($name, $mediaType) { $file = File::of( $name, - $content = $this->createMock(Content::class), + $content = Content::none(), $mediaType, ); @@ -79,7 +79,7 @@ public function testByDefaultTheMediaTypeIsOctetStream() ->then(function($name) { $file = File::of( $name, - $this->createMock(Content::class), + Content::none(), ); $this->assertSame( @@ -99,7 +99,7 @@ public function testNamedConstructorNeverAltersTheContent() ->then(function($name, $mediaType) { $file = File::named( $name->toString(), - $content = $this->createMock(Content::class), + $content = Content::none(), $mediaType, ); @@ -119,10 +119,10 @@ public function testWithContent() ->then(function($name, $mediaType) { $file = File::of( $name, - $content = $this->createMock(Content::class), + $content = Content::none(), $mediaType, ); - $file2 = $file->withContent($content2 = $this->createMock(Content::class)); + $file2 = $file->withContent($content2 = Content::none()); $this->assertNotSame($file, $file2); $this->assertNotSame($file->content(), $file2->content()); @@ -141,10 +141,10 @@ public function testWithContentKeepsTheMediaTypeByDefault() ->then(function($name, $mediaType) { $file = File::of( $name, - $this->createMock(Content::class), + Content::none(), $mediaType, ); - $file2 = $file->withContent($this->createMock(Content::class)); + $file2 = $file->withContent(Content::none()); $this->assertNotSame($file, $file2); $this->assertSame($file->mediaType(), $file2->mediaType()); @@ -161,7 +161,7 @@ public function testRename() ->then(function($name1, $name2) { $file1 = File::of( $name1, - $this->createMock(Content::class), + Content::none(), ); $file2 = $file1->rename($name2); diff --git a/tests/NameTest.php b/tests/NameTest.php index e515313..8955146 100644 --- a/tests/NameTest.php +++ b/tests/NameTest.php @@ -112,10 +112,13 @@ public function testDotFoldersAreNotAccepted() $this ->forAll(Set\Elements::of('.', '..')) ->then(function($name) { - $this->expectException(DomainException::class); - $this->expectExceptionMessage("'.' and '..' can't be used"); + try { + Name::of($name); - Name::of($name); + $this->fail('it should throw'); + } catch (DomainException $e) { + $this->assertSame("'.' and '..' can't be used", $e->getMessage()); + } }); } @@ -145,9 +148,13 @@ public function testNamesLongerThan255AreNotAccepted() )->filter(static fn(string $name): bool => $name !== '.' && $name !== '..'), ) ->then(function($name) { - $this->expectException(DomainException::class); + try { + Name::of($name); - Name::of($name); + $this->fail('it should throw'); + } catch (\Throwable $e) { + $this->assertInstanceOf(DomainException::class, $e); + } }); } diff --git a/tests/Stream/LazyStreamTest.php b/tests/Stream/LazyStreamTest.php deleted file mode 100644 index f4b7579..0000000 --- a/tests/Stream/LazyStreamTest.php +++ /dev/null @@ -1,133 +0,0 @@ -assertInstanceOf(Readable::class, $stream); - } - - public function testDoesntInitializeWhenRewindingUninitializedStream() - { - $stream = new LazyStream(Path::of( - \tempnam(\sys_get_temp_dir(), 'lazy_stream'), - )); - - $this->assertSame( - $stream, - $stream->rewind()->match( // it would fail if initialized here as the file doesn't exist - static fn($value) => $value, - static fn() => null, - ), - ); - } - - public function testCast() - { - $path = \tempnam(\sys_get_temp_dir(), 'lazy_stream'); - $stream = new LazyStream(Path::of($path)); - \file_put_contents($path, 'lorem ipsum dolor'); - - $this->assertSame('lorem ipsum dolor', $stream->toString()->match( - static fn($value) => $value, - static fn() => null, - )); - } - - public function testClose() - { - $path = \tempnam(\sys_get_temp_dir(), 'lazy_stream'); - $stream = new LazyStream(Path::of($path)); - \file_put_contents($path, 'lorem ipsum dolor'); - - $this->assertInstanceOf( - SideEffect::class, - $stream->close()->match( - static fn($value) => $value, - static fn() => null, - ), - ); - } - - public function testSize() - { - $path = \tempnam(\sys_get_temp_dir(), 'lazy_stream'); - $stream = new LazyStream(Path::of($path)); - \file_put_contents($path, 'lorem ipsum dolor'); - - $this->assertSame(17, $stream->size()->match( - static fn($size) => $size->toInt(), - static fn() => null, - )); - } - - public function testPosition() - { - $path = \tempnam(\sys_get_temp_dir(), 'lazy_stream'); - $stream = new LazyStream(Path::of($path)); - \file_put_contents($path, 'lorem ipsum dolor'); - - $this->assertSame(0, $stream->position()->toInt()); - } - - public function testEnd() - { - $path = \tempnam(\sys_get_temp_dir(), 'lazy_stream'); - $stream = new LazyStream(Path::of($path)); - \file_put_contents($path, 'lorem ipsum dolor'); - - $this->assertFalse($stream->end()); - } - - public function testSeek() - { - $path = \tempnam(\sys_get_temp_dir(), 'lazy_stream'); - $stream = new LazyStream(Path::of($path)); - \file_put_contents($path, 'lorem ipsum dolor'); - - $this->assertSame( - $stream, - $stream->seek(new Position(3))->match( - static fn($value) => $value, - static fn() => null, - ), - ); - } - - public function testRead() - { - $path = \tempnam(\sys_get_temp_dir(), 'lazy_stream'); - $stream = new LazyStream(Path::of($path)); - \file_put_contents($path, 'lorem ipsum dolor'); - - $this->assertSame('lorem', $stream->read(5)->match( - static fn($value) => $value->toString(), - static fn() => null, - )); - } -}