diff --git a/CHANGELOG.md b/CHANGELOG.md index 418c71a..f30f660 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,10 @@ # Change Log +## v0.5.1 + +Fix handling for streamed LookupResources and LookupSubjects responses that are in [JSON lines](https://jsonlines.org/) +format rather than valid JSON. + ## v0.5.0 Major new release up to date with SpiceDB 1.25+. Except for the experimental APIs, things should be stable going diff --git a/src/Client.php b/src/Client.php index 2ad8212..8eec820 100644 --- a/src/Client.php +++ b/src/Client.php @@ -38,6 +38,7 @@ use Chiphpotle\Rest\Model\WriteSchemaRequest; use Chiphpotle\Rest\Model\WriteSchemaResponse; use Chiphpotle\Rest\Normalizer\JaneObjectNormalizer; +use Chiphpotle\Rest\Runtime\Client\JsonLinesDecoder; use Http\Discovery\Psr17FactoryDiscovery; use Symfony\Component\Serializer\Encoder\JsonDecode; use Symfony\Component\Serializer\Encoder\JsonEncode; @@ -176,7 +177,8 @@ public static function create($baseUrl, $apiKey, $additionalNormalizers = []): C if (count($additionalNormalizers) > 0) { $normalizers = array_merge($normalizers, $additionalNormalizers); } - $serializer = new Serializer($normalizers, [new JsonEncoder(new JsonEncode(), new JsonDecode(['json_decode_associative' => true]))]); + $jsonDecoder = new JsonDecode(['json_decode_associative' => true]); + $serializer = new Serializer($normalizers, [new JsonEncoder(new JsonEncode(), $jsonDecoder), new JsonLinesDecoder($jsonDecoder)]); return new self($httpClient, $requestFactory, $serializer, $streamFactory); } } diff --git a/src/Endpoint/PermissionsServiceLookupResources.php b/src/Endpoint/PermissionsServiceLookupResources.php index 0745f29..0f53426 100644 --- a/src/Endpoint/PermissionsServiceLookupResources.php +++ b/src/Endpoint/PermissionsServiceLookupResources.php @@ -49,7 +49,7 @@ protected function transformResponseBody(ResponseInterface $response, Serializer $status = $response->getStatusCode(); $body = (string) $response->getBody(); if (200 === $status) { - return $serializer->deserialize($body, PermissionsResourcesPostResponse200::class, 'json'); + return $serializer->deserialize($body, PermissionsResourcesPostResponse200::class, 'jsonl'); } $this->throwRpcException($body, $serializer); } diff --git a/src/Endpoint/PermissionsServiceLookupSubjects.php b/src/Endpoint/PermissionsServiceLookupSubjects.php index 1167fdc..66af9f0 100644 --- a/src/Endpoint/PermissionsServiceLookupSubjects.php +++ b/src/Endpoint/PermissionsServiceLookupSubjects.php @@ -49,7 +49,7 @@ protected function transformResponseBody(ResponseInterface $response, Serializer $status = $response->getStatusCode(); $body = (string) $response->getBody(); if (200 === $status) { - return $serializer->deserialize($body, PermissionsSubjectsPostResponse200::class, 'json'); + return $serializer->deserialize($body, PermissionsSubjectsPostResponse200::class, 'jsonl'); } $this->throwRpcException($body, $serializer); } diff --git a/src/Model/PermissionsResourcesPostResponse200.php b/src/Model/PermissionsResourcesPostResponse200.php index a67e738..0ecda40 100644 --- a/src/Model/PermissionsResourcesPostResponse200.php +++ b/src/Model/PermissionsResourcesPostResponse200.php @@ -4,18 +4,27 @@ final class PermissionsResourcesPostResponse200 { - protected LookupResourcesResponse $result; + /** + * @var LookupResourcesResponse[] + */ + protected array $result; protected ?RpcStatus $error; - public function getResult(): LookupResourcesResponse + /** + * @return LookupResourcesResponse[] + */ + public function getResults(): array { return $this->result; } - public function setResult(LookupResourcesResponse $result): self + /** + * @param LookupResourcesResponse[] $results + */ + public function setResults(array $results): self { - $this->result = $result; + $this->result = $results; return $this; } diff --git a/src/Model/PermissionsSubjectsPostResponse200.php b/src/Model/PermissionsSubjectsPostResponse200.php index d922b0a..e0c310b 100644 --- a/src/Model/PermissionsSubjectsPostResponse200.php +++ b/src/Model/PermissionsSubjectsPostResponse200.php @@ -4,16 +4,26 @@ final class PermissionsSubjectsPostResponse200 { - protected LookupSubjectsResponse $result; + /** + * @var LookupSubjectsResponse[] + */ + protected array $result; protected RpcStatus $error; - public function getResult(): LookupSubjectsResponse + /** + * @return LookupSubjectsResponse[] + */ + public function getResults(): array { return $this->result; } - public function setResult(LookupSubjectsResponse $result): self + /** + * @param LookupSubjectsResponse[] $result + * @return $this + */ + public function setResults(array $result): self { $this->result = $result; return $this; diff --git a/src/Normalizer/PermissionsResourcesPostResponse200Normalizer.php b/src/Normalizer/PermissionsResourcesPostResponse200Normalizer.php index cd5e863..2c2aa23 100644 --- a/src/Normalizer/PermissionsResourcesPostResponse200Normalizer.php +++ b/src/Normalizer/PermissionsResourcesPostResponse200Normalizer.php @@ -3,8 +3,10 @@ namespace Chiphpotle\Rest\Normalizer; use Chiphpotle\Rest\Model\LookupResourcesResponse; +use Chiphpotle\Rest\Model\LookupSubjectsResponse; use Chiphpotle\Rest\Model\PermissionsResourcesPostResponse200; use Chiphpotle\Rest\Model\RpcStatus; +use Chiphpotle\Rest\Runtime\Client\RpcException; use Symfony\Component\Serializer\Normalizer\DenormalizerAwareInterface; use Symfony\Component\Serializer\Normalizer\DenormalizerAwareTrait; use Symfony\Component\Serializer\Normalizer\DenormalizerInterface; @@ -33,16 +35,17 @@ public function supportsNormalization($data, $format = null): bool public function denormalize(mixed $data, string $type, string $format = null, array $context = []): PermissionsResourcesPostResponse200 { $object = new PermissionsResourcesPostResponse200(); - if (null === $data || false === is_array($data)) { - return $object; + $results = []; + foreach ($data as $resultData) { + if (array_key_exists('result', $resultData)) { + $results[] = $this->denormalizer->denormalize($resultData['result'], LookupResourcesResponse::class, 'json', $context); + } elseif (array_key_exists('error', $resultData)) { + $rpcStatus = $this->denormalizer->denormalize($resultData['error'], RpcStatus::class, 'json', $context); + throw new RpcException($rpcStatus); + } } - if (array_key_exists('result', $data)) { - $object->setResult($this->denormalizer->denormalize($data['result'], LookupResourcesResponse::class, 'json', $context)); - } - if (array_key_exists('error', $data)) { - $object->setError($this->denormalizer->denormalize($data['error'], RpcStatus::class, 'json', $context)); - } - return $object; + + return $object->setResults($results); } public function normalize($object, $format = null, array $context = []): array diff --git a/src/Normalizer/PermissionsSubjectsPostResponse200Normalizer.php b/src/Normalizer/PermissionsSubjectsPostResponse200Normalizer.php index a8b8fde..df42228 100644 --- a/src/Normalizer/PermissionsSubjectsPostResponse200Normalizer.php +++ b/src/Normalizer/PermissionsSubjectsPostResponse200Normalizer.php @@ -6,6 +6,7 @@ use Chiphpotle\Rest\Model\PermissionsSubjectsPostResponse200; use ArrayObject; use Chiphpotle\Rest\Model\RpcStatus; +use Chiphpotle\Rest\Runtime\Client\RpcException; use Symfony\Component\Serializer\Normalizer\DenormalizerAwareInterface; use Symfony\Component\Serializer\Normalizer\DenormalizerAwareTrait; use Symfony\Component\Serializer\Normalizer\DenormalizerInterface; @@ -34,16 +35,18 @@ public function supportsNormalization($data, $format = null): bool public function denormalize(mixed $data, string $type, string $format = null, array $context = []): PermissionsSubjectsPostResponse200 { $object = new PermissionsSubjectsPostResponse200(); - if (null === $data || false === is_array($data)) { - return $object; - } - if (array_key_exists('result', $data)) { - $object->setResult($this->denormalizer->denormalize($data['result'], LookupSubjectsResponse::class, 'json', $context)); - } - if (array_key_exists('error', $data)) { - $object->setError($this->denormalizer->denormalize($data['error'], RpcStatus::class, 'json', $context)); + + $results = []; + foreach ($data as $resultData) { + if (array_key_exists('result', $resultData)) { + $results[] = $this->denormalizer->denormalize($resultData['result'], LookupSubjectsResponse::class, 'json', $context); + } elseif (array_key_exists('error', $resultData)) { + $rpcStatus = $this->denormalizer->denormalize($resultData['error'], RpcStatus::class, 'json', $context); + throw new RpcException($rpcStatus); + } } - return $object; + + return $object->setResults($results); } public function normalize($object, $format = null, array $context = []): array diff --git a/src/Runtime/Client/JsonLinesDecoder.php b/src/Runtime/Client/JsonLinesDecoder.php new file mode 100644 index 0000000..30821ab --- /dev/null +++ b/src/Runtime/Client/JsonLinesDecoder.php @@ -0,0 +1,35 @@ +jsonDecoder->decode($line, $format, $context); + } + } + return $decodedData; + } + + public function supportsDecoding(string $format): bool + { + return self::FORMAT === $format; + } +} diff --git a/test/ClientTest.php b/test/ClientTest.php index e507c4e..b710c9a 100644 --- a/test/ClientTest.php +++ b/test/ClientTest.php @@ -23,6 +23,8 @@ use Chiphpotle\Rest\Model\LookupResourcesRequest; use Chiphpotle\Rest\Model\LookupSubjectsRequest; use Chiphpotle\Rest\Model\ObjectReference; +use Chiphpotle\Rest\Model\PermissionsResourcesPostResponse200; +use Chiphpotle\Rest\Model\PermissionsSubjectsPostResponse200; use Chiphpotle\Rest\Model\ReadRelationshipsRequest; use Chiphpotle\Rest\Model\Relationship; use Chiphpotle\Rest\Model\RelationshipFilter; @@ -110,48 +112,60 @@ public function testDeleteRelationships() public function testLookupResources() { - $this->writeRelationship('document', 'topsecret1', 'viewer', 'user', 'alice'); + $this->writeRelationship('document', 'topsecret1', 'viewer', 'user', 'lookup_resource_test'); + $writeResults = $this->writeRelationship('document', 'topsecret2', 'viewer', 'user', 'lookup_resource_test'); $request = new LookupResourcesRequest(); $request ->setResourceObjectType("document") ->setPermission("view") - ->setSubject(SubjectReference::create("user", "alice")) - ->setConsistency(Consistency::minimizeLatency()); + ->setSubject(SubjectReference::create("user", "lookup_resource_test")) + ->setConsistency(Consistency::atLeastAsFresh($writeResults->getWrittenAt())); + $response = $this->getApiClient()->lookupResources( $request ); - $this->assertEquals( - "topsecret1", - $response->getResult()->getResourceObjectId() - ); + + $this->assertInstanceOf(PermissionsResourcesPostResponse200::class, $response); + $results = $response->getResults(); + $this->assertCount(2, $results); + + foreach ($results as $result) { + $this->assertTrue(in_array($result->getResourceObjectId(), ['topsecret1', 'topsecret2'])); + } } public function testLookupSubjects() { - $this->writeRelationship('document', 'topsecret7', 'viewer', 'user', 'alice'); + $this->writeRelationship('document', 'lookup_subject_test', 'viewer', 'user', 'alice'); + $this->writeRelationship('document', 'lookup_subject_test', 'viewer', 'user', 'mary'); $request = new LookupSubjectsRequest( - ObjectReference::create('document', 'topsecret7'), + ObjectReference::create('document', 'lookup_subject_test'), 'view', 'user' ); $response = $this->getApiClient()->lookupSubjects( $request ); - $this->assertEquals( - "alice", - $response->getResult()->getSubjectObjectId() - ); + + $this->assertInstanceOf(PermissionsSubjectsPostResponse200::class, $response); + $results = $response->getResults(); + $this->assertCount(2, $results); + + foreach ($results as $result) { + $this->assertTrue(in_array($result->getSubjectObjectId(), ['alice', 'mary'])); + } } public function testPermissionCheckValid() { - $this->writeRelationship('document', 'topsecret1', 'viewer', 'user', 'bob'); + $writeResponse = $this->writeRelationship('document', 'topsecret1', 'viewer', 'user', 'bob'); $request = new CheckPermissionRequest( ObjectReference::create("document", "topsecret1"), "view", - SubjectReference::create("user", "bob") + SubjectReference::create("user", "bob"), + Consistency::atLeastAsFresh($writeResponse->getWrittenAt()) ); $response = $this->getApiClient()->checkPermission( $request