diff --git a/src/Describable.php b/src/Describable.php new file mode 100644 index 0000000..49d4ded --- /dev/null +++ b/src/Describable.php @@ -0,0 +1,14 @@ +describeOperand($this->left); + $right = $this->describeOperand($this->right); + + return sprintf('%s %s %s', $left, $this->operator, $right); + } + + private function describeOperand(Source $source): string + { + $description = $source instanceof Describable + ? $source->describe() + : (new \ReflectionClass($source))->getShortName(); + + if ($source instanceof self) { + return sprintf('(%s)', $description); + } + + return $description; + } } diff --git a/src/Sources/StaticSource.php b/src/Sources/StaticSource.php index 85866c6..d7ea596 100644 --- a/src/Sources/StaticSource.php +++ b/src/Sources/StaticSource.php @@ -4,11 +4,18 @@ namespace Superscript\Axiom\Sources; +use SebastianBergmann\Exporter\Exporter; +use Superscript\Axiom\Describable; use Superscript\Axiom\Source; -final readonly class StaticSource implements Source +final readonly class StaticSource implements Source, Describable { public function __construct( public mixed $value, ) {} + + public function describe(): string + { + return (new Exporter())->shortenedExport($this->value); + } } diff --git a/src/Sources/SymbolSource.php b/src/Sources/SymbolSource.php index f98eb8e..380e7d7 100644 --- a/src/Sources/SymbolSource.php +++ b/src/Sources/SymbolSource.php @@ -4,12 +4,20 @@ namespace Superscript\Axiom\Sources; +use Superscript\Axiom\Describable; use Superscript\Axiom\Source; -final readonly class SymbolSource implements Source +final readonly class SymbolSource implements Source, Describable { public function __construct( public string $name, public ?string $namespace = null, ) {} + + public function describe(): string + { + return $this->namespace !== null + ? sprintf('%s.%s', $this->namespace, $this->name) + : $this->name; + } } diff --git a/src/Sources/TypeDefinition.php b/src/Sources/TypeDefinition.php index fcb4832..9290386 100644 --- a/src/Sources/TypeDefinition.php +++ b/src/Sources/TypeDefinition.php @@ -4,13 +4,25 @@ namespace Superscript\Axiom\Sources; +use Superscript\Axiom\Describable; use Superscript\Axiom\Source; use Superscript\Axiom\Types\Type; -final readonly class TypeDefinition implements Source +final readonly class TypeDefinition implements Source, Describable { public function __construct( public Type $type, public Source $source, ) {} + + public function describe(): string + { + $shortName = (new \ReflectionClass($this->type))->getShortName(); + $typeName = lcfirst(str_ends_with($shortName, 'Type') ? substr($shortName, 0, -4) : $shortName); + $sourceDescription = $this->source instanceof Describable + ? $this->source->describe() + : (new \ReflectionClass($this->source))->getShortName(); + + return sprintf('%s (as %s)', $sourceDescription, $typeName); + } } diff --git a/src/Sources/UnaryExpression.php b/src/Sources/UnaryExpression.php index 2769d97..618b599 100644 --- a/src/Sources/UnaryExpression.php +++ b/src/Sources/UnaryExpression.php @@ -1,14 +1,25 @@ operand instanceof Describable + ? $this->operand->describe() + : (new \ReflectionClass($this->operand))->getShortName(); + + return sprintf('%s%s', $this->operator, $operand); } -} \ No newline at end of file +} diff --git a/tests/Sources/DescribableTest.php b/tests/Sources/DescribableTest.php new file mode 100644 index 0000000..6af7ced --- /dev/null +++ b/tests/Sources/DescribableTest.php @@ -0,0 +1,244 @@ +assertSame("'hello'", $source->describe()); + } + + #[Test] + public function static_source_describes_integer_value(): void + { + $source = new StaticSource(42); + $this->assertSame('42', $source->describe()); + } + + #[Test] + public function static_source_describes_null_value(): void + { + $source = new StaticSource(null); + $this->assertSame('null', $source->describe()); + } + + #[Test] + public function static_source_describes_boolean_value(): void + { + $source = new StaticSource(true); + $this->assertSame('true', $source->describe()); + } + + #[Test] + public function symbol_source_describes_name(): void + { + $source = new SymbolSource('price'); + $this->assertSame('price', $source->describe()); + } + + #[Test] + public function symbol_source_describes_namespaced_name(): void + { + $source = new SymbolSource('pi', 'math'); + $this->assertSame('math.pi', $source->describe()); + } + + #[Test] + public function type_definition_describes_as_type(): void + { + $source = new TypeDefinition(new NumberType(), new SymbolSource('price')); + $this->assertSame('price (as number)', $source->describe()); + } + + #[Test] + public function type_definition_describes_string_type(): void + { + $source = new TypeDefinition(new StringType(), new StaticSource('hello')); + $this->assertSame("'hello' (as string)", $source->describe()); + } + + #[Test] + public function type_definition_describes_boolean_type(): void + { + $source = new TypeDefinition(new BooleanType(), new SymbolSource('active')); + $this->assertSame('active (as boolean)', $source->describe()); + } + + #[Test] + public function type_definition_describes_list_type(): void + { + $source = new TypeDefinition(new ListType(new NumberType()), new SymbolSource('values')); + $this->assertSame('values (as list)', $source->describe()); + } + + #[Test] + public function type_definition_with_non_describable_source_falls_back_to_class_name(): void + { + $anonymous = new class implements Source {}; + + $source = new TypeDefinition(new NumberType(), $anonymous); + + $description = $source->describe(); + $this->assertStringEndsWith('(as number)', $description); + } + + #[Test] + public function infix_expression_describes_operation(): void + { + $source = new InfixExpression( + new StaticSource(1), + '+', + new StaticSource(2), + ); + $this->assertSame('1 + 2', $source->describe()); + } + + #[Test] + public function infix_expression_describes_comparison(): void + { + $source = new InfixExpression( + new SymbolSource('age'), + '>=', + new StaticSource(18), + ); + $this->assertSame('age >= 18', $source->describe()); + } + + #[Test] + public function infix_expression_describes_custom_operator(): void + { + $source = new InfixExpression( + new SymbolSource('tags'), + 'has', + new StaticSource('featured'), + ); + $this->assertSame("tags has 'featured'", $source->describe()); + } + + #[Test] + public function infix_expression_wraps_nested_infix_in_parentheses(): void + { + $source = new InfixExpression( + new SymbolSource('price'), + '*', + new InfixExpression( + new StaticSource(1), + '-', + new SymbolSource('discount'), + ), + ); + $this->assertSame( + 'price * (1 - discount)', + $source->describe(), + ); + } + + #[Test] + public function infix_with_non_describable_left_falls_back_to_class_name(): void + { + $anonymous = new class implements Source {}; + + $source = new InfixExpression( + $anonymous, + '+', + new StaticSource(1), + ); + + $description = $source->describe(); + $this->assertStringEndsWith('+ 1', $description); + } + + #[Test] + public function infix_with_non_describable_right_falls_back_to_class_name(): void + { + $anonymous = new class implements Source {}; + + $source = new InfixExpression( + new StaticSource(1), + '+', + $anonymous, + ); + + $description = $source->describe(); + $this->assertStringStartsWith('1 + ', $description); + } + + #[Test] + public function unary_expression_describes_negation(): void + { + $source = new UnaryExpression('!', new SymbolSource('active')); + $this->assertSame('!active', $source->describe()); + } + + #[Test] + public function unary_expression_describes_negative(): void + { + $source = new UnaryExpression('-', new StaticSource(5)); + $this->assertSame('-5', $source->describe()); + } + + #[Test] + public function unary_with_non_describable_source_falls_back_to_class_name(): void + { + $anonymous = new class implements Source {}; + + $source = new UnaryExpression('!', $anonymous); + + $description = $source->describe(); + $this->assertStringStartsWith('!', $description); + } + + #[Test] + public function complex_nested_expression(): void + { + $source = new InfixExpression( + new TypeDefinition( + new NumberType(), + new SymbolSource('price'), + ), + '*', + new InfixExpression( + new StaticSource(1), + '-', + new TypeDefinition( + new NumberType(), + new SymbolSource('discount', 'rates'), + ), + ), + ); + + $this->assertSame( + 'price (as number) * (1 - rates.discount (as number))', + $source->describe(), + ); + } +}