From b3035b07a841cfffcfa1fef51d1a06085cdefcab Mon Sep 17 00:00:00 2001 From: Steve Bauman Date: Tue, 30 Jun 2026 16:58:31 -0400 Subject: [PATCH] Add fluent configuration pipeline API --- src/Configuration.php | 101 ++++++++++++++++++++++++---- src/ConfiguredChronoEngine.php | 24 ++++--- tests/system.test.php | 117 +++++++++++++++++++++------------ 3 files changed, 177 insertions(+), 65 deletions(-) diff --git a/src/Configuration.php b/src/Configuration.php index 769eaa3..a3e2471 100644 --- a/src/Configuration.php +++ b/src/Configuration.php @@ -4,6 +4,17 @@ readonly class Configuration { + /** + * Create a new parser/refiner configuration. + * + * @param array $parsers + * @param array $refiners + */ + public static function make(array $parsers = [], array $refiners = []): self + { + return new self($parsers, $refiners); + } + /** * Create a parser/refiner configuration. * @@ -11,20 +22,77 @@ * @param array $refiners */ public function __construct( - public readonly array $parsers = [], + protected readonly array $parsers = [], - public readonly array $refiners = [], + protected readonly array $refiners = [], ) {} + /** + * Get the configured parsers. + * + * @return array + */ + public function parsers(): array + { + return $this->parsers; + } + + /** + * Get the configured refiners. + * + * @return array + */ + public function refiners(): array + { + return $this->refiners; + } + + /** + * Determine whether the configuration has the given parser. + * + * @param class-string $parser + */ + public function hasParser(string $parser): bool + { + foreach ($this->parsers as $configuredParser) { + if ($configuredParser instanceof $parser) { + return true; + } + } + + return false; + } + + /** + * Determine whether the configuration has the given refiner. + * + * @param class-string $refiner + */ + public function hasRefiner(string $refiner): bool + { + foreach ($this->refiners as $configuredRefiner) { + if ($configuredRefiner instanceof $refiner) { + return true; + } + } + + return false; + } + /** * Return a configuration with the given parser added. */ - public function withParser(Parser $parser, bool $prepend = false): self + public function addParser(Parser $parser): self { - return new self( - $prepend ? [$parser, ...$this->parsers] : [...$this->parsers, $parser], - $this->refiners, - ); + return new self([...$this->parsers, $parser], $this->refiners); + } + + /** + * Return a configuration with the given parser added to the beginning. + */ + public function prependParser(Parser $parser): self + { + return new self([$parser, ...$this->parsers], $this->refiners); } /** @@ -32,7 +100,7 @@ public function withParser(Parser $parser, bool $prepend = false): self * * @param class-string $parser */ - public function withoutParser(string $parser): self + public function removeParser(string $parser): self { return new self( array_values(array_filter( @@ -46,12 +114,17 @@ public function withoutParser(string $parser): self /** * Return a configuration with the given refiner added. */ - public function withRefiner(Refiner $refiner, bool $prepend = false): self + public function addRefiner(Refiner $refiner): self { - return new self( - $this->parsers, - $prepend ? [$refiner, ...$this->refiners] : [...$this->refiners, $refiner], - ); + return new self($this->parsers, [...$this->refiners, $refiner]); + } + + /** + * Return a configuration with the given refiner added to the beginning. + */ + public function prependRefiner(Refiner $refiner): self + { + return new self($this->parsers, [$refiner, ...$this->refiners]); } /** @@ -59,7 +132,7 @@ public function withRefiner(Refiner $refiner, bool $prepend = false): self * * @param class-string $refiner */ - public function withoutRefiner(string $refiner): self + public function removeRefiner(string $refiner): self { return new self( $this->parsers, diff --git a/src/ConfiguredChronoEngine.php b/src/ConfiguredChronoEngine.php index b43a9fe..0053c4c 100644 --- a/src/ConfiguredChronoEngine.php +++ b/src/ConfiguredChronoEngine.php @@ -20,7 +20,7 @@ public function parse(string $text, Reference $reference, Options $options): arr { $results = array_merge(...array_map( fn (Parser $parser) => $parser->parse($text, $reference, $options), - $this->configuration->parsers, + $this->configuration->parsers(), )); $results = $this->attachReference($results, $reference); @@ -35,7 +35,7 @@ public function parse(string $text, Reference $reference, Options $options): arr $results = array_map(fn (array $result): ParsedResult => $result[0], $results); - foreach ($this->configuration->refiners as $refiner) { + foreach ($this->configuration->refiners() as $refiner) { $results = $this->attachReference( $refiner->refine($text, $results, $reference, $options), $reference, @@ -51,8 +51,8 @@ public function parse(string $text, Reference $reference, Options $options): arr public function clone(): self { return $this->newInstance(new Configuration( - parsers: [...$this->configuration->parsers], - refiners: [...$this->configuration->refiners], + parsers: [...$this->configuration->parsers()], + refiners: [...$this->configuration->refiners()], )); } @@ -61,7 +61,11 @@ public function clone(): self */ public function withParser(Parser $parser, bool $prepend = false): self { - return $this->newInstance($this->configuration->withParser($parser, $prepend)); + return $this->newInstance( + $prepend + ? $this->configuration->prependParser($parser) + : $this->configuration->addParser($parser), + ); } /** @@ -71,7 +75,7 @@ public function withParser(Parser $parser, bool $prepend = false): self */ public function withoutParser(string $parser): self { - return $this->newInstance($this->configuration->withoutParser($parser)); + return $this->newInstance($this->configuration->removeParser($parser)); } /** @@ -79,7 +83,11 @@ public function withoutParser(string $parser): self */ public function withRefiner(Refiner $refiner, bool $prepend = false): self { - return $this->newInstance($this->configuration->withRefiner($refiner, $prepend)); + return $this->newInstance( + $prepend + ? $this->configuration->prependRefiner($refiner) + : $this->configuration->addRefiner($refiner), + ); } /** @@ -89,7 +97,7 @@ public function withRefiner(Refiner $refiner, bool $prepend = false): self */ public function withoutRefiner(string $refiner): self { - return $this->newInstance($this->configuration->withoutRefiner($refiner)); + return $this->newInstance($this->configuration->removeRefiner($refiner)); } /** diff --git a/tests/system.test.php b/tests/system.test.php index bf93e22..bb1cee5 100644 --- a/tests/system.test.php +++ b/tests/system.test.php @@ -99,6 +99,7 @@ use DirectoryTree\Chrono\Refiner; use DirectoryTree\Chrono\Refiners\ExtractTimezoneAbbrRefiner; use DirectoryTree\Chrono\Refiners\ExtractTimezoneOffsetRefiner; +use DirectoryTree\Chrono\Refiners\ForwardDateRefiner; use DirectoryTree\Chrono\Refiners\MergeWeekdayComponentRefiner; use DirectoryTree\Chrono\Refiners\OverlapRemovalRefiner; use DirectoryTree\Chrono\Weekday; @@ -134,12 +135,12 @@ }); it('exposes source-shaped strict locale configurations separately from PHP extensions', function () { - $spanishParsers = array_map(fn (object $parser): string => $parser::class, EsChrono::createStrictConfiguration()->parsers); - $spanishRefiners = array_map(fn (object $refiner): string => $refiner::class, EsChrono::createStrictConfiguration()->refiners); - $germanParsers = array_map(fn (object $parser): string => $parser::class, DeChrono::createStrictConfiguration()->parsers); - $germanRefiners = array_map(fn (object $refiner): string => $refiner::class, DeChrono::createStrictConfiguration()->refiners); - $frenchParsers = array_map(fn (object $parser): string => $parser::class, FrChrono::createStrictConfiguration()->parsers); - $frenchRefiners = array_map(fn (object $refiner): string => $refiner::class, FrChrono::createStrictConfiguration()->refiners); + $spanishParsers = array_map(fn (object $parser): string => $parser::class, EsChrono::createStrictConfiguration()->parsers()); + $spanishRefiners = array_map(fn (object $refiner): string => $refiner::class, EsChrono::createStrictConfiguration()->refiners()); + $germanParsers = array_map(fn (object $parser): string => $parser::class, DeChrono::createStrictConfiguration()->parsers()); + $germanRefiners = array_map(fn (object $refiner): string => $refiner::class, DeChrono::createStrictConfiguration()->refiners()); + $frenchParsers = array_map(fn (object $parser): string => $parser::class, FrChrono::createStrictConfiguration()->parsers()); + $frenchRefiners = array_map(fn (object $refiner): string => $refiner::class, FrChrono::createStrictConfiguration()->refiners()); $resultTags = function (array $results): array { return array_values(array_unique(array_merge(...array_map( fn (ParsedResult $result): array => [ @@ -209,7 +210,7 @@ }); it('exposes source-shaped English refiner ordering around common configuration', function () { - $refiners = array_map(fn (object $refiner): string => $refiner::class, EnChrono::createStrictConfiguration()->refiners); + $refiners = array_map(fn (object $refiner): string => $refiner::class, EnChrono::createStrictConfiguration()->refiners()); $timezoneAbbrIndexes = array_keys($refiners, ExtractTimezoneAbbrRefiner::class, true); $lateTimezoneAbbrIndex = end($timezoneAbbrIndexes); @@ -233,12 +234,12 @@ }); it('exposes source-shaped strict configurations for Finnish Portuguese and Swedish', function () { - $finnishParsers = array_map(fn (object $parser): string => $parser::class, FiChrono::createStrictConfiguration()->parsers); - $finnishRefiners = array_map(fn (object $refiner): string => $refiner::class, FiChrono::createStrictConfiguration()->refiners); - $portugueseParsers = array_map(fn (object $parser): string => $parser::class, PtChrono::createStrictConfiguration()->parsers); - $portugueseRefiners = array_map(fn (object $refiner): string => $refiner::class, PtChrono::createStrictConfiguration()->refiners); - $swedishParsers = array_map(fn (object $parser): string => $parser::class, SvChrono::createStrictConfiguration()->parsers); - $swedishRefiners = array_map(fn (object $refiner): string => $refiner::class, SvChrono::createStrictConfiguration()->refiners); + $finnishParsers = array_map(fn (object $parser): string => $parser::class, FiChrono::createStrictConfiguration()->parsers()); + $finnishRefiners = array_map(fn (object $refiner): string => $refiner::class, FiChrono::createStrictConfiguration()->refiners()); + $portugueseParsers = array_map(fn (object $parser): string => $parser::class, PtChrono::createStrictConfiguration()->parsers()); + $portugueseRefiners = array_map(fn (object $refiner): string => $refiner::class, PtChrono::createStrictConfiguration()->refiners()); + $swedishParsers = array_map(fn (object $parser): string => $parser::class, SvChrono::createStrictConfiguration()->parsers()); + $swedishRefiners = array_map(fn (object $refiner): string => $refiner::class, SvChrono::createStrictConfiguration()->refiners()); expect($finnishParsers) ->toContain(FiMonthNameLittleEndianParser::class) @@ -292,18 +293,18 @@ }); it('exposes source-shaped strict configurations for remaining locale engines', function () { - $italianParsers = array_map(fn (object $parser): string => $parser::class, ItChrono::createStrictConfiguration()->parsers); - $italianRefiners = array_map(fn (object $refiner): string => $refiner::class, ItChrono::createStrictConfiguration()->refiners); - $dutchParsers = array_map(fn (object $parser): string => $parser::class, NlChrono::createStrictConfiguration()->parsers); - $dutchRefiners = array_map(fn (object $refiner): string => $refiner::class, NlChrono::createStrictConfiguration()->refiners); - $russianParsers = array_map(fn (object $parser): string => $parser::class, RuChrono::createStrictConfiguration()->parsers); - $russianRefiners = array_map(fn (object $refiner): string => $refiner::class, RuChrono::createStrictConfiguration()->refiners); - $ukrainianParsers = array_map(fn (object $parser): string => $parser::class, UkChrono::createStrictConfiguration()->parsers); - $ukrainianRefiners = array_map(fn (object $refiner): string => $refiner::class, UkChrono::createStrictConfiguration()->refiners); - $japaneseParsers = array_map(fn (object $parser): string => $parser::class, JaChrono::createStrictConfiguration()->parsers); - $japaneseRefiners = array_map(fn (object $refiner): string => $refiner::class, JaChrono::createStrictConfiguration()->refiners); - $vietnameseParsers = array_map(fn (object $parser): string => $parser::class, ViChrono::createStrictConfiguration()->parsers); - $vietnameseRefiners = array_map(fn (object $refiner): string => $refiner::class, ViChrono::createStrictConfiguration()->refiners); + $italianParsers = array_map(fn (object $parser): string => $parser::class, ItChrono::createStrictConfiguration()->parsers()); + $italianRefiners = array_map(fn (object $refiner): string => $refiner::class, ItChrono::createStrictConfiguration()->refiners()); + $dutchParsers = array_map(fn (object $parser): string => $parser::class, NlChrono::createStrictConfiguration()->parsers()); + $dutchRefiners = array_map(fn (object $refiner): string => $refiner::class, NlChrono::createStrictConfiguration()->refiners()); + $russianParsers = array_map(fn (object $parser): string => $parser::class, RuChrono::createStrictConfiguration()->parsers()); + $russianRefiners = array_map(fn (object $refiner): string => $refiner::class, RuChrono::createStrictConfiguration()->refiners()); + $ukrainianParsers = array_map(fn (object $parser): string => $parser::class, UkChrono::createStrictConfiguration()->parsers()); + $ukrainianRefiners = array_map(fn (object $refiner): string => $refiner::class, UkChrono::createStrictConfiguration()->refiners()); + $japaneseParsers = array_map(fn (object $parser): string => $parser::class, JaChrono::createStrictConfiguration()->parsers()); + $japaneseRefiners = array_map(fn (object $refiner): string => $refiner::class, JaChrono::createStrictConfiguration()->refiners()); + $vietnameseParsers = array_map(fn (object $parser): string => $parser::class, ViChrono::createStrictConfiguration()->parsers()); + $vietnameseRefiners = array_map(fn (object $refiner): string => $refiner::class, ViChrono::createStrictConfiguration()->refiners()); expect($italianParsers) ->toContain(IsoFormatParser::class) @@ -409,15 +410,15 @@ }); it('exposes source-shaped strict configurations for Chinese engines', function () { - $chineseParsers = array_map(fn (object $parser): string => $parser::class, ZhChrono::createStrictConfiguration()->parsers); - $chineseCasualParsers = array_map(fn (object $parser): string => $parser::class, ZhChrono::createCasualConfiguration()->parsers); - $chineseRefiners = array_map(fn (object $refiner): string => $refiner::class, ZhChrono::createStrictConfiguration()->refiners); - $hansParsers = array_map(fn (object $parser): string => $parser::class, ZhHansChrono::createStrictConfiguration()->parsers); - $hansCasualParsers = array_map(fn (object $parser): string => $parser::class, ZhHansChrono::createCasualConfiguration()->parsers); - $hansRefiners = array_map(fn (object $refiner): string => $refiner::class, ZhHansChrono::createStrictConfiguration()->refiners); - $hantParsers = array_map(fn (object $parser): string => $parser::class, ZhHantChrono::createStrictConfiguration()->parsers); - $hantCasualParsers = array_map(fn (object $parser): string => $parser::class, ZhHantChrono::createCasualConfiguration()->parsers); - $hantRefiners = array_map(fn (object $refiner): string => $refiner::class, ZhHantChrono::createStrictConfiguration()->refiners); + $chineseParsers = array_map(fn (object $parser): string => $parser::class, ZhChrono::createStrictConfiguration()->parsers()); + $chineseCasualParsers = array_map(fn (object $parser): string => $parser::class, ZhChrono::createCasualConfiguration()->parsers()); + $chineseRefiners = array_map(fn (object $refiner): string => $refiner::class, ZhChrono::createStrictConfiguration()->refiners()); + $hansParsers = array_map(fn (object $parser): string => $parser::class, ZhHansChrono::createStrictConfiguration()->parsers()); + $hansCasualParsers = array_map(fn (object $parser): string => $parser::class, ZhHansChrono::createCasualConfiguration()->parsers()); + $hansRefiners = array_map(fn (object $refiner): string => $refiner::class, ZhHansChrono::createStrictConfiguration()->refiners()); + $hantParsers = array_map(fn (object $parser): string => $parser::class, ZhHantChrono::createStrictConfiguration()->parsers()); + $hantCasualParsers = array_map(fn (object $parser): string => $parser::class, ZhHantChrono::createCasualConfiguration()->parsers()); + $hantRefiners = array_map(fn (object $refiner): string => $refiner::class, ZhHantChrono::createStrictConfiguration()->refiners()); expect($chineseParsers) ->toContain(IsoFormatParser::class) @@ -470,15 +471,15 @@ }); it('exposes source-shaped casual parser order for German French Finnish Italian Dutch Russian and Ukrainian', function () { - $germanParsers = array_map(fn (object $parser): string => $parser::class, DeChrono::createCasualConfiguration()->parsers); - $germanRefiners = array_map(fn (object $refiner): string => $refiner::class, DeChrono::createCasualConfiguration()->refiners); - $frenchParsers = array_map(fn (object $parser): string => $parser::class, FrChrono::createCasualConfiguration()->parsers); - $frenchRefiners = array_map(fn (object $refiner): string => $refiner::class, FrChrono::createCasualConfiguration()->refiners); - $finnishParsers = array_map(fn (object $parser): string => $parser::class, FiChrono::createCasualConfiguration()->parsers); - $italianParsers = array_map(fn (object $parser): string => $parser::class, ItChrono::createCasualConfiguration()->parsers); - $dutchParsers = array_map(fn (object $parser): string => $parser::class, NlChrono::createCasualConfiguration()->parsers); - $russianParsers = array_map(fn (object $parser): string => $parser::class, RuChrono::createCasualConfiguration()->parsers); - $ukrainianParsers = array_map(fn (object $parser): string => $parser::class, UkChrono::createCasualConfiguration()->parsers); + $germanParsers = array_map(fn (object $parser): string => $parser::class, DeChrono::createCasualConfiguration()->parsers()); + $germanRefiners = array_map(fn (object $refiner): string => $refiner::class, DeChrono::createCasualConfiguration()->refiners()); + $frenchParsers = array_map(fn (object $parser): string => $parser::class, FrChrono::createCasualConfiguration()->parsers()); + $frenchRefiners = array_map(fn (object $refiner): string => $refiner::class, FrChrono::createCasualConfiguration()->refiners()); + $finnishParsers = array_map(fn (object $parser): string => $parser::class, FiChrono::createCasualConfiguration()->parsers()); + $italianParsers = array_map(fn (object $parser): string => $parser::class, ItChrono::createCasualConfiguration()->parsers()); + $dutchParsers = array_map(fn (object $parser): string => $parser::class, NlChrono::createCasualConfiguration()->parsers()); + $russianParsers = array_map(fn (object $parser): string => $parser::class, RuChrono::createCasualConfiguration()->parsers()); + $ukrainianParsers = array_map(fn (object $parser): string => $parser::class, UkChrono::createCasualConfiguration()->parsers()); expect(array_slice($germanParsers, 0, 3))->toBe([ DeTimeUnitRelativeFormatParser::class, @@ -687,6 +688,36 @@ public function parse(string $text, Reference $reference, Options $options): arr ->toBe(['foo', 'foobar']); }); +it('builds parser and refiner configurations with a fluent pipeline API', function () { + $slashDateParser = new EnSlashDateParser; + $relativeParser = new EnTimeUnitCasualRelativeFormatParser; + $overlapRemoval = new OverlapRemovalRefiner; + $forwardDate = new ForwardDateRefiner; + + $configuration = Configuration::make() + ->addParser($slashDateParser) + ->prependParser($relativeParser) + ->addRefiner($overlapRemoval) + ->prependRefiner($forwardDate); + + expect($configuration->parsers())->toBe([$relativeParser, $slashDateParser]) + ->and($configuration->refiners())->toBe([$forwardDate, $overlapRemoval]) + ->and($configuration->hasParser(EnSlashDateParser::class))->toBeTrue() + ->and($configuration->hasParser(EnTimeUnitCasualRelativeFormatParser::class))->toBeTrue() + ->and($configuration->hasRefiner(OverlapRemovalRefiner::class))->toBeTrue() + ->and($configuration->hasRefiner(ForwardDateRefiner::class))->toBeTrue(); + + $withoutSlashDateParser = $configuration->removeParser(EnSlashDateParser::class); + $withoutForwardDateRefiner = $configuration->removeRefiner(ForwardDateRefiner::class); + + expect($withoutSlashDateParser->parsers())->toBe([$relativeParser]) + ->and($withoutSlashDateParser->hasParser(EnSlashDateParser::class))->toBeFalse() + ->and($withoutForwardDateRefiner->refiners())->toBe([$overlapRemoval]) + ->and($withoutForwardDateRefiner->hasRefiner(ForwardDateRefiner::class))->toBeFalse() + ->and($configuration->parsers())->toBe([$relativeParser, $slashDateParser]) + ->and($configuration->refiners())->toBe([$forwardDate, $overlapRemoval]); +}); + it('clones parser and refiner configuration like upstream chrono instances', function () { $christmas = new class implements Parser {