[Capability] Preserve optional content fields in PromptResultFormatter (#406)
* Preserve optional content fields in PromptResultFormatter The formatter's typed-content path rebuilt each Content object from only its required fields, silently dropping everything else a prompt returned (annotations on every type, _meta and explicit mimeType on embedded resource contents). Delegate to the schema classes' fromArray() instead, keeping the historical mimeType defaulting and RuntimeException-with- index error contract. ImageContent was the only content class without annotations support, so it gains the same constructor/fromArray()/jsonSerialize() handling as TextContent and AudioContent. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * Reword CHANGELOG entry to be user-facing --------- Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
parent
071476ebd8
commit
8bae3c054e
5 changed files with 283 additions and 89 deletions
|
|
@ -6,6 +6,8 @@ All notable changes to `mcp/sdk` will be documented in this file.
|
||||||
-----
|
-----
|
||||||
|
|
||||||
* Always emit `{}` for empty tool schemas: `Tool` recursively normalizes every empty sub-schema — `properties`, `items`, `additionalProperties`, `$defs`, combinators and the other draft-07 to 2020-12 schema keywords — in the constructor, for both `inputSchema` and `outputSchema`, so an object position is never serialized as `[]`.
|
* Always emit `{}` for empty tool schemas: `Tool` recursively normalizes every empty sub-schema — `properties`, `items`, `additionalProperties`, `$defs`, combinators and the other draft-07 to 2020-12 schema keywords — in the constructor, for both `inputSchema` and `outputSchema`, so an object position is never serialized as `[]`.
|
||||||
|
* Prompt generators returning content as typed arrays (`['type' => 'text', ...]` etc.) no longer lose the optional fields: `annotations` on every content type, and `_meta` and an explicit `mimeType` on embedded resource contents, now carry through to the resulting `PromptMessage` instead of being silently dropped. A missing resource `mimeType` still defaults to `text/plain`/`application/octet-stream` as before.
|
||||||
|
* Add `annotations` support to `ImageContent` (constructor, `fromArray()`, `fromFile()`, `fromString()`, `jsonSerialize()`), matching `TextContent` and `AudioContent`.
|
||||||
|
|
||||||
0.7.0
|
0.7.0
|
||||||
-----
|
-----
|
||||||
|
|
|
||||||
|
|
@ -11,15 +11,14 @@
|
||||||
|
|
||||||
namespace Mcp\Capability\Formatter;
|
namespace Mcp\Capability\Formatter;
|
||||||
|
|
||||||
|
use Mcp\Exception\InvalidArgumentException;
|
||||||
use Mcp\Exception\RuntimeException;
|
use Mcp\Exception\RuntimeException;
|
||||||
use Mcp\Schema\Content\AudioContent;
|
use Mcp\Schema\Content\AudioContent;
|
||||||
use Mcp\Schema\Content\BlobResourceContents;
|
|
||||||
use Mcp\Schema\Content\Content;
|
use Mcp\Schema\Content\Content;
|
||||||
use Mcp\Schema\Content\EmbeddedResource;
|
use Mcp\Schema\Content\EmbeddedResource;
|
||||||
use Mcp\Schema\Content\ImageContent;
|
use Mcp\Schema\Content\ImageContent;
|
||||||
use Mcp\Schema\Content\PromptMessage;
|
use Mcp\Schema\Content\PromptMessage;
|
||||||
use Mcp\Schema\Content\TextContent;
|
use Mcp\Schema\Content\TextContent;
|
||||||
use Mcp\Schema\Content\TextResourceContents;
|
|
||||||
use Mcp\Schema\Enum\Role;
|
use Mcp\Schema\Enum\Role;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -178,6 +177,9 @@ final class PromptResultFormatter
|
||||||
/**
|
/**
|
||||||
* Formats typed content arrays into Content objects.
|
* Formats typed content arrays into Content objects.
|
||||||
*
|
*
|
||||||
|
* Delegates to the schema classes' fromArray() so optional fields
|
||||||
|
* (annotations, mimeType, _meta, ...) carry over instead of being dropped.
|
||||||
|
*
|
||||||
* @param array<string, mixed> $content
|
* @param array<string, mixed> $content
|
||||||
*/
|
*/
|
||||||
private function formatTypedContent(array $content, ?int $index = null): TextContent|ImageContent|AudioContent|EmbeddedResource
|
private function formatTypedContent(array $content, ?int $index = null): TextContent|ImageContent|AudioContent|EmbeddedResource
|
||||||
|
|
@ -185,83 +187,22 @@ final class PromptResultFormatter
|
||||||
$indexStr = null !== $index ? " at index {$index}" : '';
|
$indexStr = null !== $index ? " at index {$index}" : '';
|
||||||
$type = $content['type'];
|
$type = $content['type'];
|
||||||
|
|
||||||
return match ($type) {
|
if ('resource' === $type && isset($content['resource']) && \is_array($content['resource']) && !isset($content['resource']['mimeType'])) {
|
||||||
'text' => $this->formatTextContent($content, $indexStr),
|
// EmbeddedResource::fromArray() leaves a missing mimeType unset; this
|
||||||
'image' => $this->formatImageContent($content, $indexStr),
|
// formatter has always defaulted it, so keep that for compatibility.
|
||||||
'audio' => $this->formatAudioContent($content, $indexStr),
|
$content['resource']['mimeType'] = isset($content['resource']['text']) ? 'text/plain' : 'application/octet-stream';
|
||||||
'resource' => $this->formatResourceContent($content, $indexStr),
|
|
||||||
default => throw new RuntimeException("Invalid content type '{$type}'{$indexStr}."),
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @param array<string, mixed> $content
|
|
||||||
*/
|
|
||||||
private function formatTextContent(array $content, string $indexStr): TextContent
|
|
||||||
{
|
|
||||||
if (!isset($content['text']) || !\is_string($content['text'])) {
|
|
||||||
throw new RuntimeException(\sprintf('Invalid "text" content%s: Missing or invalid "text" string.', $indexStr));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return new TextContent($content['text']);
|
try {
|
||||||
}
|
return match ($type) {
|
||||||
|
'text' => TextContent::fromArray($content),
|
||||||
/**
|
'image' => ImageContent::fromArray($content),
|
||||||
* @param array<string, mixed> $content
|
'audio' => AudioContent::fromArray($content),
|
||||||
*/
|
'resource' => EmbeddedResource::fromArray($content),
|
||||||
private function formatImageContent(array $content, string $indexStr): ImageContent
|
default => throw new RuntimeException("Invalid content type '{$type}'{$indexStr}."),
|
||||||
{
|
};
|
||||||
if (!isset($content['data']) || !\is_string($content['data'])) {
|
} catch (InvalidArgumentException $e) {
|
||||||
throw new RuntimeException("Invalid 'image' content{$indexStr}: Missing or invalid 'data' string (base64).");
|
throw new RuntimeException("Invalid '{$type}' content{$indexStr}: {$e->getMessage()}", 0, $e);
|
||||||
}
|
}
|
||||||
if (!isset($content['mimeType']) || !\is_string($content['mimeType'])) {
|
|
||||||
throw new RuntimeException("Invalid 'image' content{$indexStr}: Missing or invalid 'mimeType' string.");
|
|
||||||
}
|
|
||||||
|
|
||||||
return new ImageContent($content['data'], $content['mimeType']);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @param array<string, mixed> $content
|
|
||||||
*/
|
|
||||||
private function formatAudioContent(array $content, string $indexStr): AudioContent
|
|
||||||
{
|
|
||||||
if (!isset($content['data']) || !\is_string($content['data'])) {
|
|
||||||
throw new RuntimeException("Invalid 'audio' content{$indexStr}: Missing or invalid 'data' string (base64).");
|
|
||||||
}
|
|
||||||
if (!isset($content['mimeType']) || !\is_string($content['mimeType'])) {
|
|
||||||
throw new RuntimeException("Invalid 'audio' content{$indexStr}: Missing or invalid 'mimeType' string.");
|
|
||||||
}
|
|
||||||
|
|
||||||
return new AudioContent($content['data'], $content['mimeType']);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @param array<string, mixed> $content
|
|
||||||
*/
|
|
||||||
private function formatResourceContent(array $content, string $indexStr): EmbeddedResource
|
|
||||||
{
|
|
||||||
if (!isset($content['resource']) || !\is_array($content['resource'])) {
|
|
||||||
throw new RuntimeException("Invalid 'resource' content{$indexStr}: Missing or invalid 'resource' object.");
|
|
||||||
}
|
|
||||||
|
|
||||||
$resource = $content['resource'];
|
|
||||||
if (!isset($resource['uri']) || !\is_string($resource['uri'])) {
|
|
||||||
throw new RuntimeException("Invalid resource{$indexStr}: Missing or invalid 'uri'.");
|
|
||||||
}
|
|
||||||
|
|
||||||
if (isset($resource['text']) && \is_string($resource['text'])) {
|
|
||||||
$resourceObj = new TextResourceContents($resource['uri'], $resource['mimeType'] ?? 'text/plain', $resource['text']);
|
|
||||||
} elseif (isset($resource['blob']) && \is_string($resource['blob'])) {
|
|
||||||
$resourceObj = new BlobResourceContents(
|
|
||||||
$resource['uri'],
|
|
||||||
$resource['mimeType'] ?? 'application/octet-stream',
|
|
||||||
$resource['blob']
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
throw new RuntimeException("Invalid resource{$indexStr}: Must contain 'text' or 'blob'.");
|
|
||||||
}
|
|
||||||
|
|
||||||
return new EmbeddedResource($resourceObj);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -12,12 +12,16 @@
|
||||||
namespace Mcp\Schema\Content;
|
namespace Mcp\Schema\Content;
|
||||||
|
|
||||||
use Mcp\Exception\InvalidArgumentException;
|
use Mcp\Exception\InvalidArgumentException;
|
||||||
|
use Mcp\Schema\Annotations;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
* @phpstan-import-type AnnotationsData from Annotations
|
||||||
|
*
|
||||||
* @phpstan-type ImageContentData array{
|
* @phpstan-type ImageContentData array{
|
||||||
* type: 'image',
|
* type: 'image',
|
||||||
* data: string,
|
* data: string,
|
||||||
* mimeType: string
|
* mimeType: string,
|
||||||
|
* annotations?: AnnotationsData,
|
||||||
* }
|
* }
|
||||||
*
|
*
|
||||||
* @author Kyrian Obikwelu <koshnawaza@gmail.com>
|
* @author Kyrian Obikwelu <koshnawaza@gmail.com>
|
||||||
|
|
@ -27,12 +31,14 @@ class ImageContent extends Content
|
||||||
/**
|
/**
|
||||||
* Create a new ImageContent instance.
|
* Create a new ImageContent instance.
|
||||||
*
|
*
|
||||||
* @param string $data Base64-encoded image data
|
* @param string $data Base64-encoded image data
|
||||||
* @param string $mimeType The MIME type of the image
|
* @param string $mimeType The MIME type of the image
|
||||||
|
* @param ?Annotations $annotations Optional annotations describing the content
|
||||||
*/
|
*/
|
||||||
public function __construct(
|
public function __construct(
|
||||||
public readonly string $data,
|
public readonly string $data,
|
||||||
public readonly string $mimeType,
|
public readonly string $mimeType,
|
||||||
|
public readonly ?Annotations $annotations = null,
|
||||||
) {
|
) {
|
||||||
parent::__construct('image');
|
parent::__construct('image');
|
||||||
}
|
}
|
||||||
|
|
@ -49,18 +55,23 @@ class ImageContent extends Content
|
||||||
throw new InvalidArgumentException('Missing or invalid "mimeType" in ImageContent data.');
|
throw new InvalidArgumentException('Missing or invalid "mimeType" in ImageContent data.');
|
||||||
}
|
}
|
||||||
|
|
||||||
return new self($data['data'], $data['mimeType']);
|
return new self(
|
||||||
|
$data['data'],
|
||||||
|
$data['mimeType'],
|
||||||
|
isset($data['annotations']) ? Annotations::fromArray($data['annotations']) : null
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Create a new ImageContent from a file path.
|
* Create a new ImageContent from a file path.
|
||||||
*
|
*
|
||||||
* @param string $path Path to the image file
|
* @param string $path Path to the image file
|
||||||
* @param string|null $mimeType Optional MIME type override
|
* @param string|null $mimeType Optional MIME type override
|
||||||
|
* @param ?Annotations $annotations Optional annotations describing the content
|
||||||
*
|
*
|
||||||
* @throws InvalidArgumentException If the file doesn't exist
|
* @throws InvalidArgumentException If the file doesn't exist
|
||||||
*/
|
*/
|
||||||
public static function fromFile(string $path, ?string $mimeType = null): self
|
public static function fromFile(string $path, ?string $mimeType = null, ?Annotations $annotations = null): self
|
||||||
{
|
{
|
||||||
if (!file_exists($path)) {
|
if (!file_exists($path)) {
|
||||||
throw new InvalidArgumentException(\sprintf('Image file not found: "%s".', $path));
|
throw new InvalidArgumentException(\sprintf('Image file not found: "%s".', $path));
|
||||||
|
|
@ -69,25 +80,36 @@ class ImageContent extends Content
|
||||||
$data = base64_encode(file_get_contents($path));
|
$data = base64_encode(file_get_contents($path));
|
||||||
$detectedMime = $mimeType ?? mime_content_type($path) ?: 'image/png';
|
$detectedMime = $mimeType ?? mime_content_type($path) ?: 'image/png';
|
||||||
|
|
||||||
return new self($data, $detectedMime);
|
return new self($data, $detectedMime, $annotations);
|
||||||
}
|
}
|
||||||
|
|
||||||
public static function fromString(string $data, string $mimeType): self
|
public static function fromString(string $data, string $mimeType, ?Annotations $annotations = null): self
|
||||||
{
|
{
|
||||||
return new self(base64_encode($data), $mimeType);
|
return new self(base64_encode($data), $mimeType, $annotations);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Convert the content to an array.
|
* Convert the content to an array.
|
||||||
*
|
*
|
||||||
* @return ImageContentData
|
* @return array{
|
||||||
|
* type: 'image',
|
||||||
|
* data: string,
|
||||||
|
* mimeType: string,
|
||||||
|
* annotations?: Annotations,
|
||||||
|
* }
|
||||||
*/
|
*/
|
||||||
public function jsonSerialize(): array
|
public function jsonSerialize(): array
|
||||||
{
|
{
|
||||||
return [
|
$result = [
|
||||||
'type' => $this->type,
|
'type' => $this->type,
|
||||||
'data' => $this->data,
|
'data' => $this->data,
|
||||||
'mimeType' => $this->mimeType,
|
'mimeType' => $this->mimeType,
|
||||||
];
|
];
|
||||||
|
|
||||||
|
if (null !== $this->annotations) {
|
||||||
|
$result['annotations'] = $this->annotations;
|
||||||
|
}
|
||||||
|
|
||||||
|
return $result;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -12,6 +12,10 @@
|
||||||
namespace Mcp\Tests\Unit\Capability\Formatter;
|
namespace Mcp\Tests\Unit\Capability\Formatter;
|
||||||
|
|
||||||
use Mcp\Capability\Formatter\PromptResultFormatter;
|
use Mcp\Capability\Formatter\PromptResultFormatter;
|
||||||
|
use Mcp\Exception\RuntimeException;
|
||||||
|
use Mcp\Schema\Content\AudioContent;
|
||||||
|
use Mcp\Schema\Content\EmbeddedResource;
|
||||||
|
use Mcp\Schema\Content\ImageContent;
|
||||||
use Mcp\Schema\Content\PromptMessage;
|
use Mcp\Schema\Content\PromptMessage;
|
||||||
use Mcp\Schema\Content\TextContent;
|
use Mcp\Schema\Content\TextContent;
|
||||||
use Mcp\Schema\Enum\Role;
|
use Mcp\Schema\Enum\Role;
|
||||||
|
|
@ -46,4 +50,142 @@ class PromptResultFormatterTest extends TestCase
|
||||||
$this->assertCount(1, $result);
|
$this->assertCount(1, $result);
|
||||||
$this->assertSame(Role::User, $result[0]->role);
|
$this->assertSame(Role::User, $result[0]->role);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function testFormatTypedTextContentPreservesAnnotations(): void
|
||||||
|
{
|
||||||
|
$result = (new PromptResultFormatter())->format([
|
||||||
|
[
|
||||||
|
'role' => 'user',
|
||||||
|
'content' => [
|
||||||
|
'type' => 'text',
|
||||||
|
'text' => 'Hello',
|
||||||
|
'annotations' => ['audience' => ['user'], 'priority' => 0.5],
|
||||||
|
],
|
||||||
|
],
|
||||||
|
]);
|
||||||
|
|
||||||
|
$content = $result[0]->content;
|
||||||
|
$this->assertInstanceOf(TextContent::class, $content);
|
||||||
|
$this->assertSame('Hello', $content->text);
|
||||||
|
$this->assertNotNull($content->annotations);
|
||||||
|
$this->assertSame([Role::User], $content->annotations->audience);
|
||||||
|
$this->assertSame(0.5, $content->annotations->priority);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testFormatTypedImageContentPreservesAnnotations(): void
|
||||||
|
{
|
||||||
|
$result = (new PromptResultFormatter())->format([
|
||||||
|
[
|
||||||
|
'role' => 'user',
|
||||||
|
'content' => [
|
||||||
|
'type' => 'image',
|
||||||
|
'data' => base64_encode('binary'),
|
||||||
|
'mimeType' => 'image/png',
|
||||||
|
'annotations' => ['audience' => ['assistant']],
|
||||||
|
],
|
||||||
|
],
|
||||||
|
]);
|
||||||
|
|
||||||
|
$content = $result[0]->content;
|
||||||
|
$this->assertInstanceOf(ImageContent::class, $content);
|
||||||
|
$this->assertSame('image/png', $content->mimeType);
|
||||||
|
$this->assertNotNull($content->annotations);
|
||||||
|
$this->assertSame([Role::Assistant], $content->annotations->audience);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testFormatTypedAudioContentPreservesAnnotations(): void
|
||||||
|
{
|
||||||
|
$result = (new PromptResultFormatter())->format([
|
||||||
|
[
|
||||||
|
'role' => 'user',
|
||||||
|
'content' => [
|
||||||
|
'type' => 'audio',
|
||||||
|
'data' => base64_encode('binary'),
|
||||||
|
'mimeType' => 'audio/mpeg',
|
||||||
|
'annotations' => ['priority' => 1.0],
|
||||||
|
],
|
||||||
|
],
|
||||||
|
]);
|
||||||
|
|
||||||
|
$content = $result[0]->content;
|
||||||
|
$this->assertInstanceOf(AudioContent::class, $content);
|
||||||
|
$this->assertNotNull($content->annotations);
|
||||||
|
$this->assertSame(1.0, $content->annotations->priority);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testFormatTypedResourceContentPreservesOptionalFields(): void
|
||||||
|
{
|
||||||
|
$result = (new PromptResultFormatter())->format([
|
||||||
|
[
|
||||||
|
'role' => 'user',
|
||||||
|
'content' => [
|
||||||
|
'type' => 'resource',
|
||||||
|
'resource' => [
|
||||||
|
'uri' => 'file://data.json',
|
||||||
|
'mimeType' => 'application/json',
|
||||||
|
'text' => '{"key": "value"}',
|
||||||
|
'_meta' => ['origin' => 'test'],
|
||||||
|
],
|
||||||
|
'annotations' => ['audience' => ['user']],
|
||||||
|
],
|
||||||
|
],
|
||||||
|
]);
|
||||||
|
|
||||||
|
$content = $result[0]->content;
|
||||||
|
$this->assertInstanceOf(EmbeddedResource::class, $content);
|
||||||
|
$this->assertSame('application/json', $content->resource->mimeType);
|
||||||
|
$this->assertSame(['origin' => 'test'], $content->resource->meta);
|
||||||
|
$this->assertNotNull($content->annotations);
|
||||||
|
$this->assertSame([Role::User], $content->annotations->audience);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testFormatTypedTextResourceContentDefaultsMimeType(): void
|
||||||
|
{
|
||||||
|
$result = (new PromptResultFormatter())->format([
|
||||||
|
[
|
||||||
|
'role' => 'user',
|
||||||
|
'content' => [
|
||||||
|
'type' => 'resource',
|
||||||
|
'resource' => ['uri' => 'file://a.txt', 'text' => 'plain'],
|
||||||
|
],
|
||||||
|
],
|
||||||
|
]);
|
||||||
|
|
||||||
|
$this->assertSame('text/plain', $result[0]->content->resource->mimeType);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testFormatTypedBlobResourceContentDefaultsMimeType(): void
|
||||||
|
{
|
||||||
|
$result = (new PromptResultFormatter())->format([
|
||||||
|
[
|
||||||
|
'role' => 'user',
|
||||||
|
'content' => [
|
||||||
|
'type' => 'resource',
|
||||||
|
'resource' => ['uri' => 'file://a.bin', 'blob' => base64_encode('binary')],
|
||||||
|
],
|
||||||
|
],
|
||||||
|
]);
|
||||||
|
|
||||||
|
$this->assertSame('application/octet-stream', $result[0]->content->resource->mimeType);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testFormatTypedContentRejectsInvalidDataWithIndexContext(): void
|
||||||
|
{
|
||||||
|
$this->expectException(RuntimeException::class);
|
||||||
|
$this->expectExceptionMessage("Invalid 'text' content at index 0");
|
||||||
|
|
||||||
|
(new PromptResultFormatter())->format([
|
||||||
|
['role' => 'user', 'content' => ['type' => 'text']],
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testFormatTypedContentRejectsUnknownType(): void
|
||||||
|
{
|
||||||
|
$this->expectException(RuntimeException::class);
|
||||||
|
$this->expectExceptionMessage("Invalid content type 'video' at index 0.");
|
||||||
|
|
||||||
|
(new PromptResultFormatter())->format([
|
||||||
|
['role' => 'user', 'content' => ['type' => 'video']],
|
||||||
|
]);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
87
tests/Unit/Schema/Content/ImageContentTest.php
Normal file
87
tests/Unit/Schema/Content/ImageContentTest.php
Normal file
|
|
@ -0,0 +1,87 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
/*
|
||||||
|
* This file is part of the official PHP MCP SDK.
|
||||||
|
*
|
||||||
|
* A collaboration between Symfony and the PHP Foundation.
|
||||||
|
*
|
||||||
|
* For the full copyright and license information, please view the LICENSE
|
||||||
|
* file that was distributed with this source code.
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace Mcp\Tests\Unit\Schema\Content;
|
||||||
|
|
||||||
|
use Mcp\Schema\Annotations;
|
||||||
|
use Mcp\Schema\Content\ImageContent;
|
||||||
|
use Mcp\Schema\Enum\Role;
|
||||||
|
use PHPUnit\Framework\TestCase;
|
||||||
|
|
||||||
|
final class ImageContentTest extends TestCase
|
||||||
|
{
|
||||||
|
public function testConstructorDefaultsAnnotationsToNull(): void
|
||||||
|
{
|
||||||
|
$content = new ImageContent(base64_encode('binary'), 'image/png');
|
||||||
|
|
||||||
|
$this->assertNull($content->annotations);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testConstructorAcceptsAnnotations(): void
|
||||||
|
{
|
||||||
|
$annotations = new Annotations([Role::User], 0.5);
|
||||||
|
$content = new ImageContent(base64_encode('binary'), 'image/png', $annotations);
|
||||||
|
|
||||||
|
$this->assertSame($annotations, $content->annotations);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testFromArrayDeserializesAnnotations(): void
|
||||||
|
{
|
||||||
|
$content = ImageContent::fromArray([
|
||||||
|
'type' => 'image',
|
||||||
|
'data' => base64_encode('binary'),
|
||||||
|
'mimeType' => 'image/png',
|
||||||
|
'annotations' => ['audience' => ['user'], 'priority' => 0.5],
|
||||||
|
]);
|
||||||
|
|
||||||
|
$this->assertNotNull($content->annotations);
|
||||||
|
$this->assertSame([Role::User], $content->annotations->audience);
|
||||||
|
$this->assertSame(0.5, $content->annotations->priority);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testJsonSerializeOmitsNullAnnotations(): void
|
||||||
|
{
|
||||||
|
$content = new ImageContent(base64_encode('binary'), 'image/png');
|
||||||
|
|
||||||
|
$this->assertArrayNotHasKey('annotations', $content->jsonSerialize());
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testJsonSerializeIncludesAnnotations(): void
|
||||||
|
{
|
||||||
|
$annotations = new Annotations([Role::User], 0.5);
|
||||||
|
$content = new ImageContent(base64_encode('binary'), 'image/png', $annotations);
|
||||||
|
|
||||||
|
$data = $content->jsonSerialize();
|
||||||
|
|
||||||
|
$this->assertSame($annotations, $data['annotations']);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testRoundTripWithAnnotations(): void
|
||||||
|
{
|
||||||
|
$original = new ImageContent(base64_encode('binary'), 'image/png', new Annotations([Role::User], 0.5));
|
||||||
|
|
||||||
|
$decoded = json_decode(json_encode($original), true);
|
||||||
|
$rehydrated = ImageContent::fromArray($decoded);
|
||||||
|
|
||||||
|
$this->assertSame($original->data, $rehydrated->data);
|
||||||
|
$this->assertSame($original->mimeType, $rehydrated->mimeType);
|
||||||
|
$this->assertEquals($original->annotations, $rehydrated->annotations);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testFromStringAcceptsAnnotations(): void
|
||||||
|
{
|
||||||
|
$annotations = new Annotations([Role::User]);
|
||||||
|
$content = ImageContent::fromString('binary', 'image/png', $annotations);
|
||||||
|
|
||||||
|
$this->assertSame(base64_encode('binary'), $content->data);
|
||||||
|
$this->assertSame($annotations, $content->annotations);
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Reference in a new issue