diff --git a/composer.json b/composer.json index b618ce56..0b256f78 100644 --- a/composer.json +++ b/composer.json @@ -109,7 +109,8 @@ "Kiboko\\Component\\Satellite\\Plugin\\Stream\\Service", "Kiboko\\Component\\Satellite\\Plugin\\SFTP\\Service", "Kiboko\\Component\\Satellite\\Plugin\\FTP\\Service", - "Kiboko\\Component\\Satellite\\Plugin\\Batching\\Service" + "Kiboko\\Component\\Satellite\\Plugin\\Batching\\Service", + "Kiboko\\Component\\Satellite\\Plugin\\Filtering\\Service" ] } }, diff --git a/src/Plugin/Filtering/Builder/Drop.php b/src/Plugin/Filtering/Builder/Drop.php new file mode 100644 index 00000000..1abb8402 --- /dev/null +++ b/src/Plugin/Filtering/Builder/Drop.php @@ -0,0 +1,163 @@ + */ + private array $exclusions = []; + + public function __construct() + { + } + + public function withLogger(Node\Expr $logger): self + { + $this->logger = $logger; + + return $this; + } + + public function withRejection(Node\Expr $rejection): self + { + $this->rejection = $rejection; + + return $this; + } + + public function withState(Node\Expr $state): self + { + $this->state = $state; + + return $this; + } + + public function withExclusions(Node\Expr ...$exclusions): self + { + array_push($this->exclusions, ...$exclusions); + + return $this; + } + + private function buildExclusions(Node\Expr ...$exclusions): Node\Expr + { + if (\count($exclusions) > 3) { + $length = \count($exclusions); + $middle = (int) floor($length / 2); + $left = \array_slice($exclusions, 0, $middle); + $right = \array_slice($exclusions, $middle, $length); + + return new Node\Expr\BinaryOp\BooleanAnd( + $this->buildExclusions(...$left), + $this->buildExclusions(...$right), + ); + } + + if (\count($exclusions) > 2) { + $right = array_shift($exclusions); + + return new Node\Expr\BinaryOp\BooleanAnd( + $this->buildExclusions(...$exclusions), + $right, + ); + } + + if (\count($exclusions) > 1) { + $left = array_pop($exclusions); + $right = array_pop($exclusions); + + return new Node\Expr\BinaryOp\BooleanAnd( + $left, + $right, + ); + } + + if (\count($exclusions) > 0) { + return array_pop($exclusions); + } + + return new Node\Expr\ConstFetch( + new Node\Name('false'), + ); + } + + public function getNode(): Node + { + return new Node\Expr\New_( + class: new Node\Stmt\Class_(null, [ + 'implements' => [ + new Node\Name\FullyQualified(TransformerInterface::class), + ], + 'stmts' => [ + (new Builder\Method('transform')) + ->makePublic() + ->setReturnType(new Node\Name\FullyQualified(\Generator::class)) + ->addStmts([ + new Node\Stmt\Expression( + new Node\Expr\Assign( + new Node\Expr\Variable('input'), + new Node\Expr\Yield_(), + ) + ), + new Node\Stmt\While_( + new Node\Expr\ConstFetch( + new Node\Name('true'), + ), + [ + new Node\Stmt\If_( + $this->buildExclusions(...$this->exclusions), + [ + 'stmts' => [ + new Node\Stmt\Expression( + new Node\Expr\Assign( + new Node\Expr\Variable('input'), + new Node\Expr\Yield_( + new Node\Expr\New_( + new Node\Name\FullyQualified(RejectionResultBucket::class), + ), + ), + ), + ), + new Node\Stmt\Continue_(), + ], + ] + ), + new Node\Stmt\Expression( + new Node\Expr\Assign( + new Node\Expr\Variable('input'), + new Node\Expr\Yield_( + new Node\Expr\New_( + new Node\Name\FullyQualified(AcceptanceResultBucket::class), + [ + new Node\Arg(new Node\Expr\Variable('input')), + ] + ), + ), + ), + ), + ], + ), + new Node\Stmt\Expression( + new Node\Expr\Yield_( + new Node\Expr\Variable('input') + ), + ), + ]) + ->getNode(), + ], + ]) + ); + } +} diff --git a/src/Plugin/Filtering/Builder/Reject.php b/src/Plugin/Filtering/Builder/Reject.php new file mode 100644 index 00000000..3e0af856 --- /dev/null +++ b/src/Plugin/Filtering/Builder/Reject.php @@ -0,0 +1,166 @@ + */ + private array $exclusions = []; + + public function __construct() + { + } + + public function withLogger(Node\Expr $logger): self + { + $this->logger = $logger; + + return $this; + } + + public function withRejection(Node\Expr $rejection): self + { + $this->rejection = $rejection; + + return $this; + } + + public function withState(Node\Expr $state): self + { + $this->state = $state; + + return $this; + } + + public function withExclusions(Node\Expr ...$exclusions): self + { + array_push($this->exclusions, ...$exclusions); + + return $this; + } + + private function buildExclusions(Node\Expr ...$exclusions): Node\Expr + { + if (\count($exclusions) > 3) { + $length = \count($exclusions); + $middle = (int) floor($length / 2); + $left = \array_slice($exclusions, 0, $middle); + $right = \array_slice($exclusions, $middle, $length); + + return new Node\Expr\BinaryOp\BooleanAnd( + $this->buildExclusions(...$left), + $this->buildExclusions(...$right), + ); + } + + if (\count($exclusions) > 2) { + $right = array_shift($exclusions); + + return new Node\Expr\BinaryOp\BooleanAnd( + $this->buildExclusions(...$exclusions), + $right, + ); + } + + if (\count($exclusions) > 1) { + $left = array_pop($exclusions); + $right = array_pop($exclusions); + + return new Node\Expr\BinaryOp\BooleanAnd( + $left, + $right, + ); + } + + if (\count($exclusions) > 0) { + return array_pop($exclusions); + } + + return new Node\Expr\ConstFetch( + new Node\Name('false'), + ); + } + + public function getNode(): Node + { + return new Node\Expr\New_( + class: new Node\Stmt\Class_(null, [ + 'implements' => [ + new Node\Name\FullyQualified(TransformerInterface::class), + ], + 'stmts' => [ + (new Builder\Method('transform')) + ->makePublic() + ->setReturnType(new Node\Name\FullyQualified(\Generator::class)) + ->addStmts([ + new Node\Stmt\Expression( + new Node\Expr\Assign( + new Node\Expr\Variable('input'), + new Node\Expr\Yield_(), + ) + ), + new Node\Stmt\While_( + new Node\Expr\ConstFetch( + new Node\Name('true'), + ), + [ + new Node\Stmt\If_( + $this->buildExclusions(...$this->exclusions), + [ + 'stmts' => [ + new Node\Stmt\Expression( + new Node\Expr\Assign( + new Node\Expr\Variable('input'), + new Node\Expr\Yield_( + new Node\Expr\New_( + new Node\Name\FullyQualified(RejectionResultBucket::class), + [ + new Node\Arg(new Node\Expr\Variable('input')), + ] + ), + ), + ), + ), + new Node\Stmt\Continue_(), + ], + ] + ), + new Node\Stmt\Expression( + new Node\Expr\Assign( + new Node\Expr\Variable('input'), + new Node\Expr\Yield_( + new Node\Expr\New_( + new Node\Name\FullyQualified(AcceptanceResultBucket::class), + [ + new Node\Arg(new Node\Expr\Variable('input')), + ] + ), + ), + ), + ), + ], + ), + new Node\Stmt\Expression( + new Node\Expr\Yield_( + new Node\Expr\Variable('input') + ), + ), + ]) + ->getNode(), + ], + ]) + ); + } +} diff --git a/src/Plugin/Filtering/Configuration.php b/src/Plugin/Filtering/Configuration.php new file mode 100644 index 00000000..42607a3b --- /dev/null +++ b/src/Plugin/Filtering/Configuration.php @@ -0,0 +1,57 @@ +getRootNode() + ->validate() + ->always($this->cleanupFields('reject', 'drop')) + ->end() + ->validate() + ->always(mutuallyExclusiveFields('reject', 'drop')) + ->end() + ->children() + ->arrayNode('expression_language') + ->scalarPrototype()->end() + ->end() + ->append(node: $reject->getConfigTreeBuilder()->getRootNode()) + ->append(node: $drop->getConfigTreeBuilder()->getRootNode()) + ->end() + ; + + return $builder; + } + + private function cleanupFields(string ...$fieldNames): \Closure + { + return function (array $value) use ($fieldNames) { + foreach ($fieldNames as $fieldName) { + if (!\array_key_exists($fieldName, $value)) { + continue; + } + + if (!\is_array($value[$fieldName]) || \count($value[$fieldName]) <= 0) { + unset($value[$fieldName]); + } + } + + return $value; + }; + } +} diff --git a/src/Plugin/Filtering/Configuration/Drop.php b/src/Plugin/Filtering/Configuration/Drop.php new file mode 100644 index 00000000..113efd78 --- /dev/null +++ b/src/Plugin/Filtering/Configuration/Drop.php @@ -0,0 +1,37 @@ +getRootNode() + ->arrayPrototype() + ->children() + ->scalarNode('when') + ->isRequired() + ->cannotBeEmpty() + ->validate() + ->ifTrue(isExpression()) + ->then(asExpression()) + ->end() + ->end() + ->end() + ->end() + ; + + return $builder; + } +} diff --git a/src/Plugin/Filtering/Configuration/Reject.php b/src/Plugin/Filtering/Configuration/Reject.php new file mode 100644 index 00000000..1a426ce2 --- /dev/null +++ b/src/Plugin/Filtering/Configuration/Reject.php @@ -0,0 +1,37 @@ +getRootNode() + ->arrayPrototype() + ->children() + ->scalarNode('when') + ->isRequired() + ->cannotBeEmpty() + ->validate() + ->ifTrue(isExpression()) + ->then(asExpression()) + ->end() + ->end() + ->end() + ->end() + ; + + return $builder; + } +} diff --git a/src/Plugin/Filtering/Factory/Drop.php b/src/Plugin/Filtering/Factory/Drop.php new file mode 100644 index 00000000..5236e40a --- /dev/null +++ b/src/Plugin/Filtering/Factory/Drop.php @@ -0,0 +1,78 @@ +processor = new Processor(); + $this->configuration = new Configuration(); + } + + public function configuration(): ConfigurationInterface + { + return $this->configuration; + } + + /** + * @throws Configurator\ConfigurationExceptionInterface + */ + public function normalize(array $config): array + { + try { + return $this->processor->processConfiguration($this->configuration, $config); + } catch (Symfony\InvalidTypeException|Symfony\InvalidConfigurationException $exception) { + throw new Configurator\InvalidConfigurationException($exception->getMessage(), 0, $exception); + } + } + + public function validate(array $config): bool + { + try { + $this->processor->processConfiguration($this->configuration, $config); + + return true; + } catch (Symfony\InvalidTypeException|Symfony\InvalidConfigurationException) { + return false; + } + } + + /** + * @throws Configurator\ConfigurationExceptionInterface + */ + public function compile(array $config): Repository\Drop + { + $interpreter = clone $this->interpreter; + + $builder = new Filtering\Builder\Drop(); + + $repository = new Repository\Drop($builder); + + foreach ($config as $condition) { + $builder->withExclusions( + compileExpression($interpreter, $condition['when']) + ); + } + + return $repository; + } +} diff --git a/src/Plugin/Filtering/Factory/Reject.php b/src/Plugin/Filtering/Factory/Reject.php new file mode 100644 index 00000000..047a919f --- /dev/null +++ b/src/Plugin/Filtering/Factory/Reject.php @@ -0,0 +1,78 @@ +processor = new Processor(); + $this->configuration = new Configuration(); + } + + public function configuration(): ConfigurationInterface + { + return $this->configuration; + } + + /** + * @throws Configurator\ConfigurationExceptionInterface + */ + public function normalize(array $config): array + { + try { + return $this->processor->processConfiguration($this->configuration, $config); + } catch (Symfony\InvalidTypeException|Symfony\InvalidConfigurationException $exception) { + throw new Configurator\InvalidConfigurationException($exception->getMessage(), 0, $exception); + } + } + + public function validate(array $config): bool + { + try { + $this->processor->processConfiguration($this->configuration, $config); + + return true; + } catch (Symfony\InvalidTypeException|Symfony\InvalidConfigurationException) { + return false; + } + } + + /** + * @throws Configurator\ConfigurationExceptionInterface + */ + public function compile(array $config): Repository\Reject + { + $interpreter = clone $this->interpreter; + + $builder = new Filtering\Builder\Reject(); + + $repository = new Repository\Reject($builder); + + foreach ($config as $condition) { + $builder->withExclusions( + compileExpression($interpreter, $condition['when']) + ); + } + + return $repository; + } +} diff --git a/src/Plugin/Filtering/Factory/Repository/Drop.php b/src/Plugin/Filtering/Factory/Repository/Drop.php new file mode 100644 index 00000000..f28245aa --- /dev/null +++ b/src/Plugin/Filtering/Factory/Repository/Drop.php @@ -0,0 +1,32 @@ +files = []; + $this->packages = []; + } + + public function getBuilder(): Filtering\Builder\Drop + { + return $this->builder; + } + + public function merge(Configurator\RepositoryInterface $friend): self + { + array_push($this->files, ...$friend->getFiles()); + array_push($this->packages, ...$friend->getPackages()); + + return $this; + } +} diff --git a/src/Plugin/Filtering/Factory/Repository/Reject.php b/src/Plugin/Filtering/Factory/Repository/Reject.php new file mode 100644 index 00000000..f51ba020 --- /dev/null +++ b/src/Plugin/Filtering/Factory/Repository/Reject.php @@ -0,0 +1,32 @@ +files = []; + $this->packages = []; + } + + public function getBuilder(): Filtering\Builder\Reject + { + return $this->builder; + } + + public function merge(Configurator\RepositoryInterface $friend): self + { + array_push($this->files, ...$friend->getFiles()); + array_push($this->packages, ...$friend->getPackages()); + + return $this; + } +} diff --git a/src/Plugin/Filtering/Factory/Repository/RepositoryTrait.php b/src/Plugin/Filtering/Factory/Repository/RepositoryTrait.php new file mode 100644 index 00000000..f332d595 --- /dev/null +++ b/src/Plugin/Filtering/Factory/Repository/RepositoryTrait.php @@ -0,0 +1,42 @@ + */ + private array $files; + /** @var string[] */ + private array $packages; + + public function addFiles(Packaging\FileInterface|Packaging\DirectoryInterface ...$files): Configurator\RepositoryInterface + { + array_push($this->files, ...$files); + + return $this; + } + + /** @return iterable */ + public function getFiles(): iterable + { + return $this->files; + } + + public function addPackages(string ...$packages): Configurator\RepositoryInterface + { + array_push($this->packages, ...$packages); + + return $this; + } + + /** @return iterable */ + public function getPackages(): iterable + { + return $this->packages; + } +} diff --git a/src/Plugin/Filtering/Service.php b/src/Plugin/Filtering/Service.php new file mode 100644 index 00000000..f63c4021 --- /dev/null +++ b/src/Plugin/Filtering/Service.php @@ -0,0 +1,91 @@ +processor = new Processor(); + $this->configuration = new Configuration(); + } + + public function interpreter(): ExpressionLanguage + { + return $this->interpreter; + } + + public function configuration(): Configurator\PluginConfigurationInterface + { + return $this->configuration; + } + + /** + * @throws Configurator\ConfigurationExceptionInterface + */ + public function normalize(array $config): array + { + try { + return $this->processor->processConfiguration($this->configuration, $config); + } catch (Symfony\InvalidTypeException|Symfony\InvalidConfigurationException $exception) { + throw new Configurator\InvalidConfigurationException($exception->getMessage(), 0, $exception); + } + } + + public function validate(array $config): bool + { + try { + $this->processor->processConfiguration($this->configuration, $config); + + return true; + } catch (Symfony\InvalidTypeException|Symfony\InvalidConfigurationException) { + return false; + } + } + + /** + * @throws Configurator\ConfigurationExceptionInterface + */ + public function compile(array $config): Configurator\RepositoryInterface + { + $interpreter = clone $this->interpreter; + + if (\array_key_exists('expression_language', $config) + && \is_array($config['expression_language']) + && \count($config['expression_language']) + ) { + foreach ($config['expression_language'] as $provider) { + $interpreter->registerProvider(new $provider()); + } + } + + if (\array_key_exists('reject', $config)) { + return (new Filtering\Factory\Reject($interpreter, $config['expression_language'] ?? []))->compile($config['reject']); + } + + if (\array_key_exists('drop', $config)) { + return (new Filtering\Factory\Drop($interpreter, $config['expression_language'] ?? []))->compile($config['drop']); + } + + throw new \RuntimeException('No possible pipeline step, expecting "extractor", "transformer" or "loader".'); + } +}