From c2283bbd228a3220c2c54b0c2aa7cb98cd512253 Mon Sep 17 00:00:00 2001 From: Baptiste Langlade Date: Sun, 29 Sep 2024 17:24:47 +0200 Subject: [PATCH] add commands migrations --- .gitattributes | 1 + composer.json | 5 ++ fixtures/Ref.php | 17 ++++++ proofs/commands.php | 120 +++++++++++++++++++++++++++++++++++++ src/Commands.php | 106 ++++++++++++++++++++++++++++++++ src/Commands/Migration.php | 51 ++++++++++++++++ src/Commands/Reference.php | 11 ++++ src/Commands/Run.php | 75 +++++++++++++++++++++++ 8 files changed, 386 insertions(+) create mode 100644 fixtures/Ref.php create mode 100644 proofs/commands.php create mode 100644 src/Commands.php create mode 100644 src/Commands/Migration.php create mode 100644 src/Commands/Reference.php create mode 100644 src/Commands/Run.php diff --git a/.gitattributes b/.gitattributes index df2a04b..ac3db3f 100644 --- a/.gitattributes +++ b/.gitattributes @@ -1,2 +1,3 @@ /.github export-ignore /proofs export-ignore +/fixtures export-ignore diff --git a/composer.json b/composer.json index 3333cd5..962eabb 100644 --- a/composer.json +++ b/composer.json @@ -24,6 +24,11 @@ "Formal\\Migrations\\": "src/" } }, + "autoload-dev": { + "psr-4": { + "Fixtures\\Formal\\Migrations\\": "fixtures/" + } + }, "require-dev": { "vimeo/psalm": "~5.13", "innmind/black-box": "~5.7", diff --git a/fixtures/Ref.php b/fixtures/Ref.php new file mode 100644 index 0000000..430e330 --- /dev/null +++ b/fixtures/Ref.php @@ -0,0 +1,17 @@ +atLeast(1), + Set\Strings::madeOf(Set\Chars::alphanumerical())->atLeast(1), + Set\Strings::madeOf(Set\Chars::alphanumerical())->atLeast(1), + Set\Strings::madeOf(Set\Chars::alphanumerical())->atLeast(1), + ), + ), + static function($assert, $names) { + [$a, $b, $c, $d] = $names; + $tmp = \sys_get_temp_dir().'/formal/migrations'; + @\mkdir($tmp, recursive: true); + + $os = Factory::build(); + + $migrations = Commands::of( + $storage = Manager::filesystem( + InMemory::emulateFilesystem(), + Aggregates::of( + Types::of( + Support::class( + PointInTime::class, + PointInTimeType::new($os->clock()), + ), + ), + ), + ), + $os, + null, + static fn() => static fn($command) => $command->withWorkingDirectory(Path::of($tmp)), + ); + + $versions = $migrations(Sequence::of( + Migration::of( + $a, + Command::foreground('touch test') + ->withWorkingDirectory(Path::of($tmp)), + ), + Migration::of( + $b, + Ref::rm, + ), + Migration::of( + $c, + Command::foreground('echo foo >> test') + ->withWorkingDirectory(Path::of($tmp)), + ), + Migration::of( + $d, + Ref::rm, + ), + )); + + $assert->count(4, $versions); + $assert->same( + [$a, $b, $c, $d], + $versions + ->map(static fn($version) => $version->name()) + ->toList(), + ); + $assert->same( + [$a, $b, $c, $d], + $versions + ->sort(static fn($a, $b) => $a->appliedAt()->aheadOf($b->appliedAt()) ? 1 : -1) + ->map(static fn($version) => $version->name()) + ->toList(), + ); + $stored = $storage + ->repository(Version::class) + ->all() + ->map(static fn($version) => $version->name()) + ->toList(); + $assert + ->expected($a) + ->in($stored); + $assert + ->expected($b) + ->in($stored); + $assert + ->expected($c) + ->in($stored); + $assert + ->expected($d) + ->in($stored); + + $assert->true(\file_exists($tmp.'/test')); + $assert->same( + "foo\n", + \file_get_contents($tmp.'/test'), + ); + }, + ); +}; diff --git a/src/Commands.php b/src/Commands.php new file mode 100644 index 0000000..feffa17 --- /dev/null +++ b/src/Commands.php @@ -0,0 +1,106 @@ +storage = $storage; + $this->os = $os; + $this->build = $build; + $this->configure = $configure; + } + + /** + * @param Sequence> $migrations + * + * @return Sequence + */ + public function __invoke(Sequence $migrations): Sequence + { + $versions = $this->storage->repository(Version::class); + $processes = ($this->build)($this->os); + $run = Run::of($processes, $this->configure); + + return $migrations + ->exclude(static fn($migration) => $versions->any( + Property::of( + 'name', + Sign::equality, + $migration->name(), + ), + )) + ->map(function($migration) use ($run, $versions) { + $migration($run); + + $version = Version::new( + $migration->name(), + $this->os->clock(), + ); + + $this->storage->transactional( + static function() use ($version, $versions) { + $versions->put($version); + + return Either::right(null); + }, + ); + + return $version; + }); + } + + /** + * @param ?callable(OperatingSystem): Processes $build + * @param ?callable(Reference): (callable(Command): Command) $configure + */ + public static function of( + Manager $storage, + OperatingSystem $os, + callable $build = null, + callable $configure = null, + ): self { + return new self( + $storage, + $os, + $build ?? static fn(OperatingSystem $os) => $os->control()->processes(), + $configure ?? static fn(Reference $ref) => static fn(Command $command) => $command, + ); + } +} diff --git a/src/Commands/Migration.php b/src/Commands/Migration.php new file mode 100644 index 0000000..96c8b78 --- /dev/null +++ b/src/Commands/Migration.php @@ -0,0 +1,51 @@ + + */ +final class Migration implements MigrationInterface +{ + /** + * @param non-empty-string $name + * @param Sequence $commands + */ + private function __construct( + private string $name, + private Sequence $commands, + ) { + } + + public function __invoke($kind): void + { + $_ = $this->commands->foreach( + static fn($command) => $kind($command)->match( + static fn() => null, + static fn($error) => throw new \RuntimeException($error::class), + ), + ); + } + + /** + * @no-named-arguments + * + * @param non-empty-string $name + */ + public static function of( + string $name, + Command|Reference ...$commands, + ): self { + return new self($name, Sequence::of(...$commands)); + } + + public function name(): string + { + return $this->name; + } +} diff --git a/src/Commands/Reference.php b/src/Commands/Reference.php new file mode 100644 index 0000000..832bc5e --- /dev/null +++ b/src/Commands/Reference.php @@ -0,0 +1,11 @@ +> */ + private Map $alreayRun; + + /** + * @param callable(Reference): (callable(Command): Command) $configure + */ + private function __construct( + Processes $processes, + callable $configure, + ) { + $this->processes = $processes; + $this->configure = $configure; + $this->alreayRun = Map::of(); + } + + /** + * @return Either + */ + public function __invoke(Command|Reference $command): Either + { + if ($command instanceof Command) { + return $this + ->processes + ->execute($command) + ->wait(); + } + + $result = $this + ->alreayRun + ->get($command) + ->match( + static fn($result) => $result, + fn() => $this + ->processes + ->execute(($this->configure)($command)($command->command())) + ->wait(), + ); + $this->alreayRun = ($this->alreayRun)($command, $result); + + return $result; + } + + /** + * @param callable(Reference): (callable(Command): Command) $configure + */ + public static function of( + Processes $processes, + callable $configure = null, + ): self { + return new self($processes, $configure); + } +}