diff --git a/README.md b/README.md index 7b408ca..1409bfd 100644 --- a/README.md +++ b/README.md @@ -33,14 +33,9 @@ use Superscript\Schema\Types\NumberType; use Superscript\Schema\Sources\StaticSource; use Superscript\Schema\Sources\ValueDefinition; use Superscript\Schema\Resolvers\DelegatingResolver; -use Superscript\Schema\Resolvers\StaticResolver; -use Superscript\Schema\Resolvers\ValueResolver; -// Create a resolver with basic capabilities -$resolver = new DelegatingResolver([ - StaticResolver::class, - ValueResolver::class, -]); +// Create a resolver +$resolver = new DelegatingResolver(); // Transform a string to a number $source = new ValueDefinition( @@ -62,15 +57,9 @@ use Superscript\Schema\Sources\StaticSource; use Superscript\Schema\Sources\SymbolSource; use Superscript\Schema\SymbolRegistry; use Superscript\Schema\Resolvers\DelegatingResolver; -use Superscript\Schema\Resolvers\InfixResolver; -use Superscript\Schema\Resolvers\SymbolResolver; - -// Set up resolver with symbol support -$resolver = new DelegatingResolver([ - StaticResolver::class, - InfixResolver::class, - SymbolResolver::class, -]); + +// Set up resolver +$resolver = new DelegatingResolver(); // Register symbols $resolver->instance(SymbolRegistry::class, new SymbolRegistry([ @@ -235,15 +224,14 @@ new SymbolRegistry([ - Create clear separation between different symbol contexts - Improve code maintainability with logical grouping -### Resolvers +### Resolver Architecture -Resolvers handle the evaluation of sources: +The library uses a unified resolver pattern where each source knows how to resolve itself: -- **StaticResolver**: Resolves static values -- **ValueResolver**: Applies type coercion using the `coerce()` method -- **InfixResolver**: Evaluates binary expressions -- **SymbolResolver**: Looks up named symbols -- **DelegatingResolver**: Chains multiple resolvers together +- **DelegatingResolver**: The main resolver that delegates to source-specific resolution logic +- Each source implements a `resolver()` method that returns a closure for resolving that source type +- Sources can accept dependencies (like SymbolRegistry) via the resolver's dependency injection container +- Custom resolution logic can be registered using `resolveUsing()` to override default behavior ### Operators @@ -307,30 +295,48 @@ class EmailType implements Type } ``` -### Custom Resolvers +### Custom Source Resolution -Create specialized resolvers for specific data sources: +You can create custom resolution logic for specific source types: ```php query($this->query); + return Ok(Some($result)); + }; } } + +// Register the database connection +$resolver = new DelegatingResolver(); +$resolver->instance(DatabaseConnection::class, $connection); + +// Or override resolution for a specific source class +$resolver->resolveUsing(DatabaseSource::class, function (DatabaseSource $source, DatabaseConnection $db) { + $result = $db->query($source->query); + return Ok(Some($result)); +}); + +// Resolve the source +$source = new DatabaseSource('SELECT * FROM users'); +$result = $resolver->resolve($source); ``` ## Development @@ -366,8 +372,8 @@ composer test:infection # Mutation testing The library follows several design patterns: -- **Strategy Pattern**: Different resolvers for different source types -- **Chain of Responsibility**: DelegatingResolver chains multiple resolvers +- **Strategy Pattern**: Each source type implements its own resolution strategy via the `resolver()` method +- **Dependency Injection**: DelegatingResolver uses a container to provide dependencies to source resolvers - **Factory Pattern**: Type system for creating appropriate transformations - **Functional Programming**: Extensive use of Result and Option monads diff --git a/src/Resolvers/DelegatingResolver.php b/src/Resolvers/DelegatingResolver.php index cf2d24e..d5daddb 100644 --- a/src/Resolvers/DelegatingResolver.php +++ b/src/Resolvers/DelegatingResolver.php @@ -4,28 +4,26 @@ namespace Superscript\Schema\Resolvers; +use Closure; use Illuminate\Container\Container; -use RuntimeException; use Superscript\Schema\Source; use Superscript\Monads\Option\Option; use Superscript\Monads\Result\Result; use Throwable; -final readonly class DelegatingResolver implements Resolver +final class DelegatingResolver implements Resolver { - protected Container $container; + private Container $container; /** - * @param array, class-string> $resolverMap + * @var array, Throwable>> */ - public function __construct(public array $resolverMap = []) + private array $resolveUsing = []; + + public function __construct() { $this->container = new Container(); $this->container->instance(Resolver::class, $this); - - foreach ($this->resolverMap as $resolver) { - $this->container->bind($resolver, $resolver); - } } /** @@ -36,17 +34,27 @@ public function instance(string $key, mixed $concrete): void $this->container->instance($key, $concrete); } + public function resolveUsing(string $source, Closure $resolver): self + { + $this->resolveUsing[$source] = $resolver; + + return $this; + } + /** * @return Result, Throwable> */ public function resolve(Source $source): Result { - $sourceClass = get_class($source); - - if (isset($this->resolverMap[$sourceClass]) && $this->container->has($this->resolverMap[$sourceClass])) { - return $this->container->make($this->resolverMap[$sourceClass])->resolve($source); - } + /** @var Result, Throwable> */ + return $this->container->call($this->getResolver($source)); + } - throw new RuntimeException("No resolver found for source of type " . $sourceClass); + /** + * @return Closure(never, never, never, never, never, never, never): Result, Throwable> + */ + private function getResolver(Source $source): Closure + { + return $this->resolveUsing[$source::class] ?? $source->resolver(); } } diff --git a/src/Resolvers/InfixResolver.php b/src/Resolvers/InfixResolver.php deleted file mode 100644 index d784bc6..0000000 --- a/src/Resolvers/InfixResolver.php +++ /dev/null @@ -1,42 +0,0 @@ - - */ -final readonly class InfixResolver implements Resolver -{ - public function __construct( - public Resolver $resolver, - ) {} - - public function resolve(Source $source): Result - { - return $this->resolver->resolve($source->left) - ->andThen(fn(Option $left) => $this->resolver->resolve($source->right)->map(fn(Option $right) => [$left, $right])) - ->map(function (array $option) use ($source) { - [$left, $right] = $option; - - $result = $this->getOperatorOverloader()->evaluate($left->unwrapOr(null), $right->unwrapOr(null), $source->operator); - return Option::from($result); - }); - } - - private function getOperatorOverloader(): OperatorOverloader - { - return new OverloaderManager([ - new DefaultOverloader(), - ]); - } -} diff --git a/src/Resolvers/Resolver.php b/src/Resolvers/Resolver.php index 265f495..aaf1792 100644 --- a/src/Resolvers/Resolver.php +++ b/src/Resolvers/Resolver.php @@ -9,13 +9,9 @@ use Superscript\Monads\Result\Result; use Throwable; -/** - * @template T of Source = Source - */ interface Resolver { /** - * @phpstan-param T $source * @return Result, Throwable> */ public function resolve(Source $source): Result; diff --git a/src/Resolvers/StaticResolver.php b/src/Resolvers/StaticResolver.php deleted file mode 100644 index 0df7a02..0000000 --- a/src/Resolvers/StaticResolver.php +++ /dev/null @@ -1,24 +0,0 @@ - - */ -final readonly class StaticResolver implements Resolver -{ - public function resolve(Source $source): Result - { - return Ok(is_null($source->value) ? None() : Some($source->value)); - } -} diff --git a/src/Resolvers/SymbolResolver.php b/src/Resolvers/SymbolResolver.php deleted file mode 100644 index 501e5e2..0000000 --- a/src/Resolvers/SymbolResolver.php +++ /dev/null @@ -1,27 +0,0 @@ -symbolRegistry->get($source->name, $source->namespace) - ->andThen(fn(Source $source) => $this->resolver->resolve($source)->transpose())->transpose(); - } -} diff --git a/src/Resolvers/UnaryResolver.php b/src/Resolvers/UnaryResolver.php deleted file mode 100644 index 489ced7..0000000 --- a/src/Resolvers/UnaryResolver.php +++ /dev/null @@ -1,35 +0,0 @@ - - */ -final readonly class UnaryResolver implements Resolver -{ - public function __construct( - public Resolver $resolver, - ) { - } - - public function resolve(Source $source): Result - { - return $this->resolver->resolve($source->operand) - ->andThen(fn(Option $option) => $option - ->map(fn(mixed $value) => match ($source->operator) { - '!' => Ok(!$value), - '-' => num()->matches($value) ? Ok(-$value) : Err(new InvalidArgumentException("not numeric")), - default => Err(new InvalidArgumentException("Unsupported operator: {$source->operator}")), - }) - ->transpose()); - } -} \ No newline at end of file diff --git a/src/Resolvers/ValueResolver.php b/src/Resolvers/ValueResolver.php deleted file mode 100644 index d2e4f22..0000000 --- a/src/Resolvers/ValueResolver.php +++ /dev/null @@ -1,33 +0,0 @@ - - */ -final readonly class ValueResolver implements Resolver -{ - public function __construct( - private Resolver $resolver, - ) {} - - /** - * @return Result, mixed> - */ - public function resolve(Source $source): Result - { - return $this->resolver->resolve($source->source) - ->andThen( - fn(Option $option) => $option - ->andThen(fn(mixed $result) => $source->type->coerce($result)->transpose()) - ->transpose(), - ); - } -} diff --git a/src/Source.php b/src/Source.php index ed06491..7f30b1e 100644 --- a/src/Source.php +++ b/src/Source.php @@ -4,4 +4,15 @@ namespace Superscript\Schema; -interface Source {} +use Closure; +use Superscript\Monads\Option\Option; +use Superscript\Monads\Result\Result; + +/** + * @template T = mixed + */ +interface Source +{ + /** @return Closure(never, never, never, never, never, never, never): Result, \Throwable> */ + public function resolver(): Closure; +} diff --git a/src/Sources/InfixExpression.php b/src/Sources/InfixExpression.php index c316674..4ac6332 100644 --- a/src/Sources/InfixExpression.php +++ b/src/Sources/InfixExpression.php @@ -4,6 +4,12 @@ namespace Superscript\Schema\Sources; +use Closure; +use Superscript\Monads\Option\Option; +use Superscript\Schema\Operators\DefaultOverloader; +use Superscript\Schema\Operators\OperatorOverloader; +use Superscript\Schema\Operators\OverloaderManager; +use Superscript\Schema\Resolvers\Resolver; use Superscript\Schema\Source; final readonly class InfixExpression implements Source @@ -13,4 +19,23 @@ public function __construct( public string $operator, public Source $right, ) {} + + public function resolver(): Closure + { + return fn(Resolver $resolver) => $resolver->resolve($this->left) + ->andThen(fn(Option $left) => $resolver->resolve($this->right)->map(fn(Option $right) => [$left, $right])) + ->map(function (array $option) { + [$left, $right] = $option; + + $result = $this->getOperatorOverloader()->evaluate($left->unwrapOr(null), $right->unwrapOr(null), $this->operator); + return Option::from($result); + }); + } + + private function getOperatorOverloader(): OperatorOverloader + { + return new OverloaderManager([ + new DefaultOverloader(), + ]); + } } diff --git a/src/Sources/StaticSource.php b/src/Sources/StaticSource.php index d4c03e3..c00797e 100644 --- a/src/Sources/StaticSource.php +++ b/src/Sources/StaticSource.php @@ -4,11 +4,27 @@ namespace Superscript\Schema\Sources; +use Closure; +use Superscript\Monads\Option\Option; use Superscript\Schema\Source; +use function Superscript\Monads\Result\Ok; + +/** + * @template T + * @implements Source + */ final readonly class StaticSource implements Source { + /** + * @param T $value + */ public function __construct( public mixed $value, ) {} + + public function resolver(): Closure + { + return fn() => Ok(Option::from($this->value)); + } } diff --git a/src/Sources/SymbolSource.php b/src/Sources/SymbolSource.php index 82bf846..fabd86d 100644 --- a/src/Sources/SymbolSource.php +++ b/src/Sources/SymbolSource.php @@ -4,7 +4,10 @@ namespace Superscript\Schema\Sources; +use Closure; +use Superscript\Schema\Resolvers\Resolver; use Superscript\Schema\Source; +use Superscript\Schema\SymbolRegistry; final readonly class SymbolSource implements Source { @@ -12,4 +15,10 @@ public function __construct( public string $name, public ?string $namespace = null, ) {} + + public function resolver(): Closure + { + return fn(SymbolRegistry $registry, Resolver $resolver) => $registry->get($this->name, $this->namespace) + ->andThen(fn(Source $source) => $resolver->resolve($source)->transpose())->transpose(); + } } diff --git a/src/Sources/UnaryExpression.php b/src/Sources/UnaryExpression.php index ff326d0..81ec473 100644 --- a/src/Sources/UnaryExpression.php +++ b/src/Sources/UnaryExpression.php @@ -1,14 +1,35 @@ $resolver->resolve($this->operand) + ->andThen(fn(Option $option) => $option + ->map(fn(mixed $value) => match ($this->operator) { + '!' => Ok(!$value), + '-' => num()->matches($value) ? Ok(-$value) : Err(new InvalidArgumentException("not numeric")), + default => Err(new InvalidArgumentException("Unsupported operator: {$this->operator}")), + }) + ->transpose()); } -} \ No newline at end of file +} diff --git a/src/Sources/ValueDefinition.php b/src/Sources/ValueDefinition.php index 7507cea..4491a68 100644 --- a/src/Sources/ValueDefinition.php +++ b/src/Sources/ValueDefinition.php @@ -4,13 +4,33 @@ namespace Superscript\Schema\Sources; +use Closure; +use Superscript\Monads\Option\Option; +use Superscript\Schema\Resolvers\Resolver; use Superscript\Schema\Source; use Superscript\Schema\Types\Type; +/** + * @template T + * @implements Source + */ final readonly class ValueDefinition implements Source { + /** + * @param Type $type + */ public function __construct( public Type $type, public Source $source, ) {} + + public function resolver(): Closure + { + return fn(Resolver $resolver) => $resolver->resolve($this->source) + ->andThen( + fn(Option $option) => $option + ->andThen(fn(mixed $result) => $this->type->coerce($result)->transpose()) + ->transpose(), + ); + } } diff --git a/tests/KitchenSink/KitchenSinkTest.php b/tests/KitchenSink/KitchenSinkTest.php index b432e07..95f1ad1 100644 --- a/tests/KitchenSink/KitchenSinkTest.php +++ b/tests/KitchenSink/KitchenSinkTest.php @@ -8,10 +8,6 @@ use PHPUnit\Framework\Attributes\Test; use PHPUnit\Framework\TestCase; use Superscript\Schema\Resolvers\DelegatingResolver; -use Superscript\Schema\Resolvers\InfixResolver; -use Superscript\Schema\Resolvers\StaticResolver; -use Superscript\Schema\Resolvers\SymbolResolver; -use Superscript\Schema\Resolvers\ValueResolver; use Superscript\Schema\Sources\InfixExpression; use Superscript\Schema\Sources\StaticSource; use Superscript\Schema\Sources\SymbolSource; @@ -25,12 +21,7 @@ class KitchenSinkTest extends TestCase #[Test] public function something_complex(): void { - $resolver = new DelegatingResolver([ - StaticSource::class => StaticResolver::class, - InfixExpression::class => InfixResolver::class, - ValueDefinition::class => ValueResolver::class, - SymbolSource::class => SymbolResolver::class, - ]); + $resolver = new DelegatingResolver(); $resolver->instance(SymbolRegistry::class, new SymbolRegistry([ 'A' => new StaticSource(2), @@ -56,10 +47,7 @@ public function something_complex(): void #[Test] public function transforming_a_value(): void { - $resolver = new DelegatingResolver([ - StaticSource::class => StaticResolver::class, - ValueDefinition::class => ValueResolver::class, - ]); + $resolver = new DelegatingResolver(); $source = new ValueDefinition( type: new NumberType(), diff --git a/tests/Resolvers/DelegatingResolverTest.php b/tests/Resolvers/DelegatingResolverTest.php index a5de927..bc3d0f4 100644 --- a/tests/Resolvers/DelegatingResolverTest.php +++ b/tests/Resolvers/DelegatingResolverTest.php @@ -4,23 +4,23 @@ namespace Superscript\Schema\Tests\Resolvers; +use Closure; use PHPUnit\Framework\Attributes\CoversClass; use PHPUnit\Framework\Attributes\Test; use PHPUnit\Framework\Attributes\UsesClass; use PHPUnit\Framework\TestCase; use Superscript\Schema\Resolvers\DelegatingResolver; -use Superscript\Schema\Resolvers\StaticResolver; -use Superscript\Schema\Resolvers\ValueResolver; +use Superscript\Schema\Source; use Superscript\Schema\Sources\StaticSource; use Superscript\Schema\Sources\ValueDefinition; use Superscript\Schema\Tests\Resolvers\Fixtures\Dependency; -use Superscript\Schema\Tests\Resolvers\Fixtures\ResolverWithDependency; use Superscript\Schema\Types\NumberType; +use function Superscript\Monads\Option\Some; +use function Superscript\Monads\Result\Ok; + #[CoversClass(DelegatingResolver::class)] #[UsesClass(StaticSource::class)] -#[UsesClass(StaticResolver::class)] -#[UsesClass(ValueResolver::class)] #[UsesClass(ValueDefinition::class)] #[UsesClass(NumberType::class)] class DelegatingResolverTest extends TestCase @@ -28,45 +28,47 @@ class DelegatingResolverTest extends TestCase #[Test] public function it_can_resolve_by_delegating_to_another_resolver(): void { - $resolver = new DelegatingResolver([ - StaticSource::class => StaticResolver::class, - ]); + $resolver = new DelegatingResolver(); $result = $resolver->resolve(new StaticSource('Hello world!')); $this->assertEquals('Hello world!', $result->unwrap()->unwrap()); } #[Test] - public function it_supports_resolvers_depending_on_other_resolvers(): void + public function it_can_override_source_resolution_with_resolve_using(): void { - $resolver = new DelegatingResolver([ - StaticSource::class => StaticResolver::class, - ValueDefinition::class => ValueResolver::class, - ]); + $resolver = new DelegatingResolver(); + $resolver->resolveUsing(StaticSource::class, static fn() => Ok(Some('Overridden'))); - $result = $resolver->resolve(new ValueDefinition(new NumberType(), new StaticSource('42'))); - $this->assertEquals(42, $result->unwrap()->unwrap()); + $result = $resolver->resolve(new StaticSource('Original')); + + $this->assertEquals('Overridden', $result->unwrap()->unwrap()); } #[Test] - public function it_supports_resolvers_with_dependencies(): void + public function it_supports_resolvers_depending_on_other_resolvers(): void { - $resolver = new DelegatingResolver([ - StaticSource::class => ResolverWithDependency::class, - ]); - - $resolver->instance(Dependency::class, new Dependency('hello')); + $resolver = new DelegatingResolver(); - $this->assertEquals('hello', $resolver->resolve(new StaticSource(42))->unwrap()->unwrap()); + $result = $resolver->resolve(new ValueDefinition(new NumberType(), new StaticSource('42'))); + $this->assertEquals(42, $result->unwrap()->unwrap()); } #[Test] - public function it_throws_an_exception_if_no_resolver_can_handle_the_source(): void + public function it_supports_resolvers_with_dependencies(): void { - $this->expectException(\RuntimeException::class); - $this->expectExceptionMessage('No resolver found for source of type ' . StaticSource::class); + $resolver = new DelegatingResolver(); + $resolver->instance(Dependency::class, new class implements Dependency { + public string $value = 'default'; + }); + + $source = new class implements Source { + public function resolver(): Closure + { + return fn(Dependency $dependency) => Ok(Some($dependency->value)); + } + }; - $resolver = new DelegatingResolver([]); - $resolver->resolve(new StaticSource('Hello world!')); + $this->assertEquals('default', $resolver->resolve($source)->unwrap()->unwrap()); } } diff --git a/tests/Resolvers/Fixtures/Dependency.php b/tests/Resolvers/Fixtures/Dependency.php index fcca260..137c51e 100644 --- a/tests/Resolvers/Fixtures/Dependency.php +++ b/tests/Resolvers/Fixtures/Dependency.php @@ -4,9 +4,4 @@ namespace Superscript\Schema\Tests\Resolvers\Fixtures; -final readonly class Dependency -{ - public function __construct( - public string $info, - ) {} -} +interface Dependency {} diff --git a/tests/Resolvers/Fixtures/ResolverWithDependency.php b/tests/Resolvers/Fixtures/ResolverWithDependency.php deleted file mode 100644 index 7aa30ea..0000000 --- a/tests/Resolvers/Fixtures/ResolverWithDependency.php +++ /dev/null @@ -1,24 +0,0 @@ -dependency->info)); - } -} diff --git a/tests/Resolvers/InfixResolverTest.php b/tests/Resolvers/InfixResolverTest.php index c18a4f5..4a94afb 100644 --- a/tests/Resolvers/InfixResolverTest.php +++ b/tests/Resolvers/InfixResolverTest.php @@ -11,15 +11,12 @@ use Superscript\Schema\Operators\BinaryOverloader; use Superscript\Schema\Operators\DefaultOverloader; use Superscript\Schema\Operators\OverloaderManager; -use Superscript\Schema\Resolvers\InfixResolver; -use Superscript\Schema\Resolvers\StaticResolver; -use Superscript\Schema\Source; +use Superscript\Schema\Resolvers\DelegatingResolver; use Superscript\Schema\Sources\InfixExpression; use Superscript\Schema\Sources\StaticSource; #[CoversClass(InfixExpression::class)] -#[CoversClass(InfixResolver::class)] -#[UsesClass(StaticResolver::class)] +#[UsesClass(DelegatingResolver::class)] #[UsesClass(StaticSource::class)] #[UsesClass(DefaultOverloader::class)] #[UsesClass(OverloaderManager::class)] @@ -27,14 +24,15 @@ class InfixResolverTest extends TestCase { #[Test] - public function it_can_resolve_an_infix_expression() + public function it_can_resolve_an_infix_expression(): void { - $resolver = new InfixResolver(new StaticResolver()); $source = new InfixExpression( left: new StaticSource(1), operator: '+', right: new StaticSource(2), ); - $this->assertEquals(3, $resolver->resolve($source)->unwrap()->unwrap()); + + $resolver = $source->resolver(); + $this->assertEquals(3, $resolver(new DelegatingResolver())->unwrap()->unwrap()); } } diff --git a/tests/Resolvers/StaticResolverTest.php b/tests/Resolvers/StaticResolverTest.php index 3bf40c8..1cb7e12 100644 --- a/tests/Resolvers/StaticResolverTest.php +++ b/tests/Resolvers/StaticResolverTest.php @@ -7,19 +7,16 @@ use PHPUnit\Framework\Attributes\CoversClass; use PHPUnit\Framework\Attributes\Test; use PHPUnit\Framework\TestCase; -use Superscript\Schema\Resolvers\StaticResolver; -use Superscript\Schema\Source; use Superscript\Schema\Sources\StaticSource; -#[CoversClass(StaticResolver::class)] #[CoversClass(StaticSource::class)] class StaticResolverTest extends TestCase { #[Test] public function it_resolves(): void { - $resolver = new StaticResolver(); $source = new StaticSource('Hello world!'); - $this->assertEquals('Hello world!', $resolver->resolve($source)->unwrap()->unwrap()); + $resolver = $source->resolver(); + $this->assertEquals('Hello world!', $resolver()->unwrap()->unwrap()); } } diff --git a/tests/Resolvers/SymbolResolverTest.php b/tests/Resolvers/SymbolResolverTest.php index 2e5a0f6..ce7c497 100644 --- a/tests/Resolvers/SymbolResolverTest.php +++ b/tests/Resolvers/SymbolResolverTest.php @@ -8,29 +8,29 @@ use PHPUnit\Framework\Attributes\Test; use PHPUnit\Framework\Attributes\UsesClass; use PHPUnit\Framework\TestCase; -use Superscript\Schema\Resolvers\StaticResolver; -use Superscript\Schema\Resolvers\SymbolResolver; -use Superscript\Schema\Source; +use Superscript\Schema\Resolvers\DelegatingResolver; use Superscript\Schema\Sources\StaticSource; use Superscript\Schema\Sources\SymbolSource; use Superscript\Schema\SymbolRegistry; use Superscript\Monads\Result\Result; -#[CoversClass(SymbolResolver::class)] #[CoversClass(SymbolSource::class)] #[CoversClass(SymbolRegistry::class)] -#[UsesClass(StaticResolver::class)] +#[UsesClass(DelegatingResolver::class)] #[UsesClass(StaticSource::class)] class SymbolResolverTest extends TestCase { #[Test] public function it_can_resolve_a_value(): void { - $resolver = new SymbolResolver(new StaticResolver(), new SymbolRegistry([ + $registry = new SymbolRegistry([ 'A' => new StaticSource(2), - ])); + ]); + $source = new SymbolSource('A'); - $result = $resolver->resolve($source); + $resolver = $source->resolver(); + + $result = $resolver(registry: $registry, resolver: new DelegatingResolver()); $this->assertInstanceOf(Result::class, $result); $this->assertEquals(2, $result->unwrap()->unwrap()); } @@ -38,31 +38,50 @@ public function it_can_resolve_a_value(): void #[Test] public function it_can_resolve_a_namespaced_symbol(): void { - $resolver = new SymbolResolver(new StaticResolver(), new SymbolRegistry([ + $registry = new SymbolRegistry([ 'math' => [ - 'pi' => new StaticSource(3.14), - 'e' => new StaticSource(2.71), + 'pi' => new StaticSource(3.14159), + 'e' => new StaticSource(2.71828), ], - ])); + ]); $source = new SymbolSource('pi', 'math'); - $result = $resolver->resolve($source); + $resolver = $source->resolver(); + + $result = $resolver(registry: $registry, resolver: new DelegatingResolver()); + $this->assertInstanceOf(Result::class, $result); + $this->assertEquals(3.14159, $result->unwrap()->unwrap()); + } + + #[Test] + public function it_returns_none_for_nonexistent_symbol(): void + { + $registry = new SymbolRegistry([ + 'A' => new StaticSource(2), + ]); + + $source = new SymbolSource('B'); + $resolver = $source->resolver(); + + $result = $resolver(registry: $registry, resolver: new DelegatingResolver()); $this->assertInstanceOf(Result::class, $result); - $this->assertEquals(3.14, $result->unwrap()->unwrap()); + $this->assertTrue($result->unwrap()->isNone()); } #[Test] public function it_returns_none_for_nonexistent_namespaced_symbol(): void { - $resolver = new SymbolResolver(new StaticResolver(), new SymbolRegistry([ + $registry = new SymbolRegistry([ 'math' => [ - 'pi' => new StaticSource(3.14), + 'pi' => new StaticSource(3.14159), ], - ])); + ]); // Wrong namespace $source = new SymbolSource('pi', 'physics'); - $result = $resolver->resolve($source); + $resolver = $source->resolver(); + + $result = $resolver(registry: $registry, resolver: new DelegatingResolver()); $this->assertInstanceOf(Result::class, $result); $this->assertTrue($result->unwrap()->isNone()); } @@ -70,34 +89,23 @@ public function it_returns_none_for_nonexistent_namespaced_symbol(): void #[Test] public function it_distinguishes_between_namespaced_and_non_namespaced_symbols(): void { - $resolver = new SymbolResolver(new StaticResolver(), new SymbolRegistry([ - 'value' => new StaticSource(10), + $registry = new SymbolRegistry([ + 'value' => new StaticSource(1), 'ns' => [ - 'value' => new StaticSource(20), + 'value' => new StaticSource(2), ], - ])); + ]); // Resolve without namespace $source = new SymbolSource('value'); - $result = $resolver->resolve($source); - $this->assertEquals(10, $result->unwrap()->unwrap()); + $resolver = $source->resolver(); + $result = $resolver(registry: $registry, resolver: new DelegatingResolver()); + $this->assertEquals(1, $result->unwrap()->unwrap()); // Resolve with namespace $source = new SymbolSource('value', 'ns'); - $result = $resolver->resolve($source); - $this->assertEquals(20, $result->unwrap()->unwrap()); - } - - #[Test] - public function it_preserves_backward_compatibility_with_null_namespace(): void - { - $resolver = new SymbolResolver(new StaticResolver(), new SymbolRegistry([ - 'A' => new StaticSource(42), - ])); - - // SymbolSource with null namespace (default) - $source = new SymbolSource('A', null); - $result = $resolver->resolve($source); - $this->assertEquals(42, $result->unwrap()->unwrap()); + $resolver = $source->resolver(); + $result = $resolver(registry: $registry, resolver: new DelegatingResolver()); + $this->assertEquals(2, $result->unwrap()->unwrap()); } } diff --git a/tests/Resolvers/UnaryResolverTest.php b/tests/Resolvers/UnaryResolverTest.php index 4040445..cd6e184 100644 --- a/tests/Resolvers/UnaryResolverTest.php +++ b/tests/Resolvers/UnaryResolverTest.php @@ -1,60 +1,55 @@ assertEquals(false, $resolver->resolve($source)->unwrap()->unwrap()); + $resolver = $source->resolver(); + $this->assertEquals(false, $resolver(new DelegatingResolver())->unwrap()->unwrap()); } #[Test] public function it_can_resolve_a_unary_minus_expression(): void { - $resolver = new UnaryResolver(new StaticResolver()); $source = new UnaryExpression( operator: '-', - operand: new StaticSource(42) + operand: new StaticSource(42), ); - $this->assertEquals(-42, $resolver->resolve($source)->unwrap()->unwrap()); + $resolver = $source->resolver(); + $this->assertEquals(-42, $resolver(new DelegatingResolver())->unwrap()->unwrap()); } #[Test] public function it_returns_err_for_unsupported_operators(): void { - $resolver = new UnaryResolver(new StaticResolver()); $source = new UnaryExpression( operator: '+', - operand: new StaticSource(42) + operand: new StaticSource(42), ); - $this->assertTrue($resolver->resolve($source)->isErr()); + $resolver = $source->resolver(); + $this->assertTrue($resolver(new DelegatingResolver())->isErr()); } } diff --git a/tests/Resolvers/ValueResolverTest.php b/tests/Resolvers/ValueResolverTest.php index 58a13ea..99538dd 100644 --- a/tests/Resolvers/ValueResolverTest.php +++ b/tests/Resolvers/ValueResolverTest.php @@ -8,35 +8,25 @@ use PHPUnit\Framework\Attributes\Test; use PHPUnit\Framework\Attributes\UsesClass; use PHPUnit\Framework\TestCase; -use Superscript\Schema\Resolvers\Resolver; -use Superscript\Schema\Source; +use Superscript\Schema\Resolvers\DelegatingResolver; +use Superscript\Schema\Sources\StaticSource; use Superscript\Schema\Sources\ValueDefinition; -use Superscript\Schema\Types\NumberType; use Superscript\Schema\Types\StringType; -use Superscript\Schema\Resolvers\ValueResolver; -use Superscript\Monads\Result\Result; -use function Superscript\Monads\Option\Some; -use function Superscript\Monads\Result\Ok; - -#[CoversClass(ValueResolver::class)] #[CoversClass(ValueDefinition::class)] +#[UsesClass(DelegatingResolver::class)] +#[UsesClass(StaticSource::class)] #[UsesClass(StringType::class)] class ValueResolverTest extends TestCase { #[Test] public function it_can_resolve_a_value() { - $resolver = new ValueResolver(new class implements Resolver { - public function resolve(Source $source): Result - { - return Ok(Some('Hello, World!')); - } - }); - $source = new ValueDefinition(new StringType(), new class implements Source {}); + $source = new ValueDefinition(new StringType(), new StaticSource('Hello, World!')); + + $resolver = $source->resolver(); - $result = $resolver->resolve($source); - $this->assertInstanceOf(Result::class, $result); + $result = $resolver(new DelegatingResolver()); $this->assertEquals('Hello, World!', $result->unwrap()->unwrap()); } }