Skip to content

Commit

Permalink
Feat: Chain extractors (#27)
Browse files Browse the repository at this point in the history
  • Loading branch information
bpolaszek authored Nov 10, 2023
1 parent a9f5c3d commit c0998ed
Show file tree
Hide file tree
Showing 5 changed files with 136 additions and 5 deletions.
15 changes: 14 additions & 1 deletion src/Extractor/CallableExtractor.php
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,9 @@

use BenTools\ETL\EtlState;
use Closure;
use EmptyIterator;

use function is_iterable;

final readonly class CallableExtractor implements ExtractorInterface
{
Expand All @@ -16,6 +19,16 @@ public function __construct(

public function extract(EtlState $state): iterable
{
return ($this->closure)($state);
$extracted = ($this->closure)($state);

if (null === $extracted) {
return new EmptyIterator();
}

if (!is_iterable($extracted)) {
return [$extracted];
}

return $extracted;
}
}
42 changes: 42 additions & 0 deletions src/Extractor/ChainExtractor.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
<?php

declare(strict_types=1);

namespace BenTools\ETL\Extractor;

use BenTools\ETL\EtlState;

final readonly class ChainExtractor implements ExtractorInterface
{
/**
* @var ExtractorInterface[]
*/
private array $extractors;

public function __construct(
ExtractorInterface|callable $extractor,
ExtractorInterface|callable ...$extractors,
) {
$extractors = [$extractor, ...$extractors];
foreach ($extractors as $e => $_extractor) {
if (!$_extractor instanceof ExtractorInterface) {
$extractors[$e] = new CallableExtractor($_extractor(...));
}
}
$this->extractors = $extractors;
}

public function with(ExtractorInterface|callable $extractor): self
{
return new self(...[...$this->extractors, $extractor]);
}

public function extract(EtlState $state): iterable
{
foreach ($this->extractors as $extractor) {
foreach ($extractor->extract($state) as $item) {
yield $item;
}
}
}
}
17 changes: 13 additions & 4 deletions src/Internal/EtlBuilderTrait.php
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@

use BenTools\ETL\EtlConfiguration;
use BenTools\ETL\Extractor\CallableExtractor;
use BenTools\ETL\Extractor\ChainExtractor;
use BenTools\ETL\Extractor\ExtractorInterface;
use BenTools\ETL\Loader\CallableLoader;
use BenTools\ETL\Loader\ChainLoader;
Expand All @@ -29,13 +30,21 @@ trait EtlBuilderTrait
*/
use EtlEventListenersTrait;

public function extractFrom(ExtractorInterface|callable $extractor): self
public function extractFrom(ExtractorInterface|callable $extractor, ExtractorInterface|callable ...$extractors): self
{
if (!$extractor instanceof ExtractorInterface) {
$extractor = new CallableExtractor($extractor(...));
$extractors = [$extractor, ...$extractors];

foreach ($extractors as $e => $_extractor) {
if (!$_extractor instanceof ExtractorInterface) {
$extractors[$e] = new CallableExtractor($_extractor(...));
}
}

if (count($extractors) > 1) {
return $this->cloneWith(['extractor' => new ChainExtractor(...$extractors)]);
}

return $this->cloneWith(['extractor' => $extractor]);
return $this->cloneWith(['extractor' => $extractors[0]]);
}

public function transformWith(TransformerInterface|callable $transformer, TransformerInterface|callable ...$transformers): self
Expand Down
26 changes: 26 additions & 0 deletions tests/Unit/Extractor/CallableExtractorTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,9 @@

use BenTools\ETL\EtlState;
use BenTools\ETL\Extractor\CallableExtractor;
use EmptyIterator;

use function expect;

it('converts a callable to an extractor', function () {
// Given
Expand All @@ -18,3 +21,26 @@
// Then
expect($value)->toBe(['foo', 'bar']);
});

it('returns an empty iterable when extracted content is null', function () {
// Given
$state = new EtlState();
$callable = fn () => null;

// When
$value = (new CallableExtractor($callable))->extract($state);

// Then
expect($value)->toBeInstanceOf(EmptyIterator::class);
});
it('returns an iterable of values when extracted content is not iterable', function () {
// Given
$state = new EtlState();
$callable = fn () => 'foo';

// When
$value = (new CallableExtractor($callable))->extract($state);

// Then
expect($value)->toBe(['foo']);
});
41 changes: 41 additions & 0 deletions tests/Unit/Extractor/ChainExtractorTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
<?php

declare(strict_types=1);

namespace BenTools\ETL\Tests\Unit\Extractor;

use BenTools\ETL\EtlExecutor;
use BenTools\ETL\Extractor\ChainExtractor;

use function expect;

it('chains extractors', function () {
// Given
$extractor = (new ChainExtractor(
fn () => 'banana',
fn () => yield from ['apple', 'strawberry'],
))->with(fn () => ['raspberry', 'peach']);
$executor = (new EtlExecutor($extractor));

// When
$report = $executor->process();

// Then
expect($report->output)->toBe(['banana', 'apple', 'strawberry', 'raspberry', 'peach']);
});

it('silently chains extractors', function () {
// Given
$executor = (new EtlExecutor())
->extractFrom(
fn () => 'banana',
fn () => yield from ['apple', 'strawberry'],
fn () => ['raspberry', 'peach']
);

// When
$report = $executor->process();

// Then
expect($report->output)->toBe(['banana', 'apple', 'strawberry', 'raspberry', 'peach']);
});

0 comments on commit c0998ed

Please sign in to comment.