From 987e80fe8cc33bf23840bf2b2d37d427f24c783d Mon Sep 17 00:00:00 2001 From: Alexey Karapetov Date: Fri, 20 Oct 2017 01:07:20 -0700 Subject: [PATCH 1/2] Member names validation --- .travis.yml | 5 -- composer.json | 2 +- src/Document/Container.php | 27 ++++++++++ src/Document/LinksTrait.php | 15 ++++-- src/Document/MemberName.php | 12 +++++ src/Document/Meta.php | 43 +++++++++++---- src/Document/ReservedName.php | 20 +++++++ src/Document/Resource/ResourceObject.php | 29 ++--------- src/Document/Resource/ResourceType.php | 14 +++++ src/Document/SpecialValue.php | 32 ++++++++++++ test/MemberNamesTest.php | 66 ++++-------------------- 11 files changed, 166 insertions(+), 99 deletions(-) create mode 100644 src/Document/Container.php create mode 100644 src/Document/MemberName.php create mode 100644 src/Document/ReservedName.php create mode 100644 src/Document/Resource/ResourceType.php create mode 100644 src/Document/SpecialValue.php diff --git a/.travis.yml b/.travis.yml index 06332bf..641d25d 100644 --- a/.travis.yml +++ b/.travis.yml @@ -11,8 +11,3 @@ script: - vendor/bin/php-cs-fixer fix -v --dry-run - phpunit --coverage-clover build/logs/clover.xml - vendor/bin/doc2test && vendor/bin/phpunit -c doc-test/phpunit.xml - -matrix: - exclude: - - php: '7.0' - script: vendor/bin/doc2test && vendor/bin/phpunit -c doc-test/phpunit.xml diff --git a/composer.json b/composer.json index 2f3dc68..c6dbdc6 100644 --- a/composer.json +++ b/composer.json @@ -29,6 +29,6 @@ } }, "scripts": { - "test": "php-cs-fixer fix -v --dry-run --ansi && phpunit --colors=always --coverage-text" + "test": "php-cs-fixer fix -v --dry-run --ansi && phpunit --colors=always --coverage-text && vendor/bin/doc2test && vendor/bin/phpunit -c doc-test/phpunit.xml --coverage-text" } } diff --git a/src/Document/Container.php b/src/Document/Container.php new file mode 100644 index 0000000..e7f57ec --- /dev/null +++ b/src/Document/Container.php @@ -0,0 +1,27 @@ +data) { + $this->data = (object) []; + } + $this->data->$name = $value; + } + + public function getIterator(): \Traversable + { + return $this->data; + } + + public function jsonSerialize() + { + return $this->data; + } +} diff --git a/src/Document/LinksTrait.php b/src/Document/LinksTrait.php index a246991..2e8209a 100644 --- a/src/Document/LinksTrait.php +++ b/src/Document/LinksTrait.php @@ -9,17 +9,26 @@ trait LinksTrait { /** - * @var LinkInterface[] + * @var Container|null */ protected $links; public function setLink(string $name, string $url) { - $this->links[$name] = new Link($url); + $this->init(); + $this->links->set(new MemberName($name), new Link($url)); } public function setLinkObject(string $name, LinkInterface $link) { - $this->links[$name] = $link; + $this->init(); + $this->links->set(new MemberName($name), $link); + } + + private function init() + { + if (!$this->links) { + $this->links = new Container(); + } } } diff --git a/src/Document/MemberName.php b/src/Document/MemberName.php new file mode 100644 index 0000000..fab7026 --- /dev/null +++ b/src/Document/MemberName.php @@ -0,0 +1,12 @@ +validateObject($data); - - $this->data = $data; + $this->data = $this->toContainer($data); } public static function fromArray(array $array): self @@ -24,21 +25,43 @@ public function jsonSerialize() return $this->data; } - private function validateObject($object) + private function toContainer($object): Container { + $c = new Container(); foreach ($object as $name => $value) { - if (is_string($name) && !$this->isValidMemberName($name)) { - throw new \OutOfBoundsException("Not a valid attribute name '$name'"); + if (is_object($object)) { + $name = (string) $name; + } + if ($this->canConvert($value)) { + $value = $this->toContainer($value); + } else { + $value = $this->traverse($value); } + $c->set(new MemberName($name), $value); + } + return $c; + } - if (is_array($value) || $value instanceof \stdClass) { - $this->validateObject($value); + private function traverse($value) + { + if ($this->canConvert($value)) { + return $this->toContainer($value); + } + if (is_array($value)) { + foreach ($value as $k => $v) { + $value[$k] = $this->traverse($v); } } + return $value; } - private function isValidMemberName(string $name): bool + private function canConvert($v): bool { - return preg_match('/^(?=[^-_ ])[a-zA-Z0-9\x{0080}-\x{FFFF}-_ ]*(?<=[^-_ ])$/u', $name) === 1; + return is_object($v) + || ( + is_array($v) + && $v !== [] + && array_keys($v) !== range(0, count($v) - 1) + ); } } diff --git a/src/Document/ReservedName.php b/src/Document/ReservedName.php new file mode 100644 index 0000000..50771ea --- /dev/null +++ b/src/Document/ReservedName.php @@ -0,0 +1,20 @@ +isReservedName($name)) { + throw new \InvalidArgumentException("Can not use a reserved name '$name'"); + } + } + + private function isReservedName(string $name): bool + { + return in_array($name, ['id', 'type']); + } +} diff --git a/src/Document/Resource/ResourceObject.php b/src/Document/Resource/ResourceObject.php index 337d65d..e9b68b7 100644 --- a/src/Document/Resource/ResourceObject.php +++ b/src/Document/Resource/ResourceObject.php @@ -5,6 +5,7 @@ use JsonApiPhp\JsonApi\Document\LinksTrait; use JsonApiPhp\JsonApi\Document\Meta; +use JsonApiPhp\JsonApi\Document\ReservedName; class ResourceObject implements \JsonSerializable { @@ -22,7 +23,7 @@ class ResourceObject implements \JsonSerializable public function __construct(string $type, string $id = null) { - $this->type = $type; + $this->type = new ResourceType($type); $this->id = $id; } @@ -33,12 +34,7 @@ public function setMeta(Meta $meta) public function setAttribute(string $name, $value) { - if ($this->isReservedName($name)) { - throw new \InvalidArgumentException("Can not use a reserved name '$name'"); - } - if (!$this->isValidMemberName($name)) { - throw new \OutOfBoundsException("Not a valid attribute name '$name'"); - } + $name = (string) new ReservedName($name); if (isset($this->relationships[$name])) { throw new \LogicException("Field '$name' already exists in relationships"); } @@ -47,12 +43,7 @@ public function setAttribute(string $name, $value) public function setRelationship(string $name, Relationship $relationship) { - if ($this->isReservedName($name)) { - throw new \InvalidArgumentException("Can not use a reserved name '$name'"); - } - if (!$this->isValidMemberName($name)) { - throw new \OutOfBoundsException("Not a valid attribute name '$name'"); - } + $name = (string) new ReservedName($name); if (isset($this->attributes[$name])) { throw new \LogicException("Field '$name' already exists in attributes"); } @@ -61,7 +52,7 @@ public function setRelationship(string $name, Relationship $relationship) public function toIdentifier(): ResourceIdentifier { - return new ResourceIdentifier($this->type, $this->id); + return new ResourceIdentifier((string) $this->type, $this->id); } public function jsonSerialize() @@ -92,14 +83,4 @@ public function identifies(ResourceObject $resource): bool } return false; } - - private function isReservedName(string $name): bool - { - return in_array($name, ['id', 'type']); - } - - private function isValidMemberName(string $name): bool - { - return preg_match('/^(?=[^-_ ])[a-zA-Z0-9\x{0080}-\x{FFFF}-_ ]*(?<=[^-_ ])$/u', $name) === 1; - } } diff --git a/src/Document/Resource/ResourceType.php b/src/Document/Resource/ResourceType.php new file mode 100644 index 0000000..a3b9362 --- /dev/null +++ b/src/Document/Resource/ResourceType.php @@ -0,0 +1,14 @@ +isValidMemberNameOrTypeValue($val)) { + throw new \OutOfBoundsException(sprintf($errorMessage, $val)); + } + $this->val = $val; + } + + public function jsonSerialize() + { + return $this->val; + } + + public function __toString() + { + return $this->val; + } + + protected function isValidMemberNameOrTypeValue(string $name): bool + { + return preg_match('/^(?=[^-_ ])[a-zA-Z0-9\x{0080}-\x{FFFF}-_ ]*(?<=[^-_ ])$/u', $name) === 1; + } +} diff --git a/test/MemberNamesTest.php b/test/MemberNamesTest.php index c2eb41e..219c6e1 100644 --- a/test/MemberNamesTest.php +++ b/test/MemberNamesTest.php @@ -14,7 +14,7 @@ class MemberNamesTest extends TestCase /** * @param string $name * @expectedException \OutOfBoundsException - * @expectedExceptionMessage Not a valid attribute name + * @expectedExceptionMessage Invalid member name * @dataProvider invalidAttributeNames */ public function testInvalidAttributeNamesAreNotAllowed(string $name) @@ -24,20 +24,22 @@ public function testInvalidAttributeNamesAreNotAllowed(string $name) } /** + * The values of type members MUST adhere to the same constraints as member names. + * * @param string $name - * @dataProvider validAttributeNames + * @expectedException \OutOfBoundsException + * @expectedExceptionMessage Invalid resource type + * @dataProvider invalidAttributeNames */ - public function testValidAttributeNamesCanBeSet(string $name) + public function testInvalidTypesAreNotAllowed(string $name) { - $res = new ResourceObject('books', 'abc'); - $res->setAttribute($name, 1); - $this->assertInternalType('string', json_encode($res)); + new ResourceObject($name, 'abc'); } /** * @param string $name * @expectedException \OutOfBoundsException - * @expectedExceptionMessage Not a valid attribute name + * @expectedExceptionMessage Invalid member name * @dataProvider invalidAttributeNames */ public function testInvalidRelationshipNamesAreNotAllowed(string $name) @@ -49,7 +51,7 @@ public function testInvalidRelationshipNamesAreNotAllowed(string $name) /** * @param string $name * @expectedException \OutOfBoundsException - * @expectedExceptionMessage Not a valid attribute name + * @expectedExceptionMessage Invalid member name * @dataProvider invalidAttributeNames */ public function testInvalidMetaNames(string $name) @@ -67,38 +69,6 @@ public function testInvalidMetaNames(string $name) ); } - /** - * @param string $name - * @dataProvider validAttributeNames - */ - public function testValidMetaNames(string $name) - { - $meta = Meta::fromArray( - [ - 'copyright' => 'Copyright 2015 Example Corp.', - 'authors' => [ - [ - 'firstname' => 'Yehuda', - $name => 'Katz', - ], - ], - ] - ); - - $this->assertInternalType('string', json_encode($meta)); - } - - /** - * @param string $name - * @dataProvider validAttributeNames - */ - public function testValidRelationshipNamesCanBeSet(string $name) - { - $res = new ResourceObject('books', 'abc'); - $res->setRelationship($name, Relationship::fromSelfLink(new Link('https://example.com'))); - $this->assertInternalType('string', json_encode($res)); - } - public function invalidAttributeNames(): array { return [ @@ -113,20 +83,4 @@ public function invalidAttributeNames(): array ['-abc'], ]; } - - public function validAttributeNames(): array - { - return [ - ['abcd'], - ['abcA4C'], - ['abc_d3f45'], - ['abd_eca'], - ['a'], - ['b'], - ['ab'], - ['a-bc_de'], - ['abcéêçèÇ_n'], - ['abc 汉字 abc'], - ]; - } } From bd79f87d376ea16442613695debc97b3fd37c06a Mon Sep 17 00:00:00 2001 From: Alexey Karapetov Date: Fri, 20 Oct 2017 01:09:28 -0700 Subject: [PATCH 2/2] Style --- src/Document/LinksTrait.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Document/LinksTrait.php b/src/Document/LinksTrait.php index 2e8209a..700872a 100644 --- a/src/Document/LinksTrait.php +++ b/src/Document/LinksTrait.php @@ -16,7 +16,7 @@ trait LinksTrait public function setLink(string $name, string $url) { $this->init(); - $this->links->set(new MemberName($name), new Link($url)); + $this->links->set(new MemberName($name), new Link($url)); } public function setLinkObject(string $name, LinkInterface $link) @@ -27,7 +27,7 @@ public function setLinkObject(string $name, LinkInterface $link) private function init() { - if (!$this->links) { + if (! $this->links) { $this->links = new Container(); } }