diff --git a/README.md b/README.md index 1f67465..2bef991 100644 --- a/README.md +++ b/README.md @@ -450,6 +450,14 @@ foreach($eventTypes as $eventType) { } ``` +### Listing a Specific Event Type + +To list a specific event type, call the `readEventType` function with the event type as an argument. The function returns the detailed event type, which includes the schema: + +```php +$eventType = $client->readEventType('io.eventsourcingdb.library.book-acquired'); +``` + ### Using Testcontainers Import the `Container` class, call the `start` function to run a test container, get a client, run your test code, and finally call the `stop` function to stop the test container: diff --git a/src/Client.php b/src/Client.php index fb493c0..c9b7d4f 100644 --- a/src/Client.php +++ b/src/Client.php @@ -39,8 +39,15 @@ public function ping(): void )); } - $body = $response->getStream()->getContents(); - $data = json_decode($body, true); + try { + $data = $response->getStream()->getJsonData(); + } catch (RuntimeException $runtimeException) { + throw new RuntimeException( + 'Failed to ping: ' . $runtimeException->getMessage(), + $runtimeException->getCode(), + $runtimeException, + ); + } if (!isset($data['type']) || $data['type'] !== 'io.eventsourcingdb.api.ping-received') { throw new RuntimeException('Failed to ping'); @@ -62,8 +69,15 @@ public function verifyApiToken(): void )); } - $body = $response->getStream()->getContents(); - $data = json_decode($body, true); + try { + $data = $response->getStream()->getJsonData(); + } catch (RuntimeException $runtimeException) { + throw new RuntimeException( + 'Failed to verify API token: ' . $runtimeException->getMessage(), + $runtimeException->getCode(), + $runtimeException, + ); + } if (!isset($data['type']) || $data['type'] !== 'io.eventsourcingdb.api.api-token-verified') { throw new RuntimeException('Failed to verify API token'); @@ -93,18 +107,14 @@ public function writeEvents(array $events, array $preconditions = []): array )); } - $body = $response->getStream()->getContents(); - if ($body === '') { - return []; - } - - if (!json_validate($body)) { - throw new RuntimeException('Failed to read events, after writing.'); - } - - $data = json_decode($body, true); - if (!is_array($data)) { - throw new RuntimeException('Failed to read events, expected an array.'); + try { + $data = $response->getStream()->getJsonData(); + } catch (RuntimeException $runtimeException) { + throw new RuntimeException( + 'Failed to read events, after writing: ' . $runtimeException->getMessage(), + $runtimeException->getCode(), + $runtimeException, + ); } $writtenEvents = array_map( @@ -350,4 +360,39 @@ public function readEventTypes(): iterable } } } + + public function readEventType(string $eventType): EventType + { + $response = $this->httpClient->post( + '/api/v1/read-event-type', + $this->apiToken, + [ + 'eventType' => $eventType, + ], + ); + + $status = $response->getStatusCode(); + if ($status !== 200) { + throw new RuntimeException(sprintf( + "Failed to read event type, got HTTP status code '%d', expected '200'", + $status + )); + } + + try { + $data = $response->getStream()->getJsonData(); + } catch (RuntimeException $runtimeException) { + throw new RuntimeException( + 'Failed to read event type: ' . $runtimeException->getMessage(), + $runtimeException->getCode(), + $runtimeException, + ); + } + + return new EventType( + $data['eventType'], + $data['isPhantom'], + $data['schema'] ?? [], + ); + } } diff --git a/src/Stream/Stream.php b/src/Stream/Stream.php index 5e341fd..ca67537 100644 --- a/src/Stream/Stream.php +++ b/src/Stream/Stream.php @@ -5,6 +5,7 @@ namespace Thenativeweb\Eventsourcingdb\Stream; use IteratorAggregate; +use RuntimeException; use Stringable; use Traversable; @@ -31,4 +32,24 @@ public function getContents(): string { return implode('', iterator_to_array($this)); } + + public function getJsonData(): array + { + $contents = $this->getContents(); + if ($contents === '') { + return []; + } + + if (!json_validate($contents)) { + throw new RuntimeException('invalid json string'); + } + + $data = json_decode($contents, true); + if (!is_array($data)) { + $dataType = gettype($data); + throw new RuntimeException("json data is from type '{$dataType}', expected an array"); + } + + return $data; + } } diff --git a/tests/ReadEventTypeTest.php b/tests/ReadEventTypeTest.php new file mode 100755 index 0000000..7693aa1 --- /dev/null +++ b/tests/ReadEventTypeTest.php @@ -0,0 +1,63 @@ +expectExceptionMessage("Failed to read event type, got HTTP status code '404', expected '200'"); + + $this->client->readEventType('non.existent.eventType'); + } + + public function testFailsIfTheEventTypeIsMalformed(): void + { + $this->expectExceptionMessage("Failed to read event type, got HTTP status code '400', expected '200'"); + + $this->client->readEventType('malformed.eventType.'); + } + + public function testReadAnExistingEventType(): void + { + $firstEvent = new EventCandidate( + source: 'https://www.eventsourcingdb.io', + subject: '/test', + type: 'io.eventsourcingdb.test.foo', + data: [ + 'value' => 23, + ], + ); + + $secondEvent = new EventCandidate( + source: 'https://www.eventsourcingdb.io', + subject: '/test', + type: 'io.eventsourcingdb.test.bar', + data: [ + 'value' => 42, + ], + ); + + $this->client->writeEvents([ + $firstEvent, + $secondEvent, + ]); + + $eventType = $this->client->readEventType('io.eventsourcingdb.test.foo'); + + $expected = new EventType( + eventType: 'io.eventsourcingdb.test.foo', + isPhantom: false, + schema: [], + ); + + $this->assertEquals($expected, $eventType); + } +} diff --git a/tests/Stream/StreamTest.php b/tests/Stream/StreamTest.php index 3c23e13..fc25950 100644 --- a/tests/Stream/StreamTest.php +++ b/tests/Stream/StreamTest.php @@ -6,6 +6,7 @@ use ArrayIterator; use PHPUnit\Framework\TestCase; +use RuntimeException; use Thenativeweb\Eventsourcingdb\Stream\CurlMultiHandler; use Thenativeweb\Eventsourcingdb\Stream\Stream; @@ -44,4 +45,54 @@ public function testToStringReturnsContents(): void $this->assertSame('foobar', (string) $stream); } + + public function testThrowsExceptionOnInvalidJson(): void + { + $this->expectException(RuntimeException::class); + $this->expectExceptionMessage('invalid json string'); + + $mockHandler = $this->createMock(CurlMultiHandler::class); + $mockHandler->method('contentIterator') + ->willReturn(new ArrayIterator(['{invalid json'])); + + $stream = new Stream($mockHandler); + $stream->getJsonData(); + } + + public function testThrowsExceptionIfJsonIsNotArray(): void + { + $this->expectException(RuntimeException::class); + $this->expectExceptionMessage("json data is from type 'boolean', expected an array"); + + $mockHandler = $this->createMock(CurlMultiHandler::class); + $mockHandler->method('contentIterator') + ->willReturn(new ArrayIterator(['true'])); + + $stream = new Stream($mockHandler); + $stream->getJsonData(); + } + + public function testReturnsEmptyArrayOnEmptyContents(): void + { + $mockHandler = $this->createMock(CurlMultiHandler::class); + $mockHandler->method('contentIterator') + ->willReturn(new ArrayIterator([])); + + $stream = new Stream($mockHandler); + + $this->assertSame([], $stream->getJsonData()); + } + + public function testReturnsDecodedArrayIfValidJsonArray(): void + { + $mockHandler = $this->createMock(CurlMultiHandler::class); + $mockHandler->method('contentIterator') + ->willReturn(new ArrayIterator(['{"foo":"bar"}'])); + + $stream = new Stream($mockHandler); + + $this->assertSame([ + 'foo' => 'bar', + ], $stream->getJsonData()); + } } diff --git a/tests/WriteEventsTest.php b/tests/WriteEventsTest.php index c0e7c52..29820eb 100644 --- a/tests/WriteEventsTest.php +++ b/tests/WriteEventsTest.php @@ -94,14 +94,14 @@ public function testSupportsTheIsSubjectPristinePrecondition(): void $this->expectExceptionMessage("Failed to write events, got HTTP status code '409', expected '200'"); - iterator_to_array($this->client->writeEvents( + $this->client->writeEvents( [ $secondEvent, ], [ new IsSubjectPristine('/test'), ], - )); + ); } public function testSupportsTheIsSubjectOnEventIdPrecondition(): void @@ -129,14 +129,14 @@ public function testSupportsTheIsSubjectOnEventIdPrecondition(): void ); $this->expectExceptionMessage("Failed to write events, got HTTP status code '409', expected '200'"); - iterator_to_array($this->client->writeEvents( + $this->client->writeEvents( [ $secondEvent, ], [ new IsSubjectOnEventId('/test', '1'), ], - )); + ); } public function testSupportsTheIsEventQlTruePrecondition(): void @@ -164,13 +164,13 @@ public function testSupportsTheIsEventQlTruePrecondition(): void ); $this->expectExceptionMessage("Failed to write events, got HTTP status code '409', expected '200'"); - iterator_to_array($this->client->writeEvents( + $this->client->writeEvents( [ $secondEvent, ], [ new IsEventQlTrue('FROM e IN events PROJECT INTO COUNT() == 0'), ], - )); + ); } }