From 7895d855041f7f0837239edef1150f94276ae779 Mon Sep 17 00:00:00 2001 From: Bogdan Padalko Date: Sat, 4 Nov 2017 19:11:54 +0200 Subject: [PATCH 01/14] Add support for types groupping and arrays shortcut, closes #6 --- src/Extractors/ExtractorDefinitionBuilder.php | 133 ++++++++++-- src/Specs/Builder/ParameterSpecBuilder.php | 2 +- .../ExtractorDefinitionBuilderTest.php | 199 +++++++++++++++++- 3 files changed, 318 insertions(+), 16 deletions(-) diff --git a/src/Extractors/ExtractorDefinitionBuilder.php b/src/Extractors/ExtractorDefinitionBuilder.php index f839e15..2ee8745 100644 --- a/src/Extractors/ExtractorDefinitionBuilder.php +++ b/src/Extractors/ExtractorDefinitionBuilder.php @@ -27,7 +27,49 @@ class ExtractorDefinitionBuilder implements ExtractorDefinitionBuilderInterface /** * @var string */ - protected $type_regexp = '/^((?[-_\w]+)(?:\s*\(\s*(?(?-3)*|[\w\\\\]+)\s*\))?(?:\s*\|\s*(?(?-4)))?)$/'; + protected $type_regexp = '/ + ^ + ( + (? + [-_\w]* + ) + (?: + \s* + (? + \( + \s* + (? + (?-4)* + | + [\w\\\\]+ + ) + \s* + \) + ) + )? + (?: + \s* + (?(?:\s*\[\s*\]\s*)+) + )? + (?: + \s* + \| + \s* + (?(?-6)) + )? + ) + $ + /xi'; + + protected $type_regexp2 = '/ + ^ + (?:((\w+\b(?:\(.*\))?(?:\s*\[\s*\])?)(?:\s*\|\s*(?-1))*)) + | + (?:\(\s*(?-2)\s*\)(?:\s*\[\s*\])?) + | + (\[\s*\]) + $ + /xi'; /** * {@inheritdoc} @@ -40,32 +82,53 @@ public function build(string $definition): ExtractorDefinitionInterface throw new ExtractorDefinitionBuilderException('Definition must be non-empty string'); } - if (preg_match($this->type_regexp, $definition, $matches)) { - return $this->buildExtractor($matches['name'], $matches['param'] ?? null, $matches['alt'] ?? null); + try { + if (preg_match($this->type_regexp, $definition, $matches)) { + $extractor = $this->buildExtractor($matches['name'], $matches['param'] ?? '', $matches['alt'] ?? '', $this->getDepth($matches), $this->hasGroups($matches)); + + return $extractor; + } + } catch (ExtractorDefinitionBuilderException $e) { + // We don't care about what specific issue we hit inside, + // for API user it means that the definition is invalid } throw new ExtractorDefinitionBuilderException("Unable to parse definition: '{$definition}'"); } /** - * @param string $name + * @param string $name * @param null|string $param * @param null|string $alt_definitions + * @param int $depth + * @param bool $groups * * @return ExtractorDefinitionInterface * @throws ExtractorDefinitionBuilderException */ - protected function buildExtractor(string $name, ?string $param, ?string $alt_definitions): ExtractorDefinitionInterface + protected function buildExtractor(string $name, string $param, string $alt_definitions, int $depth, bool $groups): ExtractorDefinitionInterface { $next = null; - if ($param && preg_match($this->type_regexp, $param, $matches)) { - $next = $this->buildExtractor($matches['name'], $matches['param'] ?? null, $matches['alt'] ?? null); + if ('' !== $param && preg_match($this->type_regexp, $param, $matches)) { + $next = $this->buildExtractor($matches['name'], $matches['param'] ?? '', $matches['alt'] ?? '', $this->getDepth($matches), $this->hasGroups($matches)); + } + + if ($name) { + $definition = new PlainExtractorDefinition($name, $next); + } else { + $definition = $next; + } + + if ($depth > 0) { + $definition = $this->buildArrayDefinition($definition, $depth, $groups); } - $definition = new PlainExtractorDefinition($name, $next); + if (!$definition) { + throw new ExtractorDefinitionBuilderException('Empty group is not allowed'); + } - if ($alt_definitions) { + if ('' !== $alt_definitions) { $definition = $this->buildVariableDefinition($definition, $alt_definitions); } @@ -74,7 +137,7 @@ protected function buildExtractor(string $name, ?string $param, ?string $alt_def /** * @param PlainExtractorDefinitionInterface $definition - * @param string $alt_definitions + * @param string $alt_definitions * * @return VariableExtractorDefinition * @throws ExtractorDefinitionBuilderException @@ -83,14 +146,15 @@ protected function buildVariableDefinition(PlainExtractorDefinitionInterface $de { $alt = [$definition]; - while ($alt_definitions && preg_match($this->type_regexp, $alt_definitions, $matches)) { + while ('' !== $alt_definitions && preg_match($this->type_regexp, $alt_definitions, $matches)) { // build alt - $alt[] = $this->buildExtractor($matches['name'], $matches['param'] ?? null, null); + $alt[] = $this->buildExtractor($matches['name'], $matches['param'] ?? '', '', $this->getDepth($matches), $this->hasGroups($matches)); - $alt_definitions = $matches['alt'] ?? null; + $alt_definitions = trim($matches['alt'] ?? ''); } - if ($alt_definitions) { + if ('' !== $alt_definitions) { + // UNEXPECTED // this should not be possible, but just in case we will ever get here throw new ExtractorDefinitionBuilderException('Invalid varying definition'); } @@ -98,4 +162,45 @@ protected function buildVariableDefinition(PlainExtractorDefinitionInterface $de return new VariableExtractorDefinition(...$alt); } + /** + * @param null|ExtractorDefinitionInterface $definition + * @param int $depth + * @param bool $groups + * + * @return ExtractorDefinitionInterface + * @throws ExtractorDefinitionBuilderException + */ + protected function buildArrayDefinition(?ExtractorDefinitionInterface $definition, int $depth, bool $groups): ExtractorDefinitionInterface + { + if (!$definition && $groups) { + throw new ExtractorDefinitionBuilderException('Empty group is not allowed'); + } + + while ($depth) { + $depth--; + // arrayed definition + $definition = new PlainExtractorDefinition('[]', $definition); + } + + return $definition; + } + + /** + * @param array $matches + * + * @return int + */ + private function getDepth(array $matches): int + { + if (!isset($matches['arr']) || '' === $matches['arr']) { + return 0; + } + + return substr_count($matches['arr'], '['); + } + + private function hasGroups(array $matches): bool + { + return isset($matches['group']) && '' !== $matches['group']; + } } diff --git a/src/Specs/Builder/ParameterSpecBuilder.php b/src/Specs/Builder/ParameterSpecBuilder.php index b6e2728..6ab1b72 100644 --- a/src/Specs/Builder/ParameterSpecBuilder.php +++ b/src/Specs/Builder/ParameterSpecBuilder.php @@ -56,7 +56,7 @@ class ParameterSpecBuilder implements ParameterSpecBuilderInterface \s* \: \s* - (?(\w+\b(?:\(.*\))?)(?:\s*\|\s*(?-1))*) + (?(\w*(?:\(.*\))?(?:\[\s*\])?)(?:\s*\|\s*(?-1))*) \s* )? $ diff --git a/tests/Extractors/ExtractorDefinitionBuilderTest.php b/tests/Extractors/ExtractorDefinitionBuilderTest.php index 1b73009..17ab877 100644 --- a/tests/Extractors/ExtractorDefinitionBuilderTest.php +++ b/tests/Extractors/ExtractorDefinitionBuilderTest.php @@ -4,11 +4,14 @@ namespace Pinepain\JsSandbox\Tests\Extractors; +use PHPUnit\Framework\TestCase; +use Pinepain\JsSandbox\Extractors\Definition\ExtractorDefinitionInterface; use Pinepain\JsSandbox\Extractors\Definition\PlainExtractorDefinitionInterface; +use Pinepain\JsSandbox\Extractors\Definition\VariableExtractorDefinition; use Pinepain\JsSandbox\Extractors\Definition\VariableExtractorDefinitionInterface; use Pinepain\JsSandbox\Extractors\ExtractorDefinitionBuilder; -use PHPUnit\Framework\TestCase; use Pinepain\JsSandbox\Extractors\ExtractorDefinitionBuilderInterface; +use UnexpectedValueException; class ExtractorDefinitionBuilderTest extends TestCase @@ -41,6 +44,24 @@ public function testBuildingFromInvalidStringShouldThrowException() $this->builder->build('!invalid!'); } + /** + * @expectedException \Pinepain\JsSandbox\Extractors\ExtractorDefinitionBuilderException + * @expectedExceptionMessage Unable to parse definition: '()' + */ + public function testBuildingFromEmptyGroupShouldThrowException() + { + $this->builder->build('()'); + } + + /** + * @expectedException \Pinepain\JsSandbox\Extractors\ExtractorDefinitionBuilderException + * @expectedExceptionMessage Unable to parse definition: '()[]' + */ + public function testBuildingEmptyGroupArrayedDefinition() + { + $this->builder->build('()[]'); + } + public function testBuildingPlainDefinition() { $definition = $this->builder->build('test'); @@ -53,6 +74,37 @@ public function testBuildingPlainDefinition() $this->assertSame($definition, $definition->getVariations()[0]); } + public function testBuildingEmptyArrayDefinition() + { + $definition = $this->builder->build('[]'); + + $this->assertInstanceOf(PlainExtractorDefinitionInterface::class, $definition); + + $this->assertSame('[]', $definition->getName()); + $this->assertNull($definition->getNext()); + $this->assertCount(1, $definition->getVariations()); + $this->assertSame($definition, $definition->getVariations()[0]); + } + + public function testBuildingEmptyArrayWithNestedEmptyArrayDefinition() + { + $definition = $this->builder->build('[][]'); + + $this->assertInstanceOf(PlainExtractorDefinitionInterface::class, $definition); + + $this->assertSame('[]', $definition->getName()); + $this->assertInstanceOf(PlainExtractorDefinitionInterface::class, $definition->getNext()); + $this->assertCount(1, $definition->getVariations()); + $this->assertSame($definition, $definition->getVariations()[0]); + + $next = $definition->getNext(); + + $this->assertSame('[]', $next->getName()); + $this->assertNull($next->getNext()); + $this->assertCount(1, $next->getVariations()); + $this->assertSame($next, $next->getVariations()[0]); + } + public function testBuildingPlainDefinitionWithEmptyNestedDefinition() { $definition = $this->builder->build('test()'); @@ -186,4 +238,149 @@ public function testBuildingVariableDefinitionWithNestedDefinition() $this->assertCount(1, $variation->getVariations()); $this->assertSame($variation, $variation->getVariations()[0]); } + + public function testBuildingPlainDefinitionArrayed() + { + $definition = $this->builder->build('test[]'); + + $this->assertInstanceOf(PlainExtractorDefinitionInterface::class, $definition); + + $this->assertSame('[]', $definition->getName()); + $this->assertInstanceOf(PlainExtractorDefinitionInterface::class, $definition->getNext()); + $this->assertCount(1, $definition->getVariations()); + $this->assertSame($definition, $definition->getVariations()[0]); + + $next = $definition->getNext(); + + $this->assertSame('test', $next->getName()); + $this->assertNull($next->getNext()); + $this->assertCount(1, $next->getVariations()); + $this->assertSame($next, $next->getVariations()[0]); + } + + public function testBuildingPlainGroupedDefinition() + { + $definition = $this->builder->build('(test)'); + + $this->assertInstanceOf(PlainExtractorDefinitionInterface::class, $definition); + + $this->assertSame('test', $definition->getName()); + $this->assertNull($definition->getNext()); + $this->assertCount(1, $definition->getVariations()); + } + + public function testBuildingVariableDefinitionGrouped() + { + $definition = $this->builder->build('(test|alternative|definition)'); + + $this->assertInstanceOf(VariableExtractorDefinitionInterface::class, $definition); + + $this->assertNull($definition->getName()); + $this->assertNull($definition->getNext()); + $this->assertCount(3, $definition->getVariations()); + $this->assertContainsOnlyInstancesOf(PlainExtractorDefinitionInterface::class, $definition->getVariations()); + + $variation = $definition->getVariations()[0]; + $this->assertSame('test', $variation->getName()); + $this->assertNull($variation->getNext()); + $this->assertCount(1, $variation->getVariations()); + $this->assertSame($variation, $variation->getVariations()[0]); + + $variation = $definition->getVariations()[1]; + $this->assertSame('alternative', $variation->getName()); + $this->assertNull($variation->getNext()); + $this->assertCount(1, $variation->getVariations()); + $this->assertSame($variation, $variation->getVariations()[0]); + + $variation = $definition->getVariations()[2]; + $this->assertSame('definition', $variation->getName()); + $this->assertNull($variation->getNext()); + $this->assertCount(1, $variation->getVariations()); + $this->assertSame($variation, $variation->getVariations()[0]); + } + + public function testBuildingVariableDefinitionGroupedAndArrayed() + { + $definition = $this->builder->build('(test|alternative|definition)[]'); + + $this->assertInstanceOf(PlainExtractorDefinitionInterface::class, $definition); + + $this->assertSame('[]', $definition->getName()); + $this->assertInstanceOf(VariableExtractorDefinition::class, $definition->getNext()); + $this->assertCount(1, $definition->getVariations()); + + $definition = $definition->getNext(); + + $variation = $definition->getVariations()[0]; + $this->assertSame('test', $variation->getName()); + $this->assertNull($variation->getNext()); + $this->assertCount(1, $variation->getVariations()); + $this->assertSame($variation, $variation->getVariations()[0]); + + $variation = $definition->getVariations()[1]; + $this->assertSame('alternative', $variation->getName()); + $this->assertNull($variation->getNext()); + $this->assertCount(1, $variation->getVariations()); + $this->assertSame($variation, $variation->getVariations()[0]); + + $variation = $definition->getVariations()[2]; + $this->assertSame('definition', $variation->getName()); + $this->assertNull($variation->getNext()); + $this->assertCount(1, $variation->getVariations()); + $this->assertSame($variation, $variation->getVariations()[0]); + } + + public function testBuildingGroupedAndNested() + { + $definition = $this->builder->build('(foo(bar))'); + + $str = $this->stringifyDefinition($definition); + + $this->assertSame('foo(bar)', $str); + } + + public function testBuildingGroupedAndNestedAndVariableComplex() + { + $definition = $this->builder->build('(foo(bar)|(bar(baz)|baz(bar(foo))))'); + + $str = $this->stringifyDefinition($definition); + + $this->assertSame('foo(bar)|bar(baz)|baz(bar(foo))', $str); + } + + public function testBuildingGroupedAndNestedAndVariableComplexArrayed() + { + $definition = $this->builder->build('(foo(bar)|(bar(baz)|baz(bar(foo))[]))'); + + $str = $this->stringifyDefinition($definition); + + $this->assertSame('foo(bar)|bar(baz)|baz(bar(foo))[]', $str); + } + + + protected function stringifyDefinition(ExtractorDefinitionInterface $definition) + { + if ($definition instanceof PlainExtractorDefinitionInterface) { + // build plain + + $name = $definition->getName(); + + if ('[]' == $name) { + return $this->stringifyDefinition($definition->getNext()) . $name; + } elseif ($definition->getNext()) { + return $name . '(' . $this->stringifyDefinition($definition->getNext()) . ')'; + } + + return $name; + } elseif ($definition instanceof VariableExtractorDefinitionInterface) { + $variations = []; + foreach ($definition->getVariations() as $v) { + $variations[] = $this->stringifyDefinition($v); + } + + return implode('|', $variations); + } else { + throw new UnexpectedValueException('Unexpected value type'); + } + } } From d60963fa67a5024aab143eb8e37ca973377ef53c Mon Sep 17 00:00:00 2001 From: Bogdan Padalko Date: Sun, 5 Nov 2017 13:20:18 +0200 Subject: [PATCH 02/14] Add shortcut for optional nullable param, closes #5 --- src/Specs/Builder/ParameterSpecBuilder.php | 57 ++++++++++++++++--- .../Specs/Builder/FunctionSpecBuilderTest.php | 11 ++++ .../Builder/ParameterSpecBuilderTest.php | 33 ++++++++++- 3 files changed, 91 insertions(+), 10 deletions(-) diff --git a/src/Specs/Builder/ParameterSpecBuilder.php b/src/Specs/Builder/ParameterSpecBuilder.php index 6ab1b72..8c4873e 100644 --- a/src/Specs/Builder/ParameterSpecBuilder.php +++ b/src/Specs/Builder/ParameterSpecBuilder.php @@ -35,6 +35,9 @@ class ParameterSpecBuilder implements ParameterSpecBuilderInterface \s* )? (?[_a-z]\w*) + \s* + (?\?)? + \s* (?: \s* = \s* (? @@ -86,13 +89,20 @@ public function build(string $definition): ParameterSpecInterface } if (preg_match($this->regexp, $definition, $matches)) { + + $this->validateDefinition($definition, $matches); + try { - if ($matches['rest'] ?? false) { + if ($this->hasRest($matches)) { return $this->buildVariadicParameterSpec($matches); } - if ($matches['default'] ?? false) { - return $this->buildOptionalParameterSpec($matches); + if ($this->hasDefault($matches)) { + return $this->buildOptionalParameterSpec($matches, $matches['default']); + } + + if ($this->hasNullable($matches)) { + return $this->buildOptionalParameterSpec($matches, null); } return $this->buildMandatoryParameterSpec($matches); @@ -106,16 +116,14 @@ public function build(string $definition): ParameterSpecInterface protected function buildVariadicParameterSpec(array $matches): VariadicParameterSpec { - if (isset($matches['default']) && '' !== $matches['default']) { - throw new ParameterSpecBuilderException('Variadic parameter should have no default value'); - } - return new VariadicParameterSpec($matches['name'], $this->builder->build($matches['type'])); } - protected function buildOptionalParameterSpec(array $matches): OptionalParameterSpec + protected function buildOptionalParameterSpec(array $matches, ?string $default): OptionalParameterSpec { - $default = $this->buildDefaultValue($matches['default']); + if (null !== $default) { + $default = $this->buildDefaultValue($matches['default']); + } return new OptionalParameterSpec($matches['name'], $this->builder->build($matches['type']), $default); } @@ -166,6 +174,7 @@ protected function buildDefaultValue(string $definition) } } + // UNEXPECTED // Less likely we will ever get here because it should fail at a parsing step, but just in case throw new ParameterSpecBuilderException("Unknown default value format '{$definition}'"); } @@ -176,4 +185,34 @@ private function wrappedWith(string $definition, string $starts, $ends) return $starts == $definition[0] && $ends == $definition[-1]; } + + protected function validateDefinition(string $definition, array $matches): void + { + if ($this->hasNullable($matches) && $this->hasRest($matches)) { + throw new ParameterSpecBuilderException("Variadic parameter could not be nullable"); + } + + if ($this->hasNullable($matches) && $this->hasDefault($matches)) { + throw new ParameterSpecBuilderException("Nullable parameter could not have default value"); + } + + if ($this->hasRest($matches) && $this->hasDefault($matches)) { + throw new ParameterSpecBuilderException('Variadic parameter could have no default value'); + } + } + + private function hasNullable(array $matches): bool + { + return isset($matches['nullable']) && '' !== $matches['nullable']; + } + + private function hasRest(array $matches): bool + { + return isset($matches['rest']) && '' !== $matches['rest']; + } + + private function hasDefault(array $matches): bool + { + return isset($matches['default']) && '' !== $matches['default']; + } } diff --git a/tests/Specs/Builder/FunctionSpecBuilderTest.php b/tests/Specs/Builder/FunctionSpecBuilderTest.php index 719582d..329a25b 100644 --- a/tests/Specs/Builder/FunctionSpecBuilderTest.php +++ b/tests/Specs/Builder/FunctionSpecBuilderTest.php @@ -117,7 +117,18 @@ public function testBuildSpecWithParams() $this->assertFalse($spec->needsExecutionContext()); $this->assertContainsOnlyInstancesOf(ParameterSpecInterface::class, $spec->getParameters()->getParameters()); $this->assertCount(3, $spec->getParameters()->getParameters()); + } + + public function testBuildSpecWithNullableParams() + { + $this->parameterSpecBuilderShouldBuildOn('one: param', 'two?: param'); + + $spec = $this->builder->build('(one: param, two?: param)'); + $this->assertInstanceOf(FunctionSpecInterface::class, $spec); + $this->assertFalse($spec->needsExecutionContext()); + $this->assertContainsOnlyInstancesOf(ParameterSpecInterface::class, $spec->getParameters()->getParameters()); + $this->assertCount(2, $spec->getParameters()->getParameters()); } // Test throws spec diff --git a/tests/Specs/Builder/ParameterSpecBuilderTest.php b/tests/Specs/Builder/ParameterSpecBuilderTest.php index 9c87e84..a672bf3 100644 --- a/tests/Specs/Builder/ParameterSpecBuilderTest.php +++ b/tests/Specs/Builder/ParameterSpecBuilderTest.php @@ -134,13 +134,44 @@ public function testBuildingVariadicParameter() /** * @expectedException \Pinepain\JsSandbox\Specs\Builder\Exceptions\ParameterSpecBuilderException - * @expectedExceptionMessage Variadic parameter should have no default value + * @expectedExceptionMessage Variadic parameter could have no default value */ public function testBuildingVariadicParameterWithDefaultValueShouldThrowException() { $this->builder->build('...param = []: type'); } + /** + * @expectedException \Pinepain\JsSandbox\Specs\Builder\Exceptions\ParameterSpecBuilderException + * @expectedExceptionMessage Variadic parameter could not be nullable + */ + public function testBuildingVariadicParameterWithNullableShouldThrowException() + { + $this->builder->build('...param?: type'); + } + + /** + * @expectedException \Pinepain\JsSandbox\Specs\Builder\Exceptions\ParameterSpecBuilderException + * @expectedExceptionMessage Nullable parameter could not have default value + */ + public function testBuildingNullableParameterWithDefaultValueShouldThrowException() + { + $this->builder->build('param? = "default": type'); + } + + public function testBuildingNullableParameter() + { + $this->extractorDefinitionShouldBuildOn('type'); + + $spec = $this->builder->build('param? : type'); + + $this->assertInstanceOf(OptionalParameterSpec::class, $spec); + + $this->assertSame('param', $spec->getName()); + $this->assertNull($spec->getDefaultValue()); + $this->assertInstanceOf(ExtractorDefinitionInterface::class, $spec->getExtractorDefinition()); + } + /** * @expectedException \Pinepain\JsSandbox\Specs\Builder\Exceptions\ParameterSpecBuilderException * @expectedExceptionMessage Unable to parse definition because of extractor failure: ExtractorDefinitionBuilder exception for testing From 1157ac8a94646bacabd6a85b85835984b31ea093 Mon Sep 17 00:00:00 2001 From: Bogdan Padalko Date: Sun, 5 Nov 2017 20:05:16 +0200 Subject: [PATCH 03/14] Add basic support for decorators and rework return type and throws, #7 --- src/Decorators/DecoratorSpec.php | 55 ++++++++ src/Decorators/DecoratorSpecBuilder.php | 110 ++++++++++++++++ .../DecoratorSpecBuilderException.php | 24 ++++ .../DecoratorSpecBuilderInterface.php | 28 +++++ src/Decorators/DecoratorSpecInterface.php | 29 +++++ src/Decorators/DecoratorsCollection.php | 50 ++++++++ .../DecoratorsCollectionInterface.php | 42 +++++++ .../Definitions/DecoratorInterface.php | 25 ++++ .../ExecutionContextInjectorDecorator.php | 35 ++++++ src/Extractors/ExtractorDefinitionBuilder.php | 10 -- src/Laravel/JsSandboxServiceProvider.php | 29 ++++- src/Specs/Builder/ArgumentValueBuilder.php | 75 +++++++++++ .../Builder/ArgumentValueBuilderInterface.php | 33 +++++ .../ArgumentValueBuilderException.php | 21 ++++ src/Specs/Builder/FunctionSpecBuilder.php | 93 ++++++++++++-- src/Specs/Builder/ParameterSpecBuilder.php | 97 ++++++-------- src/Specs/FunctionSpec.php | 29 +++-- src/Specs/FunctionSpecInterface.php | 6 +- .../FunctionCallHandler.php | 26 ++-- .../FunctionComponents/FunctionDecorator.php | 52 ++++++++ .../FunctionDecoratorInterface.php | 33 +++++ .../DecoratorDefinitionBuilderTest.php | 108 ++++++++++++++++ .../Builder/ArgumentValueBuilderTest.php | 93 ++++++++++++++ .../Specs/Builder/FunctionSpecBuilderTest.php | 118 ++++++++++++++---- .../Builder/ParameterSpecBuilderTest.php | 94 +++++++------- 25 files changed, 1139 insertions(+), 176 deletions(-) create mode 100644 src/Decorators/DecoratorSpec.php create mode 100644 src/Decorators/DecoratorSpecBuilder.php create mode 100644 src/Decorators/DecoratorSpecBuilderException.php create mode 100644 src/Decorators/DecoratorSpecBuilderInterface.php create mode 100644 src/Decorators/DecoratorSpecInterface.php create mode 100644 src/Decorators/DecoratorsCollection.php create mode 100644 src/Decorators/DecoratorsCollectionInterface.php create mode 100644 src/Decorators/Definitions/DecoratorInterface.php create mode 100644 src/Decorators/Definitions/ExecutionContextInjectorDecorator.php create mode 100644 src/Specs/Builder/ArgumentValueBuilder.php create mode 100644 src/Specs/Builder/ArgumentValueBuilderInterface.php create mode 100644 src/Specs/Builder/Exceptions/ArgumentValueBuilderException.php create mode 100644 src/Wrappers/FunctionComponents/FunctionDecorator.php create mode 100644 src/Wrappers/FunctionComponents/FunctionDecoratorInterface.php create mode 100644 tests/Decorators/DecoratorDefinitionBuilderTest.php create mode 100644 tests/Specs/Builder/ArgumentValueBuilderTest.php diff --git a/src/Decorators/DecoratorSpec.php b/src/Decorators/DecoratorSpec.php new file mode 100644 index 0000000..f303ada --- /dev/null +++ b/src/Decorators/DecoratorSpec.php @@ -0,0 +1,55 @@ + + * + * Licensed under the MIT license: http://opensource.org/licenses/MIT + * + * For the full copyright and license information, please view the + * LICENSE file that was distributed with this source or visit + * http://opensource.org/licenses/MIT + */ + + +namespace Pinepain\JsSandbox\Decorators; + + +class DecoratorSpec implements DecoratorSpecInterface +{ + /** + * @var string + */ + private $name; + /** + * @var array + */ + private $arguments; + + /** + * @param string $name + * @param array $arguments + */ + public function __construct(string $name, array $arguments = []) + { + $this->name = $name; + $this->arguments = $arguments; + } + + /** + * {@inheritdoc} + */ + public function getName(): string + { + return $this->name; + } + + /** + * {@inheritdoc} + */ + public function getArguments(): array + { + return $this->arguments; + } +} diff --git a/src/Decorators/DecoratorSpecBuilder.php b/src/Decorators/DecoratorSpecBuilder.php new file mode 100644 index 0000000..f9a013a --- /dev/null +++ b/src/Decorators/DecoratorSpecBuilder.php @@ -0,0 +1,110 @@ + + * + * Licensed under the MIT license: http://opensource.org/licenses/MIT + * + * For the full copyright and license information, please view the + * LICENSE file that was distributed with this source or visit + * http://opensource.org/licenses/MIT + */ + + +namespace Pinepain\JsSandbox\Decorators; + + +use Pinepain\JsSandbox\Specs\Builder\ArgumentValueBuilderInterface; +use Pinepain\JsSandbox\Specs\Builder\Exceptions\ArgumentValueBuilderException; + + +class DecoratorSpecBuilder implements DecoratorSpecBuilderInterface +{ + private $regexp = '/ + ^ + \@(?[a-z_]+(?:[\w-]*\w)?) + \s* + (?: + \( + \s* + (? + ( + (?:[^\'\"\(\)\,\s]+) # literal + | + (?:[+-]?[0-9]+\.?[0-9]*) # numbers (no exponential notation) + | + (?:\'[^\']*\') # single-quoted string + | + (?:\"[^\"]*\") # double-quoted string + | + (?:\[\s*\]) # empty array + | + (?:\{\s*\}) # empty object + | + true | false | null + )(?:\s*\,\s*((?-3))*)* + )? + \s* + \) + )? + \s* + $ + /xi'; + /** + * @var ArgumentValueBuilderInterface + */ + private $argument; + + /** + * @param ArgumentValueBuilderInterface $argument + */ + public function __construct(ArgumentValueBuilderInterface $argument) + { + $this->argument = $argument; + } + + /** + * {@inheritdoc} + */ + public function build(string $definition): DecoratorSpecInterface + { + $definition = trim($definition); + + if (!$definition) { + throw new DecoratorSpecBuilderException('Definition must be non-empty string'); + } + + try { + if (preg_match($this->regexp, $definition, $matches)) { + + $params = array_slice($matches, 5); + $decorator = $this->buildDecorator($matches['name'], $params); + + return $decorator; + } + } catch (ArgumentValueBuilderException $e) { + // We don't care about what specific issue we hit inside, + // for API user it means that the definition is invalid + } + + throw new DecoratorSpecBuilderException("Unable to parse definition: '{$definition}'"); + } + + /** + * @param string $name + * @param array $raw_args + * + * @return DecoratorSpecInterface + */ + protected function buildDecorator(string $name, array $raw_args): DecoratorSpecInterface + { + $arguments = []; + foreach ($raw_args as $raw_arg) { + $arguments[] = $this->argument->build($raw_arg, true); + } + + return new DecoratorSpec($name, $arguments); + } +} diff --git a/src/Decorators/DecoratorSpecBuilderException.php b/src/Decorators/DecoratorSpecBuilderException.php new file mode 100644 index 0000000..79511af --- /dev/null +++ b/src/Decorators/DecoratorSpecBuilderException.php @@ -0,0 +1,24 @@ + + * + * Licensed under the MIT license: http://opensource.org/licenses/MIT + * + * For the full copyright and license information, please view the + * LICENSE file that was distributed with this source or visit + * http://opensource.org/licenses/MIT + */ + + +namespace Pinepain\JsSandbox\Decorators; + + +use Pinepain\JsSandbox\Specs\Builder\Exceptions\SpecBuilderException; + + +class DecoratorSpecBuilderException extends SpecBuilderException +{ +} diff --git a/src/Decorators/DecoratorSpecBuilderInterface.php b/src/Decorators/DecoratorSpecBuilderInterface.php new file mode 100644 index 0000000..5fc42fc --- /dev/null +++ b/src/Decorators/DecoratorSpecBuilderInterface.php @@ -0,0 +1,28 @@ + + * + * Licensed under the MIT license: http://opensource.org/licenses/MIT + * + * For the full copyright and license information, please view the + * LICENSE file that was distributed with this source or visit + * http://opensource.org/licenses/MIT + */ + + +namespace Pinepain\JsSandbox\Decorators; + + +interface DecoratorSpecBuilderInterface +{ + /** + * @param string $definition + * + * @return DecoratorSpecInterface + * @throws DecoratorSpecBuilderException + */ + public function build(string $definition): DecoratorSpecInterface; +} diff --git a/src/Decorators/DecoratorSpecInterface.php b/src/Decorators/DecoratorSpecInterface.php new file mode 100644 index 0000000..c642614 --- /dev/null +++ b/src/Decorators/DecoratorSpecInterface.php @@ -0,0 +1,29 @@ + + * + * Licensed under the MIT license: http://opensource.org/licenses/MIT + * + * For the full copyright and license information, please view the + * LICENSE file that was distributed with this source or visit + * http://opensource.org/licenses/MIT + */ + +namespace Pinepain\JsSandbox\Decorators; + + +interface DecoratorSpecInterface +{ + /** + * @return string + */ + public function getName(): string; + + /** + * @return array + */ + public function getArguments(): array; +} diff --git a/src/Decorators/DecoratorsCollection.php b/src/Decorators/DecoratorsCollection.php new file mode 100644 index 0000000..f2ec9b0 --- /dev/null +++ b/src/Decorators/DecoratorsCollection.php @@ -0,0 +1,50 @@ + + * + * Licensed under the MIT license: http://opensource.org/licenses/MIT + * + * For the full copyright and license information, please view the + * LICENSE file that was distributed with this source or visit + * http://opensource.org/licenses/MIT + */ + + +namespace Pinepain\JsSandbox\Decorators; + + +use OutOfBoundsException; +use OverflowException; +use Pinepain\JsSandbox\Decorators\Definitions\DecoratorInterface; + + +class DecoratorsCollection implements DecoratorsCollectionInterface +{ + protected $decorators = []; + + /** + * {@inheritdoc} + */ + public function get(string $name): DecoratorInterface + { + if (!isset($this->decorators[$name])) { + throw new OutOfBoundsException("Decorator '{$name}' not found"); + } + + return $this->decorators[$name]; + } + + /** + * {@inheritdoc} + */ + public function put(string $name, DecoratorInterface $extractor) + { + if (isset($this->decorators[$name])) { + throw new OverflowException("Decorator with the same name ('{$name}') already exists"); + } + $this->decorators[$name] = $extractor; + } +} diff --git a/src/Decorators/DecoratorsCollectionInterface.php b/src/Decorators/DecoratorsCollectionInterface.php new file mode 100644 index 0000000..5d955a7 --- /dev/null +++ b/src/Decorators/DecoratorsCollectionInterface.php @@ -0,0 +1,42 @@ + + * + * Licensed under the MIT license: http://opensource.org/licenses/MIT + * + * For the full copyright and license information, please view the + * LICENSE file that was distributed with this source or visit + * http://opensource.org/licenses/MIT + */ + + +namespace Pinepain\JsSandbox\Decorators; + + +use OutOfBoundsException; +use OverflowException; +use Pinepain\JsSandbox\Decorators\Definitions\DecoratorInterface; + + +interface DecoratorsCollectionInterface +{ + /** + * @param string $name + * + * @return DecoratorInterface + * @throws OutOfBoundsException + */ + public function get(string $name): DecoratorInterface; + + /** + * @param string $name + * @param DecoratorInterface $extractor + * + * @return mixed + * @throws OverflowException + */ + public function put(string $name, DecoratorInterface $extractor); +} diff --git a/src/Decorators/Definitions/DecoratorInterface.php b/src/Decorators/Definitions/DecoratorInterface.php new file mode 100644 index 0000000..fd7e004 --- /dev/null +++ b/src/Decorators/Definitions/DecoratorInterface.php @@ -0,0 +1,25 @@ + + * + * Licensed under the MIT license: http://opensource.org/licenses/MIT + * + * For the full copyright and license information, please view the + * LICENSE file that was distributed with this source or visit + * http://opensource.org/licenses/MIT + */ + + +namespace Pinepain\JsSandbox\Decorators\Definitions; + + +use Pinepain\JsSandbox\Wrappers\FunctionComponents\Runtime\ExecutionContextInterface; + + +interface DecoratorInterface +{ + public function decorate(callable $callback, ExecutionContextInterface $exec): callable; +} diff --git a/src/Decorators/Definitions/ExecutionContextInjectorDecorator.php b/src/Decorators/Definitions/ExecutionContextInjectorDecorator.php new file mode 100644 index 0000000..bd5fb78 --- /dev/null +++ b/src/Decorators/Definitions/ExecutionContextInjectorDecorator.php @@ -0,0 +1,35 @@ + + * + * Licensed under the MIT license: http://opensource.org/licenses/MIT + * + * For the full copyright and license information, please view the + * LICENSE file that was distributed with this source or visit + * http://opensource.org/licenses/MIT + */ + + +namespace Pinepain\JsSandbox\Decorators\Definitions; + + +use Pinepain\JsSandbox\Wrappers\FunctionComponents\Runtime\ExecutionContextInterface; + + +class ExecutionContextInjectorDecorator implements DecoratorInterface +{ + /** + * {@inheritdoc} + */ + public function decorate(callable $callback, ExecutionContextInterface $exec): callable + { + return function (...$arguments) use ($callback, $exec) { + array_unshift($arguments, $exec); + + return $callback(...$arguments); + }; + } +} diff --git a/src/Extractors/ExtractorDefinitionBuilder.php b/src/Extractors/ExtractorDefinitionBuilder.php index 2ee8745..1367116 100644 --- a/src/Extractors/ExtractorDefinitionBuilder.php +++ b/src/Extractors/ExtractorDefinitionBuilder.php @@ -61,16 +61,6 @@ class ExtractorDefinitionBuilder implements ExtractorDefinitionBuilderInterface $ /xi'; - protected $type_regexp2 = '/ - ^ - (?:((\w+\b(?:\(.*\))?(?:\s*\[\s*\])?)(?:\s*\|\s*(?-1))*)) - | - (?:\(\s*(?-2)\s*\)(?:\s*\[\s*\])?) - | - (\[\s*\]) - $ - /xi'; - /** * {@inheritdoc} */ diff --git a/src/Laravel/JsSandboxServiceProvider.php b/src/Laravel/JsSandboxServiceProvider.php index dad3a3e..9f5ffa1 100644 --- a/src/Laravel/JsSandboxServiceProvider.php +++ b/src/Laravel/JsSandboxServiceProvider.php @@ -21,6 +21,11 @@ use InvalidArgumentException; use Pinepain\JsSandbox\Common\NativeGlobalObjectWrapper; use Pinepain\JsSandbox\Common\NativeGlobalObjectWrapperInterface; +use Pinepain\JsSandbox\Decorators\DecoratorsCollection; +use Pinepain\JsSandbox\Decorators\DecoratorsCollectionInterface; +use Pinepain\JsSandbox\Decorators\DecoratorSpecBuilder; +use Pinepain\JsSandbox\Decorators\DecoratorSpecBuilderInterface; +use Pinepain\JsSandbox\Decorators\Definitions\ExecutionContextInjectorDecorator; use Pinepain\JsSandbox\Extractors\Extractor; use Pinepain\JsSandbox\Extractors\ExtractorDefinitionBuilder; use Pinepain\JsSandbox\Extractors\ExtractorDefinitionBuilderInterface; @@ -35,9 +40,9 @@ use Pinepain\JsSandbox\Extractors\PlainExtractors\DateExtractor; use Pinepain\JsSandbox\Extractors\PlainExtractors\DateTimeExtractor; use Pinepain\JsSandbox\Extractors\PlainExtractors\FunctionExtractor; -use Pinepain\JsSandbox\Extractors\PlainExtractors\NativeObjectExtractor; use Pinepain\JsSandbox\Extractors\PlainExtractors\JsonableExtractor; use Pinepain\JsSandbox\Extractors\PlainExtractors\JsonExtractor; +use Pinepain\JsSandbox\Extractors\PlainExtractors\NativeObjectExtractor; use Pinepain\JsSandbox\Extractors\PlainExtractors\NullExtractor; use Pinepain\JsSandbox\Extractors\PlainExtractors\NumberExtractor; use Pinepain\JsSandbox\Extractors\PlainExtractors\ObjectExtractor; @@ -47,6 +52,8 @@ use Pinepain\JsSandbox\Extractors\PlainExtractors\ScalarExtractor; use Pinepain\JsSandbox\Extractors\PlainExtractors\StringExtractor; use Pinepain\JsSandbox\Extractors\PlainExtractors\UndefinedExtractor; +use Pinepain\JsSandbox\Specs\Builder\ArgumentValueBuilder; +use Pinepain\JsSandbox\Specs\Builder\ArgumentValueBuilderInterface; use Pinepain\JsSandbox\Specs\Builder\BindingSpecBuilder; use Pinepain\JsSandbox\Specs\Builder\BindingSpecBuilderInterface; use Pinepain\JsSandbox\Specs\Builder\FunctionSpecBuilder; @@ -68,6 +75,8 @@ use Pinepain\JsSandbox\Wrappers\FunctionComponents\ArgumentsExtractorInterface; use Pinepain\JsSandbox\Wrappers\FunctionComponents\FunctionCallHandler; use Pinepain\JsSandbox\Wrappers\FunctionComponents\FunctionCallHandlerInterface; +use Pinepain\JsSandbox\Wrappers\FunctionComponents\FunctionDecorator; +use Pinepain\JsSandbox\Wrappers\FunctionComponents\FunctionDecoratorInterface; use Pinepain\JsSandbox\Wrappers\FunctionComponents\FunctionExceptionHandler; use Pinepain\JsSandbox\Wrappers\FunctionComponents\FunctionExceptionHandlerInterface; use Pinepain\JsSandbox\Wrappers\FunctionComponents\FunctionWrappersCache; @@ -156,10 +165,21 @@ protected function registerCallbackGuard() protected function registerFunctionCallHandler() { $this->app->singleton(ArgumentsExtractorInterface::class, ArgumentsExtractor::class); + $this->app->singleton(FunctionDecoratorInterface::class, FunctionDecorator::class); $this->app->singleton(FunctionExceptionHandlerInterface::class, FunctionExceptionHandler::class); $this->app->singleton(ReturnValueSetterInterface::class, ReturnValueSetter::class); $this->app->singleton(FunctionCallHandlerInterface::class, FunctionCallHandler::class); + + + $this->app->singleton(DecoratorsCollectionInterface::class, function (Container $app) { + + $collection = new DecoratorsCollection(); + + $collection->put('inject-context', new ExecutionContextInjectorDecorator()); + + return $collection; + }); } protected function registerWrapper() @@ -221,7 +241,9 @@ protected function registerExtractor() { $this->app->singleton(ExtractorDefinitionBuilderInterface::class, ExtractorDefinitionBuilder::class); $this->app->singleton(PropertySpecBuilderInterface::class, PropertySpecBuilder::class); + $this->app->singleton(ArgumentValueBuilderInterface::class, ArgumentValueBuilder::class); $this->app->singleton(ParameterSpecBuilderInterface::class, ParameterSpecBuilder::class); + $this->app->singleton(DecoratorSpecBuilderInterface::class, DecoratorSpecBuilder::class); $this->app->singleton(FunctionSpecBuilderInterface::class, FunctionSpecBuilder::class); $this->app->singleton(BindingSpecBuilderInterface::class, BindingSpecBuilder::class); $this->app->singleton(ObjectSpecBuilderInterface::class, ObjectSpecBuilder::class); @@ -282,6 +304,7 @@ public function provides() CallbackGuardInterface::class, ArgumentsExtractorInterface::class, + FunctionDecoratorInterface::class, FunctionExceptionHandlerInterface::class, ReturnValueSetterInterface::class, FunctionCallHandlerInterface::class, @@ -294,7 +317,9 @@ public function provides() ExtractorDefinitionBuilderInterface::class, PropertySpecBuilderInterface::class, - ParameterSpecBuilderInterface::class . + ArgumentValueBuilderInterface::class, + ParameterSpecBuilderInterface::class, + DecoratorSpecBuilderInterface::class, FunctionSpecBuilderInterface::class, BindingSpecBuilderInterface::class, ObjectSpecBuilderInterface::class, diff --git a/src/Specs/Builder/ArgumentValueBuilder.php b/src/Specs/Builder/ArgumentValueBuilder.php new file mode 100644 index 0000000..38e6c10 --- /dev/null +++ b/src/Specs/Builder/ArgumentValueBuilder.php @@ -0,0 +1,75 @@ + + * + * Licensed under the MIT license: http://opensource.org/licenses/MIT + * + * For the full copyright and license information, please view the + * LICENSE file that was distributed with this source or visit + * http://opensource.org/licenses/MIT + */ + + +namespace Pinepain\JsSandbox\Specs\Builder; + + +use Pinepain\JsSandbox\Specs\Builder\Exceptions\ArgumentValueBuilderException; + + +class ArgumentValueBuilder implements ArgumentValueBuilderInterface +{ + /** + * {@inheritdoc} + */ + public function build(string $definition, bool $with_literal) + { + if (is_numeric($definition)) { + if (false !== strpos($definition, '.')) { + return (float)$definition; + } + + return (int)$definition; + } + + switch (strtolower($definition)) { + case 'null': + return null; + case 'true': + return true; + case 'false': + return false; + } + + if ($this->wrappedWith($definition, '[', ']')) { + return []; + } + + if ($this->wrappedWith($definition, '{', '}')) { + return []; + } + + foreach (['"', "'"] as $quote) { + if ($this->wrappedWith($definition, $quote, $quote)) { + return trim($definition, $quote); + } + } + + if (!$with_literal) { + throw new ArgumentValueBuilderException("Unknown value format '{$definition}'"); + } + + return $definition; + } + + private function wrappedWith(string $definition, string $starts, $ends) + { + if (strlen($definition) < 2) { + return false; + } + + return $starts == $definition[0] && $ends == $definition[-1]; + } +} diff --git a/src/Specs/Builder/ArgumentValueBuilderInterface.php b/src/Specs/Builder/ArgumentValueBuilderInterface.php new file mode 100644 index 0000000..bbdfa18 --- /dev/null +++ b/src/Specs/Builder/ArgumentValueBuilderInterface.php @@ -0,0 +1,33 @@ + + * + * Licensed under the MIT license: http://opensource.org/licenses/MIT + * + * For the full copyright and license information, please view the + * LICENSE file that was distributed with this source or visit + * http://opensource.org/licenses/MIT + */ + +namespace Pinepain\JsSandbox\Specs\Builder; + + +use Pinepain\JsSandbox\Specs\Builder\Exceptions\ArgumentValueBuilderException; + + +interface ArgumentValueBuilderInterface +{ + /** + * @param string $definition + * @param bool $with_literal + * + * @return mixed + * @throws ArgumentValueBuilderException + */ + public function build(string $definition, bool $with_literal); + + +} diff --git a/src/Specs/Builder/Exceptions/ArgumentValueBuilderException.php b/src/Specs/Builder/Exceptions/ArgumentValueBuilderException.php new file mode 100644 index 0000000..c370e52 --- /dev/null +++ b/src/Specs/Builder/Exceptions/ArgumentValueBuilderException.php @@ -0,0 +1,21 @@ + + * + * Licensed under the MIT license: http://opensource.org/licenses/MIT + * + * For the full copyright and license information, please view the + * LICENSE file that was distributed with this source or visit + * http://opensource.org/licenses/MIT + */ + + +namespace Pinepain\JsSandbox\Specs\Builder\Exceptions; + + +class ArgumentValueBuilderException extends SpecBuilderException +{ +} diff --git a/src/Specs/Builder/FunctionSpecBuilder.php b/src/Specs/Builder/FunctionSpecBuilder.php index 32ec91f..2222d8f 100644 --- a/src/Specs/Builder/FunctionSpecBuilder.php +++ b/src/Specs/Builder/FunctionSpecBuilder.php @@ -16,6 +16,7 @@ namespace Pinepain\JsSandbox\Specs\Builder; +use Pinepain\JsSandbox\Decorators\DecoratorSpecBuilderInterface; use Pinepain\JsSandbox\Specs\Builder\Exceptions\FunctionSpecBuilderException; use Pinepain\JsSandbox\Specs\FunctionSpec; use Pinepain\JsSandbox\Specs\FunctionSpecInterface; @@ -31,6 +32,11 @@ class FunctionSpecBuilder implements FunctionSpecBuilderInterface { + /** + * @var DecoratorSpecBuilderInterface + */ + private $decorator; + /** * @var ParameterSpecBuilderInterface */ @@ -44,10 +50,43 @@ class FunctionSpecBuilder implements FunctionSpecBuilderInterface */ private $default_return_type = 'any'; - - public function __construct(ParameterSpecBuilderInterface $builder) + private $regexp = '/ + ^ + \s* + (? + \s* + (?:\@[\w-]+(?:\s*\([^\)]*\)\s*)?\s*)+ + \s* + )? + \( + \s* + (?([^,]+)(?:\s*,\s*[^,]+)*)? + \s* + \) + (?: + \s* + \: + \s* + (?\w+\b)? + \s* + )? + (?: + \s* + throws + \s* + (?(\\\\?[a-z][\w\\\\]+)(?:\s*\,\s*(?-1))*)? + \s* + )? + \s* + $ + /xi'; + + private $decorators_regexp = '/([^\@"\']+)|("([^"]*)")|(\'([^\']*)\')/i'; + + public function __construct(DecoratorSpecBuilderInterface $decorator, ParameterSpecBuilderInterface $builder) { - $this->builder = $builder; + $this->decorator = $decorator; + $this->builder = $builder; $this->return_types = [ 'any' => new AnyReturnSpec(), @@ -66,15 +105,14 @@ public function build(string $definition): FunctionSpecInterface throw new FunctionSpecBuilderException('Definition must be non-empty string'); } - if (preg_match('/^(?\w+\b)?\s*(?\!)?\s*\(\s*(?([^,]+)(?:\s*,\s*[^,]+)*)?\s*\)\s*(?(\\\\?[a-z][\w\\\\]+)(?:\s*\|\s*(?-1))*)?\s*$/i', $definition, $matches)) { - - $needs_context = isset($matches['needs_context']) && $matches['needs_context']; + if (preg_match($this->regexp, $definition, $matches)) { - $params = $this->getParametersList($matches['params'] ?? ''); - $return = $this->getReturnType(($matches['return'] ?? '') ?: $this->default_return_type); - $throws = $this->getThrowsList($matches['throws'] ?? ''); + $decorators = $this->getDecoratorsList($matches['decorators'] ?? ''); + $params = $this->getParametersList($matches['params'] ?? ''); + $return = $this->getReturnType(($matches['return'] ?? '') ?: $this->default_return_type); + $throws = $this->getThrowsList($matches['throws'] ?? ''); - return new FunctionSpec($params, $throws, $return, $needs_context); + return new FunctionSpec($params, $throws, $return, $decorators); } throw new FunctionSpecBuilderException("Unable to parse definition: '{$definition}'"); @@ -108,7 +146,7 @@ protected function getThrowsList(string $definition): ThrowSpecListInterface $specs = []; if ($definition) { - $classes = array_filter(array_map('\trim', explode('|', $definition))); + $classes = array_filter(array_map('\trim', explode(', ', $definition))); foreach ($classes as $class) { $specs[] = new EchoThrowSpec($class); @@ -117,4 +155,37 @@ protected function getThrowsList(string $definition): ThrowSpecListInterface return new ThrowSpecList(...$specs); } + + protected function getDecoratorsList(string $definition): array + { + $definition = trim($definition); + + if (!$definition) { + return []; + } + + $separators = preg_split($this->decorators_regexp, $definition, -1, PREG_SPLIT_NO_EMPTY | PREG_SPLIT_OFFSET_CAPTURE); + + if (!$separators) { + // UNEXPECTED + throw new FunctionSpecBuilderException("Invalid decorators: '{$definition}'"); + } + + $decorators = []; + + $sequences = array_column($separators, 1); + + $sequences[] = strlen($definition); + + while (count($sequences) > 1) { + $start = array_shift($sequences); + $end = $sequences[0]; + + $part = trim(substr($definition, $start, $end - $start)); + + $decorators[] = $this->decorator->build($part); + } + + return $decorators; + } } diff --git a/src/Specs/Builder/ParameterSpecBuilder.php b/src/Specs/Builder/ParameterSpecBuilder.php index 8c4873e..a83369d 100644 --- a/src/Specs/Builder/ParameterSpecBuilder.php +++ b/src/Specs/Builder/ParameterSpecBuilder.php @@ -18,6 +18,7 @@ use Pinepain\JsSandbox\Extractors\ExtractorDefinitionBuilderException; use Pinepain\JsSandbox\Extractors\ExtractorDefinitionBuilderInterface; +use Pinepain\JsSandbox\Specs\Builder\Exceptions\ArgumentValueBuilderException; use Pinepain\JsSandbox\Specs\Builder\Exceptions\ParameterSpecBuilderException; use Pinepain\JsSandbox\Specs\Parameters\MandatoryParameterSpec; use Pinepain\JsSandbox\Specs\Parameters\OptionalParameterSpec; @@ -67,11 +68,20 @@ class ParameterSpecBuilder implements ParameterSpecBuilderInterface /** * @var ExtractorDefinitionBuilderInterface */ - private $builder; + private $extractor; + /** + * @var ArgumentValueBuilderInterface + */ + private $argument; - public function __construct(ExtractorDefinitionBuilderInterface $builder) + /** + * @param ExtractorDefinitionBuilderInterface $extractor + * @param ArgumentValueBuilderInterface $argument + */ + public function __construct(ExtractorDefinitionBuilderInterface $extractor, ArgumentValueBuilderInterface $argument) { - $this->builder = $builder; + $this->extractor = $extractor; + $this->argument = $argument; } /** @@ -90,7 +100,7 @@ public function build(string $definition): ParameterSpecInterface if (preg_match($this->regexp, $definition, $matches)) { - $this->validateDefinition($definition, $matches); + $matches = $this->prepareDefinition($matches); try { if ($this->hasRest($matches)) { @@ -116,77 +126,29 @@ public function build(string $definition): ParameterSpecInterface protected function buildVariadicParameterSpec(array $matches): VariadicParameterSpec { - return new VariadicParameterSpec($matches['name'], $this->builder->build($matches['type'])); + return new VariadicParameterSpec($matches['name'], $this->extractor->build($matches['type'])); } protected function buildOptionalParameterSpec(array $matches, ?string $default): OptionalParameterSpec { if (null !== $default) { - $default = $this->buildDefaultValue($matches['default']); - } - - return new OptionalParameterSpec($matches['name'], $this->builder->build($matches['type']), $default); - } - - protected function buildMandatoryParameterSpec(array $matches): MandatoryParameterSpec - { - return new MandatoryParameterSpec($matches['name'], $this->builder->build($matches['type'])); - } - - protected function buildDefaultValue(string $definition) - { - if (is_numeric($definition)) { - if (false !== strpos($definition, '.')) { - return (float)$definition; - } - - return (int)$definition; - } - - switch (strtolower($definition)) { - case 'null': - return null; - case 'true': - return true; - case 'false': - return false; - } - - // after this point all expected definition values MUST be at least 2 chars length - - if (strlen($definition) < 2) { - // UNEXPECTED - // Less likely we will ever get here because it should fail at a parsing step, but just in case - throw new ParameterSpecBuilderException("Unknown default value format '{$definition}'"); - } - - if ($this->wrappedWith($definition, '[', ']')) { - return []; - } - - if ($this->wrappedWith($definition, '{', '}')) { - return []; - } - - foreach (['"', "'"] as $quote) { - if ($this->wrappedWith($definition, $quote, $quote)) { - return trim($definition, $quote); + $default_definition = $matches['default']; + try { + $default = $this->argument->build($default_definition, false); + } catch (ArgumentValueBuilderException $e) { + throw new ParameterSpecBuilderException("Unknown or unsupported default value format '{$default_definition}'"); } } - // UNEXPECTED - // Less likely we will ever get here because it should fail at a parsing step, but just in case - throw new ParameterSpecBuilderException("Unknown default value format '{$definition}'"); + return new OptionalParameterSpec($matches['name'], $this->extractor->build($matches['type']), $default); } - private function wrappedWith(string $definition, string $starts, $ends) + protected function buildMandatoryParameterSpec(array $matches): MandatoryParameterSpec { - assert(strlen($definition) >= 2); - - return $starts == $definition[0] && $ends == $definition[-1]; + return new MandatoryParameterSpec($matches['name'], $this->extractor->build($matches['type'])); } - protected function validateDefinition(string $definition, array $matches): void + protected function prepareDefinition(array $matches): array { if ($this->hasNullable($matches) && $this->hasRest($matches)) { throw new ParameterSpecBuilderException("Variadic parameter could not be nullable"); @@ -199,6 +161,17 @@ protected function validateDefinition(string $definition, array $matches): void if ($this->hasRest($matches) && $this->hasDefault($matches)) { throw new ParameterSpecBuilderException('Variadic parameter could have no default value'); } + + if (!$this->hasType($matches)) { + $matches['type'] = 'any'; // special case + } + + return $matches; + } + + private function hasType(array $matches): bool + { + return isset($matches['type']) && '' !== $matches['type']; } private function hasNullable(array $matches): bool diff --git a/src/Specs/FunctionSpec.php b/src/Specs/FunctionSpec.php index b0d35d8..266e533 100644 --- a/src/Specs/FunctionSpec.php +++ b/src/Specs/FunctionSpec.php @@ -16,6 +16,7 @@ namespace Pinepain\JsSandbox\Specs; +use Pinepain\JsSandbox\Decorators\DecoratorSpecInterface; use Pinepain\JsSandbox\Specs\ReturnSpec\ReturnSpecInterface; use Pinepain\JsSandbox\Specs\ThrowSpec\ThrowSpecListInterface; @@ -36,22 +37,22 @@ class FunctionSpec implements FunctionSpecInterface */ private $return; /** - * @var bool + * @var array|DecoratorSpecInterface[] */ - private $needs_execution_context; + private $decorators; /** * @param ParametersListInterface $parameters - * @param ThrowSpecListInterface $exceptions - * @param ReturnSpecInterface $return - * @param bool $needs_execution_context + * @param ThrowSpecListInterface $exceptions + * @param ReturnSpecInterface $return + * @param DecoratorSpecInterface[] $decorators */ - public function __construct(ParametersListInterface $parameters, ThrowSpecListInterface $exceptions, ReturnSpecInterface $return, bool $needs_execution_context = false) + public function __construct(ParametersListInterface $parameters, ThrowSpecListInterface $exceptions, ReturnSpecInterface $return, array $decorators = []) { - $this->parameters = $parameters; - $this->exceptions = $exceptions; - $this->return = $return; - $this->needs_execution_context = $needs_execution_context; + $this->parameters = $parameters; + $this->exceptions = $exceptions; + $this->return = $return; + $this->decorators = $decorators; } /** @@ -78,8 +79,12 @@ public function getReturn(): ReturnSpecInterface return $this->return; } - public function needsExecutionContext(): bool + /** + * {@inheritdoc} + */ + public function getDecorators(): array { - return $this->needs_execution_context; + return $this->decorators; } + } diff --git a/src/Specs/FunctionSpecInterface.php b/src/Specs/FunctionSpecInterface.php index 09561f0..7577b1b 100644 --- a/src/Specs/FunctionSpecInterface.php +++ b/src/Specs/FunctionSpecInterface.php @@ -16,6 +16,8 @@ namespace Pinepain\JsSandbox\Specs; +use Pinepain\JsSandbox\Decorators\DecoratorSpecInterface; +use Pinepain\JsSandbox\Decorators\Definitions\DecoratorInterface; use Pinepain\JsSandbox\Specs\ReturnSpec\ReturnSpecInterface; use Pinepain\JsSandbox\Specs\ThrowSpec\ThrowSpecListInterface; @@ -38,7 +40,7 @@ public function getExceptions(): ThrowSpecListInterface; public function getReturn(): ReturnSpecInterface; /** - * @return bool + * @return DecoratorSpecInterface[] */ - public function needsExecutionContext(): bool; + public function getDecorators(): array; } diff --git a/src/Wrappers/FunctionComponents/FunctionCallHandler.php b/src/Wrappers/FunctionComponents/FunctionCallHandler.php index 26900e2..40eb426 100644 --- a/src/Wrappers/FunctionComponents/FunctionCallHandler.php +++ b/src/Wrappers/FunctionComponents/FunctionCallHandler.php @@ -28,6 +28,10 @@ class FunctionCallHandler implements FunctionCallHandlerInterface * @var ArgumentsExtractorInterface */ private $arguments_extractor; + /** + * @var FunctionDecoratorInterface + */ + private $decorator; /** * @var FunctionExceptionHandlerInterface */ @@ -38,13 +42,19 @@ class FunctionCallHandler implements FunctionCallHandlerInterface private $return_setter; /** - * @param ArgumentsExtractorInterface $arguments_extractor + * @param ArgumentsExtractorInterface $arguments_extractor + * @param FunctionDecoratorInterface $decorator * @param FunctionExceptionHandlerInterface $exception_handler - * @param ReturnValueSetterInterface $return_setter + * @param ReturnValueSetterInterface $return_setter */ - public function __construct(ArgumentsExtractorInterface $arguments_extractor, FunctionExceptionHandlerInterface $exception_handler, ReturnValueSetterInterface $return_setter) - { + public function __construct( + ArgumentsExtractorInterface $arguments_extractor, + FunctionDecoratorInterface $decorator, + FunctionExceptionHandlerInterface $exception_handler, + ReturnValueSetterInterface $return_setter + ) { $this->arguments_extractor = $arguments_extractor; + $this->decorator = $decorator; $this->exception_handler = $exception_handler; $this->return_setter = $return_setter; } @@ -54,12 +64,14 @@ public function wrap(callable $callback, FunctionSpecInterface $spec, ColdExecut return function (FunctionCallbackInfo $args) use ($callback, $spec, $cold_execution_context) { $arguments = $this->arguments_extractor->extract($args, $spec); - if ($spec->needsExecutionContext()) { + if ($spec->getDecorators()) { + // When we have decorators, we need executions context. // Execution context is simple and abstract way to write advanced functions which relies on existent // abstraction level but at the same time allow manipulate on a lower level, e.g. examine current // context, building rich v8 native objects, but not limited to. - $execution_context = $cold_execution_context->warm($args, $spec); - array_unshift($arguments, $execution_context); + $exec = $cold_execution_context->warm($args, $spec); + + $callback = $this->decorator->decorate($callback, $spec, $exec); } try { diff --git a/src/Wrappers/FunctionComponents/FunctionDecorator.php b/src/Wrappers/FunctionComponents/FunctionDecorator.php new file mode 100644 index 0000000..9df9493 --- /dev/null +++ b/src/Wrappers/FunctionComponents/FunctionDecorator.php @@ -0,0 +1,52 @@ + + * + * Licensed under the MIT license: http://opensource.org/licenses/MIT + * + * For the full copyright and license information, please view the + * LICENSE file that was distributed with this source or visit + * http://opensource.org/licenses/MIT + */ + + +namespace Pinepain\JsSandbox\Wrappers\FunctionComponents; + + +use Pinepain\JsSandbox\Decorators\DecoratorsCollectionInterface; +use Pinepain\JsSandbox\Specs\FunctionSpecInterface; +use Pinepain\JsSandbox\Wrappers\FunctionComponents\Runtime\ExecutionContextInterface; + + +class FunctionDecorator implements FunctionDecoratorInterface +{ + /** + * @var DecoratorsCollectionInterface + */ + private $collection; + + /** + * @param DecoratorsCollectionInterface $collection + */ + public function __construct(DecoratorsCollectionInterface $collection) + { + $this->collection = $collection; + } + + /** + * {@inheritdoc} + */ + public function decorate(callable $callback, FunctionSpecInterface $spec, ExecutionContextInterface $exec): callable + { + foreach ($spec->getDecorators() as $decorator_spec) { + $decorator = $this->collection->get($decorator_spec->getName()); + + $callback = $decorator->decorate($callback, $exec); + } + + return $callback; + } +} diff --git a/src/Wrappers/FunctionComponents/FunctionDecoratorInterface.php b/src/Wrappers/FunctionComponents/FunctionDecoratorInterface.php new file mode 100644 index 0000000..691c8d5 --- /dev/null +++ b/src/Wrappers/FunctionComponents/FunctionDecoratorInterface.php @@ -0,0 +1,33 @@ + + * + * Licensed under the MIT license: http://opensource.org/licenses/MIT + * + * For the full copyright and license information, please view the + * LICENSE file that was distributed with this source or visit + * http://opensource.org/licenses/MIT + */ + + +namespace Pinepain\JsSandbox\Wrappers\FunctionComponents; + + +use Pinepain\JsSandbox\Specs\FunctionSpecInterface; +use Pinepain\JsSandbox\Wrappers\FunctionComponents\Runtime\ExecutionContextInterface; + + +interface FunctionDecoratorInterface +{ + /** + * @param callable $callback + * @param FunctionSpecInterface $spec + * @param ExecutionContextInterface $exec + * + * @return callable + */ + public function decorate(callable $callback, FunctionSpecInterface $spec, ExecutionContextInterface $exec): callable; +} diff --git a/tests/Decorators/DecoratorDefinitionBuilderTest.php b/tests/Decorators/DecoratorDefinitionBuilderTest.php new file mode 100644 index 0000000..9b1ae7b --- /dev/null +++ b/tests/Decorators/DecoratorDefinitionBuilderTest.php @@ -0,0 +1,108 @@ + + * + * Licensed under the MIT license: http://opensource.org/licenses/MIT + * + * For the full copyright and license information, please view the + * LICENSE file that was distributed with this source or visit + * http://opensource.org/licenses/MIT + */ + + +namespace Pinepain\JsSandbox\Tests\Decorators; + + +use PHPUnit\Framework\TestCase; +use PHPUnit_Framework_MockObject_MockObject; +use Pinepain\JsSandbox\Decorators\DecoratorSpecBuilder; +use Pinepain\JsSandbox\Decorators\DecoratorSpecBuilderInterface; +use Pinepain\JsSandbox\Decorators\DecoratorSpecInterface; +use Pinepain\JsSandbox\Specs\Builder\ArgumentValueBuilderInterface; +use Pinepain\JsSandbox\Specs\Builder\Exceptions\ArgumentValueBuilderException; + + +class DecoratorDefinitionBuilderTest extends TestCase +{ + + /** + * @var DecoratorSpecBuilderInterface + */ + protected $builder; + + /** + * @var ArgumentValueBuilderInterface|PHPUnit_Framework_MockObject_MockObject + */ + protected $argument_builder; + + public function setUp() + { + $this->argument_builder = $this->getMockForAbstractClass(ArgumentValueBuilderInterface::class); + + $this->builder = new DecoratorSpecBuilder($this->argument_builder); + } + + /** + * @expectedException \Pinepain\JsSandbox\Decorators\DecoratorSpecBuilderException + * @expectedExceptionMessage Definition must be non-empty string + */ + public function testBuildShouldFailOnEmptyDefinition() + { + $this->builder->build(''); + } + + public function testBuild() + { + $this->argumentDefinitionShouldBuildOn('one', 'two'); + + $res = $this->builder->build('@foo(one, two)'); + + $this->assertInstanceOf(DecoratorSpecInterface::class, $res); + $this->assertSame('foo', $res->getName()); + $this->assertSame(['one', 'two'], $res->getArguments()); + } + + public function testBuildDashedShort() + { + $this->argumentDefinitionShouldBuildOn('one-two'); + + $res = $this->builder->build('@one-two'); + + $this->assertInstanceOf(DecoratorSpecInterface::class, $res); + $this->assertSame('one-two', $res->getName()); + $this->assertSame([], $res->getArguments()); + } + + /** + * @expectedException \Pinepain\JsSandbox\Decorators\DecoratorSpecBuilderException + * @expectedExceptionMessage Unable to parse definition: '@test(throw)' + */ + public function testBuildShouldFailOnArgumentError() + { + $this->argumentDefinitionShouldThrowOn('throw'); + + $this->builder->build('@test(throw)'); + } + + protected function argumentDefinitionShouldBuildOn(string ...$definitions) + { + $map = []; + + foreach ($definitions as $definition) { + $map[] = [$definition, true, $definition]; + } + + $this->argument_builder->method('build') + ->willReturnMap($map); + } + + protected function argumentDefinitionShouldThrowOn($definition) + { + $this->argument_builder->method('build') + ->with($definition, true) + ->willThrowException(new ArgumentValueBuilderException('ArgumentValueBuilderException exception for testing')); + } +} diff --git a/tests/Specs/Builder/ArgumentValueBuilderTest.php b/tests/Specs/Builder/ArgumentValueBuilderTest.php new file mode 100644 index 0000000..1cfb2f0 --- /dev/null +++ b/tests/Specs/Builder/ArgumentValueBuilderTest.php @@ -0,0 +1,93 @@ +build($raw, false); + $this->assertSame($expected, $value); + } + + /** + * @expectedException \Pinepain\JsSandbox\Specs\Builder\Exceptions\ArgumentValueBuilderException + * @expectedExceptionMessage Unknown value format 'garbage' + */ + public function testBuildingInvalidWithoutLiteralShouldThrow() + { + $builder = new ArgumentValueBuilder(); + + $builder->build('garbage', false); + } + + public function testBuildingInvalidWithLiteralShouldReturnStringifiedLiteral() + { + $builder = new ArgumentValueBuilder(); + + $value = $builder->build('garbage', true); + $this->assertSame('garbage', $value); + } + + /** + * @expectedException \Pinepain\JsSandbox\Specs\Builder\Exceptions\ArgumentValueBuilderException + * @expectedExceptionMessage Unknown value format 'x' + */ + public function testBuildingInvalidShortShouldThrow() + { + $builder = new ArgumentValueBuilder(); + + $builder->build('x', false); + } + + public function provideValidValues() + { + return [ + ['42', 42], + ['-1', -1], + ['-1.0', -1.0], + ['1.2345', 1.2345], + ['[]', []], + ['[ ]', []], + ['{}', []], + ['{ }', []], + ['null', null], + ['Null', null], + ['NulL', null], + ['true', true], + ['True', true], + ['TruE', true], + ['false', false], + ['False', false], + ['FalsE', false], + ['"string"', 'string'], + ['"StrInG"', 'StrInG'], + ["'string'", 'string'], + ["'StrInG'", 'StrInG'], + ['"str\'ing"', 'str\'ing'], + ["'str\"ing'", "str\"ing"], + ["' string '", ' string '], + ['" string "', ' string '], + ["''", ''], + ['""', ''], + ["'123'", '123'], + ['"123"', '123'], + ["'-123.456'", '-123.456'], + ['"-123.456"', '-123.456'], + ]; + } +} diff --git a/tests/Specs/Builder/FunctionSpecBuilderTest.php b/tests/Specs/Builder/FunctionSpecBuilderTest.php index 329a25b..b5b01cb 100644 --- a/tests/Specs/Builder/FunctionSpecBuilderTest.php +++ b/tests/Specs/Builder/FunctionSpecBuilderTest.php @@ -18,6 +18,8 @@ use PHPUnit\Framework\TestCase; use PHPUnit_Framework_MockObject_MockObject; +use Pinepain\JsSandbox\Decorators\DecoratorSpecBuilderInterface; +use Pinepain\JsSandbox\Decorators\DecoratorSpecInterface; use Pinepain\JsSandbox\Specs\Builder\FunctionSpecBuilder; use Pinepain\JsSandbox\Specs\Builder\FunctionSpecBuilderInterface; use Pinepain\JsSandbox\Specs\Builder\ParameterSpecBuilderInterface; @@ -34,6 +36,10 @@ class FunctionSpecBuilderTest extends TestCase */ protected $builder; + /** + * @var DecoratorSpecBuilderInterface|PHPUnit_Framework_MockObject_MockObject + */ + protected $decorators_builder; /** * @var ParameterSpecBuilderInterface|PHPUnit_Framework_MockObject_MockObject */ @@ -41,9 +47,10 @@ class FunctionSpecBuilderTest extends TestCase public function setUp() { + $this->decorators_builder = $this->getMockForAbstractClass(DecoratorSpecBuilderInterface::class); $this->parameters_builder = $this->getMockForAbstractClass(ParameterSpecBuilderInterface::class); - $this->builder = new FunctionSpecBuilder($this->parameters_builder); + $this->builder = new FunctionSpecBuilder($this->decorators_builder, $this->parameters_builder); } /** @@ -67,34 +74,26 @@ public function testBuildingFromInvalidStringShouldThrow() public function testBuildEmptySpec() { $spec = $this->builder->build('()'); - $this->assertFalse($spec->needsExecutionContext()); - $this->assertInstanceOf(FunctionSpecInterface::class, $spec); - } - - public function testBuildSpecThatNeedsExecutionContext() - { - $spec = $this->builder->build('!()'); - $this->assertTrue($spec->needsExecutionContext()); + $this->assertSame([], $spec->getDecorators()); $this->assertInstanceOf(FunctionSpecInterface::class, $spec); } - // Test return type public function testBuildingSpecWithVoidReturnType() { - $spec = $this->builder->build('void ()'); + $spec = $this->builder->build('(): void'); $this->assertInstanceOf(FunctionSpecInterface::class, $spec); - $this->assertFalse($spec->needsExecutionContext()); + $this->assertSame([], $spec->getDecorators()); $this->assertInstanceOf(VoidReturnSpec::class, $spec->getReturn()); } public function testBuildingSpecWithAnyReturnType() { - $spec = $this->builder->build('any ()'); + $spec = $this->builder->build('(): any'); $this->assertInstanceOf(FunctionSpecInterface::class, $spec); - $this->assertFalse($spec->needsExecutionContext()); + $this->assertSame([], $spec->getDecorators()); $this->assertInstanceOf(AnyReturnSpec::class, $spec->getReturn()); } @@ -103,30 +102,30 @@ public function testBuildingSpecWithNoReturnTypeIsTheSameAsWithAnyReturnType() $spec = $this->builder->build('()'); $this->assertInstanceOf(FunctionSpecInterface::class, $spec); - $this->assertFalse($spec->needsExecutionContext()); + $this->assertSame([], $spec->getDecorators()); $this->assertInstanceOf(AnyReturnSpec::class, $spec->getReturn()); } public function testBuildSpecWithParams() { - $this->parameterSpecBuilderShouldBuildOn('one: param', 'two = "default": param', '...params: rest'); + $this->parameterBuilderShouldBuildOn('one: param', 'two = "default": param', '...params: rest'); $spec = $this->builder->build('(one: param, two = "default": param, ...params: rest)'); $this->assertInstanceOf(FunctionSpecInterface::class, $spec); - $this->assertFalse($spec->needsExecutionContext()); + $this->assertSame([], $spec->getDecorators()); $this->assertContainsOnlyInstancesOf(ParameterSpecInterface::class, $spec->getParameters()->getParameters()); $this->assertCount(3, $spec->getParameters()->getParameters()); } public function testBuildSpecWithNullableParams() { - $this->parameterSpecBuilderShouldBuildOn('one: param', 'two?: param'); + $this->parameterBuilderShouldBuildOn('one: param', 'two?: param'); $spec = $this->builder->build('(one: param, two?: param)'); $this->assertInstanceOf(FunctionSpecInterface::class, $spec); - $this->assertFalse($spec->needsExecutionContext()); + $this->assertSame([], $spec->getDecorators()); $this->assertContainsOnlyInstancesOf(ParameterSpecInterface::class, $spec->getParameters()->getParameters()); $this->assertCount(2, $spec->getParameters()->getParameters()); } @@ -138,29 +137,86 @@ public function testBuildingSpecWithoutThrows() $this->assertInstanceOf(FunctionSpecInterface::class, $spec); - $this->assertFalse($spec->needsExecutionContext()); + $this->assertSame([], $spec->getDecorators()); $this->assertEmpty($spec->getExceptions()->getThrowSpecs()); } public function testBuildingSpecWithSingleThrows() { - $spec = $this->builder->build('() Test'); + $spec = $this->builder->build('() throws Test'); $this->assertInstanceOf(FunctionSpecInterface::class, $spec); - $this->assertFalse($spec->needsExecutionContext()); + $this->assertSame([], $spec->getDecorators()); $this->assertCount(1, $spec->getExceptions()->getThrowSpecs()); } + + public function testBuildingSpecWithMultipleThrows() + { + $spec = $this->builder->build('() throws Test, Foo, Bar'); + + $this->assertInstanceOf(FunctionSpecInterface::class, $spec); + $this->assertSame([], $spec->getDecorators()); + $this->assertCount(3, $spec->getExceptions()->getThrowSpecs()); + } + /** * @expectedException \Pinepain\JsSandbox\Specs\Builder\Exceptions\FunctionSpecBuilderException * @expectedExceptionMessage Invalid return type: 'invalid' */ public function testBuildingSpecWithInvalidReturnTypeStringShouldThrow() { - $this->builder->build('invalid ()'); + $this->builder->build('(): invalid'); + } + + public function testBuildSpecWithDecorator() + { + $this->decoratorBuilderShouldBuildOn('@test'); + + $spec = $this->builder->build('@test ()'); + $this->assertInstanceOf(FunctionSpecInterface::class, $spec); + $this->assertCount(1, $spec->getDecorators()); + + $this->assertInstanceOf(DecoratorSpecInterface::class, $spec->getDecorators()[0]); } - protected function parameterSpecBuilderShouldBuildOn(string ...$definitions) + public function testBuildSpecWithDashedDecorator() + { + $this->decoratorBuilderShouldBuildOn('@inject-context'); + + $spec = $this->builder->build('@inject-context (id: string)'); + $this->assertInstanceOf(FunctionSpecInterface::class, $spec); + $this->assertCount(1, $spec->getDecorators()); + + $this->assertInstanceOf(DecoratorSpecInterface::class, $spec->getDecorators()[0]); + } + + public function testBuildSpecWithMultipleDecorators() + { + $this->decoratorBuilderShouldBuildOn('@first', '@second'); + + $spec = $this->builder->build('@first @second ()'); + $this->assertInstanceOf(FunctionSpecInterface::class, $spec); + $this->assertCount(2, $spec->getDecorators()); + + $this->assertInstanceOf(DecoratorSpecInterface::class, $spec->getDecorators()[0]); + $this->assertInstanceOf(DecoratorSpecInterface::class, $spec->getDecorators()[1]); + } + + public function testBuildSpecWithMultipleDecoratorsMultiLine() + { + $this->decoratorBuilderShouldBuildOn('@first(true)', '@second(1, 2, [], {})', '@third', '@forth()'); + + $spec = $this->builder->build(' + @first(true) + @second(1, 2, [], {}) + @third @forth() + ()'); + $this->assertInstanceOf(FunctionSpecInterface::class, $spec); + $this->assertCount(4, $spec->getDecorators()); + } + + protected function parameterBuilderShouldBuildOn(string ...$definitions) { $map = []; @@ -173,4 +229,18 @@ protected function parameterSpecBuilderShouldBuildOn(string ...$definitions) $this->parameters_builder->method('build') ->willReturnMap($map); } + + protected function decoratorBuilderShouldBuildOn(string ...$definitions) + { + $map = []; + + foreach ($definitions as $definition) { + $spec = $this->getMockForAbstractClass(DecoratorSpecInterface::class); + + $map[] = [$definition, $spec]; + } + + $this->decorators_builder->method('build') + ->willReturnMap($map); + } } diff --git a/tests/Specs/Builder/ParameterSpecBuilderTest.php b/tests/Specs/Builder/ParameterSpecBuilderTest.php index a672bf3..2308826 100644 --- a/tests/Specs/Builder/ParameterSpecBuilderTest.php +++ b/tests/Specs/Builder/ParameterSpecBuilderTest.php @@ -21,6 +21,8 @@ use Pinepain\JsSandbox\Extractors\Definition\ExtractorDefinitionInterface; use Pinepain\JsSandbox\Extractors\ExtractorDefinitionBuilderException; use Pinepain\JsSandbox\Extractors\ExtractorDefinitionBuilderInterface; +use Pinepain\JsSandbox\Specs\Builder\ArgumentValueBuilderInterface; +use Pinepain\JsSandbox\Specs\Builder\Exceptions\ArgumentValueBuilderException; use Pinepain\JsSandbox\Specs\Builder\ParameterSpecBuilder; use Pinepain\JsSandbox\Specs\Builder\ParameterSpecBuilderInterface; use Pinepain\JsSandbox\Specs\Parameters\MandatoryParameterSpec; @@ -35,6 +37,11 @@ class ParameterSpecBuilderTest extends TestCase */ protected $builder; + /** + * @var ArgumentValueBuilderInterface|PHPUnit_Framework_MockObject_MockObject + */ + protected $argument_builder; + /** * @var ExtractorDefinitionBuilderInterface|PHPUnit_Framework_MockObject_MockObject */ @@ -42,9 +49,10 @@ class ParameterSpecBuilderTest extends TestCase public function setUp() { + $this->argument_builder = $this->getMockForAbstractClass(ArgumentValueBuilderInterface::class); $this->definition_builder = $this->getMockForAbstractClass(ExtractorDefinitionBuilderInterface::class); - $this->builder = new ParameterSpecBuilder($this->definition_builder); + $this->builder = new ParameterSpecBuilder($this->definition_builder, $this->argument_builder); } /** @@ -65,6 +73,18 @@ public function testBuildingFromInvalidStringShouldThrow() $this->builder->build('!invalid!'); } + public function testBuildingMandatoryParameterWithoutType() + { + $this->extractorDefinitionShouldBuildOn('any'); + + $spec = $this->builder->build('param'); + + $this->assertInstanceOf(MandatoryParameterSpec::class, $spec); + + $this->assertSame('param', $spec->getName()); + $this->assertInstanceOf(ExtractorDefinitionInterface::class, $spec->getExtractorDefinition()); + } + public function testBuildingMandatoryParameter() { $this->extractorDefinitionShouldBuildOn('type'); @@ -101,25 +121,31 @@ public function testBuildingMandatoryParameterWithVaryingType() $this->assertInstanceOf(ExtractorDefinitionInterface::class, $spec->getExtractorDefinition()); } - /** - * @param string $raw_default - * @param $expected_default - * - * @dataProvider provideValidDefaultValues - */ - public function testBuildingOptionalParameter(string $raw_default, $expected_default) + public function testBuildingOptionalParameter() { - $this->extractorDefinitionShouldBuildOn('type'); + $this->argumentDefinitionShouldBuildOn('"default"'); - $spec = $this->builder->build('param = ' . $raw_default . ': type'); + $spec = $this->builder->build('param = "default"'); $this->assertInstanceOf(OptionalParameterSpec::class, $spec); $this->assertSame('param', $spec->getName()); - $this->assertSame($expected_default, $spec->getDefaultValue()); + $this->assertSame('"default"', $spec->getDefaultValue()); $this->assertInstanceOf(ExtractorDefinitionInterface::class, $spec->getExtractorDefinition()); } + /** + * @expectedException \Pinepain\JsSandbox\Specs\Builder\Exceptions\ParameterSpecBuilderException + * @expectedExceptionMessage Unknown or unsupported default value format '"throw"' + */ + public function testBuildingOptionalParameterShouldThrowOnInvalidArgumentValue() + { + $this->argumentDefinitionShouldThrowOn('"throw"'); + + $this->builder->build('param = "throw"'); + } + + public function testBuildingVariadicParameter() { $this->extractorDefinitionShouldBuildOn('type'); @@ -183,42 +209,18 @@ public function testBuildingWhenExtractorFailsShouldAlsoFail() $this->builder->build('param :fail'); } + protected function argumentDefinitionShouldBuildOn($name) + { + $this->argument_builder->method('build') + ->with($name, false) + ->willReturn($name); + } - public function provideValidDefaultValues() - { - return [ - ['42', 42], - ['-1', -1], - ['-1.0', -1.0], - ['1.2345', 1.2345], - ['[]', []], - ['[ ]', []], - ['{}', []], - ['{ }', []], - ['null', null], - ['Null', null], - ['NulL', null], - ['true', true], - ['True', true], - ['TruE', true], - ['false', false], - ['False', false], - ['FalsE', false], - ['"string"', 'string'], - ['"StrInG"', 'StrInG'], - ["'string'", 'string'], - ["'StrInG'", 'StrInG'], - ['"str\'ing"', 'str\'ing'], - ["'str\"ing'", "str\"ing"], - ["' string '", ' string '], - ['" string "', ' string '], - ["''", ''], - ['""', ''], - ["'123'", '123'], - ['"123"', '123'], - ["'-123.456'", '-123.456'], - ['"-123.456"', '-123.456'], - ]; + protected function argumentDefinitionShouldThrowOn($name) + { + $this->argument_builder->method('build') + ->with($name, false) + ->willThrowException(new ArgumentValueBuilderException('ArgumentValueBuilderException exception for testing')); } protected function extractorDefinitionShouldBuildOn($name) From a8cd558b8f6fd8b7c2a3634cf28209f04b13f50a Mon Sep 17 00:00:00 2001 From: Bogdan Padalko Date: Sun, 5 Nov 2017 20:07:22 +0200 Subject: [PATCH 04/14] Pass runtime function to allow on the fly callback swap --- src/Wrappers/FunctionComponents/FunctionCallHandler.php | 9 ++++++--- .../FunctionComponents/FunctionCallHandlerInterface.php | 3 ++- src/Wrappers/FunctionWrapper.php | 2 +- 3 files changed, 9 insertions(+), 5 deletions(-) diff --git a/src/Wrappers/FunctionComponents/FunctionCallHandler.php b/src/Wrappers/FunctionComponents/FunctionCallHandler.php index 40eb426..b71a89c 100644 --- a/src/Wrappers/FunctionComponents/FunctionCallHandler.php +++ b/src/Wrappers/FunctionComponents/FunctionCallHandler.php @@ -16,8 +16,8 @@ namespace Pinepain\JsSandbox\Wrappers\FunctionComponents; -use Pinepain\JsSandbox\Specs\FunctionSpecInterface; use Pinepain\JsSandbox\Wrappers\FunctionComponents\Runtime\ColdExecutionContextInterface; +use Pinepain\JsSandbox\Wrappers\Runtime\RuntimeFunctionInterface; use Throwable; use V8\FunctionCallbackInfo; @@ -59,9 +59,12 @@ public function __construct( $this->return_setter = $return_setter; } - public function wrap(callable $callback, FunctionSpecInterface $spec, ColdExecutionContextInterface $cold_execution_context) + public function wrap(RuntimeFunctionInterface $function, ColdExecutionContextInterface $cold_execution_context) { - return function (FunctionCallbackInfo $args) use ($callback, $spec, $cold_execution_context) { + return function (FunctionCallbackInfo $args) use ($function, $cold_execution_context) { + $spec = $function->getSpec(); + $callback = $function->getCallback(); + $arguments = $this->arguments_extractor->extract($args, $spec); if ($spec->getDecorators()) { diff --git a/src/Wrappers/FunctionComponents/FunctionCallHandlerInterface.php b/src/Wrappers/FunctionComponents/FunctionCallHandlerInterface.php index b10fb34..ed281b3 100644 --- a/src/Wrappers/FunctionComponents/FunctionCallHandlerInterface.php +++ b/src/Wrappers/FunctionComponents/FunctionCallHandlerInterface.php @@ -18,9 +18,10 @@ use Pinepain\JsSandbox\Specs\FunctionSpecInterface; use Pinepain\JsSandbox\Wrappers\FunctionComponents\Runtime\ColdExecutionContextInterface; +use Pinepain\JsSandbox\Wrappers\Runtime\RuntimeFunctionInterface; interface FunctionCallHandlerInterface { - public function wrap(callable $callback, FunctionSpecInterface $spec, ColdExecutionContextInterface $cold_execution_context); + public function wrap(RuntimeFunctionInterface $function, ColdExecutionContextInterface $cold_execution_context); } diff --git a/src/Wrappers/FunctionWrapper.php b/src/Wrappers/FunctionWrapper.php index 9971079..c3c4666 100644 --- a/src/Wrappers/FunctionWrapper.php +++ b/src/Wrappers/FunctionWrapper.php @@ -77,7 +77,7 @@ public function wrap(Isolate $isolate, Context $context, $value): FunctionObject $cold_execution_context = new ColdExecutionContext($this->wrapper, $value); - $callback = $this->handler->wrap($value->getCallback(), $value->getSpec(), $cold_execution_context); + $callback = $this->handler->wrap($value, $cold_execution_context); $callback = $this->guard->guard($callback); $f = new FunctionObject($context, $callback); From a467bd9558e5e6e1909b20e1fe633f29fcf7b046 Mon Sep 17 00:00:00 2001 From: Bogdan Padalko Date: Sun, 5 Nov 2017 20:19:05 +0200 Subject: [PATCH 05/14] Put arguments extractor after decoration to match failures prioriy --- src/Wrappers/FunctionComponents/FunctionCallHandler.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Wrappers/FunctionComponents/FunctionCallHandler.php b/src/Wrappers/FunctionComponents/FunctionCallHandler.php index b71a89c..2324c03 100644 --- a/src/Wrappers/FunctionComponents/FunctionCallHandler.php +++ b/src/Wrappers/FunctionComponents/FunctionCallHandler.php @@ -65,8 +65,6 @@ public function wrap(RuntimeFunctionInterface $function, ColdExecutionContextInt $spec = $function->getSpec(); $callback = $function->getCallback(); - $arguments = $this->arguments_extractor->extract($args, $spec); - if ($spec->getDecorators()) { // When we have decorators, we need executions context. // Execution context is simple and abstract way to write advanced functions which relies on existent @@ -77,6 +75,8 @@ public function wrap(RuntimeFunctionInterface $function, ColdExecutionContextInt $callback = $this->decorator->decorate($callback, $spec, $exec); } + $arguments = $this->arguments_extractor->extract($args, $spec); + try { $ret = $callback(...$arguments); } catch (Throwable $e) { From 42cb00686d9280dedfae25d34cfeafb05a404f38 Mon Sep 17 00:00:00 2001 From: Bogdan Padalko Date: Sun, 5 Nov 2017 21:47:38 +0200 Subject: [PATCH 06/14] Rename array => [] extractor to match convention, #6 --- src/Laravel/JsSandboxServiceProvider.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Laravel/JsSandboxServiceProvider.php b/src/Laravel/JsSandboxServiceProvider.php index 9f5ffa1..51b32c5 100644 --- a/src/Laravel/JsSandboxServiceProvider.php +++ b/src/Laravel/JsSandboxServiceProvider.php @@ -256,8 +256,8 @@ protected function registerExtractor() // TODO: register basic extractor + $collection->put('[]', $array = new ArrayExtractor()); $collection->put('raw', $raw = new RawExtractor()); - $collection->put('array', $array = new ArrayExtractor()); $collection->put('primitive', $primitive = new PrimitiveExtractor()); $collection->put('string', $string = new StringExtractor()); From 0be1e186d46a4aeb5359f9eecdaf1e2c2453f31c Mon Sep 17 00:00:00 2001 From: Bogdan Padalko Date: Sun, 5 Nov 2017 21:48:50 +0200 Subject: [PATCH 07/14] Add dashes support in extractor name --- src/Laravel/JsSandboxServiceProvider.php | 2 +- src/Specs/Builder/ParameterSpecBuilder.php | 2 +- tests/Specs/Builder/ParameterSpecBuilderTest.php | 12 ++++++++++++ 3 files changed, 14 insertions(+), 2 deletions(-) diff --git a/src/Laravel/JsSandboxServiceProvider.php b/src/Laravel/JsSandboxServiceProvider.php index 51b32c5..9574446 100644 --- a/src/Laravel/JsSandboxServiceProvider.php +++ b/src/Laravel/JsSandboxServiceProvider.php @@ -271,7 +271,7 @@ protected function registerExtractor() $collection->put('datetime', $datetime = new DateTimeExtractor()); $collection->put('object', $object = new ObjectExtractor()); $collection->put('function', $function = new FunctionExtractor()); - $collection->put('native_object', $instance = $app->make(NativeObjectExtractor::class)); + $collection->put('native-object', $instance = $app->make(NativeObjectExtractor::class)); $collection->put('assoc', $assoc = new AssocExtractor()); $collection->put('json', $json = new JsonExtractor()); diff --git a/src/Specs/Builder/ParameterSpecBuilder.php b/src/Specs/Builder/ParameterSpecBuilder.php index a83369d..bf03ba8 100644 --- a/src/Specs/Builder/ParameterSpecBuilder.php +++ b/src/Specs/Builder/ParameterSpecBuilder.php @@ -60,7 +60,7 @@ class ParameterSpecBuilder implements ParameterSpecBuilderInterface \s* \: \s* - (?(\w*(?:\(.*\))?(?:\[\s*\])?)(?:\s*\|\s*(?-1))*) + (?([\w\-]*(?:\(.*\))?(?:\[\s*\])?)(?:\s*\|\s*(?-1))*) \s* )? $ diff --git a/tests/Specs/Builder/ParameterSpecBuilderTest.php b/tests/Specs/Builder/ParameterSpecBuilderTest.php index 2308826..cd3a879 100644 --- a/tests/Specs/Builder/ParameterSpecBuilderTest.php +++ b/tests/Specs/Builder/ParameterSpecBuilderTest.php @@ -97,6 +97,18 @@ public function testBuildingMandatoryParameter() $this->assertInstanceOf(ExtractorDefinitionInterface::class, $spec->getExtractorDefinition()); } + public function testBuildingMandatoryParameterWithDashedType() + { + $this->extractorDefinitionShouldBuildOn('type-dash'); + + $spec = $this->builder->build('param: type-dash'); + + $this->assertInstanceOf(MandatoryParameterSpec::class, $spec); + + $this->assertSame('param', $spec->getName()); + $this->assertInstanceOf(ExtractorDefinitionInterface::class, $spec->getExtractorDefinition()); + } + public function testBuildingMandatoryParameterWithComplexType() { $this->extractorDefinitionShouldBuildOn('instance( Some\Class )'); From 2b62e5299485256b9269ad8dc84c5b128564eb38 Mon Sep 17 00:00:00 2001 From: Bogdan Padalko Date: Sun, 5 Nov 2017 22:21:04 +0200 Subject: [PATCH 08/14] Allow to restrict assoc to js array object only --- .../PlainExtractors/ArrayExtractor.php | 70 +++-------- .../PlainExtractors/AssocExtractor.php | 116 ++++++++++++++---- src/Laravel/JsSandboxServiceProvider.php | 5 +- 3 files changed, 112 insertions(+), 79 deletions(-) diff --git a/src/Extractors/PlainExtractors/ArrayExtractor.php b/src/Extractors/PlainExtractors/ArrayExtractor.php index 843b9b5..8359899 100644 --- a/src/Extractors/PlainExtractors/ArrayExtractor.php +++ b/src/Extractors/PlainExtractors/ArrayExtractor.php @@ -21,73 +21,31 @@ use Pinepain\JsSandbox\Extractors\ExtractorInterface; use V8\ArrayObject; use V8\Context; -use V8\IntegerValue; -use V8\ObjectValue; use V8\Value; class ArrayExtractor implements PlainExtractorInterface { + /** + * @var AssocExtractor + */ + private $extractor; + + /** + * @inheritDoc + */ + public function __construct(AssocExtractor $extractor) + { + $this->extractor = $extractor; + } + /** * {@inheritdoc} */ public function extract(Context $context, Value $value, PlainExtractorDefinitionInterface $definition, ExtractorInterface $extractor) { if ($value instanceof ArrayObject) { - $length = $value->length(); - $isolate = $context->getIsolate(); - - $out = []; - - $next = $definition->getNext(); - - for ($i = 0; $i < $length; $i++) { - $item = $value->get($context, new IntegerValue($isolate, $i)); - - if ($next) { - try { - $out[] = $extractor->extract($context, $item, $next); - } catch (ExtractorException $e) { - throw new ExtractorException("Failed to convert array item #{$i}: " . $e->getMessage()); - } - } else { - $out[] = $item; - } - } - - return $out; - } - - if ($value instanceof ObjectValue) { - $own_properties = $value->getOwnPropertyNames($context); - - $length = $own_properties->length(); - $isolate = $context->getIsolate(); - - $out = []; - - $next = $definition->getNext(); - - for ($i = 0; $i < $length; $i++) { - /** @var \V8\PrimitiveValue $prop */ - $prop = $own_properties->get($context, new IntegerValue($isolate, $i)); - $item = $value->get($context, $prop); - - $prop_name = $prop->value(); - - if ($next) { - try { - $out[$prop_name] = $extractor->extract($context, $item, $next); - } catch (ExtractorException $e) { - throw new ExtractorException("Failed to convert array item #{$prop_name}: " . $e->getMessage()); - } - } else { - $out[$prop_name] = $item; - } - } - - return $out; - + return $this->extractor->extract($context, $value, $definition, $extractor); } throw new ExtractorException('Value must be of array type, ' . $value->typeOf()->value() . ' given instead'); diff --git a/src/Extractors/PlainExtractors/AssocExtractor.php b/src/Extractors/PlainExtractors/AssocExtractor.php index 92c7845..3d9a1b6 100644 --- a/src/Extractors/PlainExtractors/AssocExtractor.php +++ b/src/Extractors/PlainExtractors/AssocExtractor.php @@ -19,6 +19,7 @@ use Pinepain\JsSandbox\Extractors\Definition\PlainExtractorDefinitionInterface; use Pinepain\JsSandbox\Extractors\ExtractorException; use Pinepain\JsSandbox\Extractors\ExtractorInterface; +use V8\ArrayObject; use V8\Context; use V8\IntegerValue; use V8\ObjectValue; @@ -27,43 +28,116 @@ class AssocExtractor implements PlainExtractorInterface { + /** + * @var bool + */ + private $array_with_props; + + public function __construct(bool $array_with_props = true) + { + $this->array_with_props = $array_with_props; + } + /** * {@inheritdoc} */ public function extract(Context $context, Value $value, PlainExtractorDefinitionInterface $definition, ExtractorInterface $extractor) { - if ($value instanceof ObjectValue) { - $own_properties = $value->getOwnPropertyNames($context); + if ($value instanceof ArrayObject) { + $items = $this->extractArrayValues($context, $value, $definition, $extractor); - $length = $own_properties->length(); - $isolate = $context->getIsolate(); + if (!$this->array_with_props) { + return $items; + } - $out = []; + $props = $this->extractObjectValues($context, $value, $definition, $extractor); - $next = $definition->getNext(); + // length is a built-in property which we are not interested here + unset($props['length']); - for ($i = 0; $i < $length; $i++) { - /** @var \V8\PrimitiveValue $prop */ - $prop = $own_properties->get($context, new IntegerValue($isolate, $i)); - $item = $value->get($context, $prop); + return array_merge($items, $props); + } - $prop_name = $prop->value(); + if ($value instanceof ObjectValue) { + return $this->extractObjectValues($context, $value, $definition, $extractor); + } + + throw new ExtractorException('Value must be of array or object type, ' . $value->typeOf()->value() . ' given instead'); + } + + /** + * @param Context $context + * @param ArrayObject $value + * @param PlainExtractorDefinitionInterface $definition + * @param ExtractorInterface $extractor + * + * @return array + * @throws ExtractorException + */ + protected function extractArrayValues(Context $context, ArrayObject $value, PlainExtractorDefinitionInterface $definition, ExtractorInterface $extractor): array + { + $out = []; + $length = $value->length(); + $isolate = $context->getIsolate(); - if ($next) { - try { - $out[$prop_name] = $extractor->extract($context, $item, $definition); - } catch (ExtractorException $e) { - throw new ExtractorException("Failed to convert assoc item #{$prop_name}: " . $e->getMessage()); - } - } else { - $out[$prop_name] = $item; + $next = $definition->getNext(); + + for ($i = 0; $i < $length; $i++) { + $item = $value->get($context, new IntegerValue($isolate, $i)); + + if ($next) { + try { + $out[] = $extractor->extract($context, $item, $next); + } catch (ExtractorException $e) { + throw new ExtractorException("Failed to convert array item #{$i}: " . $e->getMessage()); } + } else { + $out[] = $item; } + } + + return $out; + } + + /** + * @param Context $context + * @param ObjectValue $value + * @param PlainExtractorDefinitionInterface $definition + * @param ExtractorInterface $extractor + * + * @return array + * @throws ExtractorException + */ + protected function extractObjectValues(Context $context, ObjectValue $value, PlainExtractorDefinitionInterface $definition, ExtractorInterface $extractor): array + { + $own_properties = $value->getOwnPropertyNames($context); + + $length = $own_properties->length(); + $isolate = $context->getIsolate(); + + $out = []; + + $next = $definition->getNext(); - return $out; + for ($i = 0; $i < $length; $i++) { + /** @var \V8\PrimitiveValue $prop */ + $prop = $own_properties->get($context, new IntegerValue($isolate, $i)); + $item = $value->get($context, $prop); + $prop_name = $prop->value(); + + if ($next) { + try { + $out[$prop_name] = $extractor->extract($context, $item, $next); + } catch (ExtractorException $e) { + throw new ExtractorException("Failed to convert array item #{$prop_name}: " . $e->getMessage()); + } + } else { + $out[$prop_name] = $item; + } } - throw new ExtractorException('Value must be of object type, ' . $value->typeOf()->value() . ' given instead'); + return $out; } + } diff --git a/src/Laravel/JsSandboxServiceProvider.php b/src/Laravel/JsSandboxServiceProvider.php index 9574446..942e04f 100644 --- a/src/Laravel/JsSandboxServiceProvider.php +++ b/src/Laravel/JsSandboxServiceProvider.php @@ -256,7 +256,9 @@ protected function registerExtractor() // TODO: register basic extractor - $collection->put('[]', $array = new ArrayExtractor()); + $collection->put('[]', $array = new AssocExtractor()); + $collection->put('array', $array = new ArrayExtractor(new AssocExtractor(false))); + $collection->put('raw', $raw = new RawExtractor()); $collection->put('primitive', $primitive = new PrimitiveExtractor()); @@ -273,7 +275,6 @@ protected function registerExtractor() $collection->put('function', $function = new FunctionExtractor()); $collection->put('native-object', $instance = $app->make(NativeObjectExtractor::class)); - $collection->put('assoc', $assoc = new AssocExtractor()); $collection->put('json', $json = new JsonExtractor()); $collection->put('jsonable', $json = new JsonableExtractor()); From 99fdf8a4b8aba9161e0c1bb167a7545034f2b32c Mon Sep 17 00:00:00 2001 From: Bogdan Padalko Date: Sun, 5 Nov 2017 22:28:52 +0200 Subject: [PATCH 09/14] Update modules to follow new papameter definition notation, #7 --- src/Modules/ModulesService.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Modules/ModulesService.php b/src/Modules/ModulesService.php index ef6a3fa..2f0ba62 100644 --- a/src/Modules/ModulesService.php +++ b/src/Modules/ModulesService.php @@ -85,11 +85,11 @@ protected function createFunctionObject(Context $context, RuntimeFunctionInterfa protected function createRuntimeFunction(RequireCallbackInterface $require_object): RuntimeFunctionInterface { - $require_spec = $this->function->build('!(string id)'); + $require_spec = $this->function->build('@inject-context (id: string)'); $require_function_object_spec = new AnonymousObjectSpec($this->object->build([ 'main' => 'get: getMain()', - 'resolve' => '(string id)', + 'resolve' => '(id: string)', ])); return new RuntimeFunction('require', [$require_object, 'callback'], $require_spec, $require_object, $require_function_object_spec); From 928a4f4ff54817e57ee0139d4b8e9e2fe968e198 Mon Sep 17 00:00:00 2001 From: Bogdan Padalko Date: Sun, 5 Nov 2017 22:35:10 +0200 Subject: [PATCH 10/14] Value constraint should not affect object extractor result --- src/Extractors/PlainExtractors/ObjectExtractor.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Extractors/PlainExtractors/ObjectExtractor.php b/src/Extractors/PlainExtractors/ObjectExtractor.php index 64b5e99..24939ef 100644 --- a/src/Extractors/PlainExtractors/ObjectExtractor.php +++ b/src/Extractors/PlainExtractors/ObjectExtractor.php @@ -36,7 +36,7 @@ public function extract(Context $context, Value $value, PlainExtractorDefinition if ($definition->getNext()) { // we have value constraint try { - return $extractor->extract($context, $value, $definition->getNext()); + $extractor->extract($context, $value, $definition->getNext()); } catch (ExtractorException $e) { throw new ExtractorException('Object value constraint failed: ' . $e->getMessage()); } From 40cc8a5d275e531d96554af32601acf65ce4e5bf Mon Sep 17 00:00:00 2001 From: Bogdan Padalko Date: Sun, 5 Nov 2017 23:00:46 +0200 Subject: [PATCH 11/14] Add "any" extractor and make it default for [] Vast majority cases of [] usage means that so let's make it shorter --- .../RecursiveExtractorDefinition.php | 25 +++++++++ src/Extractors/ExtractorDefinitionBuilder.php | 25 +++++++-- .../PlainExtractors/AnyExtractor.php | 53 +++++++++++++++++++ src/Laravel/JsSandboxServiceProvider.php | 4 +- .../ExtractorDefinitionBuilderTest.php | 17 ++++-- 5 files changed, 115 insertions(+), 9 deletions(-) create mode 100644 src/Extractors/Definition/RecursiveExtractorDefinition.php create mode 100644 src/Extractors/PlainExtractors/AnyExtractor.php diff --git a/src/Extractors/Definition/RecursiveExtractorDefinition.php b/src/Extractors/Definition/RecursiveExtractorDefinition.php new file mode 100644 index 0000000..a553f57 --- /dev/null +++ b/src/Extractors/Definition/RecursiveExtractorDefinition.php @@ -0,0 +1,25 @@ + + * + * Licensed under the MIT license: http://opensource.org/licenses/MIT + * + * For the full copyright and license information, please view the + * LICENSE file that was distributed with this source or visit + * http://opensource.org/licenses/MIT + */ + + +namespace Pinepain\JsSandbox\Extractors\Definition; + + +class RecursiveExtractorDefinition extends AbstractExtractorDefinition implements PlainExtractorDefinitionInterface +{ + public function __construct(string $name) + { + parent::__construct($name, $this, [$this]); + } +} diff --git a/src/Extractors/ExtractorDefinitionBuilder.php b/src/Extractors/ExtractorDefinitionBuilder.php index 1367116..8dce918 100644 --- a/src/Extractors/ExtractorDefinitionBuilder.php +++ b/src/Extractors/ExtractorDefinitionBuilder.php @@ -19,6 +19,7 @@ use Pinepain\JsSandbox\Extractors\Definition\ExtractorDefinitionInterface; use Pinepain\JsSandbox\Extractors\Definition\PlainExtractorDefinition; use Pinepain\JsSandbox\Extractors\Definition\PlainExtractorDefinitionInterface; +use Pinepain\JsSandbox\Extractors\Definition\RecursiveExtractorDefinition; use Pinepain\JsSandbox\Extractors\Definition\VariableExtractorDefinition; @@ -93,7 +94,7 @@ public function build(string $definition): ExtractorDefinitionInterface * @param int $depth * @param bool $groups * - * @return ExtractorDefinitionInterface + * @return null|ExtractorDefinitionInterface * @throws ExtractorDefinitionBuilderException */ protected function buildExtractor(string $name, string $param, string $alt_definitions, int $depth, bool $groups): ExtractorDefinitionInterface @@ -105,7 +106,7 @@ protected function buildExtractor(string $name, string $param, string $alt_defin } if ($name) { - $definition = new PlainExtractorDefinition($name, $next); + $definition = $this->buildPlainExtractor($name, $next); } else { $definition = $next; } @@ -162,14 +163,19 @@ protected function buildVariableDefinition(PlainExtractorDefinitionInterface $de */ protected function buildArrayDefinition(?ExtractorDefinitionInterface $definition, int $depth, bool $groups): ExtractorDefinitionInterface { - if (!$definition && $groups) { - throw new ExtractorDefinitionBuilderException('Empty group is not allowed'); + // special case for blank brackets [] which should be the same as any[] + if (!$definition) { + if ($groups) { + throw new ExtractorDefinitionBuilderException('Empty group is not allowed'); + } + + $definition = $this->buildPlainExtractor('any'); } while ($depth) { $depth--; // arrayed definition - $definition = new PlainExtractorDefinition('[]', $definition); + $definition = $this->buildPlainExtractor('[]', $definition); } return $definition; @@ -193,4 +199,13 @@ private function hasGroups(array $matches): bool { return isset($matches['group']) && '' !== $matches['group']; } + + private function buildPlainExtractor(string $name, ?ExtractorDefinitionInterface $next = null): PlainExtractorDefinitionInterface + { + if ('any' === $name && !$next) { + return new RecursiveExtractorDefinition($name); + } + + return new PlainExtractorDefinition($name, $next); + } } diff --git a/src/Extractors/PlainExtractors/AnyExtractor.php b/src/Extractors/PlainExtractors/AnyExtractor.php new file mode 100644 index 0000000..346827f --- /dev/null +++ b/src/Extractors/PlainExtractors/AnyExtractor.php @@ -0,0 +1,53 @@ + + * + * Licensed under the MIT license: http://opensource.org/licenses/MIT + * + * For the full copyright and license information, please view the + * LICENSE file that was distributed with this source or visit + * http://opensource.org/licenses/MIT + */ + + +namespace Pinepain\JsSandbox\Extractors\PlainExtractors; + + +use Pinepain\JsSandbox\Extractors\Definition\PlainExtractorDefinitionInterface; +use Pinepain\JsSandbox\Extractors\ExtractorException; +use Pinepain\JsSandbox\Extractors\ExtractorInterface; +use V8\Context; +use V8\Value; + + +class AnyExtractor implements PlainExtractorInterface +{ + /** + * @var PlainExtractorInterface[] + */ + private $extractors; + + public function __construct(PlainExtractorInterface ...$extractors) + { + $this->extractors = $extractors; + } + + /** + * {@inheritdoc} + */ + public function extract(Context $context, Value $value, PlainExtractorDefinitionInterface $definition, ExtractorInterface $extractor) + { + foreach ($this->extractors as $plain_extractor) { + try { + return $plain_extractor->extract($context, $value, $definition, $extractor); + } catch (ExtractorException $e) { + // + } + } + + throw new ExtractorException('Unable to pick proper extractor for ' . $value->typeOf()->value() . 'type'); + } +} diff --git a/src/Laravel/JsSandboxServiceProvider.php b/src/Laravel/JsSandboxServiceProvider.php index 942e04f..853c0af 100644 --- a/src/Laravel/JsSandboxServiceProvider.php +++ b/src/Laravel/JsSandboxServiceProvider.php @@ -34,6 +34,7 @@ use Pinepain\JsSandbox\Extractors\ExtractorsCollectionInterface; use Pinepain\JsSandbox\Extractors\ObjectComponents\ExtractorsObjectStore; use Pinepain\JsSandbox\Extractors\ObjectComponents\ExtractorsObjectStoreInterface; +use Pinepain\JsSandbox\Extractors\PlainExtractors\AnyExtractor; use Pinepain\JsSandbox\Extractors\PlainExtractors\ArrayExtractor; use Pinepain\JsSandbox\Extractors\PlainExtractors\AssocExtractor; use Pinepain\JsSandbox\Extractors\PlainExtractors\BoolExtractor; @@ -256,7 +257,7 @@ protected function registerExtractor() // TODO: register basic extractor - $collection->put('[]', $array = new AssocExtractor()); + $collection->put('[]', $assoc = new AssocExtractor()); $collection->put('array', $array = new ArrayExtractor(new AssocExtractor(false))); $collection->put('raw', $raw = new RawExtractor()); @@ -279,6 +280,7 @@ protected function registerExtractor() $collection->put('jsonable', $json = new JsonableExtractor()); $collection->put('scalar', $scalar = new ScalarExtractor($string, $number, $bool, $null, $undefined)); + $collection->put('any', $any = new AnyExtractor($scalar, $regexp, $datetime, $assoc)); return $collection; }); diff --git a/tests/Extractors/ExtractorDefinitionBuilderTest.php b/tests/Extractors/ExtractorDefinitionBuilderTest.php index 17ab877..95e29a1 100644 --- a/tests/Extractors/ExtractorDefinitionBuilderTest.php +++ b/tests/Extractors/ExtractorDefinitionBuilderTest.php @@ -57,11 +57,20 @@ public function testBuildingFromEmptyGroupShouldThrowException() * @expectedException \Pinepain\JsSandbox\Extractors\ExtractorDefinitionBuilderException * @expectedExceptionMessage Unable to parse definition: '()[]' */ - public function testBuildingEmptyGroupArrayedDefinition() + public function testBuildingEmptyGroupArrayedDefinitionShouldFail() { $this->builder->build('()[]'); } + /** + * @expectedException \Pinepain\JsSandbox\Extractors\ExtractorDefinitionBuilderException + * @expectedExceptionMessage Unable to parse definition: '((()))[]' + */ + public function testBuildingEmptyGroupArrayedDefinitionWithNestedGroups() + { + $this->builder->build('((()))[]'); + } + public function testBuildingPlainDefinition() { $definition = $this->builder->build('test'); @@ -81,7 +90,8 @@ public function testBuildingEmptyArrayDefinition() $this->assertInstanceOf(PlainExtractorDefinitionInterface::class, $definition); $this->assertSame('[]', $definition->getName()); - $this->assertNull($definition->getNext()); + $this->assertInstanceOf(PlainExtractorDefinitionInterface::class, $definition->getNext()); + $this->assertSame('any', $definition->getNext()->getName()); $this->assertCount(1, $definition->getVariations()); $this->assertSame($definition, $definition->getVariations()[0]); } @@ -100,7 +110,8 @@ public function testBuildingEmptyArrayWithNestedEmptyArrayDefinition() $next = $definition->getNext(); $this->assertSame('[]', $next->getName()); - $this->assertNull($next->getNext()); + $this->assertInstanceOf(PlainExtractorDefinitionInterface::class, $next->getNext()); + $this->assertSame('any', $next->getNext()->getName()); $this->assertCount(1, $next->getVariations()); $this->assertSame($next, $next->getVariations()[0]); } From bf9272485441ea57f554ae4a5d0fdc8f6c1e39f4 Mon Sep 17 00:00:00 2001 From: Bogdan Padalko Date: Sun, 5 Nov 2017 23:31:56 +0200 Subject: [PATCH 12/14] Add parameter type guessing by default value --- src/Extractors/ExtractorDefinitionBuilder.php | 5 - src/Specs/Builder/ParameterSpecBuilder.php | 33 ++++- .../Builder/ParameterSpecBuilderTest.php | 136 +++++++++++++++++- 3 files changed, 165 insertions(+), 9 deletions(-) diff --git a/src/Extractors/ExtractorDefinitionBuilder.php b/src/Extractors/ExtractorDefinitionBuilder.php index 8dce918..6436d81 100644 --- a/src/Extractors/ExtractorDefinitionBuilder.php +++ b/src/Extractors/ExtractorDefinitionBuilder.php @@ -181,11 +181,6 @@ protected function buildArrayDefinition(?ExtractorDefinitionInterface $definitio return $definition; } - /** - * @param array $matches - * - * @return int - */ private function getDepth(array $matches): int { if (!isset($matches['arr']) || '' === $matches['arr']) { diff --git a/src/Specs/Builder/ParameterSpecBuilder.php b/src/Specs/Builder/ParameterSpecBuilder.php index bf03ba8..4c73fc2 100644 --- a/src/Specs/Builder/ParameterSpecBuilder.php +++ b/src/Specs/Builder/ParameterSpecBuilder.php @@ -24,7 +24,6 @@ use Pinepain\JsSandbox\Specs\Parameters\OptionalParameterSpec; use Pinepain\JsSandbox\Specs\Parameters\ParameterSpecInterface; use Pinepain\JsSandbox\Specs\Parameters\VariadicParameterSpec; -use function strlen; class ParameterSpecBuilder implements ParameterSpecBuilderInterface @@ -138,6 +137,10 @@ protected function buildOptionalParameterSpec(array $matches, ?string $default): } catch (ArgumentValueBuilderException $e) { throw new ParameterSpecBuilderException("Unknown or unsupported default value format '{$default_definition}'"); } + + if (!$this->hasType($matches)) { + $matches['type'] = $this->guessTypeFromDefault($default); + } } return new OptionalParameterSpec($matches['name'], $this->extractor->build($matches['type']), $default); @@ -162,8 +165,9 @@ protected function prepareDefinition(array $matches): array throw new ParameterSpecBuilderException('Variadic parameter could have no default value'); } - if (!$this->hasType($matches)) { - $matches['type'] = 'any'; // special case + if (!$this->hasDefault($matches) && !$this->hasType($matches)) { + // special case when no default value set and no type provided + $matches['type'] = 'any'; } return $matches; @@ -188,4 +192,27 @@ private function hasDefault(array $matches): bool { return isset($matches['default']) && '' !== $matches['default']; } + + private function guessTypeFromDefault($default): string + { + if (is_array($default)) { + return '[]'; + } + + if (is_numeric($default)) { + return 'number'; + } + + if (is_bool($default)) { + return 'bool'; + } + + if (is_string($default)) { + return 'string'; + } + + // it looks like we have nullable parameter which could be anything + + return 'any'; + } } diff --git a/tests/Specs/Builder/ParameterSpecBuilderTest.php b/tests/Specs/Builder/ParameterSpecBuilderTest.php index cd3a879..e088de6 100644 --- a/tests/Specs/Builder/ParameterSpecBuilderTest.php +++ b/tests/Specs/Builder/ParameterSpecBuilderTest.php @@ -210,6 +210,118 @@ public function testBuildingNullableParameter() $this->assertInstanceOf(ExtractorDefinitionInterface::class, $spec->getExtractorDefinition()); } + public function testBuildingParameterWithArrayTypeGuessing() + { + $this->argumentDefinitionShouldBuildOn('[]'); + $this->extractorDefinitionShouldBuildOn('[]'); + + $spec = $this->builder->build('param = []'); + + $this->assertInstanceOf(OptionalParameterSpec::class, $spec); + + $this->assertSame('param', $spec->getName()); + $this->assertSame([], $spec->getDefaultValue()); + $this->assertInstanceOf(ExtractorDefinitionInterface::class, $spec->getExtractorDefinition()); + } + + public function testBuildingParameterWithBoolTrueTypeGuessing() + { + $this->argumentDefinitionShouldBuildOn('true'); + $this->extractorDefinitionShouldBuildOn('bool'); + + $spec = $this->builder->build('param = true'); + + $this->assertInstanceOf(OptionalParameterSpec::class, $spec); + + $this->assertSame('param', $spec->getName()); + $this->assertSame(true, $spec->getDefaultValue()); + $this->assertInstanceOf(ExtractorDefinitionInterface::class, $spec->getExtractorDefinition()); + } + + public function testBuildingParameterWithBoolFalseTypeGuessing() + { + $this->argumentDefinitionShouldBuildOn('false'); + $this->extractorDefinitionShouldBuildOn('bool'); + + $spec = $this->builder->build('param = false'); + + $this->assertInstanceOf(OptionalParameterSpec::class, $spec); + + $this->assertSame('param', $spec->getName()); + $this->assertSame(false, $spec->getDefaultValue()); + $this->assertInstanceOf(ExtractorDefinitionInterface::class, $spec->getExtractorDefinition()); + } + + public function testBuildingParameterWithNullableTypeGuessing() + { + $this->argumentDefinitionShouldBuildOn('null'); + $this->extractorDefinitionShouldBuildOn('any'); + + $spec = $this->builder->build('param?'); + + $this->assertInstanceOf(OptionalParameterSpec::class, $spec); + + $this->assertSame('param', $spec->getName()); + $this->assertSame(null, $spec->getDefaultValue()); + $this->assertInstanceOf(ExtractorDefinitionInterface::class, $spec->getExtractorDefinition()); + } + + public function testBuildingParameterWithDefaultNullTypeGuessing() + { + $this->argumentDefinitionShouldBuildOn('null'); + $this->extractorDefinitionShouldBuildOn('any'); + + $spec = $this->builder->build('param = null'); + + $this->assertInstanceOf(OptionalParameterSpec::class, $spec); + + $this->assertSame('param', $spec->getName()); + $this->assertSame(null, $spec->getDefaultValue()); + $this->assertInstanceOf(ExtractorDefinitionInterface::class, $spec->getExtractorDefinition()); + } + + public function testBuildingParameterWithDefaultIntNumberTypeGuessing() + { + $this->argumentDefinitionShouldBuildOn('123'); + $this->extractorDefinitionShouldBuildOn('number'); + + $spec = $this->builder->build('param = 123'); + + $this->assertInstanceOf(OptionalParameterSpec::class, $spec); + + $this->assertSame('param', $spec->getName()); + $this->assertEquals(123, $spec->getDefaultValue()); + $this->assertInstanceOf(ExtractorDefinitionInterface::class, $spec->getExtractorDefinition()); + } + + public function testBuildingParameterWithDefaultFloatNumberTypeGuessing() + { + $this->argumentDefinitionShouldBuildOn('123.42'); + $this->extractorDefinitionShouldBuildOn('number'); + + $spec = $this->builder->build('param = 123.42'); + + $this->assertInstanceOf(OptionalParameterSpec::class, $spec); + + $this->assertSame('param', $spec->getName()); + $this->assertSame(123.42, $spec->getDefaultValue()); + $this->assertInstanceOf(ExtractorDefinitionInterface::class, $spec->getExtractorDefinition()); + } + + public function testBuildingParameterWithDefaultStringTypeGuessing() + { + $this->argumentDefinitionShouldBuildOn('"test"'); + $this->extractorDefinitionShouldBuildOn('string'); + + $spec = $this->builder->build('param = "test"'); + + $this->assertInstanceOf(OptionalParameterSpec::class, $spec); + + $this->assertSame('param', $spec->getName()); + $this->assertSame('"test"', $spec->getDefaultValue()); + $this->assertInstanceOf(ExtractorDefinitionInterface::class, $spec->getExtractorDefinition()); + } + /** * @expectedException \Pinepain\JsSandbox\Specs\Builder\Exceptions\ParameterSpecBuilderException * @expectedExceptionMessage Unable to parse definition because of extractor failure: ExtractorDefinitionBuilder exception for testing @@ -223,9 +335,31 @@ public function testBuildingWhenExtractorFailsShouldAlsoFail() protected function argumentDefinitionShouldBuildOn($name) { + $retval = $name; + + if ('[]' == $name) { + $retval = []; + } + + if ('true' === $name) { + $retval = true; + } + + if ('false' === $name) { + $retval = false; + } + + if (is_numeric($name)) { + $retval = is_int($name) ? (int)$name : (float) $name; + } + + if ('null' === $name) { + $retval = null; + } + $this->argument_builder->method('build') ->with($name, false) - ->willReturn($name); + ->willReturn($retval); } protected function argumentDefinitionShouldThrowOn($name) From e49f0ed63bd1f8eae4fa56495ca3669f3912def2 Mon Sep 17 00:00:00 2001 From: Bogdan Padalko Date: Sun, 5 Nov 2017 23:39:43 +0200 Subject: [PATCH 13/14] Set null type implicitly for nullable paramater definition --- src/Specs/Builder/ParameterSpecBuilder.php | 5 +++++ tests/Specs/Builder/ParameterSpecBuilderTest.php | 4 ++-- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/src/Specs/Builder/ParameterSpecBuilder.php b/src/Specs/Builder/ParameterSpecBuilder.php index 4c73fc2..67be992 100644 --- a/src/Specs/Builder/ParameterSpecBuilder.php +++ b/src/Specs/Builder/ParameterSpecBuilder.php @@ -143,6 +143,11 @@ protected function buildOptionalParameterSpec(array $matches, ?string $default): } } + if ($this->hasNullable($matches)) { + // nullable means that null is a valid value and thus we should explicitly enable null extractor here + $matches['type'] = 'null|' . $matches['type']; + } + return new OptionalParameterSpec($matches['name'], $this->extractor->build($matches['type']), $default); } diff --git a/tests/Specs/Builder/ParameterSpecBuilderTest.php b/tests/Specs/Builder/ParameterSpecBuilderTest.php index e088de6..91063db 100644 --- a/tests/Specs/Builder/ParameterSpecBuilderTest.php +++ b/tests/Specs/Builder/ParameterSpecBuilderTest.php @@ -199,7 +199,7 @@ public function testBuildingNullableParameterWithDefaultValueShouldThrowExceptio public function testBuildingNullableParameter() { - $this->extractorDefinitionShouldBuildOn('type'); + $this->extractorDefinitionShouldBuildOn('null|type'); $spec = $this->builder->build('param? : type'); @@ -255,7 +255,7 @@ public function testBuildingParameterWithBoolFalseTypeGuessing() public function testBuildingParameterWithNullableTypeGuessing() { $this->argumentDefinitionShouldBuildOn('null'); - $this->extractorDefinitionShouldBuildOn('any'); + $this->extractorDefinitionShouldBuildOn('null|any'); $spec = $this->builder->build('param?'); From f893e81790043516caec6d90512325e24d892d9a Mon Sep 17 00:00:00 2001 From: Bogdan Padalko Date: Mon, 6 Nov 2017 00:11:15 +0200 Subject: [PATCH 14/14] Reflect groupping and short array syntax in property spec --- src/Specs/Builder/PropertySpecBuilder.php | 2 +- .../Specs/Builder/PropertySpecBuilderTest.php | 28 +++++++++++++++++++ 2 files changed, 29 insertions(+), 1 deletion(-) diff --git a/src/Specs/Builder/PropertySpecBuilder.php b/src/Specs/Builder/PropertySpecBuilder.php index fda3b61..6fcfe84 100644 --- a/src/Specs/Builder/PropertySpecBuilder.php +++ b/src/Specs/Builder/PropertySpecBuilder.php @@ -100,7 +100,7 @@ protected function getSpecMethodsOrType(PropertySpecPrototype $proto, string $de return; } - if (preg_match('/^(?\w+(?:\(.*\))?)$/', $definition, $matches)) { + if (preg_match('/^(?([\w\-]*(?:\(.*\))?(?:\[\s*\])?)(?:\s*\|\s*(?-1))*)$/', $definition, $matches)) { $proto->definition = $this->builder->build($matches['type']); return; diff --git a/tests/Specs/Builder/PropertySpecBuilderTest.php b/tests/Specs/Builder/PropertySpecBuilderTest.php index 2b7dcf6..fbb2a90 100644 --- a/tests/Specs/Builder/PropertySpecBuilderTest.php +++ b/tests/Specs/Builder/PropertySpecBuilderTest.php @@ -87,6 +87,34 @@ public function testBuildingTyped() $this->assertNull($spec->getSetterName()); } + public function testBuildingTypedArray() + { + $this->extractorDefinitionShouldBuildOn('[]'); + + $spec = $this->builder->build('[]'); + + $this->assertInstanceOf(PropertySpecInterface::class, $spec); + + $this->assertInstanceOf(ExtractorDefinitionInterface::class, $spec->getExtractorDefinition()); + $this->assertFalse($spec->isReadonly()); + $this->assertNull($spec->getGetterName()); + $this->assertNull($spec->getSetterName()); + } + + public function testBuildingTypedAndGroupedArray() + { + $this->extractorDefinitionShouldBuildOn('(foo|bar[])[]'); + + $spec = $this->builder->build('(foo|bar[])[]'); + + $this->assertInstanceOf(PropertySpecInterface::class, $spec); + + $this->assertInstanceOf(ExtractorDefinitionInterface::class, $spec->getExtractorDefinition()); + $this->assertFalse($spec->isReadonly()); + $this->assertNull($spec->getGetterName()); + $this->assertNull($spec->getSetterName()); + } + public function testBuildingReadonlyTyped() { $this->extractorDefinitionShouldBuildOn('test');